设计一个简单的并发服务器

当服务一个客户请求可能花费较长时间时,我们并不希望真个服务器被单个客户长期占用,而是希望同时服务多个客户。Unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户。

pid_t pid;
int listenfd,connfd;
listenfd=e(...);
        /* fill in sockaddr_in{} with server`s well_known port*/
bind(listenfd, ...);
listen(listenfd, LISTENQ);
for( ; ; ){
    connfd = accept(listenfd, ...);        //probably blocks
    if( (pid = fork()) ==0){                
        close(listenfd);            //children closes listening socket
        doit(connfd);                //process the request
        close(connfd);                //done with the client
        exit(0);                    //child terminates
    }
    close(connfd);                //parent closes connected socket
}

这是一个典型的并发服务器程序轮廓

    当一个连接建立时,accept返回,服务器接着调用fock,然后由子进程服务客户,父进程则等待另一个连接。既然新的客户由子进程提供服务,父进程就关闭已连接的套接字。

    在上面的代码中,我们假设由函数doit执行服务客户所需的所有操作。当该函数返回时,我们在子进程中显示地关闭已连接套接字。这一点并非必需,因为下一个语句就是调用exit,而进程终止的部分工作就是关闭所有由内核打开的描述符。是否显示调用close只和个人编码风格有关。

    对一个TCP套接字调用close会导致发送一个FIN,随后是正常的TCP终止连接步骤。为什么上面代码中,调用父进程对connfd调用close没有终止它与客户的连接呢?为了便于理解,我们必须知道每个文件或套接字都有一个引用计数。引用计数在文件表项中维护(APUE第58~59页),它是当前打开着的引用该文件或套接字的描述符的个数。代码中,socket返回后与listenfd关联的文件的引用计数为1。accept返回后与connfd关联的文件表项的引用计数也还是1。然而fork返回后,这两个描述符就在父进程和子进程之间共享(就是被复制),因此与这两个套接字相关联的文件表项的引用计数值都为2。这么一来,当父进程关闭connfd时,他只是把相应的引用计数值从2减到1。该套接字真正的清理和资源释放要等到其引用计数值为0时才发生。这会在稍后子进程也关闭connfd时发生。

    我们利用下图将上面代码套接字和连接表现出来。首先给出了在服务器阻塞于accept调用且来自客户的连接请求到达时客户和服务器的状态(第一次连接到达)

    从accept返回后,我们就有了图4-15所示状态。连接被内核接收,新的套接字connfd被创建。这是一个已连接套接字,可由此跨连接读写数据。(三次握手之后,连接已建立)


并发服务器的下一步是调用fork,图4-16给出了从fork返回后的状态


    注意,此时listenfd和connfd这两个描述符都在父进程和子进程之间共享(被复制)。再下一步就是有父进程关闭已连接套接字,子进程关闭监听套接字。


    这是两个套接字所期望的最终状态。子进程处理与客户的连接,父进程则可以在监听套接字上再次调用accept来处理下一个客户连接。

猜你喜欢

转载自blog.csdn.net/godop/article/details/79901608