1. 程式人生 > >【Linux】Linux網路程式設計(含常見伺服器模型,下篇)

【Linux】Linux網路程式設計(含常見伺服器模型,下篇)

高階巢狀字函式

前面介紹的一些函式(read、write等)都是網路程式裡最基本的函式,也是最原始的通訊函式。下面介紹一下幾個網路程式設計的高階函式:

recv()函式

int recv(int s, void *buf, int len, unsigned int flags);

函式說明:經socket接收資料,recv()用來接收遠端主機指定的socket傳來的資料,並把資料存到引數buf指向的記憶體空間,引數len為可接收資料的最大長度。引數flags一般設為0,其數值定義如下:

MSG_OOB:接收以out-of-band送出的資料;MSG_PEEK:返回來的資料並不會在系統內刪除,如果再呼叫recv()會返回相同的資料內容;MSG_WAITALL:強迫接收到len大小的資料後才能返回,除非有錯誤或訊號發生;MSG_NOSIGNAL:此操作不願被SIGPIPE訊號中斷。

返回值:成功則返回接收到的字元數,失敗則返回-1,錯誤碼儲存在errno中。

send()函式

int send(int s, const void *msg, int len, unsigned int flags);

函式說明:經socket傳送資料,send()用來將資料由指定的socket傳給對方主機。引數s為已建好連線的socket,引數msg指向欲連線的資料內容,引數len則為資料長度,引數flags一般設為0,其數值定義如下:

MSG_OOB:傳送的資料以out-of-band送出;MSG_DONTROUTE:取消路由表查詢;MSG_DONTWAIT:設定為不可阻斷運作;MSG_NOSIGNAL:此動作不願被SIGPIPE訊號中斷。

返回值:成功則返回接收到的字元數,失敗則返回-1。

recvmsg()函式

int recvmsg(int s, struct msghdr *msg, unsigned int flags);

函式說明:經socket接收資料,recvmsg()用來接收遠端主機指定的socket傳來的資料。引數s為已建立好連線的socket,如果利用UDP協議,則不需要經過連線操作。引數msg指向欲年限的資料結構內容,引數glags一般設定為0。

返回值:成功則返回接收到的字元數,失敗則返回-1。

sendmsg()函式

int sendmsg(int s, struct msghdr *msg, unsigned int flags);

函式說明:經socket傳送資料,sendmsg()用來將資料由指定的socket傳給對方主機。引數s為已建好連線的socket,如果利用UDP協議,則不需要經過連線操作。引數msg指向欲連線的資料內容,引數len則為資料長度,引數flags一般設為0。

返回值:成功則返回接收到的字元數,失敗則返回-1。

結構msghdr的定義如下:

struct msghdr
{
        void *msg_name;                //傳送接收地址
        socklen_t msg_namelen;                //地址長度
        struct iovec *msg_iov;                //傳送/接收資料量
        size_t msg_iovlen;                //元素個數
        void *msg_control;                //補充資料
        size_t msg_controllen;                //補充資料緩衝長度
        int msg_flags;                //接收訊息標識
};

巢狀字的關閉

關閉巢狀字有兩個函式close()和shutdown(),用close()時和使用者關閉檔案類似。

int shutdown(int s, int how);

函式說明:終止socket通訊,shutdown()用來終止引數s所指定的socket連線。引數how有三種情況:

how=0:終止讀取操作;how=1:終止傳送操作;how=2:終止讀取及傳送操作。

返回值:成功則返回0,失敗則返回-1。

close()和shutdown()的區別:

  • 對應的系統呼叫不同,close()函式對應的系統呼叫是sys_close(),在fs/open.c中定義。shutdown()函式對應的系統呼叫是sys_shutdown(),在net/socket.c中定義;
  • shutdown()只能用於套接字檔案,close()可以用於所有檔案型別;
  • shutdown()可以選擇關閉全雙工連線的讀通道或者寫通道,並沒有釋放檔案描述符,close()會同時關閉全雙工連線的讀寫通道,除了關閉連線外,還會釋放套接字佔用的檔案描述符;
  • close()函式會關閉套接字ID,如果有多個程序共享一個套接字,close()每被呼叫一次,計數減1,直到計數為0時,也就是所用程序都呼叫了close(),套接字將被釋放。而shutdown()會切斷程序共享的套接字的所有連線,不管這個套接字的引用計數是否為零。

巢狀字選項

有時使用者要控制巢狀字的行為(如修改緩衝區的大小),這個時候使用者就要控制巢狀字的選項了。

getsockopt()函式

int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);

函式說明:取得socket狀態,getsockopt()會將引數s所指定的socket狀態返回。引數optname代表欲取得何種選項狀態,而引數optval則指向欲儲存結果的記憶體地址,引數optlen為該空間的大小。

返回值:成功則返回0,若有錯誤則返回-1。

例子:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>

int main()
{
        int s,optval,optlen=sizeof(int);
        if((s=socket(AF_INET,SOCK_STREAM,0))<0)
                perror("socket");
        getsockopt(s,SOL_SOCKET,SO_TYPE,&optval,&optlen);
        printf("optval=%d\n",optval);
        close(s);
        
        return 0;
}

setsockopt()函式

int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);

函式說明:設定socket狀態,setsockopt()用來設定引數s所指定的socket狀態。引數level代表欲設定的網路層,一般設為SOL_SOCKET以存取socket層,引數optname代表欲設定的選項,有以下幾種數值:

SO_DEBUG:開啟或關閉排錯模式;SO_REUSEADDR:允許在bind()過程中本地地址可重複使用;SO_TYPE:返回socket形態;SO_ERROR:返回socket已發生的錯誤原因;SO_DONTROUTE:送出的資料包不要利用路由裝置來傳輸;SO_BROADCAST:使用廣播方式傳送;SO_SNDBUF:設定送出的暫存區大小;SO_RCVBUF:設定接收的暫存區大小;SO_KEEPALIVE:定期確認連線是否已終止;SO_OOBINLINE:當接收到OOB資料時會馬上送至標準輸入裝置;SO_LINGER:確保資料安全且可靠地傳送出去。

返回值:成功則返回0,若有錯誤則返回-1。

ioctl()函式

int ioctl(int handle, int cmd, [int *argdx, int argcx]);

函式說明:ioctl()是裝置驅動程式中對裝置的IO通道進行管理的函式。所謂對IO通道進行管理,就是對裝置的一些特性進行控制,例如串列埠的傳輸波特率、馬達的轉速等。它的呼叫格式如下:

int ioctl(int fd, int cmd, ...);

其中:fd就是使用者程式開啟裝置時使用open()函式返回的檔案識別符號,cmd就是使用者程式對裝置的控制命令,後面的省略號是一些補充引數,一般最多一個,有或沒有是和cmd的意義相關的。ioctl()函式是檔案結構中的一個屬性分量,也就是說,如果驅動程式提供了對ioctl()的支援,使用者就能在使用者程式中使用ioatl()函式控制裝置的IO通道。

返回值:成功則返回0,若有錯誤則返回-1。

伺服器模型

迴圈伺服器:UDP伺服器

UDP迴圈伺服器的實現非常簡單。UDP伺服器每次從巢狀字上讀取一個客戶端的請求並處理,然後將結果返回給客戶端。可以用下面的演算法來實現:

socket(...);
bind(...);

while(1){
        recvfrom(...);
        ...                //伺服器處理函式
        sendto(...);
}

因為UDP是面向非連線的,沒有一個客戶端可以總是佔住服務端,只要處理過程不是死迴圈,伺服器對於每個客戶端的請求總是能夠滿足。

迴圈伺服器:TCP伺服器

TCP伺服器接收一個客戶端的連線,然後處理,完成該客戶端的所有請求後,斷開連線。演算法如下:

socket(...);
bind(...);
listen(...);

while(1){
        accept(...);
        
        while(1){
                read(...);
                ...                    //伺服器處理函式
                write(...);
        }
}

TCP迴圈伺服器一次只能處理一個客戶端的請求。只有在該客戶端的所有請求都滿足後,伺服器才可以繼續響應後面的請求。如果有一個客戶端佔住伺服器不釋放時,其他的客戶端都不能工作了。因此,TCP伺服器很少採用迴圈伺服器模型。

併發伺服器:TCP伺服器

為了彌補迴圈TCP伺服器的缺陷,人們又想出了併發伺服器的模型。併發伺服器的思想是,每一個客戶端的請求並不由伺服器直接處理,而是由伺服器建立一個子程序來處理。演算法如下:

socket(...);
bind(...);
listen(...);

while(1){
        accept(...);

        if(fork(...)==0){                //子程序
                while(1){
                        read(...);
                        ...                //伺服器處理函式
                        write(...);
                }
                close(...);
                exit(...);
        }

        close(...);
}

TCP併發伺服器可以解決TCP迴圈伺服器中客戶端獨佔伺服器的情況。不過也同時帶來一個較大的問題,為了響應客戶端的請求,伺服器要建立子程序來處理,而建立子程序是一個非常消耗資源的操作。

併發伺服器:多路複用IO

為了解決建立子程序帶來的系統資源消耗,人們又想出了多路複用IO模型。最常用的函式為select。

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

函式說明:IO多工機制,select()用來等待檔案描述符狀態的改變。引數n代表最大的檔案描述符加1,引數readfds、writefds、exceptfds稱為描述片語,是用來回傳該描述詞的讀、寫或例外的狀況,引數timeout為結構timeval,用來設定select()的等待時間。下列巨集提供了處理這三種描述片語的方式:

FD_CLR:用來清除描述片語set中相關fd的位;FD_ISSET:用來測試描述片語set中相關fd的位是否為真;FD_SET:用來設定描述片語set中相關fd的位;FD_ZERO:用來清除描述片語set全部的位。

返回值:如果引數timeout設為NULL則表示select()沒有timeout。

常見的程式片段為fs_set readset:

FD_ZERO(&readset);
FD_SET(fd,&readset);
   
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){
        ...
}

使用select後用戶的伺服器程式就變成:

初始化(socket,bind,listen);

while(1){
        設定監聽讀寫檔案描述符(FD_*);
        呼叫select;
        
        如果傾聽巢狀字就緒,就說明一個新的連線請求建立{
                建立連線(accept);
                加入到監聽檔案描述符中去;
        }否則說明是一個已經連線過的描述符{
                進行操作(read或者write);
        }
}

多路複用可以解決資源限制的問題。該模型實際上是將UDP迴圈模型用在了TCP上面。這也就帶來了一些問題,比如,由於伺服器依次處理客戶端的請求,所以可能會導致有的客戶端會等待很久。

併發伺服器:UDP伺服器

人們把併發的概念用於UDP就得到了併發UDP伺服器模型。併發UDP伺服器模型其實很簡單。和併發的TCP伺服器模型類似,都是建立一個子程序來處理。

除非伺服器處理客戶端請求所用的時間比較長,人們實際上很少使用這種模型。