网络程序 -- TCP版服务器

博客围绕支持多客户端的TCP服务器展开。先介绍多进程版,阐述核心功能、子进程创建及父进程非阻塞状态设置;接着说明多线程版,通过原生线程库实现多客户端通信;最后讲解线程池版,指出其适合短任务,还给出解决持久通信会话问题的两个方向。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 多进程版TCP服务器

1.1 核心功能

  对于之前编写的 字符串回响程序 来说,如果只有一个客户端进行连接并通信,是没有问题的,但如果有多个客户端发起连接请求,并尝试进行通信,服务器是无法应对的

  原因在于 服务器是一个单进程版本,处理连接请求 和 业务处理 是串行化执行的,如果想处理下一个连接请求,需要把当前的业务处理完成。

具体表现为下面这种情况:

 为什么客户端B会显示当前已经连接成功?

  这是因为是客户端是主动发起连接请求的一方,在请求发出后,如果出现连接错误,客户端就认为已经连接成功了,但实际上服务器还没有处理这个连接请求.

  这显然是服务器的问题,处理连接请求业务处理 应该交给两个不同的执行流完成,可以使用多进程或者多线程解决,这里先采用多进程的方案

  所以当前需要实现的网络程序核心功能为:当服务器成功处理连接请求后,fork 新建一个子进程,用于进行业务处理,原来的进程专注于处理连接请求。

1.2 创建子进程

注:当前的版本的修改只涉及 StartServer() 函数

创建子进程使用 fork() 函数,它的返回值含义如下

  • ret == 0 表示创建子进程成功,接下来执行子进程的代码
  • ret > 0 表示创建子进程成功,接下来执行父进程的代码
  • ret < 0 表示创建子进程失败

  子进程创建成功后,会继承父进程的文件描述符表,能轻而易举的获取客户端的 socket 套接字,从而进行网络通信

当然不止文件描述符表,得益于 写时拷贝 机制,子进程还会共享父进程的变量,当发生修改行为时,才会自己创建。

注意: 当子进程取走客户端的 socket 套接字进行通信后,父进程需要将其关闭(因为它不需要了),避免文件描述符泄漏

StartServer() 服务器启动函数 — 位于 server.hppTcpServer

// 进程创建、等待所需要的头文件
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>


 //启动服务器
        void StartServer(){
            // 忽略 SIGCHLD 信号
            //signal(SIGCHLD, SIG_IGN);
            while(!_quit){
                //1 处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int sock = accept(_listensock,(struct sockaddr*)&client,&len);

                //2 如果连接失败 继续尝试连接
                if(sock == -1){
                    std::cerr<< "Accept Fail!:"<<strerror(errno)<<std::endl;
                    continue;
                }

                // 连接成功,获取客户端信息
                std::string clientip = inet_ntoa(client.sin_addr);
                uint16_t clientport = ntohs(client.sin_port);
              
                std::cout<<"Server accept"<<clientip + "-"<<clientport<<sock<<" from "<<_listensock << "success!"<<std::endl;

                //3 创建子进程 
                pid_t id=fork();
                if(id<0){
                    // 创建子进程失败,暂时不与当前客户端建立通信会话
                   close(sock);
                   std::cerr<<"Fork Fail!"<<std::endl;
                }
                else if( 0 == id){
                   //进入子进程
                   // 子进程拥有父进程相同的文件描述符,建议把不用的关闭
                 close(_listensock);
                  // 执行业务处理函数
                 //4 这里因为是字节流传递,一般而言我们会自己写一个函数
                 Service(sock,clientip,clientport);
                 exit(0);
                }
                else {
                  // 父进程需要等待子进程
                    pid_t ret = waitpid(id, nullptr, 0); // 默认为阻塞式等待
                    //更改为非阻塞
                    // pid_t ret = waitpid(id,nullptr,WNOHANG);
                    if(ret == id){
                     std::cout << "Wait " << id << " success!";
                    }
                }
            }
        }

  虽然此时成功创建了子进程,但父进程(处理连接请求)仍然需要等待子进程退出后,才能继续运行,而不能和我们想象中一样单独进行处理连接请求函数,说白了就是 父进程现在处于阻塞等待状态,需要设置为 非阻塞等待.

1.3 设置非阻塞状态

设置父进程为非阻塞的方式有很多,这里来一一列举

方式一:通过参数设置为非阻塞等待(不推荐)

可以直接给 waitpid() 函数的参数3传递 WNOHANG,表示当前为 非阻塞等待.

pid_t ret = waitpid(id, nullptr, WNOHANG); // 设置为非阻塞式等待

  这种方法可行,但不推荐,原因如下:虽然设置成了非阻塞式等待,但父进程终究是需要通过 waitpid() 函数来尝试等待子进程,倘若父进程一直卡在 accept() 函数处,会导致子进程退出后暂时无人收尸,进而导致资源泄漏。

方式二:忽略 SIGCHLD 信号(推荐使用)

  这是一个子进程在结束后发出的信号,默认动作是什么都不做;父进程需要检测并回收子进程,我们可以直接忽略该信号,这里的忽略是个特例,只是父进程不对其进行处理,转而由 操作系统 对其负责,自动清理资源并进行回收,不会产生 僵尸进程。

 //启动服务器
        void StartServer(){
            // 忽略 SIGCHLD 信号
            signal(SIGCHLD, SIG_IGN);
            while(!_quit){
                //1 处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int sock = accept(_listensock,(struct sockaddr*)&client,&len);

                //2 如果连接失败 继续尝试连接
                if(sock == -1){
                    std::cerr<< "Accept Fail!:"<<strerror(errno)<<std::endl;
                    continue;
                }

                // 连接成功,获取客户端信息
                std::string clientip = inet_ntoa(client.sin_addr);
                uint16_t clientport = ntohs(client.sin_port);
              
                std::cout<<"Server accept"<<clientip + "-"<<clientport<<sock<<" from "<<_listensock << "success!"<<std::endl;

                //3 创建子进程 
                pid_t id=fork();
                if(id<0){
                    // 创建子进程失败,暂时不与当前客户端建立通信会话
                   close(sock);
                   std::cerr<<"Fork Fail!"<<std::endl;
                }
                else if( 0 == id){
                   //进入子进程
                   // 子进程拥有父进程相同的文件描述符,建议把不用的关闭
                 close(_listensock);
                  // 执行业务处理函数
                 //4 这里因为是字节流传递,一般而言我们会自己写一个函数
                 Service(sock,clientip,clientport);
                 exit(0);
                }
                // else {
                //   // 父进程需要等待子进程
                //     //pid_t ret = waitpid(id, nullptr, 0); // 默认为阻塞式等待
                //     //更改为非阻塞
                //      pid_t ret = waitpid(id,nullptr,WNOHANG);
                //     if(ret == id){
                //      std::cout << "Wait " << id << " success!";
                //     }
                // }
            }
        }

强烈推荐使用该方案,因为操作简单,并且没有后患之忧。

方式三:设置 SIGCHLD 信号的处理动作为子进程回收(不是很推荐)

  当子进程退出并发送该信号时,执行父进程回收子进程的操作。

  设置 SIGCHLD 信号的处理动作为 回收子进程后,父进程同样不必再考虑回收子进程的问题

  注意: 因为现在处于 TcpServer 类中,handler() 函数需要设置为静态(避免隐含的 this 指针),避免不符合 signal() 函数中信号处理函数的参数要求。

 // 需要设置为静态
        static void handler(int signo){
            printf("进程 %d 捕捉到了 %d 号信号\n", getpid(), signo);
            // 这里的 -1 表示父进程等待时,只要是已经退出了的子进程,都可以进行回收
            while (1){
                pid_t ret = waitpid(-1, NULL, WNOHANG);
                if (ret > 0)
                    printf("父进程: %d 已经成功回收了 %d 号进程\n", getpid(), ret);
                else
                    break;
            }
            printf("子进程回收成功\n");
        }
        
        //启动服务器
        void StartServer(){
            // 设置 SIGCHLD 信号的处理动作
            signal(SIGCHLD, handler);
            // 忽略 SIGCHLD 信号
            // signal(SIGCHLD, SIG_IGN);
            while(!_quit){
                //1 处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int sock = accept(_listensock,(struct sock
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值