47-将多进程并发服务器改成 IO 复用

来源:互联网 发布:国家网络应急 编辑:程序博客网 时间:2024/05/19 23:13

只有趟过各种坑,解决了各种离奇古怪的网络编程异常,才能一步一步的提升,其次再学习新的技术,就不会感觉困惑。前面的程序,我们只是先拿客户端进行了开刀,将其修改成了 IO 多路复用模型,因为它最简单。后面我还看到,即使这样很简单,但是还有很多很多的坑等着我们去填,其中包括批量输入产生的问题,IO 缓冲与 IO 复用混合使用的问题。

在没有系统的学习前,你去趟网络这趟浑水,很可能一不小心你就犯了错误,而且这种隐秘的错误你根本发现不了,乍一看程序运行起来很正常。前辈们为我们总结了这些宝贵的经验,我们一定要汲取,不要等到工作了,写真正的项目代码的时候,再用血的教训来教会我们如何写出正确严谨的程序。

在搞定了客户端后,我们就把之前的多进程并发服务器改成单进程的 IO 复用模型的并发服务器。

1. 程序路径

代码托管在 gitos 上,请使用下面的命令获取:

git clone https://git.oschina.net/ivan_allen/unp.git

如果你已经 clone 过这个代码了,请使用 git pull 更新一下。本节程序所使用的程序路径是 unp/program/echo/multiplexing_select_server

2. 使用 select 修改服务器

2.1 修改思路

服务器的修改并没有客户端那么容易,但是也是是很复杂。我们知道服务器主要做两件事情:

  • 接收新的连接请求,主要由监听套接字来完成
  • 和已连接的客户端交互数据,主要由已连接套接字来完成(accept 函数返回的那个值)

因此,服务器要使用 select 同时管理监听套接已连接套接字

我们需要事先准备一个 fd_set 类型的读集合 rfds,该集合保存了所有的监听套接已连接套接字。另一方面,当 IO 事件产生时,我们需要挨个遍历 rfds 中的每个描述符,但是 rfds 并不像 C++ 中的 set 集合那么方便,它本身不提供任何一种方法来帮助我们遍历所有元素。因此,我们需要单独再使用一个数组 fds[1024] 来保存所有套接字。

2.2 伪代码

void server_routine() {   //...   // 全部初始化为 -1,表示该位置没有保存描述符   int fds[FD_SETSIZE] = { -1, /*...*/ };   fd_set rfds; // select 的参数   int maxfd, nready;   listen(listenfd);   fds[0] = listenfd;   while(1) {     // 根据数组 fds 创建一个 rfds,并返回值最大的描述符     maxfd = makefdset(fds, FD_SETSIZE, &rfds);     // nready 是发生 IO 事件的个数,伪代码不进行错误处理     nready = selece(maxfd + 1, &rfds, NULL, NULL, NULL);      if (listenfd in rfds) {// 监听套接字有事件发生       sockfd = accept(listenfd);       // 在 fds 中寻找一个空闲的位置(值为 -1 的地方),将已连接 sockfd 加进去       insert(fds, sockfd);       // 如果已发生事件个数减 1 后小于等于 0,说明 IO 事件已经处理完       if (--nready <= 0) continue;     }     // 处理已连接套接字上的 IO 事件     for (i = 0; i < FD_SETSIZE; ++i) {       if (fds[i] != -1 && fds[i] in rfds) {         ret = doServer(fds[i]);         // 如果 ret == 0,说明客户端主动关闭,将其从 fds 中移除         if (ret == 0) {           close(fds[i]);           fds[i] = -1;         }         // 如果 IO 事件处理完成,直接退出循环         if (--nready <= 0) break;       }     }   }}

即便是伪代码,也很长呐……

上面的代码有几点要说明一下:

第一是 FD_SETSIZE 这个宏,它是 fd_set 集合最大容量,默认大小是 1024,这意味着我们的服务器最多只能并发 1024 - 4 个连接,为什么减 4,因为要除去标准输入、标准输出、标准错误以及 listenfd 这 4 个。

第二是使用 nready 来避免无谓的循环,每次处理完一个 IO 事件,就让 nready 的值减 1,如果 nready 的值为 0 了,说明 IO 事件已经处理完,再进行循环已经没有意义。

3. 程序运行

  • flower 主机上启动服务器
flower $ ./echo -s -h flower
  • 在 sun 和 moon 上启动客户端
sun $ ./echo -h flower
moon $ ./echo -h flower
  • 运行结果


这里写图片描述
图1 运行结果

4. 总结

  • 掌握使用 IO 复用改写服务器的方法
0 0