天天看点

玩转重要的select函数并分析其行为

       说明:  尽管select函数在Windows和Linux上的用法有些差异, 且这些差异值得我们特别注意, 但从功能上来讲, 他们还是差不多的。 本文, 我们仅仅考虑Windows上的select函数。

      关于select函数的原型和用途, 百度和谷歌的介绍到处都是, 在本文中, 我就不赘述了, 我们仅仅来玩代码并作简要分析。 如果有不对或者偏颇的地方, 大家可以各抒己见, 共同进步,我也必定会认真核实后给予回应

玩转重要的select函数并分析其行为

, 也建议大家多多实践。

       程序1, 服务端程序:

#include <stdio.h>
#include <winsock2.h> // winsock接口
#pragma comment(lib, "ws2_32.lib") // winsock实现

int main()
{
	WORD wVersionRequested;  // 双字节,winsock库的版本
	WSADATA wsaData;         // winsock库版本的相关信息
	
	wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
	
	// 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
	WSAStartup( wVersionRequested, &wsaData );

	// AF_INET 表示采用TCP/IP协议族
	// SOCK_STREAM 表示采用TCP协议
	// 0是通常的默认情况
	unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;

	addrSrv.sin_family = AF_INET; // TCP/IP协议族
	addrSrv.sin_addr.S_un.S_addr = INADDR_ANY; 
	addrSrv.sin_port = htons(8888); // socket对应的端口

	// 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
	bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

	// 将socket设置为监听模式,5表示等待连接队列的最大长度
	listen(sockSrv, 5);

	SOCKADDR_IN addrClient;  
    int len = sizeof(SOCKADDR);  

	unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);

	while(1)
	{
		getchar();
		char sendBuf[100] = "hello";
		send(sockConn, sendBuf, strlen(sendBuf) + 1, 0); // 发送数据到客户端,最后一个参数一般设置为0
	}

	closesocket(sockConn);	
	closesocket(sockSrv);
	WSACleanup();

	
	return 0;
}
           

        程序2, 客户端程序:

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	wVersionRequested = MAKEWORD(1, 1);
	
	WSAStartup( wVersionRequested, &wsaData );

	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	
	int ret = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
	
	fd_set read_set;
	struct timeval t;
	FD_ZERO(&read_set); 
	FD_SET(sockClient, &read_set); 
	t.tv_sec = 20; 
	t.tv_usec = 0;

	while(1)
	{
		ret = select(-1, &read_set, NULL, NULL, &t);
		printf("ret is %d\n", ret);
		Sleep(1000);
	}

	closesocket(sockClient);
	WSACleanup();

	return 0;
}
           

       我们先开启服务端程序1, 然后运行客户端程序2, 然后不要动服务端和客户端, 静静等待, 等20s后, 发现程序2的结果是:

ret is 0

ret is -1

ret is -1

ret is -1

ret is -1

...........

       可以看到, 20s后, select函数超时, 返回0. 为什么呢? 因为select函数检测到sockClient对应的内核缓冲区没有数据可读, 以超时形式返回。

       好, 我们重新启动程序1对应的服务端, 然后重新启动程序2对应的客户端, 此时(不用等20s), 我们在服务端上按一下Enter键, 向客户端发送"hello"(包括最后的'\0').  然后, 我们看一下程序2的结果:

ret is 1

ret is 1

ret is 1

ret is 1

ret is 1

ret is 1

ret is 1

ret is 1

ret is 1

......

      上面的打印直到“永远”。为什么呢? 因为select函数检测到sockClient对应的内核缓冲区有数据可读(是一直有), 返回1. 

       好, 我们稍微修改一下程序2, 形成如下的程序3:

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	wVersionRequested = MAKEWORD(1, 1);
	
	WSAStartup( wVersionRequested, &wsaData );

	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	
	int ret = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
	
	fd_set read_set;
	struct timeval t;
	FD_ZERO(&read_set); 
	FD_SET(sockClient, &read_set); 
	t.tv_sec = 20; 
	t.tv_usec = 0;

	while(1)
	{
		printf("xxx\n");

		ret = select(-1, &read_set, NULL, NULL, &t);
		printf("ret is %d\n", ret);

		printf("yyy\n");

		char recvBuf[100] = {0};  
        recv(sockClient, recvBuf, 100, 0);  
        printf("%s\n", recvBuf); 

		Sleep(1000);
		printf("zzz\n");
	}

	closesocket(sockClient);
	WSACleanup();

	return 0;
}
           

       我们重新启动程序1对应的服务端, 然后重新启动程序3对应的客户端, 此时(不用等20s)我们在服务端上按一下Enter键, 向客户端发送"hello"(包括最后的'\0').  然后, 我们看一下程序3的结果, 程序会立即输出:

xxx

ret is 1

yyy

hello

zzz

xxx

     然后, 过20s,  程序结果为:

xxx

ret is 1

yyy

hello

zzz

xxx

ret is 0

yyy

         然后, 就一直阻塞在此。 我们来分析一下, 第一次进入while的时候, 服务端发送数据过来, 客户端的select函数检测到sockClient对应的内核缓冲区有数据可读, 于是立即返回, 所以有对应的结果。 当程序第二次进入while后, 客户端的select没有感知到sockClient对应的内核缓冲区没有数据可读(因为已经读取了), 故以超时返回, 于是有了对应的结果。最后结果一直如此, 是因为阻塞在recv处。

       我们继续来做有趣的实验, 我们把程序3中的recv函数稍微改一下, 形成程序4:

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	wVersionRequested = MAKEWORD(1, 1);
	
	WSAStartup( wVersionRequested, &wsaData );

	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	
	int ret = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
	
	fd_set read_set;
	struct timeval t;
	FD_ZERO(&read_set); 
	FD_SET(sockClient, &read_set); 
	t.tv_sec = 20; 
	t.tv_usec = 0;

	while(1)
	{
		printf("xxx\n");

		ret = select(-1, &read_set, NULL, NULL, &t);
		printf("ret is %d\n", ret);

		printf("yyy\n");

		char recvBuf[100] = {0};  
        recv(sockClient, recvBuf, 100, MSG_PEEK);  
        printf("%s\n", recvBuf); 

		Sleep(1000);
		printf("zzz\n");
	}

	closesocket(sockClient);
	WSACleanup();

	return 0;
}
           

        好, 我们以程序1做服务端, 以程序4做客户端。 进行如上类似的实验, 让服务端向客户端端发送"hello", 此时, 程序4一直在循环不停地打印如下信息, 直到“永远”:

xxx

ret is 1

yyy

hello

zzz

       我们来分析一下, 程序4的结果和程序3为什么不同。 之前说过了, MSG_PEEK值从内核缓冲区中偷窥一下信息, 并没有偷取, 也就是说, 是复制而不是剪切, 换句话说, 也就是sockClient对应的内核缓冲区一直数据可读,内核缓冲区中的"hello"还在那里, 不增不减。 因此, select函数每次都能监测到可读, 因此, 立即返回1. select函数还是真的有点意思哈。

       不要停止, 我们继续看。 现在, 我们稍微修改一下程序3中的recv函数, 形成程序5:

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	wVersionRequested = MAKEWORD(1, 1);
	
	WSAStartup( wVersionRequested, &wsaData );

	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	
	int ret = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
	
	fd_set read_set;
	struct timeval t;
	FD_ZERO(&read_set); 
	FD_SET(sockClient, &read_set); 
	t.tv_sec = 20; 
	t.tv_usec = 0;

	while(1)
	{
		printf("xxx\n");

		ret = select(-1, &read_set, NULL, NULL, &t);
		printf("ret is %d\n", ret);

		printf("yyy\n");

		char recvBuf[100] = {0};  
        recv(sockClient, recvBuf, 1, 0);  
        printf("%s\n", recvBuf); 

		Sleep(1000);
		printf("zzz\n");
	}

	closesocket(sockClient);
	WSACleanup();

	return 0;
}
           

         好, 我们以程序1做服务端, 以程序5做客户端。 进行如上类似的实验, 让服务端向客户端发送"hello", 此时, 程序5的结果如下:

xxx

ret is 1

yyy

h

zzz

xxx

ret is 1

yyy

e

zzz

xxx

ret is 1

yyy

l

zzz

xxx

ret is 1

yyy

l

zzz

xxx

ret is 1

yyy

o

zzz

xxx

ret is 1

yyy

zzz

xxx

         然后再等20s, 结果如下:

xxx

ret is 1

yyy

h

zzz

xxx

ret is 1

yyy

e

zzz

xxx

ret is 1

yyy

l

zzz

xxx

ret is 1

yyy

l

zzz

xxx

ret is 1

yyy

o

zzz

xxx

ret is 1

yyy

zzz

xxx

ret is 0

yyy

        然后, 结果就一直这样了。 为什么是这种现象呢?  我们看到,"hello"这个串(包括最后的'\0')中的6个字符被不断取出, 此时, 在取出之前, select函数进行6次检测, 6次都发现有数据可读, 所以6次都立即返回。 等把数据读后, 发现没数据可读了, 于是不会立即返回, 而是以超时形式进行返回。 最后一直阻塞在recv处。 妙哉妙哉。

       以上只介绍了客户端select的读特性, 以后, 我们肯定还会与select函数见面的, 今天先到此为止。 最后欢迎大家提出不同意见, 共同进步

玩转重要的select函数并分析其行为

继续阅读