天天看點

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

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

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