学科分类
目录
Linux编程

多进程并发服务器

在多进程并发服务器中,若有用户请求到达,服务器将会调用fork()函数,创建一个子进程,之后父进程将继续调用accept(),而子进程则去处理用户请求。下面将通过案例来展示使用多进程并发服务器实现网络通信的方法,并结合案例,对多进程并发服务器进行分析。

案例1:搭建多进程并发服务器,使服务器端可接收多个客户端的数据,并将接收到的数据转为大写,写回客户端;使客户端可向服务器发送数据,并将服务器返回的数据打印到终端。

案例实现如下:

fserver.c //服务器

 1    #include <arpa/inet.h>
 2    #include <signal.h>
 3    #include <sys/wait.h>
 4    #include <sys/types.h>
 5    #include "wrap.h"
 6    #define MAXLINE 80
 7    #define SERV_PORT 8000
 8    //子进程回收函数
 9    void do_sigchild(int num)
 10    {
 11        while (waitpid(0, NULL, WNOHANG) > 0);
 12    }
 13    int main()
 14    {
 15        struct sockaddr_in servaddr, cliaddr;
 16        socklen_t cliaddr_len;
 17        int listenfd, connfd;
 18        char buf[MAXLINE];
 19        char str[INET_ADDRSTRLEN];
 20        int i, n;
 21        pid_t pid;
 22        struct sigaction newact;
 23        newact.sa_handler = do_sigchild;
 24        sigaction(SIGCHLD, &newact, NULL);        //信号捕获与处理(回收子进程)
 25        listenfd = Socket(AF_INET, SOCK_STREAM, 0);
 26         //设置服务器端口地址
 27        bzero(&servaddr, sizeof(servaddr));
 28        servaddr.sin_family = AF_INET;
 29        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 30        servaddr.sin_port = htons(SERV_PORT);
 31         //使服务器与端口绑定
 32        Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 33        Listen(listenfd, 20);
 34        printf("Accepting connections ...\n");
 35        while (1) {
 36            cliaddr_len = sizeof(cliaddr);
 37            connfd=Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
 38            pid = fork();            //创建子进程
 39            if (pid == 0) {
 40                 //子进程处理客户端请求
 41                Close(listenfd);
 42                while (1) {
 43                    n = Read(connfd, buf, MAXLINE);
 44                    if (n == 0) {
 45                        printf("the other side has been closed.\n");
 46                        break;
 47                    }
 48                     //打印客户端端口信息
 49                    printf("received from %s at PORT %d\n",
 50                        inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),
 51                            ntohs(cliaddr.sin_port));
 52                    for (i = 0; i < n; i++)
 53                        buf[i] = toupper(buf[i]);
 54                    Write(connfd, buf, n);
 55                }
 56                Close(connfd);
 57                return 0;
 58            }
 59            else if (pid > 0) {
 60                Close(connfd);
 61            }
 62            else
 63                perr_exit("fork");
 64        }
 65        Close(listenfd);
 66        return 0;
 67    }

服务器进程中的核心业务代码为第35~64行。对用户而言,服务器需一直保持运转,以便能及时与客户端连接,处理客户端请求,因此,服务器的accept功能应处于while循环中。当服务器通过Accept()成功与客户端连接后,服务器创建子进程,将请求处理功能交予子进程,需要注意的是,此时父子进程打开了相同的文件描述符,因此在父进程中应调用Close()函数关闭由Accept()函数获取到的文件描述符。

在进程机制中,子进程由父进程回收。通过对前面章节的学习,我们知道可以通过调用wait()、waitpid()函数或使用信号机制,来回收子进程。其中wait()函数用于等待回收子进程,若没有子进程终止,父进程将会阻塞,此时服务器将无法接收客户端请求,此种方式显然不合适;若使用信号,子进程终止时产生的SIGCHLD信号会使父进程中断,进而使服务器的稳定性受到影响,因此信号机制也不适用。

程序fserver.c中选用waitpid()实现子进程的回收及资源释放。waitpid()函数采用非阻塞方式回收子进程,调用waitpid()函数不会使父进程阻塞,且当其第一个参数pid被设置为0时,可回收进程组中所有已终止的子进程,因此可搭配信号捕获函数sigaction(),捕获子进程终止时产生的SIGCHLD信号,在空闲时刻回收所有已终止的子进程。当然,若服务器中的子进程较多,也可创建一个子进程专门回收服务器中的其它子进程,以保证服务器的性能。

fclient.c //客户端

 1    #include <stdio.h>
 2    #include <string.h>
 3    #include <unistd.h>
 4    #include <netinet/in.h>
 5    #include "wrap.h"
 6    #define MAXLINE 80
 7    #define SERV_PORT 8000
 8    int main()
 9    {
 10        struct sockaddr_in servaddr;
 11        char buf[MAXLINE];
 12        int sockfd, n;
 13        sockfd = Socket(AF_INET, SOCK_STREAM, 0);
 14        bzero(&servaddr, sizeof(servaddr));
 15        servaddr.sin_family = AF_INET;
 16        inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
 17        servaddr.sin_port = htons(SERV_PORT);
 18        Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 19        while (fgets(buf, MAXLINE, stdin) != NULL) {
 20            Write(sockfd, buf, strlen(buf));
 21            n = Read(sockfd, buf, MAXLINE);
 22            if (n == 0)
 23                printf("the other side has been closed.\n");
 24            else
 25                Write(STDOUT_FILENO, buf, n);
 26        }
 27        Close(sockfd);
 28        return 0;
 29    }

分别使用以下语句编译服务器端程序与客户端程序:

gcc fserver.c wrap.c -o server
gcc fclient.c wrap.c -o client

程序编译完成后,先执行服务器程序,打开服务器,之后在一个终端运行客户端程序(记为客户端1),并在该终端中输入客户端需要发送的数据,此时客户端与服务器端中打印的信息分别如下:

客户端1:

hello
HELLO

服务器端:

Accepting connections ...
received from 127.0.0.1 at PORT 60315

打开新的终端,在该终端中再次运行客户端程序(记为客户端2),并输入要发送的数据,此时终端2与服务器端中打印的信息分别如下:

客户端2:

itheima
ITHEIMA

服务器端:

Accepting connections ...
received from 127.0.0.1 at PORT 60315
received from 127.0.0.1 at PORT 60316

由以上程序执行结果可知,服务器端进程可以同时处理不止一个客户端请求,多进程并发服务器实现成功。

相比第10章中搭建的服务器,多进程并发服务器不但提升了服务器的效率,还有较高的稳定性,若有处理请求的子进程因异常终止,服务器中其它进程的状态不会受到该进程的影响。此外,需要注意的是,Linux系统中每个进程可打开的文件描述符数量是有限的(1024个),受文件描述符的限制,多进程并发服务器同时最多不过能创建1000多个连接;且系统的内存空间有限,若系统中同时存在的进程数量过多,可能会耗尽系统内存,因此多进程并发服务器不适用于对连接数量要求较高的项目。

点击此处
隐藏目录