玩转重要的select函数并分析其行为
来源:程序员人生 发布时间:2015-03-04 08:49:08 阅读次数:7456次
说明: 虽然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); // 发送数据到客户端,最后1个参数1般设置为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(⑴, &read_set, NULL, NULL, &t);
printf("ret is %d
", ret);
Sleep(1000);
}
closesocket(sockClient);
WSACleanup();
return 0;
}
我们先开启服务端程序1, 然后运行客户端程序2, 然后
不要动服务端和客户端, 静静等待, 等20s后, 发现程序2的结果是:
ret is 0
ret is ⑴
ret is ⑴
ret is ⑴
ret is ⑴
...........
可以看到, 20s后, select函数超时, 返回0. 为何呢? 由于select函数检测到sockClient对应的内核缓冲区没有数据可读, 以超时情势返回。
好, 我们重新启动程序1对应的服务端, 然后重新启动程序2对应的客户端, 此时(不用等20s), 我们在服务端上按1下Enter键, 向客户端发送"hello"(包括最后的' '). 然后, 我们看1下程序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直有), 返回1.
好, 我们略微修改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
");
ret = select(⑴, &read_set, NULL, NULL, &t);
printf("ret is %d
", ret);
printf("yyy
");
char recvBuf[100] = {0};
recv(sockClient, recvBuf, 100, 0);
printf("%s
", recvBuf);
Sleep(1000);
printf("zzz
");
}
closesocket(sockClient);
WSACleanup();
return 0;
}
我们重新启动程序1对应的服务端, 然后重新启动程序3对应的客户端, 此时(不用等20s)我们在服务端上按1下Enter键, 向客户端发送"hello"(包括最后的' '). 然后, 我们看1下程序3的结果, 程序会立即输出:
xxx
ret is 1
yyy
hello
zzz
xxx
然后, 过20s, 程序结果为:
xxx
ret is 1
yyy
hello
zzz
xxx
ret is 0
yyy
然后, 就1直阻塞在此。 我们来分析1下, 第1次进入while的时候, 服务端发送数据过来, 客户真个select函数检测到sockClient对应的内核缓冲区有数据可读, 因而立即返回, 所以有对应的结果。 当程序第2次进入while后, 客户真个select没有感知到sockClient对应的内核缓冲区没有数据可读(由于已读取了), 故以超时返回, 因而有了对应的结果。最后结果1直如此, 是由于阻塞在recv处。
我们继续来做有趣的实验, 我们把程序3中的recv函数略微改1下, 构成程序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
");
ret = select(⑴, &read_set, NULL, NULL, &t);
printf("ret is %d
", ret);
printf("yyy
");
char recvBuf[100] = {0};
recv(sockClient, recvBuf, 100, MSG_PEEK);
printf("%s
", recvBuf);
Sleep(1000);
printf("zzz
");
}
closesocket(sockClient);
WSACleanup();
return 0;
}
好, 我们以程序1做服务端, 以程序4做客户端。 进行如上类似的实验, 让服务端向客户端端发送"hello", 此时, 程序41直在
循环不停地打印以下信息, 直到“永久”:
xxx
ret is 1
yyy
hello
zzz
我们来分析1下, 程序4的结果和程序3为何不同。 之前说过了, MSG_PEEK值从内核缓冲区中偷窥1下信息, 并没有偷取, 也就是说, 是复制而不是剪切, 换句话说, 也就是sockClient对应的内核缓冲区1直数据可读,内核缓冲区中的"hello"还在那里, 不增不减。 因此, select函数每次都能监测到可读, 因此, 立即返回1. select函数还是真的有点意思哈。
不要停止, 我们继续看。 现在, 我们略微修改1下程序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
");
ret = select(⑴, &read_set, NULL, NULL, &t);
printf("ret is %d
", ret);
printf("yyy
");
char recvBuf[100] = {0};
recv(sockClient, recvBuf, 1, 0);
printf("%s
", recvBuf);
Sleep(1000);
printf("zzz
");
}
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
然后, 结果就1直这样了。 为何是这类现象呢? 我们看到,"hello"这个串(包括最后的' ')中的6个字符被不断取出, 此时, 在取出之前, select函数进行6次检测, 6次都发现有数据可读, 所以6次都立即返回。 等把数据读后, 发现没数据可读了, 因而不会立即返回, 而是以超时情势进行返回。 最后1直阻塞在recv处。 妙哉妙哉。
以上只介绍了客户端select的读特性, 以后, 我们肯定还会与select函数见面的, 今天先到此为止。 最后欢迎大家提出不同意见, 共同进步
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠