多进程并发服务器
在多进程并发服务器中,若有用户请求到达,服务器将会调用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多个连接;且系统的内存空间有限,若系统中同时存在的进程数量过多,可能会耗尽系统内存,因此多进程并发服务器不适用于对连接数量要求较高的项目。