阻塞型的网络编程接口几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv()等接口开始的。使用这些接口可以很方便的构建服务器/客户机的模型。我们假设希望建立一个简单的服务器程序,实现向单个客户机提供类似于“一问一答”的内容服务。我们注意到,大部分的socket接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。实际上,除非特别指定,几乎所有的IO接口(包括socket接口)都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,很多程序员可能会选择多线程的方式来解决这个问题。多线程服务器程序应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用pthread_create()创建新线程,fork()创建新进程。我们假设对上述的服务器/客户机模型,提出更高的要求,即让服务器同时为多个客户机提供一问一答的服务。于是有了如下的模型。很多初学者可能不明白为何一个socket可以accept多次。实际上,socket的设计者可能特意为多客户机的情况留下了伏笔,让accept()能够返回一个新的socket。下面是accept接口的原型:intaccept(ints,structsockaddr*addr,socklen_t*addrlen);输入参数s是从socket(),bind()和listen()中沿用下来的socket句柄值。执行完bind()和listen()后,操作系统已经开始在指定的端口处监听所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用accept()接口正是从sockets的请求队列抽取第一个连接信息,创建一个与s同类的新的socket返回句柄。新的socket句柄即是后续read()和recv()的输入参数。如果请求队列当前没有请求,则accept()将进入阻塞状态直到有请求进入队列。上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。使用select()接口的基于事件驱动的服务器模型大部分Unix/Linux都支持select函数,该函数用于探测多个文件句柄的状态变化。下面给出select接口的原型:FD_ZERO(intfd,fd_set*fds)FD_SET(intfd,fd_set*fds)FD_ISSET(intfd,fd_set*fds)FD_CLR(intfd,fd_set*fds)intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout)这里,fd_set类型可以简单的理解为按bit位标记句柄的队列,例如要在某fd_set中标记一个值为16的句柄,则该fd_set的第16个bit位被标记为1。具体的置位、验证可使用FD_SET、FD_ISSET等宏实现。在select()函数中,readfds、writefds和exceptfds同时作为输入参数和输出参数。如果输入的readfds标记了16号句柄,则select()将检测16号句柄是否可读。在select()返回后,可以通过检查readfds有否标记16号句柄,来判断该“可读”事件是否发生。另外,用户可以设置timeout时间。下面将重新模拟上例中从多个客户端接收数据的模型。