【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伺服器模型類似,都是建立一個子程序來處理。
除非伺服器處理客戶端請求所用的時間比較長,人們實際上很少使用這種模型。