5种网络IO模型(有图,很清楚)( 二 )


5种网络IO模型(有图,很清楚)文章插图
图3 多线程的服务器模型
在上述的线程 / 时间图例中 , 主线程持续等待客户端的连接请求 , 如果有连接 , 则创建新线程 , 并在新线程中提供为前例同样的问答服务 。
很多初学者可能不明白为何一个socket可以accept多次 。 实际上socket的设计者可能特意为多客户机的情况留下了伏笔 , 让accept()能够返回一个新的socket 。 下面是 accept 接口的原型:
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
输入参数s是从socket() , bind()和listen()中沿用下来的socket句柄值 。 执行完bind()和listen()后 , 操作系统已经开始在指定的端口处监听所有的连接请求 , 如果有请求 , 则将该连接请求加入请求队列 。 调用accept()接口正是从 socket s 的请求队列抽取第一个连接信息 , 创建一个与s同类的新的socket返回句柄 。 新的socket句柄即是后续read()和recv()的输入参数 。 如果请求队列当前没有请求 , 则accept() 将进入阻塞状态直到有请求进入队列 。
上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求 , 但其实并不尽然 。 如果要同时响应成百上千路的连接请求 , 则无论多线程还是多进程都会严重占据系统资源 , 降低系统对外界响应效率 , 而线程与进程本身也更容易进入假死状态 。
很多程序员可能会考虑使用“线程池”或“连接池” 。 “线程池”旨在减少创建和销毁线程的频率 , 其维持一定合理数量的线程 , 并让空闲的线程重新承担新的执行任务 。 “连接池”维持连接的缓存池 , 尽量重用已有的连接、减少创建和关闭连接的频率 。 这两种技术都可以很好的降低系统开销 , 都被广泛应用很多大型系统 , 如websphere、tomcat和各种数据库等 。 但是 , “线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用 。 而且 , 所谓“池”始终有其上限 , 当请求大大超过上限时 , “池”构成的系统对外界的响应并不比没有池的时候效果好多少 。 所以使用“池”必须考虑其面临的响应规模 , 并根据响应规模调整“池”的大小 。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求 , “线程池”或“连接池”或许可以缓解部分压力 , 但是不能解决所有问题 。 总之 , 多线程模型可以方便高效的解决小规模的服务请求 , 但面对大规模的服务请求 , 多线程模型也会遇到瓶颈 , 可以用非阻塞接口来尝试解决这个问题 。
2、非阻塞IO(non-blocking IO) Linux下 , 可以通过设置socket使其变为non-blocking 。 当对一个non-blocking socket执行读操作时 , 流程是这个样子:
5种网络IO模型(有图,很清楚)文章插图
图4 非阻塞IO
从图中可以看出 , 当用户进程发出read操作时 , 如果kernel中的数据还没有准备好 , 那么它并不会block用户进程 , 而是立刻返回一个error 。 从用户进程角度讲, 它发起一个read操作后 , 并不需要等待 , 而是马上就得到了一个结果 。 用户进程判断结果是一个error时 , 它就知道数据还没有准备好 , 于是它可以再次发送read操作 。 一旦kernel中的数据准备好了 , 并且又再次收到了用户进程的system call , 那么它马上就将数据拷贝到了用户内存 , 然后返回 。
所以 , 在非阻塞式IO中 , 用户进程其实是需要不断的主动询问kernel数据准备好了没有 。
非阻塞的接口相比于阻塞型接口的显著差异在于 , 在被调用之后立即返回 。 使用如下的函数可以将某句柄fd设为非阻塞状态 。
fcntl( fd, F_SETFL, O_NONBLOCK );
下面将给出只用一个线程 , 但能够同时从多个连接中检测数据是否送达 , 并且接受数据的模型 。
5种网络IO模型(有图,很清楚)文章插图
图5 使用非阻塞的接收数据模型
在非阻塞状态下 , recv() 接口在被调用后立即返回 , 返回值代表了不同的含义 。 如在本例中 ,* recv() 返回值大于 0 , 表示接受数据完毕 , 返回值即是接受到的字节数;
* recv() 返回 0 , 表示连接已经正常断开;
* recv() 返回 -1 , 且 errno 等于 EAGAIN , 表示 recv 操作还没执行完成;
* recv() 返回 -1 , 且 errno 不等于 EAGAIN , 表示 recv 操作遇到系统错误 errno 。
可以看到服务器线程可以通过循环调用recv()接口 , 可以在单个线程内实现对所有连接的数据接收工作 。 但是上述模型绝不被推荐 。 因为 , 循环调用recv()将大幅度推高CPU 占用率;此外 , 在这个方案中recv()更多的是起到检测“操作是否完成”的作用 , 实际操作系统提供了更为高效的检测“操作是否完成“作用的接口 , 例如select()多路复用模式 , 可以一次检测多个连接是否活跃 。