<<<版權聲明>>>(必需加入到轉貼文章)
"Linux Socket Programming 淺談 -- 教你的程式如何透過網路溝通"文章作物版權屬於"Kam Tik "第一擁有者。為了推廣網路的使用,除了下列的限制之外,任何人均可以任何型式複製或修改所有講義。
一、不得有任何的商業行為
二、複製或修改這份講義時,必須將本版權聲明列入,並註明之
三、第一身擁有者不對修改過後的內容負任何的責任
PS.所POST的文章是以 GNU Free Documentation License 1.2 分發。
───────────────────────────


編程語言

眾所周知,Networking 一向也是 Unix/Linux 的強項,你可以架設各種不同的 Linux 伺服器,但是,你又否想過你也可以在 Linux 上寫自己的伺服器程式呢?這次我便為大家淺談如何寫一個簡單的 Server-client program,讓大家一嘗自己寫伺服器程式的滋味。


作者: Kam Tik Email: kamtik@www.linux.org.hk

當然,你必需先學會寫 C 才可以寫 server-client program。在這裡,我會假設大家已經學會。這裡我會分開兩部份,分別是用戶端和伺服端。先看看如何寫用戶端程式吧,為了了解 client program 的基本運作原理,我們首先看看 client program 的流程圖。

由上圖可以看到,程式的流程是先建立了 socket data,接著便是連接伺服器,然後便可以寫進或讀取資料,而這個過程可以重複,直至寫入和讀取完所需資訊後,才關閉連接,過程非常似在硬碟中存取資料。

因此,我們的第一步是要建立 socket data。首先我們要匯入一個程式館 (library):

#include
#include

這兩個 header file 內含所需的函式和 structure 定義。像使用 open() 函式去建立一個 file descriptor 用來存取硬碟中的資料一樣,我們可以用 socket() 函式去建立一個 socket descriptor,socket() 的一般用法如下:

Prototype:
int socket (int domain, int type, int protocol);

其中 domain 可以是常數值 PF_INET、PF_LOCAL、PF_IPX 或 PF_INET6:
PF_LOCALPF_IPX PF_INET6
PF_INET        使用 IPv4 協定制式
一般只用作 system logger 或 print queue
Novell 網絡協定制式
使用 IPv6 協定制式

而 type 就選用 SOCK_STREAM 常數值,可以作 byte stream 傳輸。而 protocol 是 network byte order ,SOCK_STREAM 只支援 protocol = 0。如果 socket() 函式發生錯誤,便會回傳負數值。

socket() 的例子:

int sockfd;
sockfd = socket(PF_INET, SOCK_STREAM, 0);

建立了 socket descriptor 後,我們便可以用 connect() 函式連接到伺服端了:

connect() 的 prototype:

int connect(int sd, struct sockaddr *server, int addr_len);

其中 sd 是剛才的 socket descriptor,*server 是一個 structure 包含著伺服端的描述, addr_len 是 *server 所指著的 structure 的大小。要建立這個 structure,我們先要宣告這個 structure:

struct sockaddr_in dest;

然後再初始化 (initialize) 這個 structure 的某些值,如下:

bzero(&dest, sizeof(dest));
dest.sin_family = PF_INET;
dest.sin_port = htons(8889);
inet_aton("127.0.0.1", &dest.sin_addr);

bzero 會把 dest 中的變數的值變為 0,然後把 dest 中的 sin_family 設為 domain (PF_INET, PF_LOCAL, PF_IPX, PF_INET6),第三句中 8889 是 port number,是伺服端中要連接到的那一個 port number,而第四句中 "127.0.0.1" 這個字串是伺服器的 IP 位址,在這裡即是機器本身。

connect() 的例子:

connect(sockfd, &dest, sizeof(dest));

如果 connect 過程中有錯誤 (例如未連接網路),便會回傳 0。

到第三階段,可以存取資料了,要讀取資料,我們可以用 recv() 函式:

recv() 的 prototype:

int recv(int sockfd, void* buf, int maxbuf, int options);

其中 sockfd 是 socket 傳回的 socket descriptor,buf 是收到資料後存放的緩沖位置,maxbuf 是緩沖區 buf 的大小,options 是一些選項 (MSG_OOB, MSG_PEEK, MSG_WAITALL, MSG_ERRQUEUE, MSG_NOSIGNAL, MSG_ERRQUEUE),在這裡的示範會使用 0。recv() 會回傳收到資訊的大小值,如有錯誤,會回傳負數值。

recv() 的例子:

char buffer[128];
recv(sockfd, buffer, sizeof(buffer), 0);

要傳輸資料,我們可以用 send() 函式:

send() 的 prototype:

int send(int sockfd, void *buffer, int msg_len, int options);

其中 sockfd、buffer 和 msg_len 和 recv() 的相同,只不過是這次把要傳輸的資訊先放進 buffer 罷了。而 options 有 MSG_OOB, MSG_DONTROUTE, MSG_DONTWAIT, MSG_NOSIGNAL,在這裡我們也會使用 0。和 recv() 一樣,send() 會回傳傳輸的總大小值。

send() 的例子:

char buffer[] = "Hello World!";
send(sockfd, buffer, sizeof(buffer), 0);

最後可以關閉連結了!方法非常簡單,只要把 socket descriptor 交給 close() 函式便可。

close() 例子:

close(sockfd);

結合以上多項,我們便可以合為一個用戶端程式,範例如下 (sample-client.c):
注: 並沒有 error handling,如果伺服器不是機器本身,請修改 "127.0.0.1" 為伺服器的 IP。例如 "192.168.1.1"。

#include
#include
#include

int main()
{
        int sockfd;
        struct sockaddr_in dest;
        char buffer[128];

        /* create socket */
        sockfd = socket(PF_INET, SOCK_STREAM, 0);
       
        /* initialize value in dest */
        bzero(&dest, sizeof(dest));
        dest.sin_family = PF_INET;
        dest.sin_port = htons(8889);
        inet_aton("127.0.0.1", &dest.sin_addr);

        /* Connecting to server */
        connect(sockfd, (struct sockaddr*)&dest, sizeof(dest));
       
        /* Receive message from the server and print to screen */
        bzero(buffer, 128);
        recv(sockfd, buffer, sizeof(buffer), 0);
        printf("%s", buffer);

        /* Close connection */
        close(sockfd);

        return 0;
}

要編釋這個程式,使用指令: gcc sample-client.c -o sample-client
要執行程式可以使用指令: ./sample-client
不過在伺服器並未架設前請不要執行程式。

好了,這次我們要學寫伺服器程式,並實伺服器和用戶端程式的流程分別不大,只是多了幾個步驟:

中間由 close(client) 到 accept 的箭咀是個循環 (loop),而且這個循環是無限的,代表它不斷接受用戶端的連接。

我們要用 bind() 來設定一個 port number 給這個伺服器程式:

bind() 的 prototype:

int bind(int sockfd, struct sockaddr* addr, int addrlen);

其中 addr 也是 sockaddr structure,和先前的初始化過程一樣,不過 sin_addr.s_addr 就改設為 INADDR_ANY。而 addrlen 便是 addr 的大小。

接著用 listen() 使程式可接收 socket:

listen() 的 prototype:

int listen(int sockfd, int queue_len);

queue_len 是可被連接數目的最大值。如沒有錯誤,它回傳 0。

要等待及接受連接,我們要使用 accept() 函式:

accept 的 prototype:

int accept(int sockfd, struct sockaddr *addr, int *addr_len)

它會回傳一個 socket descriptor,用作存取。

結合這些新的元素和新的流程圖,便可以寫伺服器程式了。這個範例是 sample-client 的伺服器程式 (sample-server.c):注: 並沒有 error handling

#include
#include
#include

int main()
{
        int sockfd;
        struct sockaddr_in dest;
        char buffer[] = "Hello World!";

        /* create socket , same as client */
        sockfd = socket(PF_INET, SOCK_STREAM, 0);

        /* initialize structure dest */
        bzero(&dest, sizeof(dest));
        dest.sin_family = PF_INET;
        dest.sin_port = htons(8889);
        /* this line is different from client */
        dest.sin_addr.s_addr = INADDR_ANY;

        /* Assign a port number to socket */
        bind(sockfd, (struct sockaddr*)&dest, sizeof(dest));

        /* make it listen to socket with max 20 connections */
        listen(sockfd, 20);

        /* infinity loop -- accepting connection from client forever */
        while(1)
        {
                int clientfd;
                struct sockaddr_in client_addr;
                int addrlen = sizeof(client_addr);

                /* Wait and Accept connection */
                clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &addrlen);

                /* Send message */
                send(clientfd, buffer, sizeof(buffer), 0);

                /* close(client) */
                close(clientfd);
        }

        /* close(server) , but never get here because of the loop */
        close(sockfd);
        return 0;
}

編釋: gcc sample-server.c -o sample-server
執行: ./sample-server

執行後它便開始接受連接,可以執行 sample-client 試試,便可以接收到 "Hello World!" 這個訊息了!要結束 sample-server 可以按 Ctrl-C。要把 sample-server 放在 background 中工作,可以用 ./sample-server &

如各位想下載源程式碼回來試試,可以到:

http://www.linux.org.hk/~kamtik/linuxhall/sample-client.c
http://www.linux.org.hk/~kamtik/linuxhall/sample-server.c

原文刊載在 LinuxHall 第 16 期

    全站熱搜

    立你斯 發表在 痞客邦 留言(0) 人氣()