在服务器开发中,处理多个客户端连接是一个常见需求。多进程模型是一种经典的并发处理方式,它通过创建子进程来分担任务,提高服务器的吞吐量和响应能力。本文将基于技术笔记,详细解析多进程服务器的核心概念和实现细节,帮助您理解其工作原理。

进程的特点(有血缘关系的进程)

在多进程模型中,进程通常通过 fork()系统调用创建子进程,这些进程之间存在血缘关系(即父子关系)。以下是关键特点:

  • 创建子进程​:使用 fork()函数创建子进程。父进程调用 fork()后,会复制自身创建一个新的子进程。子进程与父进程几乎完全相同,但拥有独立的进程ID。

  • 虚拟地址空间复制​:子进程会复制父进程的整个虚拟地址空间,包括用户区和内核区。这意味着:

    • 用户区​:复制堆区、栈区、全局数据区和代码区。子进程初始时拥有父进程数据的副本,但后续修改互不影响(例如,父进程修改数据不会影响子进程)。

    • 内核区​:复制文件描述符表等资源。这使得子进程可以继承父进程打开的文件或网络连接。

  • 数据关系​:父进程的数据在创建子进程时被复制,但之后各自独立。如果父进程修改了某些数据(如变量),子进程不会受到影响,因为它们运行在隔离的地址空间中。

  • 资源回收​:子进程结束后,需要通过父进程回收资源(如使用 wait()waitpid()系统调用),以避免僵尸进程的产生。父进程负责监控和清理子进程的资源,确保系统稳定性。

这些特点使得多进程模型在并发处理中非常有效,但需要注意内存开销和进程间通信的复杂性。

多进程版的服务器程序实现

在多进程服务器中,服务器端需要处理监听客户端连接和与客户端通信两个主要任务。以下是典型的实现方式:

  • 服务器端功能​:

    • 监听连接​:服务器父进程调用 accept()函数监听端口,等待客户端连接。这是一个阻塞操作,意味着父进程会一直等待直到有客户端连接。

    • 处理通信​:每建立一个客户端连接,服务器就会创建一个子进程专门负责与该客户端通信。这样,多个客户端可以同时被服务,而父进程继续监听新连接。

  • 进程分工​:

    • 父进程(1个)​​:专职监听。它运行在一个循环中,不断调用 accept()来接受新连接。一旦有连接到来,父进程调用 fork()创建子进程,然后返回监听状态。

    • 子进程(多个)​​:专职通信。每个子进程负责与一个客户端进行数据交换:

      • 接收数据​:使用 read()recv()函数(默认阻塞)从客户端读取数据。

      • 发送数据​:使用 write()send()函数(默认阻塞)向客户端发送响应。

        子进程在处理完通信后正常退出,由父进程回收资源。

这种设计的好处是简单易懂,且能有效利用多核CPU。然而,它也有局限性,如进程创建和销毁的开销较大,以及需要 careful 处理进程间同步和资源管理。在实际应用中,常结合信号处理(如 SIGCHLD)来优化资源回收

服务端

//服务端

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>  //多进程头文件
#include<arpa/inet.h>  //套接字头文件
#include<signal.h>

//子进程通信函数
void working(int cfd);
//子进程回收
void recycle(int signum);
int main ()
{
    //1.创建客户端插座
    int lfd =socket(AF_INET,SOCK_STREAM,0);
    
    //2.绑定插座的端口与ip
    struct sockaddr_in addr;
    //设定地址族
    addr.sin_family=AF_INET;
    //设定端口
    addr.sin_port=htons(9999);
    //设定ip
    addr.sin_addr.s_addr=INADDR_ANY;
    int ret=bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
    //3.启动插座
    ret=listen(lfd,128);
    //4.建立连接
    struct sockaddr_in cli;
    int clilen=sizeof(cli);
    //注册信号
    //通过信号捕捉来控制回收子进程的时机
    struct sigaction act;
    act.sa_handler=recycle;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD,&act,NULL);
    while(1)
    {
        int cfd=accept(lfd,(struct sockaddr*)&cli,&clilen);
        if (cfd==-1)
        {
            perror("建立连接失败,重试尝试连接中...\n");
            continue;
        }
        //连接建立成功后输出检验一下
        char myip[24];
        printf("客户端ip = %s,端口 = %d\n"
        ,inet_ntop(AF_INET,&cli.sin_addr.s_addr,myip,sizeof(myip))
        ,ntohs(cli.sin_port)
        );
        
        //连接已建立,开一个子进程去通信
        pid_t pid=fork();
        if (pid==0)//子进程进入
        {
            //子进程关闭监听文件描述符(因为子进程用cfd通信就行了)
            close(lfd);
            //和客户端通信
            working(cfd);
            //退出子进程
            exit(0);
        }
        
        else
        {
            //父进程关闭通信文件描述符(因为通信的cfd交给子进程去干了,父不需要cfd)
            close(cfd);
        }
        
    }
    return 0;
}

void working (int cfd)
{
    while (1)
    {
        //接受客户端数据
        char buf[1024];
        memset(buf,0,sizeof(buf));
        int len=read(cfd,buf,sizeof(buf));
        if (len==0)
        {
            printf("客户端断开连接...\n");
            break;
        }
        else if(len==-1)
        {
            perror("读取异常\n");
            break;
        }
        else
        {
            printf("从客户端读到了数据,内容为:%s\n",buf);
            //回复数据
            write(cfd,buf,len);
        }
    }
    //关闭客户端通信文件
    close(cfd);
    
}
//子进程回收
void recycle(int signum)
{
    while(1)
    {
        pid_t pid=waitpid(-1,NULL,WNOHANG); //设置为非阻塞式
        //回收成功返回>0
        if (pid>0)
        {
		   //这里要加\n,不加这个print不出来
		     //因为linux是一行一行输出到终端的
            printf("子进程被回收了,pid = %d\n",pid); 
        }
        else break;
    }
}

客户端

//客户端

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
int main ()
{
    //1.创建客户端插座
    int cfd=socket(AF_INET,SOCK_STREAM,0);
    if (cfd==-1)
    {
        perror("socket创建失败\n");
        exit(0);
    }
    //2.连接服务器
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;//ipv4
    addr.sin_port=htons(9999);//端口,转为网络字节序
    inet_pton(AF_INET,"192.168.88.129",&addr.sin_addr.s_addr);//ip转换为网络字节序
    int ret=connect(cfd,(struct sockaddr*)&addr,sizeof(addr));
    if (ret==-1)
    {
        perror("连接失败\n");
        exit(0);
    }
    //3.通信
    int num=0;
    while (1)
    {
        //发送数据
        char buf[1024];
        sprintf(buf,"这是来自客户端的数据,%d,......",num++);
        send(cfd,buf,strlen(buf)+1,0);
        //接受数据
        memset(buf,0,sizeof(buf));
        int len=recv(cfd,buf,sizeof(buf),0);//没数据时阻塞在这里
        //判断读写情况
        if (len==0)
        {
            printf("服务器端已断开连接\n");
            break;
        }else if(len>0)
        {
            printf ("收到了服务端的回复,内容为: %s\n",buf);
        }
        else
        {
            perror("接受数据失败\n");
            break;
        }
        sleep(1);

    }
    //6.断开连接
    close(cfd);//通信编号
    return 0;
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐