最初的服务器都是迭代服务器, 服务器处理完一个客户的请求, 再接受下一个客户的请求. 但是我们的期望应该是一台服务器同时为多个客户服务. 实现并发服务器最简单的办法就是为每个客户均 fork 一个子进程.
基本思路
基本流程是, 建立连接, accept 返回后, 服务器调用 fork, 子进程通过已连接套接口 (connfd) 为客户提供服务, 父进程通过监听套接口 (listenfd) 等待另一个连接. 子进程开始处理客户后, 父进程便关闭已连接套接口.
- ...
- listenfd = socket(AF_INET, SOCK_STREAM, 0);
- bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
- listen(listenfd, LISTENQ);
- for ( ; ; ) {
- connfd = accept(listenfd, (SA *) &cliaddr, &clilen));
- if ( (childpid = Fork()) == 0) { /* 子进程 */
- close(listenfd); /* 子进程关闭监听套接口 */
- doit(connfd); /* 子进程通过已连接套接口处理请求 */
- close(connfd); /* 子进程处理请求完毕, 关闭已连接套接口
- /* 这一步可省略, 因为 exit 会关闭所有由内核打开的描述字 */
- exit(0); /* 子进程结束 */
- }
- close(connfd); /* 父进程关闭已连接套接口 */
- }
描述字访问计数
对 TCP 套接口调用 close 会引发 FIN, 终止连接. 但是上面父进程的 Close(connfd)却并没有影响子进程使用这个描述字进行客户请求处理, 这是因为, 父进程调用 close 只是将它的文件表项访问计数减一, 文件表项访问计数值为 0 时才真正关闭.
accept 返回, connfd 计数 = 1
fork 返回, connfd 计数 = 2
父进程 close,connfd 计数 = 1
子进程 close,connfd 计数 = 0, 引发 FIN, 终止连接
分步骤状态图解
下图是服务器阻塞于 accept 调用, 连接请求从客户到达时客户和服务器的状态.
accept 返回前客户 - 服务器的状态
accept 返回后, 就有下图的状态. 连接被内核接受, 新的套接口即 connfd 被创建, 这个已连接套接口, 可用来读写数据.
accept 返回后客户 - 服务器的状态
并发服务器的下一步是调用 fork, 下图是从 fork 返回后的状态. 此时描述字 listenfd 和 connfd 是父进程 - 子进程共享的.
fork 返回后客户 - 服务器的状态
下一步是父进程关闭已连接套接口, 子进程关闭监听套接口.
父子进程关闭相应套接口后客户 - 服务器的状态
最后的结果是子进程处理与客户的连接, 父进程可对监听套接口调用 accept 来处理下一个连接.
来源: https://www.qcloud.com/developer/article/1351072