1. 程式人生 > >選擇模型--非阻塞套接字詳解

選擇模型--非阻塞套接字詳解

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;
}

如果想要完整原始碼可以點選這裡----完整非阻塞套接字原始碼