linux fork多进程并发服务器模型之C/C++代码实战

时间:2022-07-24 00:06:03

        在很早的文章中, 我们一起聊过服务器如何与多个客户端进行通信, 那时, 我们要么用select, 要么用多线程, 却没有用多进程。 其实, 多进程也可以实现与多个客户端进行通信。 

        如果是在while中循环accept,  然后循环处理事情, 此时, 这种服务是迭代服务, 只能逐一处理客户端的请求, 后一个请求必须等前一个请求处理完毕, 无法并发处理, 真是急死人。 要实现并发, 我们可以考虑多线程, 也可以考虑多进程, 本文来说说后者。 在我们的多进程服务器模型中, 我们用父进程来处理连接(监听socket), 用fork子进程的方法来处理通信(通信socket), 各司其职, 美哉。

        一旦涉及到fork, 就必须注意僵尸进程的处理, 所以, 我们要用waitpid进行收尸, 这一点, 我们已经说过了。 另外, 要注意, 父子进程共享socket句柄的文件表(如果不理解的话, 建议看看APUE), 所以close socket的时候, 只是使引用计数减1, 并不是真正地直接关闭socket(减为0才是真正的关闭), 这一点, 我们也已经说过了。


        废话少说, 直接上菜。

        服务端程序为:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

void sigChildFun(int sigNO)
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) // 循环收尸(僵尸进程), 此时waitpid不会阻塞
{
NULL;
}

return;
}

int main()
{
sockaddr_in servAddr;
memset(&servAddr,0,sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = INADDR_ANY;
servAddr.sin_port = htons(8765);

int iListenSocket = socket(AF_INET, SOCK_STREAM, 0);
bind(iListenSocket, (sockaddr *)&servAddr, sizeof(servAddr));
listen(iListenSocket,5);

signal(SIGCHLD, sigChildFun);

while(1)
{
sockaddr_in clientAddr;
socklen_t iSize = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));

int iConnSocket = accept(iListenSocket,(sockaddr*)&clientAddr, &iSize);
if(iConnSocket < 0)
{
if(errno == EINTR || errno == ECONNABORTED)
{
continue;
}
else
{
printf("accept error, server\n");
return -1;
}
}

int tmpPid = fork();
if(tmpPid == 0)
{
close(iListenSocket); // 子进程让监听socket的计数减1, 并非直接关闭监听socket

char szBuf[1024] = {0};
snprintf(szBuf, sizeof(szBuf), "server pid[%u], client ip[%s]", getpid(), inet_ntoa(clientAddr.sin_addr));
write(iConnSocket, szBuf, strlen(szBuf) + 1);

while(1)
{
if(read(iConnSocket, szBuf, 1) <= 0)
{
close(iConnSocket); // 子进程让通信的socket计数减1
return -2; // 子进程退出
}
}

close(iConnSocket); // 子进程让通信的socket计数减1
return 0; // 子进程退出
}

close(iConnSocket); // 父进程让通信的socket计数减1
}

getchar();
close(iListenSocket); // 父进程让监听socket计数减1, 此时会关掉监听socket(因为之前子进程已经有此操作)
return 0;
}

       启动它。 

  

       客户端程序为:

#include <unistd.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>

int main()
{
int sockClient = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrSrv;
addrSrv.sin_addr.s_addr = inet_addr("10.100.70.140");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8765);
connect(sockClient, ( const struct sockaddr *)&addrSrv, sizeof(struct sockaddr_in));

char szBuf[2048] = {0};
int iRet = recv(sockClient, szBuf, sizeof(szBuf) - 1 , 0);
printf("msg from server: %s\n", szBuf);

getchar();
close(sockClient);

return 0;
}
       我们开启一个客户端, 此时如下:

       客户端信息为:

xxxxxx$ ./client
msg from server: server pid[16402], client ip[10.100.70.139]

       服务端信息为:

xxxxxx$ ps -ef | grep -w server
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
user_00 16402 16096 0 19:42 pts/18 00:00:00 ./server
      可以看到, 服务端16402子进程是与客户端通信的进程, 父进程16096是监听的父进程(主进程)。

       另外再开启一个客户端(不要关闭旧的客户端), 此时如下:

       新客户端信息为:

xxxxxx$ ./client
msg from server: server pid[16497], client ip[10.100.70.139]

       服务端信息为:

xxxxxx$ ps -ef | grep -w server
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
user_00 16402 16096 0 19:42 pts/18 00:00:00 ./server
user_00 16497 16096 0 19:42 pts/18 00:00:00 ./server
      可以看到, 父进程16096新开了一个子进程16497来与新的客户端进行通信。


      我们关闭第一个客户端, 然后看到服务端为:

xxxxxx$ ps -ef | grep -w server
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
user_00 16497 16096 0 19:42 pts/18 00:00:00 ./server

      我们再关闭第二个客户端, 然后看到服务端为:

xxxxxx$ ps -ef | grep -w server
user_00 16096 32164 0 19:42 pts/18 00:00:00 ./server
        显然, 客户端退出后, 发FIN包, 服务端子进程的recv函数就为0, 退出子进程的while循环了, 因此, 对应的子进程就over了, 而且不会留下僵尸进程(有waitpid)。 而且, 我们可以看到, 负责连接管理(accept)的父进程(主进程)依然安然无恙, 优哉游哉地等待下一个客户端连接。

       在这里, 我们可以看到, 这个服务器是并发的, 而不是迭代的。 什么意思呢? 你看, 即使子进程处理业务需要很久很久, 那么上述服务依然能并发地响应n个几乎同时到达的客户端, 此时,父进程开启n个子进程, 并发地工作, 并发地与客户端进行通信, 而且还互不干扰, 大大提升了服务满意度。 

       客户自然满意多了, 因为服务方专门派人一对一地提供服务啊, 你要是再不满意, 这个客户就没良心了。


       先说到这里, 吃个晚饭, 然后干点正事。