先來談談為什麼會出現select函數,也就是select是解決什麼問題的?
平常使用的recv函數時阻塞的,也就是如果沒有資料可讀,recv就會一直阻塞在那裡,這是如果有另外一個連接配接過來,就得一直等待,這樣實時性就不是太好。
這個問題的幾個解決方法:1. 使用ioctlsocket函數,将recv函數設定成非阻塞的,這樣不管套接字上有沒有資料都會立刻傳回,可以重複調用recv函數,這種方式叫做輪詢(polling),但是這樣效率很是問題,因為,大多數時間實際上是無資料可讀的,花費時間不斷反複執行read系統調用,這樣就比較浪費CPU的時間。并且循環之間的間隔不好确定。2. 使用fork,使用多程序來解決,這裡終止會比較複雜(待研究)。 3.使用多線程來解決,這樣避免了終止的複雜性,但卻要求處理線程之間的同步,在減少複雜性方面這可能會得不償失。4. 使用異步IO(待研究)。5. 就是本文所使用的I/O多路轉接(多路複用)--其實就是在套接字阻塞和非阻塞之間做了一個均衡,我們稱之為半阻塞。
經過對select的初步了解,在windows和linux下的實作小有差別,是以分開來寫。這裡先寫windows下的select機制。
select的大概思想:将多個套接字放在一個集合裡,然後統一檢查這些套接字的狀态(可讀、可寫、異常等),調用select後,會更新這些套接字的狀态,然後做判斷,如果套接字可讀,就執行read操作。這樣就巧妙地避免了阻塞,達到同時處理多個連接配接的目的。當然如果沒有事件發生,select會一直阻塞,如果不想一直讓它等待,想去處理其它事情,可以設定一個最大的等待時間。
/***********************************************************************************************************/
int select(
_In_ int nfds,
_Inout_ fd_set *readfds,
_Inout_ fd_set *writefds,
_Inout_ fd_set *exceptfds,
_In_ const struct timeval *timeout
);
函數的參數,第一個是輸入參數nfds,表示滿足條件的套接字的個數,windows下可以設定為0,因為fd_set結構體中已經包含了這個參數,這個參數已經是多餘的了,之是以還存在,隻是是為了與FreeBSD相容。
第二三四參數都是輸入輸出參數(值-結果參數,輸入和輸出會不一樣),表示套接字的可讀、可寫和異常三種狀态的集合。調用select之後,如果指定套接字不可讀或者不可寫,就會從相應隊列中清除,這樣就可以判斷哪些套接字可讀或者可寫。
說明一下,這裡的可讀性是指:如果有客戶的連接配接請求到達,套接口就是可讀的,調用accept能夠立即完成,而不發生阻塞;如果套接口接收隊列緩沖區中的位元組數大于0,調用recv或者recvfrom就不會阻塞。可寫性是指,可以向套接字發送資料(套接字建立成功後,就是可寫的)。當然不是套接字可寫就會去發送資料,就像不是看到電話就去打電話一樣,而是由打電話的需求了,才去看電話是否可打;可讀就不一樣了,電話響了,自然要去接電話(除非,你有事忙或者不想接,一般都是要接的)。可讀已經包含了緩沖區中有資料可以讀取,可寫隻是說明了緩沖區有空間讓你寫,你需不需要寫就要看你有沒有資料要寫了.關于異常,就是指一些意外情況,自己用的比較少,以後用到了,再過來補上。
第五個參數是等待的最大時間,是一個結構體:struct timeval,它的定義是:
/*
* Structure used in select() call, taken from the BSD file sys/time.h.
*/
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
具體到秒和微妙,按照等待的時間長短可以分為不等待、等待一定時間、一直等待。對應的設定分别為,(0,0)是不等待,這是select是非阻塞的,(x,y)最大等待時間x秒y微妙(如果有事件就會提前傳回,而不繼續等待),NULL表示一直等待,直到有事件發生。這裡可以将timeout分别設定成0(不阻塞)或者1微妙(阻塞很短的時間),然後觀察CPU的使用率,會發現設定成非阻塞後,CPU的使用率已下載下傳就上升到了50%左右,這樣可以看出非阻塞占用CPU很多,但使用率不高。
跟select配合使用的幾個宏和fd_set結構體介紹:
套接字描述符為了友善管理是放在一個集合裡的,這個集合是fd_set,它的具體定義是:
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
fd_count是集合中已經設定的套接口描述符的數量。fd_array數組儲存已經設定的套接口描述符,其中FD_SETSIZE的定義是:
#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif /* FD_SETSIZE */
這個預設值在一般的程式中已經夠用,如果需要,可以将其更改為更大的值。
集合的管理操作,比如元素的清空、加入、删除以及判斷元素是否在集合中都是用宏來完成的。四個宏是:
FD_ZERO(*set)
FD_SET(s, *set)
FD_ISSET(s, *set)
FD_CLR(s, *set)
下面一一介紹這些宏的作用和定義:
FD_ZERO(*set),是把集合清空(初始化為0,确切的說,是把集合中的元素個數初始化為0,并不修改描述符數組).使用集合前,必須用FD_ZERO初始化,否則集合在棧上作為自動變量配置設定時,fd_set配置設定的将是随機值,導緻不可預測的問題。它的宏定義如下:
#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
FD_SET(s,*set),向集合中加入一個套接口描述符(如果該套接口描述符s沒在集合中,并且數組中已經設定的個數小于最大個數時,就把該描述符加入到集合中,集合元素個數加1)。這裡是将s的值直接放入數組中。它的宏定義如下:
#define FD_SET(fd, set) do { \
if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) \
((fd_set FAR *)(set))->fd_array[((fd_set FAR *)(set))->fd_count++]=(fd);\
} while(0)
FD_ISSET(s,*set),檢查描述符是否在集合中,如果在集合中傳回非0值,否則傳回0. 它的宏定義并沒有給出具體實作,但實作的思路很簡單,就是搜尋集合,判斷套接字s是否在數組中。它的宏定義是:
#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))
FD_CLR(s,*set),從集合中移出一個套接口描述符(比如一個套接字連接配接中斷後,就應該移除它)。實作思路是,在數組集合中找到對應的描述符,然後把後面的描述依次前移一個位置,最後把描述符的個數減1. 它的宏定義是:
#define FD_CLR(fd, set) do { \
u_int __i; \
for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
((fd_set FAR *)(set))->fd_array[__i] = \
((fd_set FAR *)(set))->fd_array[__i+1]; \
__i++; \
} \
((fd_set FAR *)(set))->fd_count--; \
break; \
} \
} \
至此,一些基礎的點基本就講完了,然後給出大概流程和一個示例:
1.調用FD_ZERO來初始化套接字狀态;
2.調用FD_SET将感興趣的套接字描述符加入集合中(每次循環都要重新加入,因為select更新後,會将一些沒有滿足條件的套接字移除隊列);
3.設定等待時間後,調用select函數--更新套接字的狀态;
4.調用FD_ISSET,來判斷套接字是否有相應狀态,然後做相應操作,比如,如果套接字可讀,就調用recv函數去接收資料。
關鍵技術:套接字隊列和狀态的表示與處理。
server端得程式如下(套接字管理隊列一個很重要的作用就是儲存套接字描述符,因為accept得到的套接字描述符會覆寫掉原來的套接字描述符,而readfs中的描述符在select後會删除這些套接字描述符):
// server.cpp :
//程式中加入了套接字管理隊列,這樣管理起來更加清晰、友善,當然也可以不用這個東西
#include "winsock.h"
#include "stdio.h"
#pragma comment (lib,"wsock32.lib")
struct socket_list{
SOCKET MainSock;
int num;
SOCKET sock_array[64];
void init_list(socket_list *list)
{
int i;
list->MainSock = 0;
list->num = 0;
for(i = 0;i < 64;i ++){
list->sock_array[i] = 0;
}
}
void insert_list(SOCKET s,socket_list *list)
for(i = 0;i < 64; i++){
if(list->sock_array[i] == 0){
list->sock_array[i] = s;
list->num += 1;
break;
}
void delete_list(SOCKET s,socket_list *list)
if(list->sock_array[i] == s){
list->sock_array[i] = 0;
list->num -= 1;
void make_fdlist(socket_list *list,fd_set *fd_list)
FD_SET(list->MainSock,fd_list);
for(i = 0;i < 64;i++){
if(list->sock_array[i] > 0){
FD_SET(list->sock_array[i],fd_list);
int main(int argc, char* argv[])
SOCKET s,sock;
struct sockaddr_in ser_addr,remote_addr;
int len;
char buf[128];
WSAData wsa;
int retval;
struct socket_list sock_list;
fd_set readfds,writefds,exceptfds;
timeval timeout; //select的最多等待時間,防止一直等待
unsigned long arg;
WSAStartup(0x101,&wsa);
s = socket(AF_INET,SOCK_STREAM,0);
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(0x1234);
bind(s,(sockaddr*)&ser_addr,sizeof(ser_addr));
listen(s,5);
timeout.tv_sec = 5; //如果套接字集合中在1s内沒有資料,select就會傳回,逾時select傳回0
timeout.tv_usec = 0;
init_list(&sock_list);
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
sock_list.MainSock = s;
arg = 1;
ioctlsocket(sock_list.MainSock,FIONBIO,&arg);
while(1){
make_fdlist(&sock_list,&readfds);
//make_fdlist(&sock_list,&writefds);
//make_fdlist(&sock_list,&exceptfds);
retval = select(0,&readfds,&writefds,&exceptfds,&timeout); //超過這個時間,就不阻塞在這裡,傳回一個0值。
if(retval == SOCKET_ERROR){
retval = WSAGetLastError();
else if(retval == 0) {
printf("select() is time-out! There is no data or new-connect coming!\n");
continue;
if(FD_ISSET(sock_list.MainSock,&readfds)){
len = sizeof(remote_addr);
sock = accept(sock_list.MainSock,(sockaddr*)&remote_addr,&len);
if(sock == SOCKET_ERROR)
continue;
printf("accept a connection\n");
insert_list(sock,&sock_list);
for(i = 0;i < 64;i++){
if(sock_list.sock_array[i] == 0)
sock = sock_list.sock_array[i];
if(FD_ISSET(sock,&readfds)){
retval = recv(sock,buf,128,0);
if(retval == 0){
closesocket(sock);
printf("close a socket\n");
delete_list(sock,&sock_list);
continue;
}else if(retval == -1){
retval = WSAGetLastError();
if(retval == WSAEWOULDBLOCK)
continue;
delete_list(sock,&sock_list); //連接配接斷開後,從隊列中移除該套接字
}
buf[retval] = 0;
printf("->%s\n",buf);
send(sock,"ACK by server",13,0);
}
//if(FD_ISSET(sock,&writefds)){
//}
//if(FD_ISSET(sock,&exceptfds)){
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
closesocket(sock_list.MainSock);
WSACleanup();
return 0;
關于linux下的select跟windows下的差別還有待學習。
參考書籍:
《WinSock網絡程式設計經絡》第19章
《UNIX環境進階程式設計》