服务器模式:多路复用

DinS          Written on 2017/9/15

本文介绍服务器模式之多路复用(multiplexing),确保阅读过《socket概述》。

多路复用模型是针对多线程模型的一种改进尝试。既然开辟过多的线程不太好,那么就不使用线程的方式来处理多个客户端。那为之奈何?使用一个关键函数select,在linux下类似的还有poll和epoll。

select的作用是什么呢?它可以监视一组socket,一旦有socket准备好了,即可以accept或者recv,就会告知程序。既然socket已经就绪,那么调用accept或者recv就不会阻塞了,这样就解决了阻塞和程序执行的矛盾。select本身会阻塞程序,但是可以给其设置一个阻塞时限,如果这个时限设置为0,就相当于非阻塞了。

强烈推荐这篇科普文章https://www.zhihu.com/question/32163005,介绍了多路复用(Multiplexing)的翻译导致的理解问题,以及三个关键函数的发展历史和性能。

图片来源:http://blog.csdn.net/hguisu/article/details/7453390

使用select需要一些技巧,肯定要比多线程模式复杂,而且其中涉及处理一组socket的操作有些费解,下面结合实际代码进行讲解。
参考了http://blog.csdn.net/feier7501/article/details/10858357。

(点击这里看大图)

到listen都一样。

为了使用select,我们需要准备socket集合,这里准备了两套,一套用于recv一套用于send。集合是一个称为fd_set的结构,内部有两个值:一个记录个数一个是装socket的数组。数组默认是64个,可以增加,不过据说最大1024。

对集合的操作全部用宏,视作API即可。
用于接收信息的结合中需要把监听socket放入,用于后面accept。

很显然比多线程复杂的多,具体的注意事项已经在注释中说明,下面说一下整体思路。
最明显的依然是不断while,在while中select,根据select返回值做不同操作。在进入while前,我们准备了两组socket集合,一个用来接收信息一个用来发送信息,最费解的估计是每次while都需要备份socket集合,这是因为select函数会改变传入集合的内容,变成就绪集合。
当select成功后,我们就得到了两个集合:一个是总集合,另一个是就绪集合,接下来当然是对就绪集合操作,我们使用的是遍历总集合,并确认每一个socket是否在就绪集合中,如果在,就accept或者recv。
因为必然已经就绪,所以不会阻塞进程。对于新得到的socket,加入总集合,如果recv没内容,就关闭对应socket并移除出总集合。
发送信息的话逻辑更简单,不过多解释了。

客户端使用了阻塞+多线程的代码
运行结果如下:

大图点这里

因为客户端是在同一台机器上运行的,所以IP地址一样。
第三个客户端故意隔了一段时间才打开,所以可以看到中间有一个timeout输出,然后在1和2号客户端待机的同时,3号客户端说话,服务器可以响应。
这证明我们的设计成功了。

小结:

多路复用模型是一个很常用的socket设计模型。

优点:
虽然是阻塞操作,并且都在主线程完成,但是程序运行和监听并不会被阻塞。由此避免了开辟过多线程带来的隐患。可以同时实现对多个socket的监听,能处理大并发情况。

缺点:
select的socket的数量存在上限,如果是1万+那么肯定不行。 效率较低,一是每一次while都需要拷贝socket集合,二是需要遍历总集合才能找到就绪socket。

不过多路复用的缺点已经被克服了,linux上提供了epoll函数,这个函数是select的升级版,使用这个不需要制作副本,也不需要遍历,并且socket没有最大上限。遗憾的是这个epoll只在linux上实现了。Linux做服务器是很自然的事情吧。
这种多路复用模型是一个实用的解决方法。