逐步实现TCP服务端Step04-10:小结

整个Step04阶段所做的工作是让后台系统能支持多客户并发。我们的基本设定是,负责网络通信的netio进程,以及负责业务处理的s进程都是单线程结构。要在这个大前提下对系统进行改造。

首先,不论何种结构,总要对多个客户的信息进行维护。对于netio来说,就是围绕socket的一些数据。我们设定了SocketInfo结构去整合它们,每个客户对应一个SocketInfo结构,用一个list去管理。对于s来说,每一个cilent,就是业务的“用户”,这些用户分别有自己的User对象。我们假设了client的socket值,就是它在业务层面的用户ID,这样,s的每个User对象就有了一个用户ID,基于这个ID可以hash到User对象。同时,User对象中会记录client的SocketInfo结构在list中的索引值,这么一来,就能将netio维护的SocketInfo与s所维护的User对象对应起来。

再说netio对list的处理。最直接的办法,就是无休止地遍历list,对每个socket尝试I/O操作。这么做当然可以实现多用户并发,但是需要做大量的无用功。如果能确切地知道哪些socket有I/O需求的话,就能免去何种盲目地轮询操作。

I/O多路复用机制提供了这种服务,基于它可以一次性查询出当前就绪的文件描述符。我们在netio中依次使用了selectpollepoll机制。最后,尝试了一种常与epoll放在一起讨论的IOCP,它是Windows平台提供的高性能模型。其并非I/O多路复用机制,它是对异步I/O完成结果通知方式的改良,基于它可以方便地实现,用多线程处理异步结果通知。

现阶段我们只是实现了多用户并发处理,还谈不上高性能。今后要通过测试,不断地优化改进。不进行性能测试,单从代码层面看,目前存在这么几个问题:

首先,不论使用selectpollepoll这些多路复用机制,还是重叠(异步)I/O+IOCP的机制,我们都未处理accept操作。accept独立于这些机制之外,在循环中无休止地被调用,而不论当前是不是有客户连进来。这就让accept沦为了一个I/O状态的查询者,就像一开始遍历list时,不停地调用recv一样。这是要改进地方。

其次,selectpollepoll这三种I/O多路复用,其实是“同步”的事件通知机制。说它们是事件通知机制,因为我们用它们去监测I/O事件的发生;说“同步”,因为我们是主动调用接口去向内核查询当前发生了哪些I/O事件。这不像Windows平台的WSAAsyncSelect机制,其利用Windows系统的消息驱动机制去通知相关事件。同步操作,不是将任务完全交给内核去办,让内核主动通知完成结果,而是适时地通过系统调用向内核“查询”办事结果。若使用异步事件通知,则会免去多余的系统调用。

最后,说一下I/O操作。目前的I/O操作均为非阻塞式,使用非阻塞I/O是单线程处理并发的必然选择。其实并非出于效率考虑,因为若不使用非阻塞,连基本的并发功能都无法实现。之前已经说过,非阻塞I/O实际是一种同步I/O方式,与阻塞I/O相比它只不过是在I/O未准备就绪的时候不阻塞罢了。在I/O准备就绪的情况下,调用非阻塞I/O,就会发生“实际”(从进程的角度看,I/O调用产生了实际效果)的I/O,如:把内核缓冲中的数据拷贝到进程空间。这一步操作,阻塞和非阻塞都是一样的。进程主动调用系统接口以完成最终的I/O操作,即:数据拷贝,这种同步的做法需要进程花时间去等待拷贝完成,这个时间从cpu的角度看,其实不短。以后可以改用异步I/O的方式以提高I/O效率。

改用异步I/O的方式,还可以免去对I/O事件检测工作,因为I/O工作已完全交给内核去做,进程只管发出I/O指令即可,根本不用管目前的I/O是否就绪。

总结

经过上面的讨论,对netio工作模式的完整描述应该是:

  • 单进程单线程 + 同步的就绪事件通知(selectpollepoll)+ 同步I/O

  • 单进程单线程 + 异步I/O + 同步的完成事件通知(IOCP

  • 单进程多线程 + 异步I/O + 同步的完成事件通知(IOCP

第一个模式,由于使用同步I/O,线程需知道哪些I/O就绪了,因此要使用就绪事件通知机制。第二个模式,使用异步I/O(Windows平台的重叠I/O),不需要关注I/O是否就绪,但需要内核通报I/O完成的结果,线程基于IOCP去查询完成结果,这是同步的方式。第三个模式,多个线程基于IOCP去查询完成结果,这是IOCP的优势所在,不过目前netio只实现了单线程操作。


<==  index  ==>