多进程并发服务器

我们来考虑有多个客户同时连接一个服务器的情况。在前面的TCP套接字编程的例子中,我们已经看到,服务器程序在接受来自客户端的一个新连接时,会创建出一个新的套接字(已连接套接字),而原先的监听套接字则继续监听后面的连接请求。如果服务器不能立刻接受后来的连接,它们将被放到队列中以等待处理。
原先的套接字仍然可用并且套接字的行为就像文件描述符,这一事实给我们提供了一种同时服务多个客户的方法。如果服务器调用fork为自己创建第二份副本,打开的套接字就将被新的子进程所继承。新的子进程可以和连接的客户进行通信,而主服务器进程可以继续接受以后的客户连接。

为了了解这是如何工作的,假设我们有两个客户端和一个服务器,服务器正在监听一个监听套接字(比如描述符3)上的连接请求。
现在假设服务器接受了客户端1的连接请求,并返回一个已连接套接字(比如描述符4),如图1所示。

图1:第一步:服务器接受客户端的连接请求

在接受连接请求后,服务器派生一个子进程,这个子进程获得服务器描述符表的完整拷贝。子进程关闭它的拷贝中的监听套接字(描述符为3),而父进程关闭它的已连接套接字(描述符为4)的拷贝,因为不需要这些描述符了。这就得到图2中的状态,其中的子进程正忙于为客户端提供服务。
因为父、子进程中的已连接套接字描述符都指向同一个文件表表项,所以父进程关闭它的已连接套接字描述符的拷贝是至关重要的。否则,将永远不会释放已连接套接字描述符4的文件表表项,这会导致存储器资源泄漏并将最终消耗尽可用的存储器,使系统崩溃。


图2:第二步:服务器派生一个子进程为这个客户端服务

现在,假设在父进程为客户端1创建了子进程后,它接受一个新的客户端2的连接请求,并返回一个新的已连接套接字(比如描述符5),如图3所示。


图3:第三步:服务器接受另一个连接请求

然后父进程又派生另一个子进程,这个子进程利用已连接套接字(描述符为5)为它的客户端提供服务,如图4所示。


图4:服务器派生另一个子进程为新的客户端服务

此时,父进程继续等待下一个连接请求,而两个子进程正在并发地为它们各自的客户端提供服务。

例子

我们现在来看下如何使用代码来实现多进程并发服务器。在编写代码时,有几点需要着重强调的:

  • 因为我们创建子进程,但并不等待子进程的完成,所以安排服务器忽略SIGCHLD信号以避免出现僵尸进程。
  • 父子进程必须关闭它们各自的已连接套接字拷贝,如上面所述,这样才能避免存储器资源泄漏。
  • 因为套接字的文件表项中的引用计数,直到父子进程的已连接套接字描述符都关闭了,到客户端的连接才会终止。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int listenfd, connfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;

    // 创建套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    // 命名套接字
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(6240);
    server_len = sizeof(server_address);
    bind(listenfd, (struct sockaddr*)&server_address, server_len);

    // 创建套接字队列
    listen(listenfd, 5);

    // 避免出现僵尸进程
    signal(SIGCHLD, SIG_IGN);

    // 接受客户连接
    while (1) {
        char ch;
        printf("server waiting\n");

        client_len = sizeof(client_address);
        connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_len);

        // 创建子进程,为这个客户创建一个子进程,并判断当前是运行是在父进程还是在子进程中
        if (fork() == 0) {
            // 在子进程中
            close(listenfd);
            read(connfd, &ch, 1);
            ch++;
            write(connfd, &ch, 1);
            close(connfd);
            sleep(5);
            printf("subprocess, ch: %d, exit\n", ch);
            exit(0);
        } else {
            // 在父进程中
            close(connfd);
        }
    }
}

运行上面的代码,然后使用客户端测试程序(见本文附录)来测试多进程并发服务器的实现。
运行客户端程序:

$ ./client3 & ./client3 & ./client3

客户端终端输出:

char from server = B
char from server = B
char from server = B

同时,可以看到服务器程序输出:

server waiting
server waiting
server waiting
server waiting
subprocess, ch: 66, exit
subprocess, ch: 66, exit
subprocess, ch: 66, exit

多进程优劣

在父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。这样一来,一个进程不可能覆盖另一个进程的用户地址空间。这就消除了许多令人迷惑的错误。这是多进程实现并发服务器的优点。
另一方面,独立的地址空间使用进程共享状态信息变得困难,为了共享信息,必须使用IPC(进程间通信机制)。多进程的另一个缺点是,它们往往比较慢,因为进程控制和IPC的开销都较高。

参考资料

  1. 深入理解计算机系统,第2版,机械工业出版社
  2. Linux程序设计(第4版),Neil Matthew等著,人民邮电出版社,2010年
  3. UNIX 网络编程卷1:套接字联网API(第三版), W.Richard Stevens 等著

附:客户端测试代码

/*  Make the necessary includes and set up the variables.  */

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    int sockfd;
    int len;
    struct sockaddr_in address;
    int result;
    char ch = 'A';

/*  Create a socket for the client.  */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

/*  Name the socket, as agreed with the server.  */
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(6240);
    len = sizeof(address);

/*  Now connect our socket to the server's socket.  */
    result = connect(sockfd, (struct sockaddr *)&address, len);
    if(result == -1) {
        perror("oops: client3");
        exit(1);
    }

/*  We can now read/write via sockfd.  */
    write(sockfd, &ch, 1);
    read(sockfd, &ch, 1);
    printf("char from server = %c\n", ch);
    close(sockfd);
    exit(0);
}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
TCP多进程并发服务器管道是一种常见的服务器架构,它可以同时处理多个客户端连接并实现数据交互。在这种架构中,服务器主进程会创建多个子进程来处理客户端连接,每个子进程都可以独立地与客户端进行数据交换。为了实现子进程之间的数据交互,可以使用管道来进行进程间通信。 具体实现步骤如下: 1. 服务器主进程创建一个TCP监听套接字,并绑定到指定的地址和端口。 2. 服务器主进程进入一个无限循环,不断接收客户端连接请求,并将连接请求分配给空闲的子进程处理。 3. 子进程通过管道与其他子进程通信,主要用于同步管理和数据传输。 4. 子进程处理客户端连接请求后,向管道写入数据,通知其他子进程当前客户端连接已经被处理。 5. 其他子进程通过管道读取数据,得知当前客户端连接已经被处理,可以继续处理下一个客户端连接。 6. 子进程与客户端进行数据交互,直到客户端断开连接或出现异常。 7. 子进程关闭客户端连接,向管道写入数据,通知其他子进程当前客户端连接已经关闭。 8. 其他子进程通过管道读取数据,得知当前客户端连接已经关闭,可以继续处理下一个客户端连接。 9. 子进程退出,释放资源。 需要注意的是,在使用管道进行进程间通信时,需要注意管道缓冲区的大小,以及进程间加锁的问题,以避免出现数据丢失、竞争等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值