選擇模型--非阻塞套接字詳解
0x01什麼是選擇模型
普通套接字程式設計,常常會遇到阻塞主程序,比如recv,read等,如果沒有資料發過來會一直等待。有沒有辦法讓程序等待一段時間,再退出呢。這時候使用選擇模型就能解決這個問題。
原理–I/O多路複用:通過一個fd_set集合來管理套接字,當某個socket可讀或者可寫的時候,它可以給你一 個通知。這樣配合非阻塞的socket使用時,只有當系統通知我哪個描述符可讀了,我才去執行read操作。
詳細講解都在註釋裡
select函式
核心函式:select。
/**
*@breaf 用於監視集合中檔案描述符的變化情況——讀寫或是異常。
*@param nfds[in],指定被監聽的檔案描述符總數。通常被設定為檔案描述符中最大值加1
*@param readfds[in],可讀檔案描述符集合,NULL忽略讀操作
*@param writefds[in],可寫檔案描述符集合,NULL忽略寫操作
*@param exceptfds[in],異常檔案描述符集合,NULL忽略異常操作
*@param timeout[in],等待時間,為空則一直等待
*@return >0,就緒描述字的正數目,0超時,-1出錯
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
核心函式–引數2,3,4:fd_set結構體
fd_set集合,由一個整形來存放套接字數量,和一個long型別的陣列構成,每一個數組元素都能與一開啟的檔案控制代碼(不管是socket控制代碼,還是其他檔案或命名管道或裝置控制代碼)建立聯絡
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
對於fd_set有這些操作,以下式子中的fd為socket控制代碼。
fd_set set;
FD_ZERO(&set); /*清空set集合*/
FD_SET(fd, &set); /*將fd加入set集合*/
FD_CLR(fd, &set); /*將fd從set集合中清除*/
FD_ISSET(fd, &set); /*在呼叫select()函式後,用FD_ISSET來檢測fd是否在set集合中,當檢測到fd在set中則返回真,否則,返回假(0)*/
核心函式–引數5:struct timeval結構體
struct timeval結構體是一個精確的時間結構體,成員1為秒,成員2為微妙
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
其他函式–ioctlsocket
/**
*@breaf 控制套介面的模式。可用於任一狀態的任一套介面。
*@param s[in],一個標識套介面的描述字。
*@param cmd[in],對套介面s的操作命令。
*@param argp[in],指向cmd命令所帶引數的指標
*@return 0,成功,-1錯誤
*/
int ioctlsocket( int s, long cmd, u_long * argp);
ioctlsocket–引數cmd命令
FIONBIO:允許或禁止套介面s的非阻塞模式。
FIONREAD:確定套介面s自動讀入的資料量
SIOCATMARK:確認是否所有的帶外資料都已被讀入。
ioctlsocket–引數argp命令引數
0—禁用,1-----使用
其他函式–setsockopt
/**
*@breaf 用於任意型別、任意狀態套介面的設定選項值
*@param sockfd[in],標識一個套介面的描述字。
*@param level[in],選項定義的層次;支援SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
*@param optname[in],需設定的選項。
*@param optval[in],指標,指向存放選項待設定的新值的緩衝區。
*@param optlen[in],optval緩衝區長度。
*@return 0,成功,非0錯誤
*/
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
setsockopt引數 optname
有許多引數,具體參考文件
SO_BROADCAST BOOL 允許套介面傳送廣播資訊。
SO_DEBUG BOOL 記錄除錯資訊。
。。。。。
SO_SNDTIMEO int 傳送超時。
SO_TYPE int 套介面型別。
IP_OPTIONS 在IP頭中設定選項。
引數level在套接字中,常常用到SOL_SOCKET。有許多套接字的設定。具體參考文件如下:
設定套接字傳送時限:
int nNetTimeout = 1000; //1秒
setsockopt( socket, SOL_SOCKET, SO_SNDTIMEO, ( char * )&nNetTimeout, sizeof( int ) );
設定套接字 接收緩衝區大小
int nRecvBufLen = 32 * 1024; //設定為32K
setsockopt( s, SOL_SOCKET, SO_RCVBUF, ( const char* )&nRecvBufLen, sizeof( int ) );
套接字基礎
struct sockaddr_in
sockaddr_in和sockaddr是並列的結構,指向sockaddr_in的結構體的指標也可以指向
struct sockaddr_in
{
short sin_family;/*指代協議族,在socket程式設計中只能是AF_INET*/
unsigned short sin_port;/*埠號(使用網路位元組順序),在linux下,埠號的範圍0~65535,同時0~1024範圍的埠號已經被系統使用或保留*/
struct in_addr sin_addr;/*儲存IP地址,使用in_addr這個資料結構*/
unsigned char sin_zero[8];/*為了讓sockaddr與sockaddr_in兩個資料結構保持大小相同而保留的空位元組*/
socket()
/**
*@breaf 建立套接字
*@param domain[in],domain:協議域,又稱協議族(family)。常用的協議族有AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等
*@param type[in],指定Socket型別。常用的socket型別有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)針對於面向連線的TCP服務應用
*@param protocol[in],指定協議。常用協議有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等
*@return 套接字描述符(>0),成功。-1錯誤
*/
int socket(int domain, int type, int protocol);
connect ()
/**
*@breaf 用來將引數sockfd 的socket 連至引數serv_addr 指定的網路地址
*@param sockfd[in],套接字描述符
*@param serv_addr[in],指向資料結構sockaddr的指標,其中包括目的埠和IP地址
*@param addrlen[in],引數二sockaddr的長度
*@return 0,成功,非0錯誤
*/
int connect (int sockfd, struct sockaddr * serv_addr, int addrlen);
程式碼
看完上面的api是不是有點懵呢,沒事,再看看程式碼實現就懂了,如下,套接字連線伺服器的包裝非阻塞程式碼。
typedef struct TcpInfo
{
int connected;//套接字連線狀態
int ret;//函式執行接收返回值
int flag;//設定套接字的命令引數
fd_set set;//套接字的寫操作變化介面
fd_set rset;//套接字的讀操作變化介面
struct sockaddr_in remote_addr; //伺服器端網路地址結構體
int sock;//套接字
}TcpInfo;
void *openTcp(char *ServerIp,int ServerPort,int TimeOut)
{
TcpInfo *nTcpInfo;
nTcpInfo=(TcpInfo *)malloc(sizeof(TcpInfo));
if(nTcpInfo==NULL)
{
printf("failed nSubsessionInfo NULL\n");
return NULL;
}
memset(nTcpInfo,0,sizeof(TcpInfo));
nTcpInfo->ret=-1;
if((nTcpInfo->sock=socket(AF_INET,SOCK_STREAM,0))<0) //ipv4,流式套接字,協議某種型別
{
printf("Creating socket failed.%d",Error);
free(nTcpInfo);
return NULL;
}
memset(&nTcpInfo->remote_addr,0,sizeof(struct sockaddr)); //資料初始化--清零
nTcpInfo->remote_addr.sin_family=AF_INET; //設定為IP通訊
nTcpInfo->remote_addr.sin_addr.s_addr=inet_addr(ServerIp);//伺服器IP地址
nTcpInfo->remote_addr.sin_port=htons(ServerPort); //伺服器埠號
nTcpInfo->flag = 1;
ioctlsocket (nTcpInfo->sock, FIONBIO, (unsigned long *) &nTcpInfo->flag);//控制套介面的模式,設定套接字為非阻塞(1為設定) 套接字,命令,命令引數
nTcpInfo->connected = connect(nTcpInfo->sock, (struct sockaddr *) &nTcpInfo->remote_addr, sizeof(struct sockaddr));
//套接字為非阻塞後,connect不會阻塞直接返回-1
if (nTcpInfo->connected != 0 )
{
struct timeval tm;
tm.tv_sec = TimeOut;
tm.tv_usec = 0;
FD_ZERO(&nTcpInfo->set);//清空描述符集合
FD_ZERO(&nTcpInfo->rset);
FD_SET(nTcpInfo->sock,&nTcpInfo->set);//將套接字加入描述符集合
FD_SET(nTcpInfo->sock,&nTcpInfo->rset);
//socklen_t len;
nTcpInfo->ret = select(nTcpInfo->sock+1,&nTcpInfo->rset,&nTcpInfo->set,NULL,&tm);
//用於檢測檔案描述符的變化(讀/寫/異常),第四參為等待時間,為空為一直
//返回-1為異常,0為超時,>0為獲得訊息
if(nTcpInfo->ret < 0)
{
printf("network error in connect failed.%d",Error);
free(nTcpInfo);
return NULL;
}
else if(nTcpInfo->ret == 0)
{
printf("connect time out\n");
free(nTcpInfo);
return NULL;
}
else if (1 == nTcpInfo->ret)
{
if(FD_ISSET(nTcpInfo->sock,&nTcpInfo->set))//用於測試套接字是否在集合中
{
int timeout = 3000; //3s
nTcpInfo->flag = 0;
ioctlsocket (nTcpInfo->sock, FIONBIO, (unsigned long *) &nTcpInfo->flag);//設定套接字為阻塞
setsockopt(nTcpInfo->sock,SOL_SOCKET,SO_SNDTIMEO,(char*)&timeout,sizeof(timeout));//傳送時限
return nTcpInfo;
}
else
{
nTcpInfo->ret = -3;
printf("other error when select fail.%d",Error);
}
free(nTcpInfo);
return NULL;
}
}
return NULL;
}
如果想要完整原始碼可以點選這裡----完整非阻塞套接字原始碼