fork了一个多进程并发服务器

本文通过一个实例展示了如何使用Linux的fork函数创建多进程并发服务器,详细解释了服务端和客户端的代码实现,强调了并发处理客户端请求的能力以及避免僵尸进程的方法。通过实际运行和观察进程状态,加深了对多进程并发服务器的理解。
摘要由CSDN通过智能技术生成

大家好,我是涛哥。

 

今天还在假期中,最近被朋友拉进了一个乒乓球群,发现了一个打球的地方,又近又好,球友也多,打得挺好,今年减肥有望啦。

有很多读者对网络编程很感兴趣,觉得挺有意思,希望我多写一些网络编程方面的实战例子。没问题,这就来了。我写的这些网络程序例子,都是自己亲自调试过的,大家可以直接拿去运行。

在后端开发中,并发服务是一个基本的要求,大家手机上的APP连接到后端服务器,这些服务器是怎样处理成千上万的请求的呢?方法有很多,今天,我们来实战fork一下多进程并发服务器。

服务端实现

我们思考一下:服务端程序该怎么写?如果是在while中循环accept,  然后循环处理事情,此时只能逐一处理客户端的请求,后一个请求必须等前一个请求处理完毕,真是急死人。

可以考虑多线程,也可以考虑多进程,本文来说说后者。在多进程服务器模型中,用父进程来处理连接(监听socket),用fork子进程来处理通信(通信socket),各司其职,美哉。

关于fork这个单词(不是fuck),我们应该很熟悉:叉子。在linux系统中,这个fork函数也是很常见的,其作用是分叉一个子进程出来。在很多笔试面试中,fork几乎是必考的内容。

提到fork,就必须注意僵尸进程的处理。另外,父子进程共享socket句柄的文件表,所以,在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

另外再开启一个客户端进程(不要关闭旧的客户端),连接服务端,此时新客户端显示的信息为:

xxxxxx$ ./clientmsg 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个子进程,并发地工作,并发地与客户端来通信。

而且,这种并发的行为互不干扰,大大提升了服务满意度。客户自然满意多了,因为服务方专门派人一对一地提供服务啊,你要是再不满意,那这个客户就没良心了。

建议有兴趣的同学实际玩一下这个例子,加深对多进程并发服务器的理解,提高实战能力。同时,对fork的理解也会更加深刻。

网络编程就是这样,定好思路,多编程,多抓包,多调试,爽爽哒!大家如果有疑问请留言。晚上又要去打球了,减肥走起来。 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值