网络通信基础学习笔记03:select 函数用法 select 函数用法 select 函数是网络通信编程中非常常用的一个函数,因此应该熟练掌握它。虽然它是 BSD 标准之一的 Socket 函数之一,但在 Linux 和 Windows 平台,其行为表现还是有点区别的。我们先来看一下 Linux 平台上的 select 函数。
Linux 平台下的 select 函数 select 函数的作用是检测一组 socket 中某个或某几个是否有“事件 ”就绪,这里的“事件 ”一般分为如下三类:
读事件就绪 :
socket 内核中,接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞的读该文件描述符, 并且返回值大于0; TCP 连接的对端关闭连接,此时调用 recv 或 read 函数对该 socket 读,则返回 0; 侦听 socket 上有新的连接请求; socket 上有未处理的错误。 写事件就绪:
socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0; socket 的写操作被关闭(调用了 close 或者 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号); socket 使⽤非阻塞 connect 连接成功或失败之后; 异常事件就绪
socket 上收到带外数据。
函数原型如下:
1 2 3 4 5 int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ;
参数说明:
参数 nfds , Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数监听的 fd 中最大 fd 值加 1。
参数 readfds ,需要监听可读事件的 fd 集合。
参数 writefds ,需要监听可写事件的 fd 集合。
参数 exceptfds ,需要监听异常事件 fd 集合。
readfds 、writefds 和 exceptfds 类型都是 fd_set ,这是一个结构体信息,其定义位于 /usr/include/sys/select.h 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef long int __fd_mask;#undef __NFDBITS #define __NFDBITS (8 * (int) sizeof (__fd_mask)) #define __FD_ELT(d) ((d) / __NFDBITS) #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) typedef struct { #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];# define __FDS_BITS(set) ((set)->fds_bits) #else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->__fds_bits) #endif } fd_set;#define FD_SETSIZE __FD_SETSIZE
我们假设未定义宏 __USE_XOPEN ,将上面的代码整理一下:
1 2 3 4 typedef struct { long int __fds_bits[16 ]; } fd_set;
将一个 fd 添加到 fd_set 这个集合中需要使用 FD_SET 宏,其定义如下:
1 void FD_SET (int fd, fd_set *set) ;
其实现如下:
1 #define FD_SET(fd,fdsetp) __FD_SET(fd,fdsetp)
FD_SET 在内部又是通过宏 __FD_SET 来实现的,**__FD_SET** 的定义如下(位于 /usr/include/bits/select.h 中):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #if defined __GNUC__ && __GNUC__ >= 2 # if __WORDSIZE == 64 # define __FD_ZERO_STOS "stosq" # else # define __FD_ZERO_STOS "stosl" # endif # define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory" ); \ } while (0) #else # define __FD_ZERO(set) \ do { \ unsigned int __i; \ fd_set *__arr = (set); \ for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \ __FDS_BITS (__arr)[__i] = 0; \ } while (0) #endif #define __FD_SET(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) #define __FD_CLR(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
重点看这一行:
1 ((void ) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
__FD_MASK 和 __FD_ELT 宏在上面的代码中已经给出定义:
1 2 #define __FD_ELT(d) ((d) / __NFDBITS) #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
__NFDBITS 的值是 64 (8 * 8 ),也就是说 **__FD_MASK (d) ** 先计算 fd 与 64 的余数 n,然后执行 1 << n,这一操作实际上是将 fd 的值放在 0~63 这 64 的位置上去,这个位置索引就是 fd 与 64 取模的结果;同理 __FD_ELT(d) 就是计算位置索引值了。举个例子,假设现在 fd 的 值是 57,那么在这 64 个位置的 57 位,其值在 64 个长度的二进制中置位是:
1 0000 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
这个值就是 **1 << (57 % 64) **得到的数字。
但是前面 fd 数组的定义是:
1 2 3 4 typedef struct { long int __fds_bits[16 ]; } fd_set;
long int 占 8 个字节,每个字节 8 bit,一共 16 个 long int ,如果换成二进制的位( bit )就是 8 * 8 * 16 = 1024 , 这说明在我的机器上,select 函数支持操作的最大 fd 数量是 1024。
同理,如果我们需要从 fd_set 上删除一个 fd,我们可以调用 FD_CLR ,其定义如下:
1 void FD_CLR (int fd, fd_set *set) ;
原理和 FD_SET 相同,即将对应标志清零即可。
如果,我们需要将 fd_set 中所有的 fd 都清掉,则使用宏 FD_ZERO :
1 void FD_ZERO (fd_set *set) ;
当 select 函数返回时, 我们使用 FD_ISSET 宏来判断某个 fd 是否有我们关心的事件,FD_ISSET 宏的定义如下:
1 int FD_ISSET (int fd, fd_set *set) ;
FD_ISSET 宏本质上就是检测对应的位置上是否置 1,实现如下:
1 2 #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
提醒一下: __FD_ELT 和 __FD_MASK 宏前文的代码已经给过具体实现了。
说了这么多理论知识,我们先看一个具体的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <sys/time.h> #include <vector> #include <errno.h> #define INVALID_FD -1 int main (int argc, char * argv[]) { int listenfd = socket (AF_INET, SOCK_STREAM, 0 ); if (listenfd == -1 ) { std::cout << "create listen socket error." << std::endl; return -1 ; } struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl (INADDR_ANY); bindaddr.sin_port = htons (3000 ); if (bind (listenfd, (struct sockaddr *)&bindaddr, sizeof (bindaddr)) == -1 ) { std::cout << "bind listen socket error." << std::endl; close (listenfd); return -1 ; } if (listen (listenfd, SOMAXCONN) == -1 ) { std::cout << "listen error." << std::endl; close (listenfd); return -1 ; } std::vector<int > clientfds; int maxfd = listenfd; while (true ) { fd_set readset; FD_ZERO (&readset); FD_SET (listenfd, &readset); int clientfdslength = clientfds.size (); for (int i = 0 ; i < clientfdslength; ++i) { if (clientfds[i] != INVALID_FD) { FD_SET (clientfds[i], &readset); } } timeval tm; tm.tv_sec = 1 ; tm.tv_usec = 0 ; int ret = select (maxfd + 1 , &readset, NULL , NULL , &tm); if (ret == -1 ) { if (errno != EINTR) break ; } else if (ret == 0 ) { continue ; } else { if (FD_ISSET (listenfd, &readset)) { struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof (clientaddr); int clientfd = accept (listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); if (clientfd == -1 ) { break ; } std:: cout << "accept a client connection, fd: " << clientfd << std::endl; clientfds.push_back (clientfd); if (clientfd > maxfd) maxfd = clientfd; } else { char recvbuf[64 ]; int clientfdslength = clientfds.size (); for (int i = 0 ; i < clientfdslength; ++i) { if (clientfds[i] != -1 && FD_ISSET (clientfds[i], &readset)) { memset (recvbuf, 0 , sizeof (recvbuf)); int length = recv (clientfds[i], recvbuf, 64 , 0 ); if (length <= 0 && errno != EINTR) { std::cout << "recv data error, clientfd: " << clientfds[i] << std::endl; close (clientfds[i]); clientfds[i] = INVALID_FD; continue ; } std::cout << "clientfd: " << clientfds[i] << ", recv data: " << recvbuf << std::endl; } } } } } int clientfdslength = clientfds.size (); for (int i = 0 ; i < clientfdslength; ++i) { if (clientfds[i] != INVALID_FD) { close (clientfds[i]); } } close (listenfd); return 0 ; }
我们编译并运行程序:
1 2 [root@localhost testsocket]# g++ -g -o select_server select_server.cpp [root@localhost testsocket]# ./select_server
然后,我们再多开几个 shell 窗口,我们这里不再专门编写客户端程序了,我们使用 Linux 下的 nc 指令模拟出两个客户端。
shell 窗口1,连接成功以后发送字符串 hello123 :
1 2 3 4 [root@localhost ~]# nc -v 127.0.0.1 3000 Ncat: Version 6.40 ( http://nmap.org/ncat ) Ncat: Connected to 127.0.0.1:3000. hello123
shell 窗口2,连接成功以后发送字符串 helloworld :
1 2 3 4 [root@localhost ~]# nc -v 127.0.0.1 3000 Ncat: Version 6.40 ( http://nmap.org/ncat ) Ncat: Connected to 127.0.0.1:3000. helloworld
此时服务器端输出结果如下:
注意,由于 nc 发送的数据是按换行符来区分的,每一个数据包默认的换行符以\n 结束(当然,你可以 -C 选项换成\r\n),所以服务器收到数据后,显示出来的数据每一行下面都有一个空白行。
当断开各个客户端连接时,服务器端 select 函数对各个客户端 fd 检测时,仍然会触发可读事件,此时对这些 fd 调用 recv 函数会返回 0 (recv 函数返回0,表明对端关闭了连接,这是一个很重要的知识点,下文我们会有一章节专门介绍这些函数的返回值),服务器端也关闭这些连接就可以了。
客户端断开连接后,服务器端的运行输出结果:
以上代码是一个简单的服务器程序实现的基本流程,代码虽然简单,但是非常具有典型性和代表性,而且同样适用于客户端网络通信,如果用于客户端的话,只需要用 select 检测连接 socket 就可以了,如果连接 socket 有可读事件,调用 recv 函数来接收数据,剩下的逻辑都是一样的。
关于上述代码在实际开发中有几个需要注意的事项,这里逐一来说明一下:
select 函数调用前后会修改 readfds、writefds 和 exceptfds 这三个集合中的内容(如果有的话),所以如果您想下次调用 select 复用这个变量,记得在下次调用前再次调用 select 前先使用 FD_ZERO 将集合清零,然后调用 FD_SET 将需要检测事件的 fd 再次添加进去 。
select 函数调用之后,readfds 、writefds 和 exceptfds 这三个集合中存放的不是我们之前设置进去的 fd,而是有相关有读写或异常事件的 fd,也就是说 select 函数会修改这三个参数的内容,这也要求我们当一个 fd_set 被 select 函数调用后,这个 fd_set 就已经发生了改变,下次如果我们需要使用它,必须使用 FD_ZERO 宏先清零,再重新将我们关心的 fd 设置进去 。这点我们从 FD_ISSET 源码也可以看出来:
1 2 #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
如果调用 select 函数之后没有改变 fd_set 集合,那么即使某个 socket 上没有事件,调用 select 函数之后我们用 **FD_ISSET** 检测,会原路得到原来设置上去的 socket。这是很多初学者在学习 select 函数容易犯的一个错误,我们通过一个示例来验证一下,这次我们把 select 函数用在客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <errno.h> #include <string.h> #define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 3000 int main (int argc, char * argv[]) { int clientfd = socket (AF_INET, SOCK_STREAM, 0 ); if (clientfd == -1 ) { std::cout << "create client socket error." << std::endl; return -1 ; } struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr (SERVER_ADDRESS); serveraddr.sin_port = htons (SERVER_PORT); if (connect (clientfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)) == -1 ) { std::cout << "connect socket error." << std::endl; close (clientfd); return -1 ; } fd_set readset; FD_ZERO (&readset); FD_SET (clientfd, &readset); timeval tm; tm.tv_sec = 5 ; tm.tv_usec = 0 ; int ret; int count = 0 ; fd_set backup_readset; memcpy (&backup_readset, &readset, sizeof (fd_set)); while (true ) { if (memcmp (&readset, &backup_readset, sizeof (fd_set)) == 0 ) { std::cout << "equal" << std::endl; } else { std::cout << "not equal" << std::endl; } ret = select (clientfd + 1 , &readset, NULL , NULL , &tm); std::cout << "tm.tv_sec: " << tm.tv_sec << ", tm.tv_usec: " << tm.tv_usec << std::endl; if (ret == -1 ) { if (errno != EINTR) break ; } else if (ret == 0 ){ std::cout << "no event in specific time interval, count:" << count << std::endl; ++count; continue ; } else { if (FD_ISSET (clientfd, &readset)) { char recvbuf[32 ]; memset (recvbuf, 0 , sizeof (recvbuf)); int n = recv (clientfd, recvbuf, 32 , 0 ); if (n < 0 ) { if (errno != EINTR) break ; } else if (n == 0 ) { break ; } else { std::cout << "recv data: " << recvbuf << std::endl; } } else { std::cout << "other socket event." << std::endl; } } } close (clientfd); return 0 ; }
在 shell 窗口输入以下命令编译程序产生可执行文件 select_client :
这次产生的是客户端程序,服务器程序我们这里使用 Linux nc 命令来模拟一下,由于客户端连接的是 127.0.0.1:3000 这个地址和端口号,所以我们在另外一个shell 窗口的 nc 命令的参数可以这么写:
执行效果如下:
接着我们启动客户端 select_client :
1 [root@myaliyun testsocket]# ./select_client
需要注意的是,这里我故意将客户端代码中 select 函数的超时时间设置为5秒,以足够我们在这 5 秒内给客户端发一个数据。如果我们在 5 秒内给客户端发送 hello 字符串:
客户端输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [root@myaliyun testsocket]# ./select_client equal recv data: hello ...部分数据省略... not equaltm .tv_sec: 0 , tm .tv_usec: 0 no event in specific time interval, count :31454 not equaltm .tv_sec: 0 , tm .tv_usec: 0 no event in specific time interval, count :31455 not equaltm .tv_sec: 0 , tm .tv_usec: 0 no event in specific time interval, count :31456 not equaltm .tv_sec: 0 , tm .tv_usec: 0 no event in specific time interval, count :31457 ...部分输出省略...
除了第一次 select_client 会输出 equal 字样,后面再也没输出,而 select 函数以后的执行结果也是超时,即使此时服务器端再次给客户端发送数据。因此验证了:select 函数执行后,确实会对三个参数的 fd_set 进行修改 。
因此在调用 select 函数以后, 原来位置的的标志位可能已经不复存在,这也就是为什么我们的代码中调用一次 select 函数以后,即使服务器端再次发送数据过来,select 函数也不会再因为存在可读事件而返回了,因为第二次 clientfd 已经不在那个 read_set 中了。因此如果复用这些 fd_set 变量,必须按上文所说的重新清零再重新添加关心的 socket 到集合中去。
select 函数也会修改 timeval 结构体的值,这也要求我们如果像复用这个变量,必须给 timeval 变量重新设置值。
注意观察上面的例子的输出,我们在调用 select 函数一次之后,变量 tv 的值也被修改了。具体修改成多少,得看系统的表现。当然这种特性却不是跨平台的,在 Linux 系统中是这样的,而在其他操作系统上却不一定是这样(Windows 上就不会修改这个结构体的值),这点在 Linux man 手册 select 函数的说明中说的很清楚:
1 On Linux, select () modifies timeout to reflect the amount of time not slept; most other implementations do not do this.(POSIX.1 -2001 permits either behavior.) This causes problems both when Linux code which reads timeout is ported to other operating systems, and when code is ported to Linux that reuses a struct timeval for multiple select ()s in a loop without reinitializing it. Consider timeout to be undefined after select () returns .
由于不同系统的实现不一样,man 手册的建议将 select 函数修改 timeval 结构体的值的行为当作是未定义的,言下之意是如果你要下次使用 select 函数复用这个变量时,记得重新赋值 。这是 select 函数需要注意的第二个地方。
select 函数的 timeval 结构体的 tv_sec 和 tv_sec 如果两个值设置为 0,即检测事件总时间设置为0,其行为是 select 会检测一下相关集合中的 fd,如果没有需要的事件,则立即返回 。
我们将上述 select_client.cpp 修改一下,修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <errno.h> #include <string.h> #define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 3000 int main (int argc, char * argv[]) { int clientfd = socket (AF_INET, SOCK_STREAM, 0 ); if (clientfd == -1 ) { std::cout << "create client socket error." << std::endl; return -1 ; } struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr (SERVER_ADDRESS); serveraddr.sin_port = htons (SERVER_PORT); if (connect (clientfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)) == -1 ) { std::cout << "connect socket error." << std::endl; close (clientfd); return -1 ; } int ret; while (true ) { fd_set readset; FD_ZERO (&readset); FD_SET (clientfd, &readset); timeval tm; tm.tv_sec = 0 ; tm.tv_usec = 0 ; ret = select (clientfd + 1 , &readset, NULL , NULL , &tm); std::cout << "tm.tv_sec: " << tm.tv_sec << ", tm.tv_usec: " << tm.tv_usec << std::endl; if (ret == -1 ) { if (errno != EINTR) break ; } else if (ret == 0 ){ std::cout << "no event in specific time interval." << std::endl; continue ; } else { if (FD_ISSET (clientfd, &readset)) { char recvbuf[32 ]; memset (recvbuf, 0 , sizeof (recvbuf)); int n = recv (clientfd, recvbuf, 32 , 0 ); if (n < 0 ) { if (errno != EINTR) break ; } else if (n == 0 ) { break ; } else { std::cout << "recv data: " << recvbuf << std::endl; } } else { std::cout << "other socket event." << std::endl; } } } close (clientfd); return 0 ; }
执行结果确实如我们预期的,这里 select 函数只是简单地检测一下 clientfd,并不会等待固定的时间,然后立即返回。
如果将 select 函数的 timeval 参数设置为 NULL,则 select 函数会一直阻塞下去,直到我们需要的事件触发。
我们将上述代码再修改一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <errno.h> #include <string.h> #define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 3000 int main (int argc, char * argv[]) { int clientfd = socket (AF_INET, SOCK_STREAM, 0 ); if (clientfd == -1 ) { std::cout << "create client socket error." << std::endl; return -1 ; } struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr (SERVER_ADDRESS); serveraddr.sin_port = htons (SERVER_PORT); if (connect (clientfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)) == -1 ) { std::cout << "connect socket error." << std::endl; close (clientfd); return -1 ; } int ret; while (true ) { fd_set readset; FD_ZERO (&readset); FD_SET (clientfd, &readset); ret = select (clientfd + 1 , &readset, NULL , NULL , NULL ); if (ret == -1 ) { if (errno != EINTR) break ; } else if (ret == 0 ){ std::cout << "no event in specific time interval." << std::endl; continue ; } else { if (FD_ISSET (clientfd, &readset)) { char recvbuf[32 ]; memset (recvbuf, 0 , sizeof (recvbuf)); int n = recv (clientfd, recvbuf, 32 , 0 ); if (n < 0 ) { if (errno != EINTR) break ; } else if (n == 0 ) { break ; } else { std::cout << "recv data: " << recvbuf << std::endl; } } else { std::cout << "other socket event." << std::endl; } } } close (clientfd); return 0 ; }
我们先在另外一个 shell 窗口用 nc 命令模拟一个服务器,监听的 ip 地址和端口号是 0.0.0.0:3000 :
1 2 3 [root@myaliyun ~]# nc -v -l 0.0.0.0 3000 Ncat: Version 6.40 ( http://nmap.org /ncat )Ncat: Listening on 0.0 .0 .0 :3000
然后回到原来的 shell 窗口,编译上述 select_client_tvnull.cpp ,并使用 gdb 运行程序,这次使用 gdb 运行程序的目的是为了当程序“卡”在某个位置时,我们可以使用 Ctrl + C 把程序中断下来看看程序阻塞在哪个函数调用处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [root@myaliyun testsocket] [root@myaliyun testsocket] Reading symbols from /root/testsocket/select_client_tvnull...done. (gdb) r Starting program: /root/testsocket/select_client_tvnull ^C Program received signal SIGINT, Interrupt. 0x00007ffff72e7783 in __select_nocancel () from /lib64/libc.so.6 Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-16.el7_4.1.x86_64 libstdc++-4.8.5-16.el7_4.1.x86_64 (gdb) bt (gdb) c Continuing. recv data: hello ^C Program received signal SIGINT, Interrupt. 0x00007ffff72e7783 in __select_nocancel () from /lib64/libc.so.6 (gdb) c Continuing. recv data: world
如上输出结果所示,我们使用 gdb 的 r 命令(run)将程序跑起来后,程序卡在某个地方,我们按 Ctrl + C
(代码中的 ^C )中断程序后使用 bt 命令查看当前程序的调用堆栈,发现确实阻塞在 select 函数调用处;接着我们在服务器端给客户端发送一个 hello 数据:
1 2 3 4 5 6 [root@myaliyun ~]# nc -v -l 0.0.0.0 3000 Ncat: Version 6.40 ( http://nmap.org /ncat )Ncat: Listening on 0.0 .0 .0 :3000 Ncat: Connection from 127.0 .0 .1 .Ncat: Connection from 127.0 .0 .1 :55968. hello
客户端收到数据后,select 函数满足条件,立即返回,并将数据输出来后继续进行下一轮 select 检测,我们使用 Ctrl + C 将程序中断,发现程序又阻塞在 select 调用处;输入 c 命令(continue)让程序继续运行, 此时,我们再用服务器端给客户端发送 world 字符串,select 函数再次返回,并将数据打印出来,然后继续进入下一轮 select 检测,并继续在 select 处阻塞。
1 2 3 4 5 6 7 [root@myaliyun ~]# nc -v -l 0.0.0.0 3000 Ncat: Version 6.40 ( http://nmap.org /ncat )Ncat: Listening on 0.0 .0 .0 :3000 Ncat: Connection from 127.0 .0 .1 .Ncat: Connection from 127.0 .0 .1 :55968. hello world
在 Linux 平台上,select 函数的第一个参数必须设置成需要检测事件的所有 fd 中的最大值加 1 。所以上文中 select_server.cpp 中,每新产生一个 clientfd,我都会与当前最大的 maxfd 作比较,如果大于当前的 maxfd 则将 maxfd 更新成这个新的最大值。其最终目的是为了在 select 调用时作为第一个参数(加 1)传进去。在 Windows 平台上,select 函数的第一个值传任意值都可以,Windows 系统本身不使用这个值,只是为了兼容性而保留了这个参数,但是在实际开发中为了兼容跨平台代码,也会按惯例,将这个值设置为最大 socket 加 1。这点请读者注意。
以上是我总结的 Linux 下 select 使用的五个注意事项 ,希望读者能理解它们。
Linux select 函数的缺点也是显而易见的:
每次调用 select 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大; 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下; select 函数在每次调用之前都要对传入参数进行重新设定,这样做比较麻烦而且会降低性能。在 Linux 平台上,select 函数的实现是利用 poll 函数的,有兴趣的读者可以查找一下相关的资料来阅读一下。关于 poll 函数的使用,接下来我们会介绍。
Windows 平台上 select 函数不会修改 timeval 的值 上文提到,在 Windows 系统上,select 函数结束后,不会修改其参数 timeval 的值。我们可以使用下面这段代码来验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 bool Connect (const char * pServer, short nPort) { SOCKET hSocket = ::socket (AF_INET, SOCK_STREAM, 0 ); if (hSocket == INVALID_SOCKET) return false ; unsigned long on = 1 ; if (::ioctlsocket (hSocket, FIONBIO, &on) == SOCKET_ERROR) return false ; struct sockaddr_in addrSrv = { 0 }; struct hostent * pHostent = NULL ; unsigned int addr = 0 ; if ((addrSrv.sin_addr.s_addr = inet_addr (pServer) == INADDR_NONE) { pHostent = ::gethostbyname (pServer); if (!pHostent) return false ; else addrSrv.sin_addr.s_addr = *((unsigned long *)pHostent->h_addr); } addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons ((nPort); int ret = ::connect (hSocket, (struct sockaddr*)&addrSrv, sizeof (addrSrv)); if (ret == 0 ) return true ; if (ret == SOCKET_ERROR && WSAGetLastError () != WSAEWOULDBLOCK) return false ; fd_set writeset; FD_ZERO (&writeset); FD_SET (hSocket, &writeset); struct timeval tm = { 3 , 200 }; if (::select (hSocket + 1 , NULL , &writeset, NULL , &tm) != 1 ) { printf ("tm.tv_sec: %d, tm.tv_usec: %d\n" , tm.tv_sec, tm.tv_usec); return false ; } printf ("tm.tv_sec: %d, tm.tv_usec: %d\n" , tm.tv_sec, tm.tv_usec); return true ; }
上述代码中,38 行调用了 select 函数,无论 select 是成功还是出错,我们都会打印出其参数的 tm 的值(40 和 44 行),经测试验证 tm 结构体的两个成员值在 select 函数调用前后并没有发生改变。
虽然 Windows 系统并不会改变 select 的超时时间参数的值,但是为了代码的跨平台性,我们在实际开发中不应该依赖这种特性,而是每次调用 select 函数前都重新给超时时间参数重新设置值。