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

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

在这里插入图片描述

在这里插入图片描述


前面几篇文章中使用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 初始化方法

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

  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. 绑定 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 循环监听

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

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

  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_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

  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服务器构建 - 从理解到实现 做了一个较为详细的介绍,不知道对你有没有帮助呢

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

评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值