上海古都建筑设计集团,上海办公室装修设计公司,上海装修公司高质量的内容分享社区,上海装修公司我们不是内容生产者,我们只是上海办公室装修设计公司内容的搬运工平台

网络编程『简易TCP网络程序』

guduadmin291月前

🔭个人主页: 北 海

🛜所属专栏: Linux学习之旅、神奇的网络世界

💻操作环境: CentOS 7.6 阿里云远程服务器

网络编程『简易TCP网络程序』,成就一亿技术人,第1张


文章目录

  • 🌤️前言
  • 🌦️正文
    • TCP网络程序
    • 1.字符串回响
      • 1.1.核心功能
      • 1.2.程序结构
      • 服务器
      • 1.3.初始化服务器
      • 1.4.启动服务器
        • 1.4.1.处理连接请求
        • 1.4.2.业务处理
        • 1.4.3.回调函数
        • 1.5.服务器源文件
        • 客户端
        • 1.6.初始化客户端
        • 1.7.启动客户端
          • 1.7.1.尝试进行连接
          • 1.7.2.业务处理
          • 2.多进程版服务器
            • 2.1.核心功能
            • 2.2.创建子进程
            • 2.3.设置非阻塞
            • 3.多线程版服务器
              • 3.1.核心功能
              • 3.2.使用原生线程库
              • 3.3.使用线程池
              • 4.日志输出
                • 4.1.日志的重要性
                • 4.2.可变参数
                • 4.3.日志器实现
                • 4.4.应用于程序中
                • 4.5.持久化存储
                • 5.守护进程
                  • 5.1.会话、进程组、进程
                  • 5.2.守护进程化
                  • 6.完整代码
                  • 🌨️总结

                    🌤️前言

                    随着数字时代的来临,TCP网络程序已成为程序员不可或缺的技术领域。本博客将带领读者深入研究,从最基础的字符串回响开始,逐步探索至多进程、多线程服务器的高级实践。我们将详细探讨每个环节的核心功能和实现细节,致力于帮助读者深刻理解网络编程的本质。通过系统学习本博客内容,读者将获得构建稳健网络应用的重要技能,更加自信地应对日益复杂的软件开发挑战。这里将为你的编程旅程提供扎实的基础和深远的启示。


                    🌦️正文

                    TCP网络程序

                    接下来实现一批基于 TCP 协议的网络程序


                    1.字符串回响

                    1.1.核心功能

                    字符串回响程序类似于 echo 指令,客户端向服务器发送消息,服务器在收到消息后会将消息发送给客户端,该程序实现起来比较简单,同时能很好的体现 socket 套接字编程的流程

                    网络编程『简易TCP网络程序』,第2张

                    1.2.程序结构

                    这个程序我们已经基于 UDP 协议实现过了,换成 TCP 协议实现时,程序的结构是没有变化的,同样需要 server.hpp、server.cc、client.hpp、client.cc 这几个文件

                    创建 server.hpp 服务器头文件

                    #pragma once
                    #include 
                    #include 
                    #include 
                    #include 
                    #include 
                    #include "err.hpp"
                    namespace nt_server
                    {
                        const uint16_t default_port = 8888; // 默认端口号
                        class TcpServer
                        {
                        public:
                            TcpServer(const uint16_t port = default_port)
                                :port_(port)
                            {}
                            ~TcpServer()
                            {}
                            // 初始化服务器
                            void InitServer()
                            {}
                            // 启动服务器
                            void StartServer()
                            {}
                        private:
                            int sock_; // 套接字(存疑)
                            uint16_t port_; // 端口号
                        };
                    }
                    

                    注意: 这里的 sock_ 套接字成员后面需要修改

                    创建 server.cc 服务器源文件

                    #include  // 智能指针头文件
                    #include "server.hpp"
                    using namespace std;
                    using namespace nt_server;
                    int main()
                    {
                        unique_ptr usvr (new TcpServer());
                        usvr->InitServer();
                        usvr->StartServer();
                        return 0;
                    }
                    

                    创建 client.hpp 客户端头文件

                    #pragma once
                    #include 
                    #include 
                    #include 
                    #include 
                    #include 
                    #include 
                    #include "err.hpp"
                    namespace nt_client
                    {
                        class TcpClient
                        {
                        public:
                            TcpClient(const std::string& ip, const uint16_t port)
                                :server_ip_(ip), server_port_(port)
                            {}
                            ~TcpClient()
                            {}
                            // 初始化客户端
                            void InitClient()
                            {}
                            // 启动客户端
                            void StartClient()
                            {}
                            
                        private:
                            int sock_; // 套接字
                            std::string server_ip_; // 服务器IP
                            uint16_t server_port_; // 服务器端口号
                        };
                    }
                    

                    创建 client.cc 客户端源文件

                    #include 
                    #include "client.hpp"
                    using namespace std;
                    using namespace nt_client;
                    void Usage(const char *program)
                    {
                        cout << "Usage:" << endl;
                        cout << "\t" << program << " ServerIP ServerPort" << endl;
                    }
                    int main(int argc, char *argv[])
                    {
                        if (argc != 3)
                        {
                            // 错误的启动方式,提示错误信息
                            Usage(argv[0]);
                            return USAGE_ERR;
                        }
                        // 服务器IP与端口号
                        string ip(argv[1]);
                        uint16_t port = stoi(argv[2]);
                        unique_ptr usvr(new TcpClient(ip, port));
                        usvr->InitClient();
                        usvr->StartClient();
                        return 0;
                    }
                    

                    同时需要一个 Makefile 文件,用于快速编译和清理可执行程序

                    创建 Makefile 文件

                    .PHONY:all
                    all:server client
                    server:server.cc
                    	g++ -o $@ $^ -std=c++11
                    	
                    client:client.cc
                    	g++ -o $@ $^ -std=c++11
                    .PHONY:clean
                    clean:
                    	rm -rf server client
                    

                    最后为了方便判断程序错误,可以增加上一篇文章中的 err.hpp 头文件,里面包含错误码与简易错误信息

                    创建 err.hpp 错误码头文件

                    #pragma once
                    enum
                    {
                        USAGE_ERR = 1,
                        SOCKET_ERR,
                        BIND_ERR
                    };
                    

                    接下来开始填充代码内容


                    服务器


                    1.3.初始化服务器

                    基于 TCP 协议实现的网络程序也需要 创建套接字、绑定 IP 和端口号

                    在使用 socket 函数创建套接字时,UDP 协议需要指定参数2为 SOCK_DGRAM,TCP 协议则是指定参数2为 SOCK_STREAM

                    注:关于 socket、bind、sockaddr 的细节,可以看看这篇文章《网络编程『socket套接字 ‖ 简易UDP网络程序』》

                    InitServer() 初始化服务器函数 — 位于 server.hpp 服务器头文件中的 TcpServer 类

                    const uint16_t default_port = 8888; // 默认端口号
                        
                    // 初始化服务器
                    void InitServer()
                    {
                        // 1.创建套接字
                        sock_ = socket(AF_INET, SOCK_STREAM, 0);
                        if(sock_ == -1)
                        {
                            std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
                            exit(SOCKET_ERR);
                        }
                        std::cout << "Create Socket Success! " << sock_ << std::endl;
                        // 2.绑定IP地址与端口号
                        struct sockaddr_in local;
                        memset(&local, 0, sizeof(local)); // 清零
                        local.sin_family = AF_INET;
                        local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                        local.sin_port = htons(port_);
                        
                        if(bind(listensock_, (const sockaddr*)&local, sizeof(local)))
                        {
                            std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                            exit(BIND_ERR);
                        }
                                
                        // 3.TODO
                    }
                    

                    注意: 在绑定端口号时,一定需要把主机序列转换为网络序列

                    为什么在绑定端口号阶段需要手动转换为网络序列,而在发送信息阶段则不需要?

                    这是因为在发送信息阶段,recvfrom / sendto 等函数会自动将需要发送的信息转换为网络序列,接收信息时同样会将其转换为主机序列,所以不需要手动转换

                    如果使用的 UDP 协议,那么初始化服务器到此就结束了,但我们本文中使用的是 TCP 协议,这是一个 面向连接 的传输层协议,意味着在初始化服务器时,需要设置服务器为 监听 状态

                    使用到的函数是 listen 函数

                    #include           /* See NOTES */
                    #include 
                    int listen(int sockfd, int backlog);
                    

                    参数解读:

                    • sockfd 通过该套接字进行监听
                    • backlog 全连接队列最大长度

                      返回值:监听成功返回 0,失败返回 -1

                      网络编程『简易TCP网络程序』,第3张

                      这里的参数2需要设置一个整数,通常为 16、32、64...,表示 全连接队列 的最大长度,关于 全连接队列 的详细知识放到后续博客中讲解,这里只需要直接使用

                      server.hpp 服务器头文件

                      #pragma once
                      #include 
                      #include 
                      #include 
                      #include 
                      #include 
                      #include 
                      #include 
                      #include "err.hpp"
                      namespace nt_server
                      {
                          const uint16_t default_port = 8888; // 默认端口号
                          const int backlog = 32; // 全连接队列的最大长度
                          class TcpServer
                          {
                          public:
                              TcpServer(const uint16_t port = default_port)
                                  :port_(port)
                              {}
                              ~TcpServer()
                              {}
                              // 初始化服务器
                              void InitServer()
                              {
                                  // 1.创建套接字
                                  sock_ = socket(AF_INET, SOCK_STREAM, 0);
                                  if(sock_ == -1)
                                  {
                                      std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
                                      exit(SOCKET_ERR);
                                  }
                                  std::cout << "Create Socket Success! " << sock_ << std::endl;
                                  // 2.绑定IP地址与端口号
                                  struct sockaddr_in local;
                                  memset(&local, 0, sizeof(local)); // 清零
                                  local.sin_family = AF_INET;
                                  local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                                  local.sin_port = htons(port_);
                                  if(bind(listensock_, (const sockaddr*)&local, sizeof(local)))
                                  {
                                      std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                                      exit(BIND_ERR);
                                  }
                           
                                  // 3.监听
                                  if(listen(sock_, backlog) == -1)
                                  {
                                      std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
                                      exit(LISTEN_ERR);
                                  }
                                  std::cout << "Listen Success!" << std::endl;
                              }
                              // 启动服务器
                              void StartServer()
                              {}
                          private:
                              int sock_; // 套接字(存疑)
                              uint16_t port_; // 端口号
                          };
                      }
                      

                      至此基于 TCP 协议实现的初始化服务器函数就填充完成了,编译并运行服务器,显示初始化服务器成功

                      网络编程『简易TCP网络程序』,第4张

                      1.4.启动服务器

                      1.4.1.处理连接请求

                      TCP 是面向连接,当有客户端发起连接请求时,TCP 服务器需要正确识别并尝试进行连接,当连接成功时,与其进行通信,可使用 accept 函数进行连接

                      #include           /* See NOTES */
                      #include 
                      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
                      

                      参数解读:

                      • sockfd 服务器用于处理连接请求的 socket 套接字
                      • addr 客户端的 sockaddr 结构体信息
                      • addrlen 客户端的 sockaddr 结构体大写

                        其中 addr 与 addrlen 是一个 输入输出型 参数,类似于 recvfrom 中的参数

                        返回值:连接成功返回一个用于通信的 socket 套接字(文件描述符),失败返回 -1

                        网络编程『简易TCP网络程序』,第5张

                        网络编程『简易TCP网络程序』,第6张

                        这也就意味着之前我们在 TcpServer 中创建的类内成员 sock_ 并非是用于通信,而是专注于处理连接请求,在 TCP 服务器中,这种套接字称为 监听套接字

                        使用 accept 函数处理连接请求

                        server.hpp 服务器头文件

                        #pragma once
                        #include 
                        #include 
                        #include 
                        #include 
                        #include 
                        #include 
                        #include 
                        #include "err.hpp"
                        namespace nt_server
                        {
                            const uint16_t default_port = 8888; // 默认端口号
                            const int backlog = 32; // 全连接队列的最大长度
                            class TcpServer
                            {
                            public:
                                TcpServer(const uint16_t port = default_port)
                                    :port_(port), quit_(false)
                                {}
                                ~TcpServer()
                                {}
                                // 初始化服务器
                                void InitServer()
                                {
                                    // 1.创建监听套接字
                                    listensock_ = socket(AF_INET, SOCK_STREAM, 0);
                                    if(listensock_ == -1)
                                    {
                                        std::cerr << "Create ListenSocket Fail!" << strerror(errno) << std::endl;
                                        exit(SOCKET_ERR);
                                    }
                                    std::cout << "Create ListenSocket Success! " << listensock_ << std::endl;
                                    // 2.绑定IP地址与端口号
                                    struct sockaddr_in local;
                                    memset(&local, 0, sizeof(local)); // 清零
                                    local.sin_family = AF_INET;
                                    local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                                    local.sin_port = htons(port_);
                                    if(bind(listensock_, (const sockaddr*)&local, sizeof(local)))
                                    {
                                        std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                                        exit(BIND_ERR);
                                    }
                             
                                    // 3.监听
                                    if(listen(listensock_, backlog) == -1)
                                    {
                                        std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
                                        exit(LISTEN_ERR);
                                    }
                                    std::cout << "Listen Success!" << std::endl;
                                }
                                // 启动服务器
                                void StartServer()
                                {
                                    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.根据 sock 套接字进行通信
                                        Service(sock, clientip, clientport);
                                    }
                                }
                            private:
                                int listensock_; // 监听套接字
                                uint16_t port_; // 端口号
                                bool quit_; // 判断服务器是否结束运行
                            };
                        }
                        
                        1.4.2.业务处理

                        对于 TCP 服务器来说,它是面向字节流传输的,我们之前使用的文件相关操作也是面向字节流,凑巧的是在 Linux 中网络是以挂接在文件系统的方式实现的,种种迹象表明:可以通过文件相关接口进行通信

                        • read 从文件中读取信息(接收消息)
                        • write 向文件中写入信息(发送消息)

                          这两个系统调用的核心参数是 fd(文件描述符),即服务器与客户端在连接成功后,获取到的 socket 套接字,所以接下来可以按文件操作的套路,完成业务处理

                          Service() 业务处理函数 — 位于 server.hpp 服务器头文件中的 TcpServer 类

                          // 业务处理
                          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); // 预留 '\0' 的位置
                                  if (n > 0)
                                  {
                                      // 读取成功
                                      buff[n] = '\0';
                                      std::cout << "Server get: " << buff << " from " << 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;
                                  }
                              }
                          }
                          
                          1.4.3.回调函数

                          为了更好的实现功能解耦,这里将真正的业务处理函数交给上层处理,编写完成后传给 TcpServer 对象即可,当然,在 TcpServer 类中需要添加对应的类型

                          这里设置回调函数的返回值为 string,参数同样为 string

                          server.hpp 服务器头文件

                          #pragma once
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include "err.hpp"
                          namespace nt_server
                          {
                              const uint16_t default_port = 8888; // 默认端口号
                              const int backlog = 32;             // 全连接队列的最大长度
                              using func_t = std::function; // 回调函数类型
                              class TcpServer
                              {
                              public:
                                  TcpServer(const func_t &func, const uint16_t port = default_port)
                                      : func_(func), port_(port), quit_(false)
                                  {
                                  }
                                  ~TcpServer()
                                  {
                                  }
                                  // 初始化服务器
                                  void InitServer()
                                  {
                                      // 1.创建监听套接字
                                      listensock_ = socket(AF_INET, SOCK_STREAM, 0);
                                      if (listensock_ == -1)
                                      {
                                          std::cerr << "Create ListenSocket Fail!" << strerror(errno) << std::endl;
                                          exit(SOCKET_ERR);
                                      }
                                      std::cout << "Create ListenSocket Success! " << listensock_ << std::endl;
                                      // 2.绑定IP地址与端口号
                                      struct sockaddr_in local;
                                      memset(&local, 0, sizeof(local)); // 清零
                                      local.sin_family = AF_INET;
                                      local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                                      local.sin_port = htons(port_);
                                      if (bind(listensock_, (const sockaddr *)&local, sizeof(local)))
                                      {
                                          std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                                          exit(BIND_ERR);
                                      }
                                      // 3.监听
                                      if (listen(listensock_, backlog) == -1)
                                      {
                                          std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
                                          exit(LISTEN_ERR);
                                      }
                                      std::cout << "Listen Success!" << std::endl;
                                  }
                                  // 启动服务器
                                  void StartServer()
                                  {
                                      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.根据 sock 套接字进行通信
                                          Service(sock, clientip, clientport);
                                      }
                                  }
                                  // 业务处理
                                  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); // 预留 '\0' 的位置
                                          if (n > 0)
                                          {
                                              // 读取成功
                                              buff[n] = '\0';
                                              std::cout << "Server get: " << buff << " from " << 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 listensock_; // 监听套接字
                                  uint16_t port_;  // 端口号
                                  bool quit_;      // 判断服务器是否结束运行
                                  func_t func_;    // 回调函数
                              };
                          }
                          

                          服务器头文件准备完成,接下来就是填充 server.cc 服务器源文件

                          1.5.服务器源文件

                          对于当前的 TCP 网络程序(字符串回响)来说,业务处理函数逻辑非常简单,无非就是直接将客户端发送过来的消息,重新转发给客户端

                          server.cc 服务器源文件

                          #include  // 智能指针头文件
                          #include 
                          #include "server.hpp"
                          using namespace std;
                          using namespace nt_server;
                          // 业务处理回调函数(字符串回响)
                          string echo(string request)
                          {
                              return request;
                          }
                          int main()
                          {
                              unique_ptr usvr (new TcpServer(echo)); // 将回调函数进行传递
                              usvr->InitServer();
                              usvr->StartServer();
                              return 0;
                          }
                          

                          尝试编译并运行服务器,可以看到当前 bash 已经被我们的服务器程序占用了,重新打开一个终端,并通过 netstat 命令查看网络使用情况(基于 TCP 协议)

                          netstat -nltp
                          

                          网络编程『简易TCP网络程序』,第7张

                          当前服务确实使用的是 8888 端口,并且采用的是 TCP 协议

                          网络编程『简易TCP网络程序』,第8张


                          客户端


                          1.6.初始化客户端

                          对于客户端来说,服务器的 IP 地址与端口号是两个不可或缺的元素,因此在客户端类中,server_ip 和 server_port 这两个成员是少不了的,当然得有 socket 套接字

                          初始化客户端只需要干一件事:创建套接字,客户端是主动发起连接请求的一方,也就意味着它不需要使用 listen 函数设置为监听状态

                          注意: 客户端也是需要 bind 绑定的,但不需要自己手动绑定,由操作系统帮我们自动完成

                          client.hpp 客户端头文件

                          #pragma once
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include 
                          #include "err.hpp"
                          namespace nt_client
                          {
                              class TcpClient
                              {
                              public:
                                  TcpClient(const std::string& ip, const uint16_t port)
                                      :server_ip_(ip), server_port_(port)
                                  {}
                                  ~TcpClient()
                                  {}
                                  // 初始化客户端
                                  void InitClient()
                                  {
                                      // 创建套接字
                                      sock_ = socket(AF_INET, SOCK_STREAM, 0);
                                      if (sock_ == -1)
                                      {
                                          std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
                                          exit(SOCKET_ERR);
                                      }
                                      
                                      std::cout << "Create Sock Succeess! " << sock_ << std::endl;
                                  }
                                  // 启动客户端
                                  void StartClient()
                                  {}
                                  
                              private:
                                  int sock_; // 套接字
                                  std::string server_ip_; // 服务器IP
                                  uint16_t server_port_; // 服务器端口号
                              };
                          }
                          

                          编译并运行客户端,显示 socket 套接字创建成功

                          网络编程『简易TCP网络程序』,第9张

                          1.7.启动客户端

                          1.7.1.尝试进行连接

                          因为 TCP 协议是面向连接的,服务器已经处于处理连接请求的状态了,客户端现在需要做的就是尝试进行连接,使用 connect 函数进行连接

                          #include           /* See NOTES */
                          #include 
                          int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
                          

                          参数解读:

                          • sockfd 需要进行连接的套接字
                          • addr 服务器的 sockaddr 结构体信息
                          • addrlen 服务器的 sockaddr 结构体大小

                            返回值:连接成功返回 0,连接失败返回 -1

                            网络编程『简易TCP网络程序』,第10张

                            网络编程『简易TCP网络程序』,第11张

                            在连接过程中,可能遇到很多问题,比如 网络传输失败、服务器未启动 等,这些问题的最终结果都是客户端连接失败,如果按照之前的逻辑(失败就退出),那么客户端的体验感会非常不好,因此在面对连接失败这种常见问题时,客户端应该尝试重连,如果重连数次后仍然失败,才考虑终止进程

                            注意: 在进行重连时,可以使用 sleep() 等函数使程序睡眠一会,给网络恢复留出时间

                            StartClient() 启动客户端函数 — 位于 client.hpp 中的 TcpClient 类

                            // 启动客户端
                            void StartClient()
                            {
                                // 填充服务器的 sockaddr_in 结构体信息
                                struct sockaddr_in server;
                                socklen_t len = sizeof(server);
                                memset(&server, 0, len);
                                server.sin_family = AF_INET;
                                inet_aton(server_ip_.c_str(), &server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法
                                server.sin_port = htons(server_port_);
                                // 尝试重连 5 次
                                int n = 5;
                                while(n)
                                {
                                    int ret = connect(sock_, (const struct sockaddr*)&server, len);
                                    if(ret == 0)
                                    {
                                        // 连接成功,可以跳出循环
                                        break;
                                    }
                                    // 尝试进行重连
                                    std::cerr << "网络异常,正在进行重连... 剩余连接次数: " << --n << std::endl;
                                    sleep(1);
                                }
                                // 如果剩余重连次数为 0,证明连接失败
                                if(n == 0)
                                {
                                    std::cerr << "连接失败! " << strerror(errno) << std::endl;
                                    close(sock_);
                                    exit(CONNECT_ERR);
                                }
                                // 连接成功
                                std::cout << "连接成功!" << std::endl;
                                // 进行业务处理
                                // Service();
                            }  
                            

                            当然相应的错误码也得添加

                            err.hpp 错误码头文件

                            #pragma once
                            enum
                            {
                                USAGE_ERR = 1,
                                SOCKET_ERR,
                                BIND_ERR,
                                LISTEN_ERR,
                                CONNECT_ERR
                            };
                            

                            现在先不启动服务器,编译并启动客户端,模拟连接失败的情况

                            网络编程『简易TCP网络程序』,第12张

                            如果在数秒之后启动再服务器,可以看到重连成功

                            网络编程『简易TCP网络程序』,第13张

                            这种重连机制在实际中非常常见,出现这种

                            网络编程『简易TCP网络程序』,第14张

                            1.7.2.业务处理

                            客户端在进行业务处理时,同样可以使用 read 和 write 进行网络通信

                            Service() 业务处理函数 — 位于 client.hpp 客户端头文件中的 TcpClient 类

                            // 业务处理
                            void Service()
                            {
                                char buff[1024];
                                std::string who = server_ip_ + "-" + std::to_string(server_port_);
                                while(true)
                                {
                                    // 由用户输入信息
                                    std::string msg;
                                    std::cout << "Please Enter >> ";
                                    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;
                                    }
                                }
                            }
                            

                            至此整个 基于 TCP 协议的字符串回响程序 就完成了,下面来看看效果

                            网络编程『简易TCP网络程序』,第15张

                            可以看到,当客户端向服务器发起连接请求时,服务器可以识别并接受连接,双方建立连接关系后,可以正常进行通信;当客户端主动退出(断开连接),服务器也能感知到,并判断出是谁断开了连接

                            如果在通信过程中,服务器主动断开了连接,客户端也能感知到

                            网络编程『简易TCP网络程序』,第16张

                            如果我们此时立马重启服务器,会发现短期内无法再次启动服务(显示端口正在被占用),这是由于 TCP 协议断开连接时的特性导致的(正在处于 TIME_WAIT 状态),详细原因将会在后续博客中讲解

                            网络编程『简易TCP网络程序』,第17张


                            2.多进程版服务器

                            2.1.核心功能

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

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

                            网络编程『简易TCP网络程序』,第18张

                            具体表现为下面这种情况

                            网络编程『简易TCP网络程序』,第19张

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

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

                            网络编程『简易TCP网络程序』,第20张

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

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

                            网络编程『简易TCP网络程序』,第21张

                            2.2.创建子进程

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

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

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

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

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

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

                              StartServer() 服务器启动函数 — 位于 server.hpp 的 TcpServer 类

                              // 进程创建、等待所需要的头文件
                              #include 
                              #include 
                              #include 
                              // 启动服务器
                              void StartServer()
                              {
                                  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(id == 0)
                                      {
                                          // 子进程内
                                          close(listensock_); // 子进程不需要监听(建议关闭)
                                          // 执行业务处理函数
                                          Service(sock, clientip, clientport);
                                          exit(0); // 子进程退出
                                      }
                                      else
                                      {
                                          // 父进程需要等待子进程
                                          pid_t ret = waitpid(id, nullptr, 0); // 默认为阻塞式等待
                                          if(ret == id)
                                              std::cout << "Wait " << id << " success!";
                                      }
                                  }
                              }
                              

                              虽然此时成功创建了子进程,但父进程(处理连接请求)仍然需要等待子进程退出后,才能继续运行,说白了就是 父进程现在处于阻塞等待状态,需要设置为 非阻塞等待

                              网络编程『简易TCP网络程序』,第22张

                              网络编程『简易TCP网络程序』,第23张

                              2.3.设置非阻塞

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

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

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

                              详见 《Linux进程控制【创建、终止、等待】》

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

                              网络编程『简易TCP网络程序』,第24张

                              网络编程『简易TCP网络程序』,第25张

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


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

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

                              详见 《Linux进程信号【信号处理】》

                              直接在 StartServer() 服务器启动函数刚开始时,使用 signal() 函数设置 SIGCHLD 信号的执行动作为 忽略

                              忽略了该信号后,就不需要父进程等待子进程退出了(由操作系统承担)

                              #include  // 信号处理相关头文件
                              // 启动服务器
                              void StartServer()
                              {
                                  // 忽略 SIGCHLD 信号
                                  signal(SIGCHLD, SIG_IGN);
                                  
                                  while (!quit_)
                                  {
                                     	// ...
                                     	
                                      // 3.创建子进程
                                      pid_t id = fork();
                                      if(id < 0)
                                      {
                                          // 创建子进程失败,暂时不与当前客户端建立通信会话
                                          close(sock);
                                          std::cerr << "Fork Fail!" << std::endl;
                                      }
                                      else if(id == 0)
                                      {
                                          // 子进程内
                                          close(listensock_); // 子进程不需要监听(建议关闭)
                                          // 执行业务处理函数
                                          Service(sock, clientip, clientport);
                                          exit(0); // 子进程退出
                                      }
                                  }
                              }
                              

                              网络编程『简易TCP网络程序』,第26张

                              网络编程『简易TCP网络程序』,第27张

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


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

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

                              详见 《Linux进程信号【信号处理】》

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

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

                              #include  // 信号处理相关头文件
                              // 需要设置为静态
                              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);
                                  
                                  while (!quit_)
                                  {
                                     	// ...
                                     	
                                      // 3.创建子进程
                                      pid_t id = fork();
                                      if(id < 0)
                                      {
                                          // 创建子进程失败,暂时不与当前客户端建立通信会话
                                          close(sock);
                                          std::cerr << "Fork Fail!" << std::endl;
                                      }
                                      else if(id == 0)
                                      {
                                          // 子进程内
                                          close(listensock_); // 子进程不需要监听(建议关闭)
                                          // 执行业务处理函数
                                          Service(sock, clientip, clientport);
                                          exit(0); // 子进程退出
                                      }
                                  }
                              }
                              

                              网络编程『简易TCP网络程序』,第28张

                              网络编程『简易TCP网络程序』,第29张

                              为什么不是很推荐这种方法?因为这种方法实现起来比较麻烦,不如直接忽略 SIGCHLD 信号


                              方式四:设置孙子进程(不是很推荐)

                              众所周知,父进程只需要对子进程负责,至于孙子进程交给子进程负责,如果某个子进程的父进程终止运行了,那么它就会变成 孤儿进程,父进程会变成 1 号进程,也就是由操作系统领养,回收进程的重担也交给了操作系统

                              可以利用该特性,在子进程内部再创建一个子进程(孙子进程),然后子进程退出,父进程可以直接回收(不必阻塞),子进程(孙子进程)的父进程变成 1 号进程

                              这种实现方法比较巧妙,而且与我们后面即将学到的 守护进程 有关

                              注意: 使用这种方式时,父进程是需要等待子进程退出的

                              // 启动服务器
                              void StartServer()
                              {
                                  while (!quit_)
                                  {
                                 		// ...
                                 		
                                      // 3.创建子进程
                                      pid_t id = fork();
                                      if(id < 0)
                                      {
                                          // 创建子进程失败,暂时不与当前客户端建立通信会话
                                          close(sock);
                                          std::cerr << "Fork Fail!" << std::endl;
                                      }
                                      else if(id == 0)
                                      {
                                          // 子进程内
                                          close(listensock_); // 子进程不需要监听(建议关闭)
                                          // 再创建孙子进程
                                          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 << " success!";
                                      }
                                  }
                              }
                              

                              网络编程『简易TCP网络程序』,第30张

                              网络编程『简易TCP网络程序』,第31张

                              这种方法代码也很简单,但依旧不推荐,因为倘若连接请求变多,会导致孤儿进程变多,孤儿进程由操作系统接管,数量变多会给操作系统带来负担

                              以上就是设置 非阻塞 的四种方式,推荐使用方式二:忽略 SIGCHLD 信号

                              至此我们的 字符串回响程序 可以支持多客户端了

                              网络编程『简易TCP网络程序』,第32张

                              网络编程『简易TCP网络程序』,第33张


                              细节补充:当子进程取走 sock 套接字进行网络通信后,父进程就不需要使用 sock 套接字了,可以将其进行关闭,下次连接时继续使用,避免文件描述符不断增长

                              StartServer() 服务器启动函数 — 位于 server.hpp 服务器头文件中的 TcpServer 类

                              // 启动服务器
                              void StartServer()
                              {
                                  // 忽略 SIGCHLD 信号
                                  signal(SIGCHLD, SIG_IGN);
                                  while (!quit_)
                                  {
                                      // 1.处理连接请求
                                      // ...
                                      // 2.如果连接失败,继续尝试连接
                                      // ...
                                      // 连接成功,获取客户端信息
                                      // ...
                                      // 3.创建子进程
                                      // ...
                                      close(sock); // 父进程不再需要资源(建议关闭)
                                  }
                              }
                              

                              网络编程『简易TCP网络程序』,第34张

                              这个补丁可以减少资源消耗,建议加上,前面是忘记加了,并且不太好修改,server.hpp 服务器头文件完整代码如下

                              #pragma once
                              #include 
                              #include 
                              #include 
                              #include 
                              #include 
                              #include 
                              #include  // 信号处理相关头文件
                              #include  // 进程等待时需要包含该头文件
                              #include 
                              #include 
                              #include 
                              #include 
                              #include "err.hpp"
                              namespace nt_server
                              {
                                  const uint16_t default_port = 8888; // 默认端口号
                                  const int backlog = 32;             // 全连接队列的最大长度
                                  using func_t = std::function; // 回调函数类型
                                  class TcpServer
                                  {
                                  public:
                                      TcpServer(const func_t &func, const uint16_t port = default_port)
                                          : func_(func), port_(port), quit_(false)
                                      {
                                      }
                                      ~TcpServer()
                                      {
                                      }
                                      // 初始化服务器
                                      void InitServer()
                                      {
                                          // 1.创建监听套接字
                                          listensock_ = socket(AF_INET, SOCK_STREAM, 0);
                                          if (listensock_ == -1)
                                          {
                                              std::cerr << "Create ListenSocket Fail!" << strerror(errno) << std::endl;
                                              exit(SOCKET_ERR);
                                          }
                                          std::cout << "Create ListenSocket Success! " << listensock_ << std::endl;
                                          // 2.绑定IP地址与端口号
                                          struct sockaddr_in local;
                                          memset(&local, 0, sizeof(local)); // 清零
                                          local.sin_family = AF_INET;
                                          local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                                          local.sin_port = htons(port_);
                                          if (bind(listensock_, (const sockaddr *)&local, sizeof(local)))
                                          {
                                              std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                                              exit(BIND_ERR);
                                          }
                                          // 3.监听
                                          if (listen(listensock_, backlog) == -1)
                                          {
                                              std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
                                              exit(LISTEN_ERR);
                                          }
                                          std::cout << "Listen Success!" << std::endl;
                                      }
                                      // 启动服务器
                                      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(id == 0)
                                              {
                                                  // 子进程内
                                                  close(listensock_); // 子进程不需要监听(建议关闭)
                                                  // 执行业务处理函数
                                                  Service(sock, clientip, clientport);
                                                  exit(0); // 子进程退出
                                              }
                                              close(sock); // 父进程不再需要资源(必须关闭)
                                          }
                                      }
                                      // 业务处理
                                      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); // 预留 '\0' 的位置
                                              if (n > 0)
                                              {
                                                  // 读取成功
                                                  buff[n] = '\0';
                                                  std::cout << "Server get: " << buff << " from " << 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 listensock_; // 监听套接字
                                      uint16_t port_;  // 端口号
                                      bool quit_;      // 判断服务器是否结束运行
                                      func_t func_;    // 回调函数
                                  };
                              }
                              

                              3.多线程版服务器

                              3.1.核心功能

                              通过多线程,实现支持多客户端同时通信的服务器

                              核心功能:服务器与客户端成功连接后,创建一个线程,服务于客户端的业务处理

                              网络编程『简易TCP网络程序』,第35张

                              这里先通过 原生线程库 模拟实现

                              3.2.使用原生线程库

                              关于 原生线程库 中对于线程的操作可以看看这篇文章《Linux多线程【线程控制】》

                              线程的回调函数中需要 Service() 业务处理函数中的所有参数,同时也需要具备访问 Service() 业务处理函数的能力,单凭一个 void* 的参数是无法解决的,为此可以创建一个类,里面可以包含我们所需要的参数

                              ThreadData 类 — 位于 server.hpp 服务器头文件中

                              // 包含我们所需参数的类型
                              class ThreadData
                              {
                              public:
                                  ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr)
                                      :sock_(sock), clientip_(ip), clientport_(port), current_(ptr)
                                  {}
                              // 设置为公有是为了方便访问
                              public:
                                  int sock_;
                                  std::string clientip_;
                                  uint16_t clientport_;
                                  TcpServer* current_; // 指向 TcpServer 对象的指针
                              };
                              

                              接下来就可以考虑如何借助多线程了

                              线程创建后,需要关闭不必要的 socket 套接字吗?

                              • 不需要,线程之间是可以共享这些资源的,无需关闭

                                如何设置主线程不必等待次线程退出?

                                • 可以把次线程进行分离

                                  所以接下来我们需要在连接成功后,创建次线程,利用已有信息构建 ThreadData 对象,为次线程编写回调函数(最终目的是为了执行 Service() 业务处理函数)

                                  注意: 因为当前在类中,线程的回调函数需要使用 static 设置为静态函数

                                  server.hpp 服务器头文件

                                  #pragma once
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include  // 原生线程库
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include "err.hpp"
                                  namespace nt_server
                                  {
                                      const uint16_t default_port = 8888; // 默认端口号
                                      const int backlog = 32;             // 全连接队列的最大长度
                                      using func_t = std::function; // 回调函数类型
                                      class TcpServer; // 前置声明
                                      // 包含我们所需参数的类型
                                      class ThreadData
                                      {
                                      public:
                                          ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr)
                                              :sock_(sock), clientip_(ip), clientport_(port), current_(ptr)
                                          {}
                                      // 设置为公有是为了方便访问
                                      public:
                                          int sock_;
                                          std::string clientip_;
                                          uint16_t clientport_;
                                          TcpServer* current_; // 指向 TcpServer 对象的指针
                                      };
                                      class TcpServer
                                      {
                                      public:
                                          TcpServer(const func_t &func, const uint16_t port = default_port)
                                              : func_(func), port_(port), quit_(false)
                                          {
                                          }
                                          ~TcpServer()
                                          {
                                          }
                                          // 初始化服务器
                                          void InitServer()
                                          {
                                              // 1.创建监听套接字
                                              listensock_ = socket(AF_INET, SOCK_STREAM, 0);
                                              if (listensock_ == -1)
                                              {
                                                  std::cerr << "Create ListenSocket Fail!" << strerror(errno) << std::endl;
                                                  exit(SOCKET_ERR);
                                              }
                                              std::cout << "Create ListenSocket Success! " << listensock_ << std::endl;
                                              // 2.绑定IP地址与端口号
                                              struct sockaddr_in local;
                                              memset(&local, 0, sizeof(local)); // 清零
                                              local.sin_family = AF_INET;
                                              local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                                              local.sin_port = htons(port_);
                                              if (bind(listensock_, (const sockaddr *)&local, sizeof(local)))
                                              {
                                                  std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                                                  exit(BIND_ERR);
                                              }
                                              // 3.监听
                                              if (listen(listensock_, backlog) == -1)
                                              {
                                                  std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
                                                  exit(LISTEN_ERR);
                                              }
                                              std::cout << "Listen Success!" << std::endl;
                                          }
                                          // 启动服务器
                                          void StartServer()
                                          {
                                              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.创建线程及所需要的线程信息类
                                                  ThreadData* td = new ThreadData(sock, clientip, clientport, this);
                                                  pthread_t p;
                                                  pthread_create(&p, nullptr, Routine, td);
                                              }
                                          }
                                          // 线程回调函数
                                          static void* Routine(void* args)
                                          {
                                              // 线程分离
                                              pthread_detach(pthread_self());
                                              ThreadData* td = static_cast(args);
                                              // 调用业务处理函数
                                              td->current_->Service(td->sock_, td->clientip_, td->clientport_);
                                              // 销毁对象
                                              delete td;
                                          }
                                          // 业务处理
                                          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); // 预留 '\0' 的位置
                                                  if (n > 0)
                                                  {
                                                      // 读取成功
                                                      buff[n] = '\0';
                                                      std::cout << "Server get: " << buff << " from " << 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 listensock_; // 监听套接字
                                          uint16_t port_;  // 端口号
                                          bool quit_;      // 判断服务器是否结束运行
                                          func_t func_;    // 回调函数
                                      };
                                  }
                                  

                                  因为当前使用了 原生线程库,所以在编译时,需要加上 -lpthread

                                  Makefile 文件

                                  .PHONY:all
                                  all:server client
                                  server:server.cc
                                  	g++ -o $@ $^ -std=c++11 -lpthread
                                  	
                                  client:client.cc
                                  	g++ -o $@ $^ -std=c++11 -lpthread
                                  .PHONY:clean
                                  clean:
                                  	rm -rf server client
                                  

                                  接下来就是编译并运行程序,可以看到 当前只有一个进程,同时有五个线程在运行

                                  网络编程『简易TCP网络程序』,第36张

                                  网络编程『简易TCP网络程序』,第37张

                                  使用 原生线程库 过于单薄了,并且这种方式存在问题:连接都准备好了,才创建线程,如果创建线程所需要的资源较多,会拖慢服务器整体连接效率

                                  为此可以改用之前实现的 线程池

                                  3.3.使用线程池

                                  之前在 《Linux多线程【线程池】》一文中实现了多个版本的线程池,这里我们直接使用最终版,也就是 单例模式版线程池

                                  网络编程『简易TCP网络程序』,第38张

                                  部分组件不需要修改,代码如下:

                                  ThreadPool.hpp 线程池头文件

                                  #pragma once
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include 
                                  #include "Task.hpp"
                                  #include "Thread.hpp"
                                  #include "BlockingQueue.hpp" // CP模型
                                  namespace Yohifo
                                  {
                                  #define THREAD_NUM 10
                                      template
                                      class ThreadPool
                                      {
                                      private:
                                          ThreadPool(int num = THREAD_NUM)
                                              :_num(num)
                                          {
                                          }
                                          ~ThreadPool()
                                          {
                                              // 等待线程退出
                                              for(auto &t : _threads)
                                                  t.join();
                                          }
                                          // 删除拷贝构造
                                          ThreadPool(const ThreadPool &) = delete;
                                      public:
                                          static ThreadPool* getInstance()
                                          {
                                              // 双检查
                                              if(_inst == nullptr)
                                              {
                                                  // 加锁
                                                  LockGuard lock(&_mtx);
                                                  if(_inst == nullptr)
                                                  {
                                                      // 创建对象
                                                      _inst = new ThreadPool();
                                                      // 初始化及启动服务
                                                      _inst->init();
                                                      _inst->start();
                                                  }
                                              }
                                              return _inst;
                                          }
                                      public:
                                          void init()
                                          {
                                              // 创建一批线程
                                              for(int i = 0; i < _num; i++)
                                                  _threads.push_back(Thread(i, threadRoutine, this));
                                          }
                                          void start()
                                          {
                                              // 启动线程
                                              for(auto &t : _threads)
                                                  t.run();
                                          }
                                          // 提供给线程的回调函数(已修改返回类型为 void)
                                          static void threadRoutine(void *args)
                                          {
                                              // 避免等待线程,直接剥离
                                              pthread_detach(pthread_self());
                                              auto ptr = static_cast*>(args);
                                              while (true)
                                              {
                                                  // 从CP模型中获取任务
                                                  T task = ptr->popTask();
                                                  task(); // 回调函数
                                              }
                                          }
                                          // 装载任务
                                          void pushTask(const T& task)
                                          {
                                              _blockqueue.Push(task);
                                          }
                                      
                                      protected:
                                          T popTask()
                                          {
                                              T task;
                                              _blockqueue.Pop(&task);
                                              return task;
                                          }
                                      private:
                                          std::vector _threads;
                                          int _num; // 线程数量
                                          BlockQueue _blockqueue; // 阻塞队列
                                          // 创建静态单例对象指针及互斥锁
                                          static ThreadPool *_inst;
                                          static pthread_mutex_t _mtx;
                                      };
                                      // 初始化指针
                                      template
                                      ThreadPool* ThreadPool::_inst = nullptr;
                                      // 初始化互斥锁
                                      template
                                      pthread_mutex_t ThreadPool::_mtx = PTHREAD_MUTEX_INITIALIZER;
                                  }
                                  

                                  Thread.hpp 封装实现的线程库头文件

                                  #pragma once
                                  #include 
                                  #include 
                                  #include 
                                  enum class Status
                                  {
                                      NEW = 0, // 新建
                                      RUNNING, // 运行中
                                      EXIT // 已退出
                                  };
                                  // 参数、返回值为 void 的函数类型
                                  typedef void (*func_t)(void*);
                                  class Thread
                                  {
                                  public:
                                      Thread(int num = 0, func_t func = nullptr, void* args = nullptr)
                                          :_tid(0), _status(Status::NEW), _func(func), _args(args)
                                      {
                                          // 根据编号写入名字
                                          char name[128];
                                          snprintf(name, sizeof name, "thread-%d", num);
                                          _name = name;
                                      }
                                      ~Thread()
                                      {}
                                      // 获取 ID
                                      pthread_t getTID() const
                                      {
                                          return _tid;
                                      }
                                      // 获取线程名
                                      std::string getName() const
                                      {
                                          return _name;
                                      }
                                      // 获取状态
                                      Status getStatus() const
                                      {
                                          return _status;
                                      }
                                      // 回调方法
                                      static void* runHelper(void* args)
                                      {
                                          Thread* myThis = static_cast(args);
                                          // 很简单,回调用户传进来的 func 函数即可
                                          myThis->_func(myThis->_args);
                                      }
                                      // 启动线程
                                      void run()
                                      {
                                          int ret = pthread_create(&_tid, nullptr, runHelper, this);
                                          if(ret != 0)
                                          {
                                              std::cerr << "create thread fail!" << std::endl;
                                              exit(1); // 创建线程失败,直接退出
                                          }
                                          _status =  Status::RUNNING; // 更改状态为 运行中
                                      }
                                      // 线程等待
                                      void join()
                                      {
                                          int ret = pthread_join(_tid, nullptr);
                                          if(ret != 0)
                                          {
                                              std::cerr << "thread join fail!" << std::endl;
                                              exit(1); // 等待失败,直接退出
                                          }
                                          _status = Status::EXIT; // 更改状态为 退出
                                      }
                                  private:
                                      pthread_t _tid; // 线程 ID
                                      std::string _name; // 线程名
                                      Status _status; // 线程状态
                                      func_t _func; // 线程回调函数
                                      void* _args; // 传递给回调函数的参数
                                  };
                                  

                                  BlockingQueue.hpp 生产者消费者模型头文件

                                  #pragma once
                                  #include 
                                  #include 
                                  #include 
                                  #include "LockGuard.hpp"
                                  // 命名空间,避免冲突
                                  namespace Yohifo
                                  {
                                  #define DEF_SIZE 10
                                      template
                                      class BlockQueue
                                      {
                                      public:
                                          BlockQueue(size_t cap = DEF_SIZE)
                                              :_cap(cap)
                                          {
                                              // 初始化锁与条件变量
                                              pthread_mutex_init(&_mtx, nullptr);
                                              pthread_cond_init(&_pro_cond, nullptr);
                                              pthread_cond_init(&_con_cond, nullptr);
                                          }
                                          ~BlockQueue()
                                          {
                                              // 销毁锁与条件变量
                                              pthread_mutex_destroy(&_mtx);
                                              pthread_cond_destroy(&_pro_cond);
                                              pthread_cond_destroy(&_con_cond);
                                          }
                                          // 生产数据(入队)
                                          void Push(const T& inData)
                                          {
                                              // 加锁(RAII风格)
                                              LockGuard lock(&_mtx);
                                              // 循环判断条件是否满足
                                              while(IsFull())
                                              {
                                                  pthread_cond_wait(&_pro_cond, &_mtx);
                                              }
                                              _queue.push(inData);
                                              // 可以加策略唤醒,比如生产一半才唤醒消费者
                                              pthread_cond_signal(&_con_cond);
                                              // 自动解锁
                                          }
                                          // 消费数据(出队)
                                          void Pop(T* outData)
                                          {
                                              // 加锁(RAII 风格)
                                              LockGuard lock(&_mtx);
                                              // 循环判读条件是否满足
                                              while(IsEmpty())
                                              {
                                                  pthread_cond_wait(&_con_cond, &_mtx);
                                              }
                                              *outData = _queue.front();
                                              _queue.pop();
                                              // 可以加策略唤醒,比如消费完后才唤醒生产者
                                              pthread_cond_signal(&_pro_cond);
                                              // 自动解锁
                                          }
                                      private:
                                          // 判断是否为满
                                          bool IsFull()
                                          {
                                              return _queue.size() == _cap;
                                          }
                                          
                                          // 判断是否为空
                                          bool IsEmpty()
                                          {
                                              return _queue.empty();
                                          }
                                      private:
                                          std::queue _queue;
                                          size_t _cap; // 阻塞队列的容量
                                          pthread_mutex_t _mtx; // 互斥锁
                                          pthread_cond_t _pro_cond; // 生产者条件变量
                                          pthread_cond_t _con_cond; // 消费者条件变量
                                      };
                                  }
                                  

                                  LockGuard.hpp 自动化锁头文件

                                  #pragma once
                                  #include 
                                  class LockGuard
                                  {
                                  public:
                                      LockGuard(pthread_mutex_t*pmtx)
                                          :_pmtx(pmtx)
                                      {
                                          // 加锁
                                          pthread_mutex_lock(_pmtx);
                                      }
                                      ~LockGuard()
                                      {
                                          // 解锁
                                          pthread_mutex_unlock(_pmtx);
                                      }
                                  private:
                                      pthread_mutex_t* _pmtx;
                                  };
                                  

                                  现在需要修改 Task.hpp 任务头文件中的 Task 任务类,将其修改为一个服务于 网络通信中业务处理 的任务类(也就是 Service() 业务处理函数)

                                  在 Service() 业务处理函数中,需要包含 socket 套接字、客户端 IP、客户端端口号 等必备信息,除此之外,我们还可以将 可调用对象(Service() 业务处理函数) 作为参数传递给 Task 对象

                                  Task.hpp 任务类

                                  #pragma once
                                  #include 
                                  #include 
                                  namespace Yohifo
                                  {
                                      // Service() 业务处理函数的类型
                                      using cb_t = std::function;
                                      class Task
                                      {
                                      public:
                                          // 可以再提供一个默认构造(防止部分场景中构建对象失败)
                                          Task()
                                          {}
                                          Task(int sock, const std::string& ip, const uint16_t& port, const cb_t& cb)
                                              :sock_(sock), ip_(ip), port_(port), cb_(cb)
                                          {}
                                          // 重载运算操作,用于回调 [业务处理函数]
                                          void operator()()
                                          {
                                              // 直接回调 cb [业务处理函数] 即可
                                              cb_(sock_, ip_, port_);
                                          }
                                      private:
                                          int sock_;
                                          std::string ip_;
                                          uint16_t port_;
                                          cb_t cb_; // 回调函数
                                      };
                                  }
                                  

                                  准备工作完成后,接下来就是往 server.hpp 服务器头文件中添加组件了

                                  注意:

                                  • 在构建 Task 对象时,需要使用 bind 绑定类内函数,避免参数不匹配
                                  • 当前的线程池是单例模式,在 Task 任务对象构建后,通过线程池操作句柄 push 对象即可

                                    其实也就是在 StartServer.hpp 中增加了这两句代码

                                    // 3.构建任务对象 注意:使用 bind 绑定 this 指针
                                    Yohifo::Task t(sock, clientip, clientport, std::bind(&TcpServer::Service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
                                    // 4.通过线程池操作句柄,将任务对象 push 进线程池中处理
                                    Yohifo::ThreadPool::getInstance()->pushTask(t);
                                    

                                    完整的服务器代码如下

                                    server.hpp 服务器头文件

                                    #pragma once
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include "err.hpp"
                                    #include "ThreadPool.hpp" // 线程池
                                    #include "Task.hpp" // 任务类
                                    namespace nt_server
                                    {
                                        const uint16_t default_port = 8888; // 默认端口号
                                        const int backlog = 32;             // 全连接队列的最大长度
                                        using func_t = std::function; // 回调函数类型
                                        class TcpServer; // 前置声明
                                        // 包含我们所需参数的类型
                                        class ThreadData
                                        {
                                        public:
                                            ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr)
                                                :sock_(sock), clientip_(ip), clientport_(port), current_(ptr)
                                            {}
                                        // 设置为公有是为了方便访问
                                        public:
                                            int sock_;
                                            std::string clientip_;
                                            uint16_t clientport_;
                                            TcpServer* current_; // 指向 TcpServer 对象的指针
                                        };
                                        class TcpServer
                                        {
                                        public:
                                            TcpServer(const func_t &func, const uint16_t port = default_port)
                                                : func_(func), port_(port), quit_(false)
                                            {
                                            }
                                            ~TcpServer()
                                            {
                                            }
                                            // 初始化服务器
                                            void InitServer()
                                            {
                                                // 1.创建监听套接字
                                                listensock_ = socket(AF_INET, SOCK_STREAM, 0);
                                                if (listensock_ == -1)
                                                {
                                                    std::cerr << "Create ListenSocket Fail!" << strerror(errno) << std::endl;
                                                    exit(SOCKET_ERR);
                                                }
                                                std::cout << "Create ListenSocket Success! " << listensock_ << std::endl;
                                                // 2.绑定IP地址与端口号
                                                struct sockaddr_in local;
                                                memset(&local, 0, sizeof(local)); // 清零
                                                local.sin_family = AF_INET;
                                                local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用IP地址
                                                local.sin_port = htons(port_);
                                                if (bind(listensock_, (const sockaddr *)&local, sizeof(local)))
                                                {
                                                    std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                                                    exit(BIND_ERR);
                                                }
                                                // 3.监听
                                                if (listen(listensock_, backlog) == -1)
                                                {
                                                    std::cerr << "Listen Fail!" << strerror(errno) << std::endl;
                                                    exit(LISTEN_ERR);
                                                }
                                                std::cout << "Listen Success!" << std::endl;
                                            }
                                            // 启动服务器
                                            void StartServer()
                                            {
                                                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.构建任务对象 注意:使用 bind 绑定 this 指针
                                                    Yohifo::Task t(sock, clientip, clientport, std::bind(&TcpServer::Service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
                                                    // 4.通过线程池操作句柄,将任务对象 push 进线程池中处理
                                                    Yohifo::ThreadPool::getInstance()->pushTask(t);
                                                }
                                            }
                                            // 业务处理
                                            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); // 预留 '\0' 的位置
                                                    if (n > 0)
                                                    {
                                                        // 读取成功
                                                        buff[n] = '\0';
                                                        std::cout << "Server get: " << buff << " from " << 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 listensock_; // 监听套接字
                                            uint16_t port_;  // 端口号
                                            bool quit_;      // 判断服务器是否结束运行
                                            func_t func_;    // 回调函数
                                        };
                                    }
                                    

                                    接下来编译并运行程序,当服务器启动后(此时无客户端连接),只有一个线程,这是因为我们当前的 线程池 是基于 懒汉模式 实现的,只有当第一次使用时,才会创建线程

                                    网络编程『简易TCP网络程序』,第39张

                                    接下来启动客户端,可以看到确实创建了一批次线程(十个)

                                    网络编程『简易TCP网络程序』,第40张

                                    当然可以支持多客户端同时通信

                                    网络编程『简易TCP网络程序』,第41张

                                    看似程序已经很完善了,其实隐含着一个大问题:当前线程池中的线程,本质上是在回调一个 while(true) 死循环函数,当连接的客户端大于线程池中的最大线程数时,会导致所有线程始终处于满负载状态,直接影响就是连接成功后,无法再创建通信会话(倘若客户端不断开连接,线程池中的线程就无力处理其他客户端的会话)

                                    说白了就是 线程池 比较适合用于处理短任务,对于当前的场景来说,线程池 不适合建立持久通信会话,应该将其用于处理 read 读取、write 写入 任务

                                    网络编程『简易TCP网络程序』,第42张

                                    如果想解决这个问题,有两个方向:Service() 函数中支持一次 [收 / 发],或者多线程+线程池,多线程用于构建通信会话,线程池则用于处理 [收 / 发] 任务

                                    前者实现起来比较简单,无非就是把 Service() 业务处理函数中的 while(true) 循环去掉

                                    Service() 业务处理函数

                                    // 业务处理
                                    void Service(int sock, const std::string &clientip, const uint16_t &clientport)
                                    {
                                        char buff[1024];
                                        std::string who = clientip + "-" + std::to_string(clientport);
                                        ssize_t n = read(sock, buff, sizeof(buff) - 1); // 预留 '\0' 的位置
                                        if (n > 0)
                                        {
                                            // 读取成功
                                            buff[n] = '\0';
                                            std::cout << "Server get: " << buff << " from " << 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); // 关闭文件描述符
                                        }
                                        else
                                        {
                                            // 读取出问题(暂时)
                                            std::cerr << "Read Fail!" << strerror(errno) << std::endl;
                                            close(sock); // 关闭文件描述符
                                        }
                                    }
                                    

                                    至于后者就比较麻烦了,需要结合 高级IO 相关知识,这里不再阐述


                                    4.日志输出

                                    4.1.日志的重要性

                                    在之前的编程经历中,如果我们的程序运行出现了问题,都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上,debug 阶段这样使用没啥问题,但如果出错的是一个不断在运行中的服务,那问题就大了,因为服务器是不间断运行中,直接将 错误信息 输出到屏幕上,会导致错误排查变得极为困难

                                    网络编程『简易TCP网络程序』,第43张

                                    将各种 错误信息 组织管理,就形成了日志,日志有属于自己的格式(包括时间、文件名及行号、错误等级等),利于排查问题

                                    所以接下来我们将会实现一个简易版日志器,用于定向输出我们的日志信息

                                    网络编程『简易TCP网络程序』,第44张

                                    4.2.可变参数

                                    日志需要我们指定格式并输出,依赖于可变参数

                                    在编写简易版日志器之前,需要先认识一下 C语言 中有关可变参数的使用,主要包括这几个 宏

                                    #include 
                                    va_list	// 指向可变参数列表的指针
                                    va_start()	// 将指针指向起始地址
                                    va_arg()	// 根据类型,提取可变参数列表中的参数
                                    va_end()	// 将指针置为空 
                                    

                                    关于 可变参数 更多知识详见 《【C语言】可变参数列表》

                                    比如我们可以通过 可变参数 实现参数遍历

                                    #include 
                                    #include 
                                    void foreach(int format, ...)
                                    {
                                        va_list p;
                                        va_start(p, format);
                                        // 接下来就是获取其中的每一个参数
                                        for(int i = 0; i < format; i++)
                                            printf("%d ", va_arg(p, int));
                                        printf("\n");
                                        // 置空
                                        va_end(p);
                                    }
                                    int main()
                                    {
                                        foreach(5, 1,2,3,4,5);
                                        return 0;
                                    }
                                    

                                    这种依靠自己动手的方法比较麻烦,我们也可以借助标准库提供的 vsnprintf() 函数进行参数解析

                                    4.3.日志器实现

                                    日志是有等级的,一般分为五级:

                                    1. Debug 用于调试
                                    2. Info 提示信息
                                    3. Warning 警告
                                    4. Errorr 错误
                                    5. Fatal 致命错误

                                    错误等级越高,代表影响越大

                                    当然难免有不明确的错误,可以再添加一级:UnKnow 未知错误

                                    // 日志等级
                                    enum
                                    {
                                        Debug = 0,
                                        Info,
                                        Warning,
                                        Error,
                                        Fatal
                                    };
                                    string getLevel(int level)
                                    {
                                        vector vs = {"", "", "", "", "", ""};
                                        
                                        //避免非法情况
                                        if(level < 0 || level >= vs.size() - 1)
                                            return vs[vs.size() - 1];
                                        
                                        return vs[level];
                                    }
                                    

                                    接下来是获取时间信息,可以通过 time() 函数获取当前时间戳,然后再利用 localtime() 函数构建 struct tm 结构体对象,这个对象会将时间戳解析成 年月日 时分秒 等详细信息,直接获取即可

                                    strcut tm 结构体的信息如下,细节:年份已经 -1900 了,使用时需要加上 1900;月份从 0 开始,使用时需要 +1

                                    /* Used by other time functions.  */
                                    struct tm
                                    {
                                      int tm_sec;			/* Seconds.	[0-60] (1 leap second) */
                                      int tm_min;			/* Minutes.	[0-59] */
                                      int tm_hour;			/* Hours.	[0-23] */
                                      int tm_mday;			/* Day.		[1-31] */
                                      int tm_mon;			/* Month.	[0-11] */
                                      int tm_year;			/* Year	- 1900.  */
                                      int tm_wday;			/* Day of week.	[0-6] */
                                      int tm_yday;			/* Days in year.[0-365]	*/
                                      int tm_isdst;			/* DST.		[-1/0/1]*/
                                    # ifdef	__USE_BSD
                                      long int tm_gmtoff;		/* Seconds east of UTC.  */
                                      const char *tm_zone;		/* Timezone abbreviation.  */
                                    # else
                                      long int __tm_gmtoff;		/* Seconds east of UTC.  */
                                      const char *__tm_zone;	/* Timezone abbreviation.  */
                                    # endif
                                    };
                                    

                                    可以这样获取当前时间

                                    // 获取当前时间
                                    string getTime()
                                    {
                                        time_t t = time(nullptr);   //获取时间戳
                                        struct tm *st = localtime(&t);    //获取时间相关的结构体
                                        char buff[128];
                                        snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
                                        return buff;
                                    }
                                    

                                    接下来就是获取进程 PID,这个简单,直接使用 getpid() 函数获取即可,最后是解析参数,需要用到 vsnprintf() 函数,只要传入缓冲区和 va_list 指针,该函数就可以自动解析出参数,并存入缓冲区中

                                    void logMessage(int level, const char* format, ...)
                                    {
                                        //截获主体消息
                                        char msgbuff[1024];
                                        va_list p;
                                        va_start(p, format);    //将 p 定位至 format 的起始位置
                                        vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
                                        va_end(p);
                                    }
                                    

                                    接下来就是将 日志等级 时间 PID 与 参数 进行拼接,形成日志

                                    log.hpp 日志头文件

                                    #pragma once
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    using namespace std;
                                    enum
                                    {
                                        Debug = 0,
                                        Info,
                                        Warning,
                                        Error,
                                        Fatal
                                    };
                                    string getLevel(int level)
                                    {
                                        vector vs = {"", "", "", "", "", ""};
                                        
                                        //避免非法情况
                                        if(level < 0 || level >= vs.size() - 1)
                                            return vs[vs.size() - 1];
                                        
                                        return vs[level];
                                    }
                                    string getTime()
                                    {
                                        time_t t = time(nullptr);   //获取时间戳
                                        struct tm *st = localtime(&t);    //获取时间相关的结构体
                                        char buff[128];
                                        snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
                                        return buff;
                                    }
                                    //处理信息
                                    void logMessage(int level, const char* format, ...)
                                    {
                                        //日志格式:<日志等级> [时间] [PID] {消息体}
                                        string logmsg = getLevel(level);    //获取日志等级
                                        logmsg += " " + getTime();  //获取时间
                                        logmsg += " [" + to_string(getpid()) + "]";    //获取进程PID
                                        //截获主体消息
                                        char msgbuff[1024];
                                        va_list p;
                                        va_start(p, format);    //将 p 定位至 format 的起始位置
                                        vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
                                        va_end(p);
                                        logmsg += " {" + string(msgbuff) + "}";    //获取主体消息
                                        printf("%s\n", logmsg);
                                    } 
                                    

                                    为什么日志消息最后还是向屏幕输出?这样组织日志消息的好处是什么?

                                    因为现在还在测试阶段,等测试完成后,可以将日志消息存入文件中,做到持久化存储;至于统一组织的好处不言而喻,能够确保每条日志消息都包含必要信息,便于排查错误

                                    简单测试的效果如下

                                    网络编程『简易TCP网络程序』,第45张

                                    4.4.应用于程序中

                                    接下来可以包含 log.hpp 这个日志器头文件,并进行日志输出了,比如先将 client.hpp 客户端头文件中的错误信息日志化(代码少一些,比较好改)

                                    client.hpp 客户端头文件

                                    #pragma once
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include "err.hpp"
                                    #include "log.hpp"
                                    namespace nt_client
                                    {
                                        class TcpClient
                                        {
                                        public:
                                            TcpClient(const std::string& ip, const uint16_t port)
                                                :server_ip_(ip), server_port_(port)
                                            {}
                                            ~TcpClient()
                                            {}
                                            // 初始化客户端
                                            void InitClient()
                                            {
                                                // 创建套接字
                                                sock_ = socket(AF_INET, SOCK_STREAM, 0);
                                                if (sock_ == -1)
                                                {
                                                    logMessage(Fatal, "Create Socket Fail! %s", strerror(errno));
                                                    exit(SOCKET_ERR);
                                                }
                                                logMessage(Debug, "Create Sock Succeess! %d", sock_);
                                            }
                                            // 启动客户端
                                            void StartClient()
                                            {
                                                // 填充服务器的 sockaddr_in 结构体信息
                                                struct sockaddr_in server;
                                                socklen_t len = sizeof(server);
                                                memset(&server, 0, len);
                                                server.sin_family = AF_INET;
                                                inet_aton(server_ip_.c_str(), &server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法
                                                server.sin_port = htons(server_port_);
                                                // 尝试重连 5 次
                                                int n = 5;
                                                while(n)
                                                {
                                                    int ret = connect(sock_, (const struct sockaddr*)&server, len);
                                                    if(ret == 0)
                                                    {
                                                        // 连接成功,可以跳出循环
                                                        break;
                                                    }
                                                    // 尝试进行重连
                                                    logMessage(Warning, "网络异常,正在进行重连... 剩余连接次数: %d", --n);
                                                    sleep(1);
                                                }
                                                // 如果剩余重连次数为 0,证明连接失败
                                                if(n == 0)
                                                {
                                                    logMessage(Fatal, "连接失败! %s", strerror(errno));
                                                    close(sock_);
                                                    exit(CONNECT_ERR);
                                                }
                                                // 连接成功
                                                logMessage(Info, "连接成功!");
                                                // 进行业务处理
                                                Service();
                                            }
                                            // 业务处理
                                            void Service()
                                            {
                                                char buff[1024];
                                                std::string who = server_ip_ + "-" + std::to_string(server_port_);
                                                while(true)
                                                {
                                                    // 由用户输入信息
                                                    std::string msg;
                                                    std::cout << "Please Enter >> ";
                                                    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)
                                                    {
                                                        // 读取到文件末尾(服务器关闭了)
                                                        logMessage(Error, "Server %s quit! %s", who.c_str(), strerror(errno));
                                                        close(sock_); // 关闭文件描述符
                                                        break;
                                                    }
                                                    else
                                                    {
                                                        // 读取异常
                                                        logMessage(Error, "Read Fail! %s", strerror(errno));
                                                        close(sock_); // 关闭文件描述符
                                                        break;
                                                    }
                                                }
                                            }
                                            
                                        private:
                                            int sock_; // 套接字
                                            std::string server_ip_; // 服务器IP
                                            uint16_t server_port_; // 服务器端口号
                                        };
                                    }
                                    

                                    效果就是这个样子,至于代码中其他输出错误的地方,都可以采用 简易版日志器 进行统一输出

                                    网络编程『简易TCP网络程序』,第46张

                                    改造完成的程序长这个样子

                                    网络编程『简易TCP网络程序』,第47张

                                    4.5.持久化存储

                                    所谓持久化存储就是将日志消息输出至文件中,修改 log.hpp 中的代码即可

                                    • 指定日志文件存放路径
                                    • 打开文件,将日志消息追加至文件中

                                      注意: 当前的改动中并未涉及目录创建,所以需要手动创建相关目录

                                      log.hpp 日志头文件

                                      #pragma once
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      using namespace std;
                                      enum
                                      {
                                          Debug = 0,
                                          Info,
                                          Warning,
                                          Error,
                                          Fatal
                                      };
                                      static const string file_name = "log/TcpServer.log";
                                      string getLevel(int level)
                                      {
                                          vector vs = {"", "", "", "", "", ""};
                                          
                                          //避免非法情况
                                          if(level < 0 || level >= vs.size() - 1)
                                              return vs[vs.size() - 1];
                                          
                                          return vs[level];
                                      }
                                      string getTime()
                                      {
                                          time_t t = time(nullptr);   //获取时间戳
                                          struct tm *st = localtime(&t);    //获取时间相关的结构体
                                          char buff[128];
                                          snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
                                          return buff;
                                      }
                                      //处理信息
                                      void logMessage(int level, const char* format, ...)
                                      {
                                          //日志格式:<日志等级> [时间] [PID] {消息体}
                                          string logmsg = getLevel(level);    //获取日志等级
                                          logmsg += " " + getTime();  //获取时间
                                          logmsg += " [" + to_string(getpid()) + "]";    //获取进程PID
                                          //截获主体消息
                                          char msgbuff[1024];
                                          va_list p;
                                          va_start(p, format);    //将 p 定位至 format 的起始位置
                                          vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
                                          va_end(p);
                                          logmsg += " {" + string(msgbuff) + "}";    //获取主体消息
                                          //持久化。写入文件中
                                          FILE* fp = fopen(file_name.c_str(), "a");   //以追加的方式写入
                                          if(fp == nullptr) return;   //不太可能出错
                                          fprintf(fp, "%s\n", logmsg.c_str());
                                          fflush(fp); //手动刷新一下
                                          fclose(fp);
                                          fp = nullptr;
                                      } 
                                      

                                      网络编程『简易TCP网络程序』,第48张

                                      网络编程『简易TCP网络程序』,第49张


                                      5.守护进程

                                      5.1.会话、进程组、进程

                                      接下来进入本文中的最后一个小节: 守护进程

                                      守护进程 的意思就是让进程不间断的在后台运行,即便是 bash 关闭了,也能照旧运行。守护进程 就是现实生活中的服务器,因为服务器是需要 24H 不间断运行的

                                      当前我们的程序在启动后属于 前台进程,前台进程 是由 bash 进程替换而来的,因此会导致 bash 暂时无法使用

                                      网络编程『简易TCP网络程序』,第50张

                                      如果在启动程序时,带上 & 符号,程序就会变成 后台进程,后台进程 并不会与 bash 进程冲突,bash 仍然可以使用

                                      网络编程『简易TCP网络程序』,第51张

                                      后台进程 也可以实现服务器不间断运行,但问题在于 如果当前 bash 关闭了,那么运行中的后台进程也会被关闭,最好的解决方案是使用 守护进程

                                      在正式学习 守护进程 之前,需要先了解一组概念:会话、进程组、进程

                                      分别运行一批 前台、后台进程,并通过指令查看进程运行情况

                                      sleep 1000 | sleep 2000 | sleep 3000 &
                                      sleep 100 | sleep 200 | sleep 300
                                      ps -ajx | head -1 && ps -ajx | grep sleep | grep -v grep
                                      

                                      网络编程『简易TCP网络程序』,第52张

                                      其中 会话 <-> SID、进程组 <-> PGID、进程 <-> PID,显然,sleep 1000、2000、3000 处于同一个管道中(有血缘关系),属于同一个 进程组,所以他们的 PGID 都是一样的,都是 4261;至于 sleep 100、200、300 属于另一个 进程组,PGID 为 4308;再仔细观察可以发现 每一组的进程组 PGID 都与当前组中第一个被创建的进程 PID 一致,这个进程被称为 组长进程

                                      会话 >= 进程组 >= 进程

                                      无论是 后台进程 还是 前台进程,都是从同一个 bash 中启动的,所以它们处于同一个 会话 中,SID 都是 1939,并且关联的 终端文件 TTY 都是 pts/1

                                      Linux 中一切皆文件,终端文件也是如此,这里的终端其实就是当前 bash 输出结果时使用的文件(也就是屏幕),终端文件位于 dev/pts 目录下,如果向指定终端文件中写入数据,那么对方也可以直接收到

                                      (关联终端文件说白了就是打开了文件,一方写,一方读,不就是管道吗)

                                      网络编程『简易TCP网络程序』,第53张

                                      根据当前的 会话 SID 查找目标进程,发现这玩意就是 bash 进程,bash 进程本质上就是一个不断运行中的 前台进程,并且自成 进程组

                                      在同一个 bash 中启动前台、后台进程,它们的 SID 都是一样的,属于同一个 会话,关联了同一个 终端 (SID 其实就是 bash 的 PID)

                                      网络编程『简易TCP网络程序』,第54张

                                      我们使用 XShell 等工具登录 Linux 服务器时,会在服务器中创建一个 会话(bash),可以在该会话内创建 进程,当 进程 间有关系时,构成一个 进程组,组长 进程的 PID 就是该 进程组 的 PGID

                                      网络编程『简易TCP网络程序』,第55张

                                      Linux 中的登录操作实际上就是创建了一个会话,Windows 中也是如此,当你的 Windows 变卡时,可以使用 [注销] 按钮结束整个会话,重新登录,电脑就会流畅如初

                                      网络编程『简易TCP网络程序』,第56张


                                      在同一个会话中,只允许一个前台进程在运行,默认是 bash,如果其他进程运行了,bash 就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)

                                      如何将一个 后台进程 变成 前台进程?

                                      首先通过指令查看当前 会话 中正在运行的 后台进程,获取 任务号

                                      jobs
                                      

                                      网络编程『简易TCP网络程序』,第57张

                                      接下来通过 任务号 将 后台进程 变成 前台进程,此时 bash 就无法使用了

                                      fg 1
                                      

                                      网络编程『简易TCP网络程序』,第58张

                                      那如何将 前台进程 变成 后台进程 ?

                                      首先是通过 ctrl + z 发送 19 号 SIGSTOP 信号,暂停正在运行中的 前台进程

                                      键盘输入 ctrl + z
                                      

                                      网络编程『简易TCP网络程序』,第59张

                                      然后通过 任务号,可以把暂停中的进程变成 后台进程

                                      bg 1
                                      

                                      网络编程『简易TCP网络程序』,第60张

                                      5.2.守护进程化

                                      一般网络服务器为了不受到用户登录重启的影响,会以 守护进程 的形式运行,有了上面那一批前置知识后,就可以很好的理解 守护进程 的本质了

                                      守护进程:进程单独成一个会话,并且以后台进程的形式运行

                                      说白了就是让服务器不间断运行,可以直接使用 daemon() 函数完成 守护进程化

                                      #include 
                                      int daemon(int nochdir, int noclose);
                                      

                                      参数解读:

                                      1. nochdir 改变进程的工作路径
                                      2. noclose 重定向标准输入、标准输出、标准错误

                                      返回值:成功返回 0,失败返回 -1

                                      一般情况下,daemon() 函数的两个参数都只需要传递 0,默认工作在 / 路径下,默认重定向至 /dev/null

                                      /dev/null 就像是一个 黑洞,可以把所有数据都丢入其中,相当于丢弃数据

                                      使用 damon() 函数使之前的server.cc 守护进程化

                                      server.cc 服务器源文件

                                      #include  // 智能指针头文件
                                      #include 
                                      #include 
                                      #include "server.hpp"
                                      using namespace std;
                                      using namespace nt_server;
                                      // 业务处理回调函数(字符串回响)
                                      string echo(string request)
                                      {
                                          return request;
                                      }
                                      int main()
                                      {
                                          // 直接守护进程化
                                          daemon(0, 0);
                                          
                                          unique_ptr usvr (new TcpServer(echo)); // 将回调函数进行传递
                                          usvr->InitServer();
                                          usvr->StartServer();
                                          return 0;
                                      }
                                      

                                      现在服务器启动后,会自动变成 后台进程,并且自成一个 新会话,归操作系统管(守护进程 本质上是一种比较坚强的 孤儿进程)

                                      注意: 现在标准输出、标准错误都被重定向至 /dev/null 中了,之前向屏幕输出的数据,现在都会直接被丢弃,如果想保存数据,可以选择使用日志

                                      网络编程『简易TCP网络程序』,第61张

                                      如果想终止 守护进程,需要通过 kill pid 杀死目标进程

                                      使用系统提供的接口一键 守护进程化 固然方便,不过大多数程序员都会选择手动 守护进程化(可以根据自己的需求定制操作)

                                      原理是 使用 setsid() 函数新设一个会话,谁调用,会话 SID 就是谁的,成为一个新的会话后,不会被之前的会话影响

                                      #include 
                                      pid_t setsid(void);
                                      

                                      返回值:成功返回该进程的 pid,失败返回 -1

                                      注意: 调用该函数的进程,不能是组长进程,需要创建子进程后调用

                                      手动实现守护进程时需要注意以下几点:

                                      1. 忽略异常信号
                                      2. 0、1、2 要做特殊处理(文件描述符)
                                      3. 进程的工作路径可能要改变(从用户目录中脱离至根目录)

                                      具体实现步骤如下:

                                      1、忽略常见的异常信号:SIGPIPE、SIGCHLD

                                      2、如何保证自己不是组长? 创建子进程 ,成功后父进程退出,子进程变成守护进程

                                      3、新建会话,自己成为会话的 话首进程

                                      4、(可选)更改守护进程的工作路径:chdir

                                      5、处理后续对于 0、1、2 的问题

                                      对于 标准输入、标准输出、标准错误 的处理方式有两种

                                      暴力处理:直接关闭 fd

                                      优雅处理:将 fd 重定向至 /dev/null,也就是 daemon() 函数的做法

                                      这里我们选择后者,守护进程 的函数实现如下

                                      Daemon.hpp 守护进程头文件

                                      #pragma once
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include 
                                      #include "err.hpp"
                                      #include "log.hpp"
                                      static const char *path = "/home/Yohifo";
                                      void Daemon()
                                      {
                                          // 1、忽略常见信号
                                          signal(SIGPIPE, SIG_IGN);
                                          signal(SIGCHLD, SIG_IGN);
                                          // 2、创建子进程,自己退休
                                          pid_t id = fork();
                                          if (id > 0)
                                              exit(0);
                                          else if (id < 0)
                                          {
                                              // 子进程创建失败
                                              logMessage(Error, "Fork Fail: %s", strerror(errno));
                                              exit(FORK_ERR);
                                          }
                                          // 3、新建会话,使自己成为一个单独的组
                                          pid_t ret = setsid();
                                          if (ret == -1)
                                          {
                                              // 守护化失败
                                              logMessage(Error, "Setsid Fail: %s", strerror(errno));
                                              exit(SETSID_ERR);
                                          }
                                          // 4、更改工作路径
                                          int n = chdir(path);
                                          if (n == -1)
                                          {
                                              // 更改路径失败
                                              logMessage(Error, "Chdir Fail: %s", strerror(errno));
                                              exit(CHDIR_ERR);
                                          }
                                          // 5、重定向标准输入输出错误
                                          int fd = open("/dev/null", O_RDWR);
                                          if (fd == -1)
                                          {
                                              // 文件打开失败
                                              logMessage(Error, "Open Fail: %s", strerror(errno));
                                              exit(OPEN_ERR);
                                          }
                                      	// 重定向标准输入、标准输出、标准错误
                                          dup2(fd, 0);
                                          dup2(fd, 1);
                                          dup2(fd, 2);
                                          close(fd);
                                      }
                                      

                                      当然相应的错误码也需要更新

                                      err.hpp 错误码头文件

                                      #pragma once
                                      enum
                                      {
                                          USAGE_ERR = 1,
                                          SOCKET_ERR,
                                          BIND_ERR,
                                          LISTEN_ERR,
                                          CONNECT_ERR,
                                          FORK_ERR,
                                          SETSID_ERR,
                                          CHDIR_ERR,
                                          OPEN_ERR
                                      };
                                      

                                      接下来就是在服务启动成功后,将其 守护进程化

                                      StartServer() 服务器启动函数 — 位于 server.hpp 服务器头文件中的 TcpServer 类

                                      #include "myDaemon.hpp"
                                      // 启动服务器
                                      void StartServer()
                                      {
                                          // 守护进程化
                                          Daemon();
                                      	
                                      	// ...
                                      }
                                      

                                      现在服务器在启动后,会自动新建会话,以 守护进程 的形式运行

                                      网络编程『简易TCP网络程序』,第62张

                                      关于 inet_ntoa 函数的返回值(该函数的作用是将四字节的 IP 地址转化为点分十进制的 IP 地址)

                                      inet_ntoa 返回值为 char*,转化后的 IP 地址存储在静态区,二次调用会覆盖上一次的结果,多线程场景中不是线程安全的

                                      • 不过在 CentOS 7 及更高版本中,接口进行了更新,新增了互斥锁,多线程场景中测试没问题

                                      6.完整代码

                                      下面是不同版本服务器的完整代码

                                      「朴素版,支持单客户端连接」

                                      「多进程版,支持多客户端连接」

                                      「多线程版(原生线程库),支持多客户端连接」

                                      「多线程版(线程池),支持多客户端连接」

                                      「日志版,支持简易日志输出」

                                      「守护进程版,支持服务部署」

                                      网络编程『简易TCP网络程序』,第63张


                                      🌨️总结

                                      以上是关于『简易TCP网络程序』的全部内容,作为上一篇博客的延伸,本文重新实现了字符串回响网络程序,基于TCP协议逐步改造并引入多进程、多线程、线程池、日志输出、守护进程等技术。这使得网络程序更为成熟,为后续网络和高级IO的学习提供了有力支持。同时,对套接字编程的重要性也得到了充分体现。希望本文能为读者在网络编程领域的深入学习提供实质性帮助。


                                      网络编程『简易TCP网络程序』,星辰大海,第64张

                                      相关文章推荐
                                      网络编程『socket套接字 ‖ 简易UDP网络程序』
                                      网络基础『发展 ‖ 协议 ‖ 传输 ‖ 地址』

网友评论

搜索
最新文章
热门文章
热门标签
 
 男人梦见抓到好多鱼  2019属相是什么生肖  梦见别人被水淹