云计算百科
云计算领域专业知识百科平台

【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; // 监听socket
uint16_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 初始化方法

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

  • 创建 socket (类型与UDP不同)
  • bind sockfd 和 socket addr
  • 获取连接(与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. 绑定 socket
    if (::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,失败返回 -1
    if (::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 循环监听

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

  • 获取新连接 ( 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
    // 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
    // 返回值:成功返回读入的字节数,失败返回 -1
    ssize_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
    // 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
    // 返回值:成功返回读入的字节数,失败返回 -1
    ssize_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() 的核心目的

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

    ​工作机制:

    • 为每个新连接分配独立的线程处理业务逻辑
  • 主线程(Loop 函数)​通过 accept 循环监听新连接,为每个新连接创建线程(pthread_create)传递连接信息给子线程(通过 ThreadDate 结构体)
  • 子线程(Execute 函数)​​调用 pthread_detach 分离自身(避免主线程调用 pthread_join)执行 Server 函数处理具体业务逻辑线程结束后自动释放资源(通过 delete td)
  • 业务处理(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_join
    pthread_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 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
    // 返回值:成功返回读入的字节数,失败返回 -1
    ssize_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
    // 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
    // 返回值:成功返回读入的字节数,失败返回 -1
    ssize_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

  • ​创建Socket,调用 socket(AF_INET, SOCK_STREAM, 0) 创建TCP套接字。失败时打印错误并退出。
  • ​设置服务器地址,通过 sockaddr_in 结构指定服务器的IP和端口。inet_pton 将字符串IP转换为网络字节序。
  • 建立连接,调用 connect 主动连接服务器。失败时打印错误并退出。
  • ​交互式通信,循环读取用户输入(如 hello)。通过 write 发送消息到服务器。通过 read 读取服务器回显的消息并显示。若服务器断开或读失败,退出循环。
  • ​关闭连接,调用 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服务器构建 – 从理解到实现 做了一个较为详细的介绍,不知道对你有没有帮助呢

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux网络】各版本TCP服务器构建 - 从理解到实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!