TCP Echo Server 深度解析:从单进程到线程池的演进之路(下)

TCP Echo Server 深度解析:从单进程到线程池的演进之路(中)-CSDN博客


1. 四大版本演进

1.0 Service() ------ 核心服务逻辑

复制代码
void Service(int sockfd, InetAddr &peer)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            LOG(LogLevel::DEBUG) << peer.StringAddr() << " say#" << buffer;
            std::string echo_string = "echo# " + buffer;
            write(sockfd, echo_string.c_str(), echo_string.size());
        }
        else if (n == 0)  // 对端关闭连接
        {
            LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
            close(sockfd);
            break;
        }
        else  // 读取错误
        {
            LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常了...";
            close(sockfd);
            break;
        }
    }
}

read 返回值含义

  • n > 0: 读取到 n 字节数据

  • n == 0: 对端关闭连接(读到文件结尾,类似 pipe)

  • n < 0: 读取错误

1.1 Version 0 ------ 单进程(测试版本)

复制代码
// 直接在 Run() 中调用 Service
Service(sockfd, addr);
  • 同一时刻只能服务一个客户端,后续连接必须等待。

  • 如果 Service 内部是一个 while 循环,那么后续连接永远得不到处理。

单进程只适合教学演示,毫无实用价值。

1.2 Version 1 ------ 多进程版本

复制代码
pid_t id = fork();
if (id < 0) {
    // fork 失败
} else if (id == 0) {
    // 子进程
    close(_listensockfd);  // 子进程不需要监听套接字
    
    if (fork() > 0)        // 再次 fork,子进程退出
        exit(OK);           // 父进程(第一代)等待这个子进程
    
    // 孙子进程成为孤儿进程,由 init 进程收养
    Service(sockfd, addr);
    exit(OK);
} else {
    // 父进程
    close(sockfd);           // 父进程不需要通信套接字
    waitpid(id, nullptr, 0); // 非阻塞等待(子进程立即退出)
}

为什么要 fork 两次?

为什么要两次 fork?

  • 第一次 fork 创建子进程,**之后子进程立刻再 fork 一个孙子进程,**然后子进程退出。

  • 效果孙子进程的父进程变成 init(PID=1),由 init 自动回收。

  • 好处 :父进程(原始服务器)中的 waitpid 只会等待子进程(很短的生命周期),不会阻塞。孙子进程成为孤儿,独立提供服务,服务器父进程无需关心它的回收问题,从而可以继续 accept 新连接。

同时注意:

  • 子进程关闭 _listensockfd:防止文件描述符泄漏,且不需要。

  • 父进程关闭 sockfd:每个连接的文件描述符由服务进程持有,父进程只负责监听。

多进程缺点

  • 每个连接一个进程,进程切换开销大。

  • 大并发会导致系统创建大量进程,资源紧张。

1.3 Version 2 ------ 多线程版本

复制代码
class ThreadData {
public:
    int sockfd;
    InetAddr addr;
    TcpServer *tsvr;
    ThreadData(int fd, InetAddr &ar, TcpServer *s) 
        : sockfd(fd), addr(ar), tsvr(s) {}
};

static void *Routine(void *args) {
    pthread_detach(pthread_self());  // 分离线程,自动回收
    ThreadData *td = static_cast<ThreadData *>(args);
    td->tsvr->Service(td->sockfd, td->addr);
    delete td;
    return nullptr;
}

// 在 Run() 中:
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);

线程 vs 进程

特性 多进程 多线程
资源开销 大(独立地址空间) 小(共享地址空间)
切换开销 大(需切换页表) 小(共享页表)
通信方式 IPC(管道、共享内存等) 直接共享变量
稳定性 一个进程崩溃不影响其他 一个线程崩溃可能拖垮整个进程
创建速度 慢(fork 需拷贝页表) 快(只需分配栈)
  • 线程比进程轻量,共享地址空间,传递数据更方便。

  • 需注意线程安全(例如共享的日志模块用了互斥锁)。

静态成员函数 Routine 的作用

pthread_create 要求线程入口函数具有 C 语言风格 void*(*)(void*),不能直接是成员函数(因为有隐含 this)。
所以我们定义一个静态成员函数,将 this 通过 ThreadData 传入,在内部再调用真正的 Service

复制代码
static void* Routine(void *args) {
    pthread_detach(pthread_self());  // 分离线程,自动回收
    ThreadData *td = static_cast<ThreadData*>(args);
    td->tsvr->Service(td->sockfd, td->addr);
    delete td;
    return nullptr;
}

另外调用了 pthread_detach(pthread_self()),使线程结束后自动释放资源,避免需要 pthread_join

1.4 Version 3 ------ 线程池版本

复制代码
// 线程池适合处理"短服务"(快速响应的任务)
ThreadPool<task_t>::GetInstance()->Enqueue([this, &sockfd, &addr](){
    this->Service(sockfd, addr);
});

线程池的优势

  • 预先创建一批线程,从任务队列取任务执行。

  • 任务用 lambda 表达式封装,捕获 sockfdaddr(注意按值捕获,因为变量会出作用域)。

  • **适合处理短服务,**如 HTTP 请求;对于长连接(比如聊天),线程池中的线程仍会被长期占用,此时多线程模型更合适。

1.5 客户端需要 bind 吗?

1.6 客户端不需要 listen/accept

总结细节:


2. 相关代码

2.1 TcpServer.cc

复制代码
#include "Tcpserver.hpp"
#include "Log.hpp"

using namespace LogModule;
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " port" << std::endl;
}

// ./tcoserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]); //端口号

    Enable_Console_Log_Stratege();

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Run();
    return 0;
}

2.2 TcpServer.hpp

复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <memory> //可能会用到智能指针
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include <signal.h>

using namespace LogModule;
using namespace ThreadPoolModule;

using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&)>;

const static int defaultsockfd = -1;
const static int backlog = 8;
// 服务器往往是禁止拷贝的
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port, func_t func)
        : _port(port),
          _listensockfd(defaultsockfd),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // signal(SIGCHLD,SIG_IGN); //忽略

        // 1.创建套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::DEBUG) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3

        // 2.bind总所周知的端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3

        // 3.设置socket状态为listen
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
    }

    class ThreadData
    {
        //InetAddr无需构造
    public:
        ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
        {
        }

    public:
        int sockfd;
        InetAddr addr;
        TcpServer *tsvr;
    };

    //长服务 -- 你不退出,它不退:多进程多线程合适
    void  Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1.先读取数据
            // a.n > 0 读取成功
            // b.n < 0 读取失败
            // c.n ==0 对端把链接关闭,读到了文件结尾 -- pipe
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0; // 设置为C风格的字符串  n<=sizeof(buffer)-1
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " say#" << buffer;
                // 2.写回数据
                std::string echo_string = "echo# ";
                echo_string += buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel ::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel ::DEBUG) << peer.StringAddr() << "异常了...";
                close(sockfd);
                break;
            }
        }
    }

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->addr);
        delete td;
        return nullptr;
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // a.服务器必须先获取链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 如果没有连接,accept就会阻塞
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "accept success, peer addr: " << addr.StringAddr();

            // version0 -- test version -- 单进程程序 -- 不会存在的
            // Service(sockfd, addr);

            // version1 -- 多进程版本
            // pid_t id = fork(); //父进程
            // if (id < 0)
            // {
            //     LOG(LogLevel::FATAL) << "fork error";
            //     exit(FORK_ERR);
            // }
            // else if (id == 0)
            // {
            //     // 子进程,子进程除了看到 sockfd,能看到listensocket?
            //     //我们不想让子进程访问listensocket
            //     close(_listensockfd);

            //     if(fork() > 0) //再次fork,子进程退出
            //         exit(OK);

            //     Service(sockfd,addr);  //孙子进程,孤儿进程,1,系统回收
            //     exit(OK);  //正确处理完任务
            // }
            // else
            // {
            //     // 父进程
            //     close(sockfd);

            //     //父进程是不是要等待子进程?否则会僵尸
            //     pid_t rid = waitpid(id,nullptr,0); //阻塞?不会,因为子进程立马退出了
            //     (void)rid;
            // }

            // version2:多线程版本
            // ThreadData *td = new ThreadData(sockfd, addr, this);
            // pthread_t tid;
            // pthread_create(&tid, nullptr, Routine, td);

            // version3: 线程池版本,线程池一般适合处理短服务
            //将新链接和客户端构建一个新任务,push到线程池中
             ThreadPool<task_t>::GetInstance()->Enqueue([this,&sockfd,&addr](){
                 this->Service(sockfd,addr);
             });
        }
        _isrunning = false;
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd; // 监听套接字

    bool _isrunning;
    func_t _func; // 设置回调处理
};

2.3 TcpClient.cc

复制代码
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " server_ip server_prot" << std::endl;
}

// ./tcpclient serveer_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1.创建套接字socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(SOCKET_ERR);
    }

    // 2.需要bind吗?       --> 需要!
    // 是否需要显式绑定?    --> 不需要,采用随即方式选择端口号
    // 需要listen?accpet?  --> 不需要
    // 直接向目标服务器发起建立连接的请求
    InetAddr serveraddr(serverip,serverport);
    int n = connect(sockfd,serveraddr.NetAddrPtr(),serveraddr.NetAddrLen());
    if( n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    //3.echo client
    while(true)
    {
        std::string line;
        std::cout << "Please Enter@ ";
        std::getline(std::cin,line);

        write(sockfd,line.c_str(),line.size());
        char buffer[1024];
        ssize_t size = read(sockfd,buffer,sizeof(buffer));
        if(size > 0)
        {
            buffer[size] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

2.4 Makefile

复制代码
.PHONY:all
all:tcpclient tcpserver

tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++17 
tcpserver:TcpServer.cc
	g++ -o $@ $^ -std=c++17 

.PHONY:clean
clean:
	rm -f tcpclient tcpserver
相关推荐
2301_812539671 小时前
mysql如何限制用户连接数_使用MAX_USER_CONNECTIONS优化并发
jvm·数据库·python
MongoDB 数据平台1 小时前
MongoDB 驱动效能革新:盖雅工场报表查询效率跃升8倍
数据库·mongodb
贝锐1 小时前
远程控制如何赋能全平台设备?信创环境下贝锐向日葵的跨平台优势
linux·运维·远程控制
SurpriseDPD1 小时前
Linux 内核 static_branch_likely:零开销条件分支
linux
X56611 小时前
Python Django怎么处理404_关闭DEBUG模式并自定义配置全局404与500友好错误重定向页面
jvm·数据库·python
li1670902701 小时前
第2课:Linux基础指令(上)
linux·运维·服务器
m0_748554811 小时前
golang如何集成Etcd配置中心_golang Etcd配置中心集成方法
jvm·数据库·python
li1670902701 小时前
第1课:Linux环境部署
linux·运维·服务器·vim
tian_jiangnan1 小时前
Proxmox VE – 修复 LVM Thin Pool “pve/data” 激活失败
linux·服务器·centos