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

972 篇文章 329 订阅
147 篇文章 46 订阅

       说明:  尽管select函数在Windows和Linux上的用法有些差异, 且这些差异值得我们特别注意, 但从功能上来讲, 他们还是差不多的。 本文, 我们仅仅考虑Windows上的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函数见面的, 今天先到此为止。 最后欢迎大家提出不同意见, 共同进步微笑


 

 


 

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值