网络编程基础漫谈(三)之 select 函数重难点解析 甲篇

select函数是网络通信编程中非常常用的一个函数,因此应该熟练掌握它。虽然它是 BSD 标准之一的 Socket 函数之一,但在 Linux 和 Windows 平台,其行为表现还是有点区别的。我们先来看一下 Linux 平台上的 select 函数。

Linux 平台下的 select 函数

select函数的作用是检测一组 socket 中某个或某几个是否有“事件”,这里的“事件”一般分为如下三类:

可读事件,一般意味着可以调用recvread函数从该 socket 上读取数据;如果该 socket 是侦听socket(即调用了bind函数绑定过 ip 地址和端口号,并调用了listen启动侦听的 socket),可读意味着此时可以有新的客户端连接到来,此时可调用accept函数接受新连接。

可写事件,一般意味着此时调用sendwrite函数可以将数据“发出去”。

异常事件,某个 socket 出现异常。

函数签名如下:

参数说明:

参数nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数监听的 fd 中最大 fd 值加 1。

参数readfds,需要监听可读事件的 fd 集合。

参数writefds,需要监听可写事件的 fd 集合。

参数exceptfds,需要监听异常事件 fd 集合。

readfdswritefdsexceptfds类型都是fd_set,这是一个结构体信息,其定义位于/usr/include/sys/select.h中:

我们假设未定义宏__USE_XOPEN,将上面的代码整理一下:

将一个 fd 添加到 fd_set 这个集合中需要使用FD_SET宏,其定义如下:

其实现如下:

FD_SET在内部又是通过宏__FD_SET来实现的,__FD_SET的定义如下(位于/usr/include/bits/select.h中):

重点看这一行:

__FD_MASK__FD_ELT宏在上面的代码中已经给出定义:

__NFDBITS的值是648 * 8),也就是说__FD_MASK (d)先计算 fd 与 64 的余数 n,然后执行 1 __FD_ELT(d)就是计算位置索引值了。举个例子,假设现在 fd 的 值是 57,那么在这 64 个位置的 57 位,其值在 64 个长度的二进制中置位是:

这个值就是1

但是前面 fd 数组的定义是:

long int占 8 个字节,一个 16 个long int,如果换成二进制的位(bit)就是8 * 16=128, 也就是这个数组只用了低 64 位, 高 64 位并没有使用。这说明在我的机器上,select 函数支持操作的最大 fd 数量是 64。

同理,如果我们需要从 fd_set 上删除一个 fd,我们可以调用FD_CLR,其定义如下:

原理和FD_SET相同,即将对应的标志位由变为0即可。

如果,我们需要将 fd_set 中所有的 fd 都清掉,则使用宏FD_ZERO

当 select 函数返回时, 我们使用FD_ISSET宏来判断某个 fd 是否有我们关心的事件,FD_ISSET宏的定义如下:

FD_ISSET宏本质上就是检测对应的位置上是否置 1,实现如下:

提醒一下: __FD_ELT 和 __FD_MASK 宏前文的代码已经给过具体实现了。

参数timeout,超时时间,即在这个参数设定的时间内检测这些 fd 的事件,超过这个时间后select函数将立即返回。这是一个timeval类型结构体,其定义如下:

select函数的总超时时间是timeout->tv_sectimeout->tv_usec之和, 前者的时间单位是秒,后者的时间单位是微妙。

说了这么多理论知识,我们先看一个具体的示例:

我们编译并运行程序:

然后,我们再多开几个 shell 窗口,我们这里不再专门编写客户端程序了,我们使用 Linux 下的nc指令模拟出两个客户端。

shell 窗口1,连接成功以后发送字符串hello123

shell 窗口2,连接成功以后发送字符串helloworld

此时服务器端输出结果如下:

注意,由于nc发送的数据是按换行符来区分的,每一个数据包默认的换行符以\n结束(当然,你可以-C选项换成\r\n),所以服务器收到数据后,显示出来的数据每一行下面都有一个空白行。

当断开各个客户端连接时,服务器端 select 函数对各个客户端 fd 检测时,仍然会触发可读事件,此时对这些 fd 调用 recv 函数会返回(recv 函数返回0,表明对端关闭了连接,这是一个很重要的知识点,下文我们会有一章节专门介绍这些函数的返回值),服务器端也关闭这些连接就可以了。

客户端断开连接后,服务器端的运行输出结果:

以上代码是一个简单的服务器程序实现的基本流程,代码虽然简单,但是非常具有典型性和代表性,而且同样适用于客户端网络通信,如果用于客户端的话,只需要用 select 检测连接 socket 就可以了,如果连接 socket 有可读事件,调用 recv 函数来接收数据,剩下的逻辑都是一样的。上面的代码我们画一张流程图如下:

文章未完,有兴趣的读者可以继续阅读下一篇《网络编程基础漫谈(三)之 select 函数重难点解析 乙篇》。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181225G06MXW00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券