网络Socket编程基于TCP协议模拟简易网络通信

一、TCP网络通信函数

函数返回值参数备注

// 创建socket文件描述符(TCP/UDP   服务器/客户端)

int socket(int domain, int type, int protocol);

成功:返回一个非负整数,即新创建的套接字文件描述符。

失败:返回-1,并设置errno以指示错误类型。

domain:指定通信域(地址族)。它定义了套接字将使用的地址类型。常见的值包括:

  • AF_INET:IPv4网络协议。

  • AF_INET6:IPv6网络协议。

  • AF_UNIXAF_LOCAL:Unix域套接字,用于同一台机器上的进程间通信。

  • AF_PACKET:用于访问数据链路层。

type:指定套接字类型,它定义了套接字提供哪种类型的通信。常见的值包括:

  • SOCK_STREAM:提供顺序的、可靠的、双向的和基于连接的字节流服务(TCP)

  • SOCK_DGRAM:提供数据报服务(UDP)

  • SOCK_RAW:原始套接字,可以直接访问底层协议(如IP、ICMP)

  • SOCK_SEQPACKET:顺序包套接字,提供顺序的、可靠的消息传递

protocol:选择协议类型(支持根据参数2自动推导),一般设置为0)

简单来说:
  • 参数1:domain 创建套接字用于哪种通信(网络/本地)
  • 参数2:type 选择数据传输类型(流式/数据报)
  • 参数3:protocol 选择协议类型(支持根据参数2自动推导)

备注:

  • 创建套接字的本质是为进程提供一个网络通信的接口(为套接字创建一个文件描述符,这个描述符可以用于后续的网络操作)。套接字是操作系统内核中的一个资源,允许进程通过网络与其他进程通信。它通过文件描述符的形式提供给进程,使得网络通信可以像文件操作一样进行。
// 绑定端口号(TCP/UDP    服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);
 

成功:返回0。

失败:返回-1,并设置 errno 以指示错误类型。

sockfd:由 socket 函数创建的套接字文件描述符。

addr:指向一个 sockaddr 结构体的指针,该结构体包含了要绑定的地址信息。对于TCP和UDP服务器,这通常是 sockaddr_in 结构体的实例(用于IPv4)或sockaddr_in6 结构体的实例(用于IPv6)。

addrlen:指定 addr 指向的结构体的大小。这个值应该与 addr 指向的结构体的实际大小相匹配。

备注:

  • 本质是将一个套接字(socket)与一个特定的网络端点(由IP地址和端口号组成)关联起来。
  • 它为进程设置了一个网络接口,使得进程能够通过网络端点接收和发送数据。这是建立服务器监听端口和配置客户端通信的基础。

//监听

int listen(int sockfd, int backlog);

成功:返回0。

失败:返回-1,并设置 errno 以指示错误类型。

sockfd :这是一个已经创建好的套接字描述符,通常通过 socket 函数创建。它必须是一个服务器端的套接字,并且已经通过 bind 函数绑定了一个本地地址和端口号。如果 sockfd 不是一个有效的套接字描述符,或者它没有被正确绑定,listen 函数会失败。

backlog :这是一个整数,用于指定未完成连接队列的最大长度。当有多个客户端同时发起连接请求时,服务器会按照请求到达的顺序将它们放入未完成连接队列中。如果队列满了,后续的连接请求会被拒绝。backlog 的值越大,服务器能够同时处理的连接请求就越多,但也会占用更多的系统资源。在不同的操作系统中,backlog 的实际意义可能略有不同。在 Linux 系统中,backlog 实际上是一个建议值,操作系统会根据系统的资源情况和内部算法来调整未完成连接队列的实际长度。

listen 函数的作用是将套接字从主动连接模式转换为被动监听模式,使其能够接收客户端的连接请求。在调用 listen 函数之前,服务器端的套接字必须已经创建并绑定到一个本地地址和端口号上。调用 listen 函数后,套接字会进入监听状态,等待客户端的连接请求。当客户端发起连接请求时,服务器会将连接请求放入未完成连接队列中,直到服务器调用 accept 函数来接受连接请求,将其从未完成连接队列中移出,并创建一个新的套接字用于与客户端进行通信。

//接受客户端连接请求的函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

成功:返回0。

失败:返回-1,并设置 errno 以指示错误类型。

sockfd :这是一个已经处于监听状态的套接字描述符,通常通过 socket 函数创建并调用 listen 函数将其转换为监听模式。如果 sockfd 不是一个有效的监听套接字,accept 函数会失败。

addr :这是一个指向 struct sockaddr 类型的指针,用于存储连接请求的客户端地址信息。如果不需要获取客户端的地址信息,可以将该参数设置为 NULLstruct sockaddr 是一个通用的地址结构,具体使用时通常会将其转换为特定协议的地址结构,如 struct sockaddr_in(用于 IPv4)或 struct sockaddr_in6(用于 IPv6)。

addrlen :这是一个指向 socklen_t 类型的指针,用于指定 addr 参数所指向的地址结构的大小。在调用 accept 函数之前,应该将 addrlen 设置为地址结构的大小。如果 addr 参数为 NULL,则 addrlen 也必须为 NULL

accept 函数的作用是从监听套接字的未完成连接队列中提取一个已完成的连接请求,并创建一个新的套接字用于与客户端进行通信。当客户端发起连接请求时,服务器调用 listen 函数将其放入未完成连接队列中。当 accept 函数被调用时,它会从未完成连接队列中取出一个连接请求,将其转换为已完成连接状态,并创建一个新的套接字。新的套接字描述符可以用于与客户端进行数据传输,而原来的监听套接字仍然保持在监听状态,继续接收其他客户端的连接请求。

//用于建立主动连接的函数,通常用于客户端程序,用于向服务器发起连接请求。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

成功:返回0。

失败:返回-1,并设置 errno 以指示错误类型。

sockfd :这是一个已经创建好的套接字描述符,通常通过 socket 函数创建。它必须是一个未绑定的套接字(即没有调用 bind 绑定地址和端口),并且是一个主动连接模式的套接字(即没有调用 listen)。

addr :这是一个指向 struct sockaddr 类型的指针,用于指定要连接的服务器的地址信息。通常会将其转换为特定协议的地址结构,如 struct sockaddr_in(用于 IPv4)或 struct sockaddr_in6(用于 IPv6)。这个地址结构中包含了服务器的 IP 地址和端口号。

addrlen :这是一个 socklen_t 类型的值,表示 addr 参数所指向的地址结构的大小。

connect 函数的作用是向服务器发起一个连接请求。客户端通过指定服务器的 IP 地址和端口号,向服务器发送一个连接请求。如果服务器接受连接请求,connect 函数会返回成功,并且套接字进入连接状态,客户端可以通过该套接字与服务器进行数据传输。如果服务器拒绝连接请求,或者连接超时,connect 函数会返回失败。

二、实现字符串回响的服务-客户端

(一)服务端

namespace y_TcpServer
{
    const uint16_t default_port = 8080;
    const int backlog = 32;
    using func_t = std::function<std::string(std::string)>;

    class TcpServer
    {
    public:
        TcpServer(const func_t &func, const uint16_t port = default_port)
            : _func(func), _port(port), _isrunning(false)
        {
        }
        ~TcpServer() {}

        // 初始化服务器
        void Init()
        {
            // 1.创建套接字
            _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listen_sock < 0)
            {
                std::cerr << "create socket error: " << strerror(errno) << std::endl;
                exit(ErrorCode::SOCKET_ERR);
            }
            std::cout << "create socket success:" << _listen_sock << std::endl;

            // 2.填充结构体
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 清零
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            // #define	INADDR_ANY		((in_addr_t) 0x00000000)
            local.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址

            // 3.绑定IP地址和端口号
            if (bind(_listen_sock, (struct sockaddr *)&local, sizeof(local)))
            {
                std::cerr << "bind socket error:" << strerror(errno) << std::endl;
                exit(ErrorCode::BIND_ERR);
            }

            // 4.监听
            if (listen(_listen_sock, backlog) == -1)
            {
                std::cerr << "listen error:" << strerror(errno) << std::endl;
                exit(ErrorCode::LISTEN_ERR);
            }
            std::cout << "listen success:" << std::endl;
        }
        //启动服务器-v0版本,单线程单进程
        void Start()
        {
            while(!_isrunning)
            {
                //1.处理连接请求
                struct sockaddr_in client;
                socklen_t len=sizeof(client);

                int sock=accept(_listen_sock,(struct sockaddr*)&client,&len);

                if(sock<0)
                {
                    std::cerr<<"accept fail"<<strerror(errno)<<std::endl;
                    continue;
                }

                //2.连接成功,获取客户端信息
                std::string clientIP=inet_ntoa(client.sin_addr);
                uint16_t clientPort=ntohs(client.sin_port);

                std::cout<<"server accept "<<clientIP+"-"<<clientPort<<" "
                <<sock<<" form "<<_listen_sock<<" success!"<<std::endl;

                //根据套接字进行通信业务处理
                Service(sock,clientIP,clientPort);
            }
        }

    private:
        // 业务处理
        void Service(int sock, const std::string &clientIP, const uint16_t &clientPort)
        {
            char buff[1024];
            std::string who = clientIP + "-" + std::to_string(clientPort);

            while (true)
            {
                ssize_t n = read(sock, buff, sizeof(buff) - 1);
                if (n > 0)
                {
                    buff[n] = '\0';
                    std::cout << "server get:" << buff << " form" << who << std::endl;

                    std::string respond = _func(buff);
                    write(sock, buff, strlen(buff));
                }
                else if (n == 0)
                {
                    // 表示当前读取到文件末尾了,结束读取
                    std::cout << "client " << who << " " << sock << " quit!" << std::endl;
                    close(sock);
                    break;
                }
                else
                {
                    std::cerr << "read fail" << strerror(errno) << std::endl;
                    close(sock);
                    break;
                }
            }
        }

    private:
        int _listen_sock;
        uint16_t _port;
        bool _isrunning;
        func_t _func;
    };
}

1.功能概述

这段代码实现了一个基于 TCP 协议的简单服务器,其主要功能包括:

  • 监听指定端口,接收客户端的连接请求。
  • 与客户端建立连接后,接收客户端发送的数据,通过回调函数处理数据,并将结果返回给客户端。
  • 支持单线程单进程的简单通信模式。

2.基本成员变量

  • _listen_sock:用于监听客户端连接请求的套接字。
  • _port:服务器监听的端口号,默认为 8080。
  • _isrunning:控制服务器是否运行的标志。
  • _func:一个回调函数,用于处理客户端发送的数据并返回响应。

3.初始化服务器(Init 方法)

        创建套接字:

  • 使用 socket 函数创建一个 TCP 套接字。
  • 如果创建失败,打印错误信息并退出程序。
  • 成功创建后,打印套接字编号。

        填充地址结构体:

  • 创建一个 sockaddr_in 结构体,用于存储服务器的 IP 地址和端口号。
  • 将 IP 地址设置为 INADDR_ANY,表示绑定到本机的所有 IP 地址。
  • 将端口号设置为 _port,并使用 htons 函数将其转换为网络字节序。

        绑定地址和端口号:

  • 使用 bind 函数将套接字绑定到指定的地址和端口号。
  • 如果绑定失败,打印错误信息并退出程序。

        监听连接请求:

  • 使用 listen 函数将套接字设置为监听状态,允许的最大连接队列长度为 backlog(默认为 32)。
  • 如果监听失败,打印错误信息并退出程序。

4.启动服务器(Start 方法)

        循环接收客户端连接:

  • 使用 accept 函数接收客户端的连接请求。
  • 如果接收失败,打印错误信息并继续下一次循环。

        获取客户端信息

  • 获取客户端的 IP 地址和端口号,打印连接成功的日志信息。

        处理客户端请求

  • 调用 Service 方法处理客户端的通信业务。

5.业务处理(Service 方法)

        接收客户端数据

  • 使用 read 函数从客户端套接字中读取数据。
  • 如果读取成功,将读取到的数据存储到缓冲区 buff 中,并打印接收到的数据。

        处理数据并响应

  • 将读取到的数据传递给回调函数 _func,获取处理结果。
  • 使用 write 函数将处理结果发送回客户端。

        处理异常情况

  • 如果客户端关闭连接(read 返回 0),打印日志并关闭套接字。
  • 如果读取失败,打印错误信息并关闭套接字。

(二)客户端

namespace y_TcpClient
{
    class TcpClient
    {
    public:
        TcpClient(const std::string& ip,const uint16_t port)
            :_server_ip(ip),_server_port(port)
        {}
        ~TcpClient(){}

        //初始化服务器
        void Init()
        {
            _sock=socket(AF_INET,SOCK_STREAM,0);
            if(_sock<0)
            {
                std::cerr<<"create socket fail"<<strerror(errno)<<std::endl;
                exit(ErrorCode::SOCKET_ERR);
            }
            std::cout<<"create socket success"<<_sock<<std::endl;
        }
        //启动服务器
        void Start()
        {
            struct sockaddr_in server;
            socklen_t len=sizeof(server);
            bzero(&server,len);
            server.sin_family=AF_INET;
            server.sin_port=htons(_server_port);
            inet_aton(_server_ip.c_str(),&server.sin_addr);//与inet_addr功能一样

            int n=5;
            while(n)
            {
                int ret=connect(_sock,(struct sockaddr*)&server,len);
                
                //连接成功退出重连模式
                if(ret==0) break;
                
                //连接失败重连
                std::cerr<<"正在进行重新连接...剩余次数: "<<--n<<std::endl;
                sleep(2);
            }

            //表明连接失败
            if(n==0)
            {
                std::cerr<<"连接失败!"<<strerror(errno)<<std::endl;
                close(_sock);
                exit(ErrorCode::CONNECT_ERR);
            }
            
            //连接成功
            std::cout<<"连接成功"<<std::endl;

            //进行业务处理
            Service();
        }
    private:
        void Service()
        {
            char buff[1024];
            std::string who=_server_ip+"-"+std::to_string(_server_port);

            while(true)
            {
                std::string msg;
                std::cout<<"Please Enter Message >>";
                std::getline(std::cin,msg);

                //发送消息给服务器
                write(_sock,msg.c_str(),msg.size());

                ssize_t n=read(_sock,buff,sizeof(buff)-1);
                if(n>0)
                {
                    buff[n]='\0';
                    std::cout << "Client get: " << "[ " << buff << " ]" << " from " << who << std::endl;
                }
                else if(n==0)
                {
                    std::cout<<"Server "<<who<<" quit!"<<std::endl;
                    close(_sock);
                    break;
                }
                else
                {
                    //读取异常
                    std::cerr<<"Read Fail"<<strerror(errno)<<std::endl;
                    close(_sock);
                    break;
                }
            }
        }
    private:
        int _sock;
        uint16_t _server_port;
        std::string _server_ip; 
    };
}

初始化服务器(Init函数)

  • 创建套接字:使用socket函数创建一个TCP套接字(AF_INET表示IPv4协议,SOCK_STREAM表示TCP套接字)。

  • 检查套接字创建是否成功:如果socket函数返回值小于0,说明创建失败,输出错误信息并退出程序,错误码为ErrorCode::SOCKET_ERR

  • 如果创建成功,输出创建成功的套接字描述符。

启动服务器(Start函数)

初始化服务器地址结构sockaddr_in

  • 使用bzero函数将server结构清零。

  • 设置server.sin_familyAF_INET,表示使用IPv4协议。

  • 使用htons函数将端口号从主机字节序转换为网络字节序,赋值给server.sin_port

  • 使用inet_aton函数将IP地址从点分十进制字符串转换为网络字节序的二进制形式,赋值给server.sin_addr

尝试连接服务器:

  • 使用connect函数尝试连接服务器,最多重试5次(通过变量n控制)。

  • 如果连接成功(connect返回0),退出重连模式。

  • 如果连接失败(connect返回-1),输出重连信息,等待2秒后继续重试。

  • 如果重试次数用完仍未连接成功,输出连接失败信息,关闭套接字并退出程序,错误码为ErrorCode::CONNECT_ERR

  • 连接成功后,调用Service函数进行业务处理。

业务处理(Service函数)

定义一个字符数组buff用于接收服务器发送的数据。

构造一个字符串who,表示服务器的IP和端口信息。

使用while循环不断接收用户输入的消息并发送给服务器,同时接收服务器的响应:

  • 使用std::getline函数从标准输入获取用户输入的消息。
  • 使用write函数将消息发送给服务器。
  • 使用read函数接收服务器的响应,将接收到的数据存储到buff中。
  • 如果read返回值大于0,说明接收到数据,将数据输出到标准输出。
  • 如果read返回值为0,说明服务器关闭了连接,输出服务器退出信息,关闭套接字并退出循环。
  • 如果read返回值小于0,说明读取异常,输出错误信息,关闭套接字并退出循环。

(三)服务端改良版本

1、原始版本

 //启动服务器-v0版本,单线程单进程
        void Start()
        {
            while(!_isrunning)
            {
                //1.处理连接请求
                struct sockaddr_in client;
                socklen_t len=sizeof(client);

                int sock=accept(_listen_sock,(struct sockaddr*)&client,&len);

                if(sock<0)
                {
                    std::cerr<<"accept fail"<<strerror(errno)<<std::endl;
                    continue;
                }

                //2.连接成功,获取客户端信息
                std::string clientIP=inet_ntoa(client.sin_addr);
                uint16_t clientPort=ntohs(client.sin_port);

                std::cout<<"server accept "<<clientIP+"-"<<clientPort<<" "
                <<sock<<" form "<<_listen_sock<<" success!"<<std::endl;

                //根据套接字进行通信业务处理
                Service(sock,clientIP,clientPort);
            }
        }

原始版本是一个单进程单线程的版本,他只能允许一个客户端进行连接,不适用于与客户端进行连接交互

2、多进程版本

2.1、多进程版本-阻塞式
// 启动服务器-v1版本-多进程1(阻塞式等待)
        void Start()
        {
            while (!_isrunning)
            {
                // 1.处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);

                if (sock < 0)
                {
                    std::cerr << "accept fail" << strerror(errno) << std::endl;
                    continue;
                }

                // 2.连接成功,获取客户端信息
                std::string clientIP = inet_ntoa(client.sin_addr);
                uint16_t clientPort = ntohs(client.sin_port);

                std::cout << "server accept " << clientIP + "-" << clientPort << " "
                          << sock << " form " << _listen_sock << " success!" << std::endl;

                // 多线程根据套接字进行通信业务处理
                pid_t id=fork();
                if(id<0)
                {
                    //创建子进程失败,暂时不与当前客户进行通信连接
                    close(sock);
                    std::cerr<<"fork fail"<<std::endl;
                }
                else if(id==0)
                {
                    //子进程不需要监听,关闭
                    close(_listen_sock);

                    //根据套接字进行通信业务处理
                    Service(sock,clientIP,clientPort);
                    exit(0);
                }
                else
                {
                    //父进程等待子进程
                    pid_t ret=waitpid(id,nullptr,0);//默认阻塞式等待
                    if(ret==id) std::cout<<"wait "<<id<<"sucsess!";
                }
            }
        }

创建子进程处理客户端请求

        子进程(id == 0):

  • 关闭监听套接字_listen_sock,因为子进程不需要继续监听新的连接。
  • 调用Service函数处理客户端的通信业务。
  • 处理完成后,调用exit(0)退出子进程。

        父进程(id > 0):

  • 调用waitpid函数等待子进程结束。waitpid默认是阻塞式的,会阻塞父进程直到子进程退出。
  • 如果waitpid成功(返回值等于子进程的ID),输出等待成功的信息。

缺陷:

        阻塞式waitpid导致效率低下:

  • 父进程在调用waitpid时会阻塞,直到子进程退出。这意味着父进程在等待子进程时无法继续处理新的连接请求。
  • 这会导致服务器的并发处理能力受限,尤其是在高并发场景下,服务器无法及时响应新的客户端连接。

        子进程退出后父进程可能阻塞:

  • 如果子进程在处理客户端请求时花费了较长时间,父进程会一直阻塞在waitpid函数上,无法及时响应新的连接请求。这会导致服务器的响应速度变慢,影响用户体验。
2.2、多进程版本-非阻塞式
 // 启动服务器-v2版本-多进程2(非阻塞式等待)
        void Start()
        {
            while (!_isrunning)
            {
                // 1.处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);

                if (sock < 0)
                {
                    std::cerr << "accept fail" << strerror(errno) << std::endl;
                    continue;
                }

                // 2.连接成功,获取客户端信息
                std::string clientIP = inet_ntoa(client.sin_addr);
                uint16_t clientPort = ntohs(client.sin_port);

                std::cout << "server accept " << clientIP + "-" << clientPort << " "
                          << sock << " form " << _listen_sock << " success!" << std::endl;

                // 多线程根据套接字进行通信业务处理
                pid_t id=fork();
                if(id<0)
                {
                    //创建子进程失败,暂时不与当前客户进行通信连接
                    close(sock);
                    std::cerr<<"fork fail"<<std::endl;
                }
                else if(id==0)
                {
                    //子进程不需要监听,关闭
                    close(_listen_sock);

                    //根据套接字进行通信业务处理
                    Service(sock,clientIP,clientPort);
                    exit(0);
                }
                else
                {
                    //父进程等待子进程
                    pid_t ret=waitpid(id,nullptr,WNOHANG);//非阻塞式等待
                    if(ret==id) std::cout<<"wait "<<id<<"sucsess!";
                }
            }
        }

创建子进程处理客户端请求

        子进程(id == 0):

  • 关闭监听套接字_listen_sock,因为子进程不需要继续监听新的连接。
  • 调用Service函数处理客户端的通信业务。
  • 处理完成后,调用exit(0)退出子进程。

        父进程(id > 0):

  • 调用waitpid函数以非阻塞模式等待子进程结束(通过设置WNOHANG标志)。
  • 如果waitpid成功(返回值等于子进程的ID),输出等待成功的信息。

缺陷:

        阻塞waitpid的使用问题:

  • 虽然waitpid被设置为非阻塞模式(WNOHANG),但父进程在每次循环中都会调用waitpid。如果子进程尚未退出,waitpid会立即返回0,这会导致父进程频繁调用waitpid,增加CPU的负担。

        accept的阻塞特性:

  • accept函数是阻塞式的,它会等待新的客户端连接请求。当有新的连接请求时,accept返回客户端的套接字描述符。
  • accept阻塞期间,父进程无法执行其他操作,包括调用waitpid来回收子进程资源。

        非阻塞waitpid的局限性:

  • waitpid被设置为非阻塞模式(WNOHANG),这意味着它会在没有子进程退出的情况下立即返回0
  • 如果子进程在accept阻塞期间退出,父进程无法及时调用waitpid来回收子进程资源,从而导致子进程变成僵尸进程。

        僵尸进程的产生:

  • 子进程退出后,操作系统会保留其状态信息,直到父进程调用waitwaitpid来回收这些信息。如果父进程没有及时调用这些函数,子进程就会变成僵尸进程,占用系统资源。
2.3、多进程版本-信号忽略(推荐)
 // 启动服务器-v3版本-多进程3(忽略SIGCHLD信号)
        void Start()
        {
            while (!_isrunning)
            {
                //0.忽略SIGCHLD信号-SIG_IGN表示忽略,父进程无需等待
                signal(SIGCHLD,SIG_IGN);

                // 1.处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);

                if (sock < 0)
                {
                    std::cerr << "accept fail" << strerror(errno) << std::endl;
                    continue;
                }

                // 2.连接成功,获取客户端信息
                std::string clientIP = inet_ntoa(client.sin_addr);
                uint16_t clientPort = ntohs(client.sin_port);

                std::cout << "server accept " << clientIP + "-" << clientPort << " "
                          << sock << " form " << _listen_sock << " success!" << std::endl;

                // 多线程根据套接字进行通信业务处理
                pid_t id=fork();
                if(id<0)
                {
                    //创建子进程失败,暂时不与当前客户进行通信连接
                    close(sock);
                    std::cerr<<"fork fail"<<std::endl;
                }
                else if(id==0)
                {
                    //子进程不需要监听,关闭
                    close(_listen_sock);

                    //根据套接字进行通信业务处理
                    Service(sock,clientIP,clientPort);
                    exit(0);
                }
            }
        }

在Unix和类Unix系统中,子进程退出时会向父进程发送SIGCHLD信号。父进程可以通过处理这个信号来回收子进程的资源,例如调用waitwaitpid来获取子进程的状态信息。当SIGCHLD信号被忽略时,操作系统会自动回收子进程的资源,清理子进程的状态信息。通过忽略SIGCHLD信号,父进程不需要显式调用waitwaitpid,从而避免了僵尸进程的产生。

2.4、多进程版本-修改SIGCHLD信号默认行为-回收子进程
// 启动服务器-v4版本-多进程4(设置SIGCHLD信号处理动作为子进程回收)
        static void handler(int signo)
        {
            printf("进程 %d 捕捉到了 %d 信号\n",getpid(),signo);
            pid_t ret=waitpid(-1,NULL,WNOHANG);
            if(ret>0) printf("父进程:%d 已经成功回收了 %d 号进程\n", getpid(), ret);
        }

        void Start()
        {
            while (!_isrunning)
            {
                //0.忽略SIGCHLD信号-SIG_IGN表示忽略,父进程无需等待
                signal(SIGCHLD,handler);

                // 1.处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);

                if (sock < 0)
                {
                    std::cerr << "accept fail" << strerror(errno) << std::endl;
                    continue;
                }

                // 2.连接成功,获取客户端信息
                std::string clientIP = inet_ntoa(client.sin_addr);
                uint16_t clientPort = ntohs(client.sin_port);

                std::cout << "server accept " << clientIP + "-" << clientPort << " "
                          << sock << " form " << _listen_sock << " success!" << std::endl;

                // 多线程根据套接字进行通信业务处理
                pid_t id=fork();
                if(id<0)
                {
                    //创建子进程失败,暂时不与当前客户进行通信连接
                    close(sock);
                    std::cerr<<"fork fail"<<std::endl;
                }
                else if(id==0)
                {
                    //子进程不需要监听,关闭
                    close(_listen_sock);

                    //根据套接字进行通信业务处理
                    Service(sock,clientIP,clientPort);
                    exit(0);
                }
            }
        }
2.5、多进程版本-设置孙子进程(推荐)
 启动服务器-v5版本-多进程5(设置孙子进程)
        void Start()
        {
            while (!_isrunning)
            {
                // 1.处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);

                if (sock < 0)
                {
                    std::cerr << "accept fail" << strerror(errno) << std::endl;
                    continue;
                }

                // 2.连接成功,获取客户端信息
                std::string clientIP = inet_ntoa(client.sin_addr);
                uint16_t clientPort = ntohs(client.sin_port);

                std::cout << "server accept " << clientIP + "-" << clientPort << " "
                          << sock << " form " << _listen_sock << " success!" << std::endl;

                // 多线程根据套接字进行通信业务处理
                pid_t id=fork();
                if(id<0)
                {
                    //创建子进程失败,暂时不与当前客户进行通信连接
                    close(sock);
                    std::cerr<<"fork fail"<<std::endl;
                }
                else if(id==0)
                {
                    //子进程不需要监听,关闭
                    close(_listen_sock);

                    //创建孙子进程
                    if(fork()>0) exit(0);

                    //根据套接字进行通信业务处理
                    Service(sock,clientIP,clientPort);
                    exit(0);
                }
                else
                {
                    //父进程等待子进程
                    pid_t ret=waitpid(id,nullptr,0);//阻塞式等待
                    if(ret==id) std::cout<<"wait "<<id<<"sucsess!"<<std::endl;
                    close(sock);//子进程进行网络通信后,父进程不需要使用sock套接字了,就可以将其关闭
                }
            }
        }

通过子进程创建孙子进程,使得子进程可以直接退出,防止了父进程因等待子进程而阻塞,也防止了父进程因为被accept阻塞而无法及时回收子进程,使得子进程成为僵尸进程,把孙子进程交给操作系统去管理。

3、多线程版

3.1多线程版本-使用原生线程库
 struct ThreadData
        {
        public:
            ThreadData(int sock, const std::string &ip, const uint16_t &port, TcpServer *current)
                : _sock(sock), _clientIP(ip), _clientPort(port), _current(current)
            {
            }
            ~ThreadData() {}

            int _sock;
            std::string _clientIP;
            uint16_t _clientPort;
            TcpServer *_current;
        };
 
//启动服务器-v6版本,多线程
        static void* Routiue(void* args)
        {
            //线程分离
            pthread_detach(pthread_self());

            ThreadData* td=static_cast<ThreadData*>(args);
            td->_current->Service(td->_sock,td->_clientIP,td->_clientPort);

            delete td;
            return (void*)0;
        }

        void Start()
        {
            while(!_isrunning)
            {
                //1.处理连接请求
                struct sockaddr_in client;
                socklen_t len=sizeof(client);

                int sock=accept(_listen_sock,(struct sockaddr*)&client,&len);

                if(sock<0)
                {
                    std::cerr<<"accept fail"<<strerror(errno)<<std::endl;
                    continue;
                }

                //2.连接成功,获取客户端信息
                std::string clientIP=inet_ntoa(client.sin_addr);
                uint16_t clientPort=ntohs(client.sin_port);

                std::cout<<"server accept "<<clientIP+"-"<<clientPort<<" "
                <<sock<<" form "<<_listen_sock<<" success!"<<std::endl;

                //创建线程进行通信业务处理
                ThreadData* td=new ThreadData(sock,clientIP,clientPort,this);
                pthread_t p;
                pthread_create(&p,nullptr,Routiue,td);
            }
        }

创建线程处理客户端请求

  • 为每个客户端连接创建一个线程来处理通信业务。
  • 创建一个ThreadData对象,包含客户端的套接字描述符、IP地址、端口号以及指向当前服务器对象的指针。
  • 使用pthread_create创建线程,并将ThreadData对象作为参数传递给线程函数Routiue

在线程函数Routiue中:

  • 调用pthread_detach将线程设置为分离状态,这样线程退出时资源会自动释放。
  • 调用Service函数处理客户端的通信业务。
  • 删除ThreadData对象,释放资源。

缺陷:

线程资源管理问题

  • 每个客户端连接都创建一个线程,这在高并发场景下可能会导致线程数量过多,消耗大量系统资源(如线程栈空间、线程调度开销等)。
  • 如果客户端连接频繁,可能会导致线程创建和销毁的开销过大,影响服务器性能。
3.2、多线程版本-引入线程池
//进行业务处理
using cb_t =std::function<void(int,std::string,uint16_t)>;

class Task
{
public:
    Task()=default;
    Task(int sock,const std::string& ip,const uint16_t& port,const cb_t& cb)
        :_sock(sock),_ip(ip),_port(port),_cb(cb)
    {}
    ~Task(){} 
    
    void operator()()
    {
        //直接回调cb
        _cb(_sock,_ip,_port);
    }
private:
    int _sock;
    std::string _ip;
    uint16_t _port;
    cb_t _cb;//回调函数->业务处理
};
 
// 启动服务器-v7版本,引入线程池
        void Start()
        {
            while (!_isrunning)
            {
                // 1.处理连接请求
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);

                if (sock < 0)
                {
                    std::cerr << "accept fail" << strerror(errno) << std::endl;
                    continue;
                }

                // 2.连接成功,获取客户端信息
                std::string clientIP = inet_ntoa(client.sin_addr);
                uint16_t clientPort = ntohs(client.sin_port);

                std::cout << "server accept " << clientIP + "-" << clientPort << " "
                          << sock << " form " << _listen_sock << " success!" << std::endl;

                // 3.构建任务对象
                Task t(sock, clientIP, clientPort, std::bind(&TcpServer::Service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

                // 4.通过线程池操作将对象push到线程池中处理
                MyThreadPool<Task>::getInstance()->PushTask(t);
            }
        }

缺陷:

  • 在当前的多线程服务器实现中,客户端的最大连接数受限于线程池中线程的数量。这是因为每个客户端连接都需要一个线程来处理,而线程资源是有限的。在高并发场景下,线程池的线程数量可能会成为性能瓶颈。
  • 为了突破这个限制,可以使用高级I/O多路复用技术,如epoll(在Linux上)。这些技术允许单个线程同时管理多个文件描述符(如套接字),从而显著提高服务器的并发处理能力。
  • 使用epoll,单个线程可以同时管理成千上万的连接,而不需要为每个连接创建一个线程。这大大减少了线程创建和销毁的开销,提高了服务器的性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值