当前位置: 首页 > news >正文

【Linux网络】各版本TCP服务器构建 - 从理解到实现

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈一、TcpServer.cpp
  • 🏳️‍🌈二、TcpServer.hpp
    • 2.1 枚举错误情况
    • 2.2 基本结构
    • 2.3 构造函数、析构函数
    • 2.4 初始化方法
    • 2.5 循环监听
      • 2.5.1 server 0 - 单执行流版本
      • 2.5.2 server 1 - 多进程版本
      • 2.5.3 server 2 - 多线程版本
      • 2.5.4 server 3 - 内存池
  • 🏳️‍🌈三、TcpClient.cpp
  • 👥总结


前面几篇文章中使用UDP协议实现了相关功能这篇使用TCP协议实现客户端与服务端的通信

相比与UDP协议,TCP协议更加可靠,也更加复杂!与UDP类似,我们先写主函数,然后实现相关函数!

🏳️‍🌈一、TcpServer.cpp

服务端主函数使用智能指针构造Server对象,然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!

#include "TcpServer.hpp"int main(int argc, char* argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;Die(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);tsvr->InitServer();tsvr->Loop();return 0;
}

🏳️‍🌈二、TcpServer.hpp

2.1 枚举错误情况

与 UDP 同样的,我们先枚举错误情况,将其放在 common.hpp

enum {USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};

2.2 基本结构

我们这里先实现最基本的 TCP 服务器,基本成员有端口号,文件描述符,与运行状态

class TcpServer{public:TcpServer(){}void InitServer(){}void Loop(int sockfd){}~TcpServer(){}private:int _listensockfd; // 监听socketuint16_t _port;bool _isrunning;
};

2.3 构造函数、析构函数

构造函数初始化成员变量,析构函数无需处理!

  • 这里需要端口号设置一个默认值 - 8080
static const uint16_t gport = 8080;
// 构造函数
TcpServer(uint16_t port = gport):_port(port),_sockfd(gsockfd),_isrunning(false){}
// 析构函数
~TcpServer(){}

2.4 初始化方法

初始化函数主要分为三步:

  1. 创建 socket (类型与UDP不同)
  2. bind sockfdsocket addr
  3. 获取连接(与UDP不同)

获取连接需要使用 listen 函数(将套接字设置为监听模式,以便能够接受进入的连接请求)

·listen· 需要设置一个队列,用来保存等待连接地客户端,我们可以事先设置一个 #define BACKLOG 8 来定义这个队列长度

void InitServer() {// 1. 创建 socket_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0) {LOG(LogLevel::ERROR) << "create socket error: " << strerror(errno);Die(2);}LOG(LogLevel::INFO) << "create sockfd success: " << _listensockfd;struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = htonl(INADDR_ANY);// 2. 绑定 socketif (::bind(_listensockfd, CONV(&local), sizeof(local)) < 0) {LOG(LogLevel::ERROR) << "bind socket error: " << strerror(errno);Die(3);}LOG(LogLevel::INFO) << "bind sockfd success: " << _listensockfd;// 3. 因为 tcp 是面向连接的,tcp需要未来不断地获取连接// listen 就是监听连接的意思,所以需要设置一个队列,来保存等待连接的客户端// 队列的长度为 8,表示最多可以有 8 个客户端等待连接// listen(int sockfd, int backlog)// sockfd 就是之前创建的 socket 句柄// backlog 就是队列的长度// 返回值:成功返回 0,失败返回 -1if (::listen(_listensockfd, BACKLOG) < 0) {LOG(LogLevel::ERROR) << "listen socket error: " << strerror(errno);Die(4);}LOG(LogLevel::INFO) << "listen sockfd success: " << _listensockfd;
}

我们先将 Loop() 函数设计成死循环,验证一下 初始化函数 的正确性

// 测试
void Loop() {_isrunning = true;while (_isrunning) {sleep(1);}_isrunning = false;
}

在这里插入图片描述

2.5 循环监听

执行服务函数主要分为两步:

  1. 获取新连接 ( accept函数 [从已完成连接队列的头部返回下一个已完成连接,如果队列为空,则阻塞调用进程])

在这里插入图片描述
2. 执行服务 (前提是获取到新连接)

2.5.1 server 0 - 单执行流版本

工作机制:

  • 单线程通过while(1)循环依次处理每个客户端请求,处理完一个请求后才能处理下一个。

注意:tcp协议可以直接使用read,write函数读写文件描述符的内容(因为tcp是面向字节流的)!

// server - 0 单执行流版本
// ​工作机制:单线程通过while(1)循环依次处理每个客户端请求,处理完一个请求后才能处理下一个。
void Loop() {_isrunning = true;while (_isrunning) {struct sockaddr_in client;socklen_t len = sizeof(client);// 1. 获取新连接int sockfd = ::accept(_listensockfd, CONV(&client), &len);if (sockfd < 0) {LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);continue;}InetAddr cli(client);LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr()<< " sockfd: " << sockfd;// 获取成功Server(sockfd, cli);}
}
void Server(int sockfd, InetAddr& cli) {// 长服务while (true) {char inbuffer[1024]; // 当作字符串// 1. 读文件// read// 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数// 返回值:成功返回读入的字节数,失败返回 -1ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0) {inbuffer[n] = 0;LOG(LogLevel::INFO)<< "get msg from " << cli.AddrStr() << " msg: " << inbuffer;std::string echo_string = "[server echo]# ";echo_string += inbuffer;// 2. 写文件write(sockfd, echo_string.c_str(), echo_string.size());} else if (n == 0) {LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";break;} else {LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);break;}}::close(sockfd);
}

2.5.2 server 1 - 多进程版本

​工作机制:

  • 父进程通过fork()为每个新连接创建子进程,子进程处理完请求后退出。
  • 但是进程创建/销毁开销大,​高并发时资源耗尽​
void Loop() {_isrunning = true;while (_isrunning) {struct sockaddr_in client;socklen_t len = sizeof(client);// 1. 获取新连接int sockfd = ::accept(_listensockfd, CONV(&client), &len);if (sockfd < 0) {LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);continue;}InetAddr cli(client);LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr()<< " sockfd: " << sockfd;// 获取成功pid_t id = fork();if (id == 0) {              // 子进程::close(_listensockfd); // 关闭监听套接字(子进程不需要监听)// 孙子进程的创建if (fork() > 0)exit(0); // 父进程(子进程)退出,孙子进程继续运行// 孙子进程执行实际服务逻辑Server(sockfd, cli);exit(0);}// 父进程(主进程)::close(sockfd); // 关闭连接套接字(父进程不处理具体业务)int n = waitpid(id, nullptr, 0); // 等待子进程退出if (n > 0) {LOG(LogLevel::INFO) << "wait chid success";}}_isrunning = false;
}
void Server(int sockfd, InetAddr& cli) {// 长服务while (true) {char inbuffer[1024]; // 当作字符串// 1. 读文件// read// 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数// 返回值:成功返回读入的字节数,失败返回 -1ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0) {inbuffer[n] = 0;LOG(LogLevel::INFO)<< "get msg from " << cli.AddrStr() << " msg: " << inbuffer;std::string echo_string = "[server echo]# ";echo_string += inbuffer;// 2. 写文件write(sockfd, echo_string.c_str(), echo_string.size());} else if (n == 0) {LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";break;} else {LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);break;}}::close(sockfd);
}

双重 fork() 的核心目的

  1. 避免僵尸进程​
    • 若子进程直接处理请求并退出,父进程需通过 waitpid 回收资源,否则子进程会成为僵尸进程。
    • 高并发场景下,父进程可能因频繁调用 waitpid 而阻塞,无法及时处理新连接。
  • ​解决方案:
    • 子进程创建孙子进程后立即退出,孙子进程成为 ​孤儿进程,由 init 进程(PID=1)接管并自动回收资源。
    • 父进程只需等待子进程(短暂存在)退出,避免阻塞。
  1. 父进程快速回到主循环
    • 父进程的 waitpid 仅需等待子进程(而非孙子进程),子进程退出速度极快(仅执行 fork() 和 exit()),父进程迅速返回 accept 循环。
    • 提升服务器并发处理能力。

2.5.3 server 2 - 多线程版本

​工作机制:

  • 为每个新连接分配独立的线程处理业务逻辑
  1. 主线程Loop 函数)​通过 accept 循环监听新连接,为每个新连接创建线程(pthread_create)传递连接信息给子线程(通过 ThreadDate 结构体)
  2. 子线程Execute 函数)​​调用 pthread_detach 分离自身(避免主线程调用 pthread_join)执行 Server 函数处理具体业务逻辑线程结束后自动释放资源(通过 delete td)
  3. 业务处理Server 函数)​循环读取客户端数据(read),处理业务逻辑(示例中的回显服务),发送响应(write)关闭连接(close(sockfd))
        // server - 2 多线程版本// 为每个新连接分配独立的线程处理业务逻辑// (Loop 函数)通过 accept 循环监听新连接,为每个新连接创建线程(pthread_create)传递连接信息给子线程(通过 ThreadDate 结构体)// (Execute 函数)​​调用 pthread_detach 分离自身(避免主线程调用 pthread_join)执行 Server 函数处理具体业务逻辑线程结束后自动释放资源(通过 delete td)// (Server 函数)​循环读取客户端数据(read),处理业务逻辑(示例中的回显服务),发送响应(write)关闭连接(close(sockfd))void Loop(){_isrunning = true;while(_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// 1. 获取新连接int sockfd = ::accept(_listensockfd, CONV(&client), &len);if(sockfd < 0){LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);continue;}InetAddr cli(client);LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr() << " sockfd: " << sockfd;// 获取成功pthread_t tid;ThreadDate* td = new ThreadDate(sockfd, this, cli);// pthread_create 第一个参数是线程id,第二个参数是线程属性,第三个参数是线程函数,第四个参数是线程函数参数pthread_create(&tid, nullptr, Execute, td);}_isrunning = false;}// 线程函数参数对象class ThreadDate{public:int _sockfd;TcpServer* _self;InetAddr _addr;public:ThreadDate(int sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr){}};// 线程函数static void* Execute(void* args){ThreadDate* td = static_cast<ThreadDate*>(args);// 子线程结束后由系统自动回收资源,无需主线程调用 pthread_joinpthread_detach(pthread_self()); // 分离新线程,无需主线程回收td->_self->Server(td->_sockfd, td->_addr);delete td;return nullptr;}void Server(int sockfd, InetAddr& cli){// 长服务while(true){char inbuffer[1024]; // 当作字符串// 1. 读文件// read 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数// 返回值:成功返回读入的字节数,失败返回 -1ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if(n > 0){inbuffer[n] = 0;LOG(LogLevel::INFO) << "get msg from " << cli.AddrStr() << " msg: " << inbuffer;std::string echo_string = "[server echo]# ";echo_string += inbuffer;// 2. 写文件write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";break;}else{LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);break;}}::close(sockfd);}

2.5.4 server 3 - 内存池

将执行服务的函数入线程池队列,该函数需要是参数为空和返回值为void的函数,因此需要bind绑定函数!

using task_t = std::function<void(int sockfd, InetAddr& cli)>;
// server - 3 内存池版本
void Loop() {_isrunning = true;while (_isrunning) {struct sockaddr_in client;socklen_t len = sizeof(client);// 1. 获取新连接int sockfd = ::accept(_listensockfd, CONV(&client), &len);if (sockfd < 0) {LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);continue;}InetAddr cli(client);LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr()<< " sockfd: " << sockfd;// 获取成功task_t task = std::bind(&TcpServer::Server, this, sockfd, cli);ThreadPool<task_t>::getInstance()->Equeue(task);}_isrunning = false;
}
void Server(int sockfd, InetAddr& cli) {// 长服务while (true) {char inbuffer[1024]; // 当作字符串// 1. 读文件// read// 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数// 返回值:成功返回读入的字节数,失败返回 -1ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0) {inbuffer[n] = 0;LOG(LogLevel::INFO)<< "get msg from " << cli.AddrStr() << " msg: " << inbuffer;std::string echo_string = "[server echo]# ";echo_string += inbuffer;// 2. 写文件write(sockfd, echo_string.c_str(), echo_string.size());} else if (n == 0) {LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";break;} else {LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);break;}}::close(sockfd);
}

🏳️‍🌈三、TcpClient.cpp

  1. ​创建Socket,调用 socket(AF_INET, SOCK_STREAM, 0) 创建TCP套接字。失败时打印错误并退出。
  2. ​设置服务器地址,通过 sockaddr_in 结构指定服务器的IP和端口。inet_pton 将字符串IP转换为网络字节序。
  3. 建立连接,调用 connect 主动连接服务器。失败时打印错误并退出。
  4. ​交互式通信,循环读取用户输入(如 hello)。通过 write 发送消息到服务器。通过 read 读取服务器回显的消息并显示。若服务器断开或读失败,退出循环。
  5. ​关闭连接,调用 close 关闭套接字。
#include "TcpClient.hpp"int main(int argc, char* argv[]){if(argc != 3){std::cerr << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;Die(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建套接字// AF_INET: IPv4协议// SOCK_STREAM: TCP协议// 0: 表示默认协议int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "Create socket error: " << std::strerror(errno) << std::endl;Die(2);}// 2. 设置服务器地址struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;    // IPv4协议server.sin_port = htons(serverport); // 端口号// 这句话表示将字符串形式的IP地址转换为网络字节序的IP地址// inet_pton函数的作用是将点分十进制的IP地址转换为网络字节序的IP地址// 这里的AF_INET表示IPv4协议// 这里的serverip.c_str()表示IP地址的字符串形式// &server.sin_addr表示将IP地址存储到sin_addr成员变量中::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);   // IP地址// 3. 与服务器建立连接int n = ::connect(sockfd, CONV(&server), sizeof(server));if(n < 0){std::cerr << "Connect error: " << std::strerror(errno) << std::endl;Die(5);}// 4. 发送消息while(true){std::string mag;std::cout << "Enter# ";std::getline(std::cin, mag);write(sockfd, mag.c_str(), mag.size());char echo_buf[1024];n = read(sockfd, echo_buf, sizeof(echo_buf));if(n > 0){echo_buf[n] = 0;std::cout << "Echo: " << echo_buf << std::endl;}else{break;}}// 5. 关闭套接字::close(sockfd);return 0;
}

👥总结

本篇博文对 【Linux网络】各版本TCP服务器构建 - 从理解到实现 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

相关文章:

  • 基于Python+Pytest实现自动化测试(全栈实战指南)
  • 从单点突破到链式攻击:XSS 的渗透全路径解析
  • Linux-信号
  • 【产品经理从0到1】用户研究和需求分析
  • Python 设计模式:桥接模式
  • 23种设计模式-结构型模式之桥接模式(Java版本)
  • LangChain4j 搭配 Kotlin:以协程、流式交互赋能语言模型开发
  • 联易融助力乡村振兴,仙湖茶产业焕新机
  • 智能指针之设计模式4
  • 网络安全·第五天·TCP协议安全分析
  • leetcode0207. 课程表-medium
  • WordPress 只能访问html文件,不能访问php
  • (最新)华为 2026 届校招实习-硬件技术工程师-硬件通用/单板开发—机试题—(共14套)(每套四十题)
  • flutter 插件收集
  • 联易融出席深圳链主企业供应链金融座谈会,加速对接票交所系统
  • AI 模型在前端应用中的典型使用场景和限制
  • Activity使用优化
  • Elasticsearch性能优化实践
  • Nacos 2.0.2 在 CentOS 7 上开启权限认证(含 Docker Compose 配置与接口示例)
  • linux 手动触发崩溃
  • 安徽一季度GDP为12265亿元,同比增长6.2%
  • 南阳市委副书记、政法委书记金浩任内落马
  • 对话地铁读书人|翻译Esther:先读原著,再看电影
  • 独家专访|苏童:《好天气》是一部献给中国郊区的作品
  • 海口市美兰区委副书记、区长吴升娇去世,终年41岁
  • 中国三项文献遗产新入选《世界记忆名录》