在这里插入图片描述

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!

🎬 博主简介:

在这里插入图片描述



前言:

在 Linux 网络编程体系中,TCP 协议是面向连接、可靠字节流传输的基石,HTTP/HTTPS、FTP、SSH 等绝大多数上层应用协议均基于 TCP 构建。相较于 UDP 的无连接特性,TCP 的连接管理、可靠传输、流量控制等机制,让其成为稳定网络服务的首选。Echo 回显服务器是 TCP 网络编程的经典入门案例,它完整覆盖了 TCP 服务端从套接字创建、地址绑定、连接监听到数据收发的全流程。本文将以演进式的思路,从单进程基础版到多进程并发版,最终落地多线程高并发版,拆解 TCP 编程的核心原理、API 使用、并发处理方案,同时结合配套源码深度解读工业级代码的设计思路。


一. TCP 协议核心特性与基础 API 详解

1.1 TCP 与 UDP 的核心差异

特性 TCP 协议 UDP 协议
连接特性 面向连接,需三次握手建立连接、四次挥手释放连接 无连接,无需提前建立通信链路
传输可靠性 可靠传输,保证数据有序、无重复、不丢失 不可靠传输,不保证数据送达与有序性
传输模式 面向字节流,无边界的流式数据传输 面向数据报,有边界的独立数据包传输
并发支持 需为每个连接维护独立的传输控制块 无需维护连接状态,天然支持多客户端收发
适用场景 文件传输、网页访问、远程登录等需可靠传输的场景 实时直播、游戏帧同步、DNS 查询等低延迟场景

同时 TCP 与 UDP 均支持全双工通信,同一个套接字文件描述符可同时进行读写操作,这是后续并发设计的核心基础。

1.2 TCP 服务端核心 API 全解析

TCP 服务端编程有固定的四步核心流程,对应 5 个关键系统 API,也是所有 TCP 服务的通用骨架:

  • socket():创建流式套接字,获取网络文件描述符
  • bind():将套接字与固定的 IP 地址、端口号绑定
  • listen():将套接字设置为监听状态,准备接收客户端连接
  • accept():阻塞等待并获取已完成三次握手的客户端连接,返回专属 IO 套接字
  • read()/write():通过 IO 套接字与客户端进行双向数据读写

在这里插入图片描述

1.3 TCP 客户端核心 API

客户端核心流程为「创建套接字 -> 发起连接 -> 数据收发」,核心 API 为connect(),用于向服务端发起 TCP 三次握手,建立连接。客户端无需显式调用bind(),操作系统会在connect()时自动为套接字分配随机空闲端口。


二. 项目前置工具组件解析(有的上篇博客都写过了,所以我们这里只截取片段)

本项目复用了工业级的基础组件,屏蔽底层细节的同时保证代码的健壮性与可维护性,核心组件如下:

2.1 网络地址封装:InetAddr.hpp

核心设计思路:将sockaddr_in结构体、网络 / 主机字节序转换、地址判等、格式化输出等操作完全封装,屏蔽原生 socket 接口的底层细节,让上层代码无需关心字节序转换。

核心源码片段与解读

// 网络转本地:recvfrom/accept后解析对端地址
InetAddr(struct sockaddr_in &addr): _net_addr(addr)
{
    // ntohs:16位网络字节序转主机字节序
    _port = ntohs(_net_addr.sin_port);
    // inet_ntoa:32位网络IP转点分十进制字符串
    _ip = inet_ntoa(_net_addr.sin_addr);
}

// 本地转网络:构建服务端绑定地址/客户端目标地址
InetAddr(uint16_t port, const std::string ip = "0.0.0.0")
    : _port(port), _ip(ip)
{
    _net_addr.sin_family = AF_INET;
    // htons:主机字节序转网络字节序
    _net_addr.sin_port = htons(_port);
    _net_addr.sin_addr.s_addr = inet_addr(_ip.c_str()); 
}

// 重载==运算符:IP+端口唯一标识一个网络端点
bool operator==(const InetAddr &who) const
{
    return (_ip == who._ip) && (_port == who._port);
}

解读:双构造函数实现了网络与主机字节序的双向自动转换,重载 == 运算符实现了客户端端点的唯一性判断,是后续连接管理的核心基础。

2.2 线程安全保障:Mutex.hpp 与 LockGuard

核心设计思路:基于 RAII(资源获取即初始化)机制封装互斥锁,实现锁的自动生命周期管理,彻底避免手动加解锁导致的死锁、资源泄漏问题。

核心源码片段与解读

// 互斥锁基础封装
class Mutex
{
public:
    Mutex() { pthread_mutex_init(&_lock, nullptr); }
    ~Mutex() { pthread_mutex_destroy(&_lock); }
    void Lock() { pthread_mutex_lock(&_lock); }
    void UnLock() { pthread_mutex_unlock(&_lock); }
    pthread_mutex_t* Origin() { return &_lock; }
private:
    pthread_mutex_t _lock;
};

// RAII锁守卫
class LockGuard
{
public:
    LockGuard(Mutex* lockptr) : _lockptr(lockptr)
    {
        _lockptr->Lock(); // 构造时立即加锁
    }
    ~LockGuard()
    {
        _lockptr->UnLock(); // 离开作用域析构时自动解锁
    }
private:
    Mutex* _lockptr;
};

解读:LockGuard 通过构造与析构函数实现了锁的自动管理,无论代码正常执行还是异常抛出,都能保证锁被正确释放,是多线程编程中线程安全的核心保障。

2.3 日志系统:Logger.hpp

核心设计思路:采用策略模式解耦日志的生成与输出,支持控制台 / 文件双输出策略,通过 RAII 机制实现日志的自动刷新,同时保证多线程环境下的输出安全。

在这里插入图片描述


三. TCP EchoServer 的演进式实现

3.1 三版本完整代码整合预览(后续几个板块进行细节拆解)

我们这里客户端就实现了个简单的框架就不展示了。

  • TcpEchoServer.hpp
#ifndef __TCP__ECHOSERVER__HPP
#define __TCP__ECHOSERVER__HPP

#include <csignal>
#include <cstdint>
#include <pthread.h>
#include <string>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace LogModule;

static const uint16_t gdefaultport = 8080;
static const int gbacklog = 32;

class TcpEchoServer 
{
private:
    // sockfd: 既可以支持读,又可以支持写, TCP socket也是全双工的.
    // 该函数为具体的业务处理逻辑,扮演“服务员”的角色
    void Service(int sockfd, InetAddr client)
    {
        // 长连接服务:只要客户端不主动退出,服务器就一直为其提供服务
        while (true) 
        {
            char inbuffer[1024];

            // 1. 读取数据 (类似于读取文件)
            int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if(n > 0)
            {
                // n > 0 表示成功读取到了 n 个字节的数据
                inbuffer[n] = 0; // 将读取到的字节流转换为 C 风格的字符串
                LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;
            }
            else if(n == 0)
            {
                // 注意:n == 0 在 TCP 中是一个强信号,代表对端(客户端)已经关闭了连接 (EOF)
                // 此时服务端也应该随之结束服务并退出循环
                LOG(LogLevel::INFO) << client.StringAddress() << " close sockfd: " << sockfd << ", me too!";
                break;
            }
            else 
            {
                // n < 0 表示读取发生错误(如被信号中断等)
                LOG(LogLevel::ERROR) << "read socket error";
                break;
            }

            // 加工处理数据:体现 Echo (回显) 的核心业务逻辑
            std::string echo_string = "server echo# ";
            echo_string += inbuffer;

            // 2. 写回数据 (将加工后的字符串发回给客户端)
            int m = write(sockfd, echo_string.c_str(),  echo_string.size());
            if(m < 0)
            {
                LOG(LogLevel::ERROR) << "write socket error";
                break;
            }
        }

        // 极其重要:服务结束后,必须关闭该客户端对应的通信套接字,否则会导致系统文件描述符(fd)泄漏
        close(sockfd);
    }
public:
    // 构造函数:初始化监听端口,并将监听套接字初始化为 -1(表示无效状态)
    TcpEchoServer(uint16_t port = gdefaultport): _port(port), _listensockfd(-1)
    {}

    // 服务器初始化:完成网络编程经典的“三板斧” (socket -> bind -> listen)
    void Init()
    {
        // 1. 创建套接字 (买手机)
        // AF_INET: IPv4 网络协议; SOCK_STREAM: 面向字节流的 TCP 协议
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 设置成0就可以了,系统会自动推导为 IPPROTO_TCP
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error: " << _listensockfd;
            exit(2);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        // 2.bind (办手机卡,绑定号码)
        // 我们这里是可以直接使用我们的InetAddr的,但是我们后面再用
        struct sockaddr_in local;
        socklen_t len = sizeof(local);
        memset(&local, 0, len); // 结构体清零,养成良好习惯
        local.sin_family = AF_INET;
        local.sin_port = htons(_port); // 主机字节序(Host) 转 网络字节序(Network) -> Short(16位端口)
        local.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有网卡的 IP 地址

        int n = bind(_listensockfd, (struct sockaddr*)&local, len);
        if(n < 0)
        {
            // 常见错误:端口被占用时会 bind 失败
            LOG(LogLevel::FATAL) << "bind error: " << _listensockfd;
            exit(3);
        }
        LOG(LogLevel::INFO) << "bind success: " << _listensockfd;

        // 3. 设置成监听 (开机并设置铃声,准备接听电话)
        // gbacklog (全连接队列长度) 决定了底层最多能缓存多少个未被 accept 的连接
        n = listen(_listensockfd, gbacklog);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error: " << _listensockfd;
            exit(3);
        }
        LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
    }

    // 内部类:用于在多线程环境下向新线程传递参数
    // 因为 pthread_create 的回调函数只接受一个 void* 参数,所以需要封装为一个结构体/类
    class ThreadData
    {
    public:
        ThreadData(int sockfd, InetAddr addr, TcpEchoServer *owner)
                : _sockfd(sockfd)
                , _address(addr)
                , _owner(owner)       
        {}
        ~ThreadData(){}
    public:
        int _sockfd;           // 与客户端通信的文件描述符
        InetAddr _address;     // 客户端的地址信息
        TcpEchoServer *_owner; // 指向服务器对象本身的指针,用于调用类内部的非静态成员函数 (如 Service)
    };

    // 静态的
    // 必须声明为 static:因为类的非静态成员函数默认带有 this 指针,会导致参数类型与 pthread_create 不匹配
    static void* threadRun(void* args)
    {
        // 分离:让线程执行结束后由操作系统自动回收资源,主线程无需阻塞等待 (join)
        pthread_detach(pthread_self());
        
        // 还原数据类型
        ThreadData *td = static_cast<ThreadData*>(args);
        
        // 利用传入的 _owner 指针,调用服务器对象的 Service 方法开始通信
        td->_owner->Service(td->_sockfd, td->_address);
        
        // 资源清理:由于 ThreadData 是在堆上 new 出来的,用完必须释放,防止内存泄漏
        delete td;
        return nullptr;
    }

    void Start()
    {
        // 多进程版本等待的最佳实践 (如果你启用多进程版本,建议打开此注释)
        // signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号,让内核自动回收僵尸进程

        while(true)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            
            // accept: 从全连接队列中取出一个已经建立好的连接
            // _listensockfd 负责拉客,返回的 sockfd 负责专门为这个客人服务
            int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
            if (sockfd < 0)
            {
                // accept 失败不代表服务器崩溃,可能是信号打断,继续接待下一个客人即可
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            // 网络转主机:解析出客户端的 IP 和 端口
            InetAddr clientaddress(clientaddr);
            LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
            
        
            // 处理连接, 进行IO通信

            // ==============================================================================
            // 【Version 2 -- 多线程版本 (当前生效)】
            // 优势:比多进程轻量,创建成本低,所有线程共享进程的文件描述符表。
            // ==============================================================================
            pthread_t tid;
            // 必须在堆上 new 对象:如果在栈上创建,当前循环结束时 td 会被销毁,新线程访问会引发野指针崩溃
            ThreadData* td = new ThreadData(sockfd, clientaddress, this);
            pthread_create(&tid, nullptr, threadRun, (void*)td);
            // 我们这里不要去等待, 而是在回调的函数里面使用线程分离


            // ==============================================================================
            // 【Version 1 -- 多进程版本 (已注释)】
            // 优势:进程间强隔离,一个子进程崩溃不会波及主进程和其他客户端。
            // 劣势:每次 fork 开销较大,不适合高并发海量连接。
            // ==============================================================================
            // pid_t pid = fork();
            // if(pid < 0)
            // {
            //     LOG(LogLevel::ERROR) << "fork error";
            //     close(sockfd);
            // }
            // else if(pid == 0)
            // {
            //     // 子进程,拷贝父进程的文件描述符表,从而和父进程看到同一批文件
            //     // 关闭自己不需要的文件fd (子进程只负责通信,不需要监听拉客)
            //     close(_listensockfd);
            //     // **************************************
            //     // 优雅处理僵尸进程的方案二:孙子进程法 (两次 fork)
            //     // if(fork() > 0)
            //     //     exit(0); // 子进程直接退出,让孙子进程变成孤儿进程,由系统 init/systemd 接管回收
            //     // // 孙子进程 -- 你去执行
            //     // Service(sockfd, clientaddress);
            //     // **************************************
            //     Service(sockfd, clientaddress);
            //     exit(0); // 业务处理完毕,子进程退出
            // }
            // else{}
            // // 父进程不需要这个:父进程已经把这个客户端交给了子进程处理,自己必须关闭,否则会导致 fd 耗尽
            // close(sockfd);
            // // 父进程
            // pid_t rid = waitpid(pid, nullptr, 0); // 如果这里阻塞等待,就又变成了串行。若用孙子进程法,这里会瞬间返回


            // ==============================================================================
            // 【Version 0 -- 单进程串行版本 (已注释)】
            // 致命缺陷:Service 内部是死循环,会导致主进程卡死在这里,永远无法执行下一次 accept 接待新客人。
            // 一般根本不会使用这种长连接的单进程写法。
            // ==============================================================================
            // Service(sockfd, clientaddress);
        }
    }
    ~TcpEchoServer(){}
private:
    uint16_t _port;         // 服务器绑定的端口号
    int _listensockfd;      // 监听套接字 (拉客经理)
};
#endif

在这里插入图片描述
在这里插入图片描述

  • TcpEchoServer.cpp
#include "TcpEchoServer.hpp"
#include "Logger.hpp"
#include <cstdint>
#include <memory> // 引入 <memory> 库以提供智能指针 std::unique_ptr 的支持

// 打印程序的使用说明手册
// 当用户在命令行启动程序时如果没有带上正确的参数,调用此函数进行提示
void Usage(std::string procname)
{
    // procname 接收的通常是 argv[0],即程序本身的运行路径和名称
    std::cout << "Usage: " << procname << " ServerPort" << std::endl;
}

// ./tcp_echo_server 8080
int main(int argc, char *argv[])
{
    // 1. 参数校验:检查命令行参数的个数
    // argc 必须等于 2。因为 argv[0] 是 "./tcp_echo_server" (程序名),argv[1] 是 "8080" (端口号)
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1); // 校验失败,异常退出程序,返回状态码 1
    }

    // 2. 初始化日志系统
    // 启用控制台日志输出策略,后续底层调用的 LOG() 宏都会将信息直接打印到显示器上
    ENABLE_CONSOLE_LOG_STRATEGY();
    
    // 3. 解析并转换端口号
    // argv[1] 拿到的 "8080" 只是一个字符串,需要通过 std::stoi (string to integer) 转换为 16位无符号整数
    uint16_t ServerPort = std::stoi(argv[1]);
    
    // 4. 实例化服务器对象 (现代 C++ 最佳实践:智能指针)
    // std::make_unique 是 C++14 引入的工厂函数,它安全地在堆上创建对象,并交由 unique_ptr 管理。
    // 好处 (RAII 机制):我们不需要手动去写 delete tsvr。当 main 函数运行结束时,
    // 智能指针会自动调用 TcpEchoServer 的析构函数并释放堆内存,彻底杜绝内存泄漏。
    std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>(ServerPort);

    // 5. 驱动服务器生命周期
    // 第一步:Init()。在底层执行 socket() -> bind() -> listen(),完成服务端网卡的“挂号待命”。
    tsvr->Init();
    
    // 第二步:Start()。进入死循环,开始 accept() 接待客户端,并根据配置(单进程/多进程/多线程)提供读写服务。
    tsvr->Start();
    
    return 0; // 程序正常结束
}

3.2 V1 版本:单进程基础版 EchoServer

实现思路:遵循 TCP 服务端四步核心流程,单进程循环接收客户端连接,连接建立后同步处理客户端的读写请求,实现基础的回显功能。

核心源码片段

// 核心业务:回显服务
void Service(int sockfd, InetAddr client)
{
    // TCP长连接循环读写
    while (true) 
    {
        char inbuffer[1024];
        // 1. 读取客户端数据
        int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;
        }
        else if(n == 0) // 对端关闭连接
        {
            LOG(LogLevel::INFO) << client.StringAddress() << " close connection";
            break;
        }
        else // 读取异常
        {
            LOG(LogLevel::ERROR) << "read socket error";
            break;
        }
        // 2. 回写数据给客户端
        std::string echo_string = "server echo# ";
        echo_string += inbuffer;
        write(sockfd, echo_string.c_str(),  echo_string.size());
    }
    close(sockfd); // 关闭IO套接字
}

// 服务启动主循环
void Start()
{
    while(true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        // 获取已建立的客户端连接
        int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            continue;
        }
        InetAddr clientaddress(clientaddr);
        LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress();
        
        // 同步处理业务,阻塞主循环
        Service(sockfd, clientaddress);
    }
}

源码核心解读

  • 监听套接字与 IO 套接字分离_listensockfd是监听套接字,仅用于接收客户端连接;accept返回的sockfd是专属 IO 套接字,用于和对应客户端的双向数据传输,这是 TCP 并发编程的核心设计。
  • read 返回值处理n>0为成功读取的字节数,n=0表示客户端关闭了连接,n<0表示读取异常,三种情况必须分别处理,否则会导致连接泄漏或程序崩溃。
  • 版本核心缺陷:单进程串行处理,Service函数会阻塞主循环,同一时间只能处理一个客户端的连接与请求,无法支持多客户端并发访问。
    在这里插入图片描述

3.2 V2 版本:多进程并发版 EchoServer

实现思路accept获取新连接后,通过fork()创建子进程处理客户端业务,父进程继续循环监听新连接,实现多客户端并发处理。

核心源码片段

void Start()
{
    // 忽略子进程退出信号,自动回收僵尸进程,最佳实践
    signal(SIGCHLD, SIG_IGN);
    while(true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            continue;
        }
        InetAddr clientaddress(clientaddr);
        LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress();
        
        // 创建子进程处理业务
        pid_t pid = fork();
        if(pid == 0) // 子进程
        {
            close(_listensockfd); // 子进程不需要监听套接字,关闭避免资源泄漏
            Service(sockfd, clientaddress); // 处理客户端业务
            exit(0); // 业务处理完毕子进程退出
        }
        // 父进程
        close(sockfd); // 父进程不需要IO套接字,关闭文件描述符
    }
}

源码核心解读

  • 僵尸进程处理:通过signal(SIGCHLD, SIG_IGN)让操作系统自动回收退出的子进程,避免僵尸进程占用系统资源,是 Linux 多进程服务的最佳实践。也可以使用我们上面写的孙子进程的方式
  • 文件描述符管理:父子进程会共享文件描述符表,子进程必须关闭不需要的监听套接字,父进程必须关闭不需要的 IO 套接字,否则会导致文件描述符泄漏。
  • 版本优缺点:优点是实现简单,进程间地址空间隔离,单个客户端业务崩溃不会影响整个服务;缺点是进程创建 / 销毁开销大,并发量过高时系统资源占用严重。

在这里插入图片描述

在这里插入图片描述

3.3 V3 版本:多线程高并发版 EchoServer

实现思路accept获取新连接后,创建独立的子线程处理客户端业务,主线程继续监听新连接,通过线程分离实现自动资源回收,兼顾高并发与低资源开销。

核心源码片段

// 线程参数传递类
class ThreadData
{
public:
    ThreadData(int sockfd, InetAddr addr, TcpEchoServer *owner)
            : _sockfd(sockfd), _address(addr), _owner(owner) {}
public:
    int _sockfd;
    InetAddr _address;
    TcpEchoServer *_owner; // 传递this指针,访问类内成员方法
};

// 线程入口函数必须为静态,无隐含this指针,适配pthread库接口
static void* threadRun(void* args)
{
    // 线程分离:线程退出时系统自动回收资源,无需主线程join
    pthread_detach(pthread_self());
    ThreadData *td = static_cast<ThreadData *>(args);
    // 调用业务处理方法
    td->_owner->Service(td->_sockfd, td->_address);
    // 释放堆上的参数对象
    delete td;
    close(td->_sockfd);
    return nullptr;
}

// 服务启动主循环
void Start()
{
    while(true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            continue;
        }
        InetAddr clientaddress(clientaddr);
        LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
        
        // 创建子线程处理业务
        pthread_t tid;
        ThreadData* td = new ThreadData(sockfd, clientaddress, this);
        pthread_create(&tid, nullptr, threadRun, (void*)td);
    }
}

源码核心解读

  • 静态线程入口函数:pthread 库要求线程入口函数必须是void* (*)(void*)格式,类的普通成员函数隐含 this 指针,因此必须设为静态函数,通过参数传递 this 指针访问类内成员。
  • 线程分离机制pthread_detach(pthread_self())将线程设置为分离状态,线程退出时系统自动回收资源,无需主线程调用pthread_join阻塞等待,避免主线程阻塞。
  • 线程资源管理:ThreadData 对象在堆上创建,在线程业务处理完毕后释放,避免内存泄漏;多线程共享进程的文件描述符表,因此无需关闭监听套接字,仅需在业务处理完毕后关闭 IO 套接字。
  • 版本优势:线程创建 / 销毁开销远小于进程,支持更高的并发量,多线程共享进程地址空间,数据交互更便捷,是 TCP 高并发服务的主流基础方案。

在这里插入图片描述

在这里插入图片描述

  • 线程池版本的详细介绍我们后续再讲

在这里插入图片描述


四. TCP 编程核心踩坑与高频面试考点

  • 监听套接字与 IO 套接字的本质区别:监听套接字仅用于接收客户端的连接请求,全程只有一个;IO 套接字是每个客户端连接专属的,用于和对应客户端的数据传输,有多少个客户端连接就有多少个 IO 套接字。
  • 为什么客户端不需要显式 bind(后面还会详细讲)?:服务端端口必须固定,让客户端能够找到服务;客户端端口无需固定,操作系统会在connect时自动为客户端分配随机空闲端口,显式 bind 反而可能导致端口占用,无法启动多个客户端。
  • TCP 流式传输的粘包问题:TCP 是面向字节流的协议,无数据边界,多次write的小数据包可能被内核合并发送,单次read可能读取到多个数据包,需要在应用层通过固定长度、分隔符、长度字段等方式定制协议解决。
  • 文件描述符泄漏问题:TCP 套接字是系统资源,使用完毕必须调用close关闭,多进程 / 多线程版本中必须正确关闭不需要的文件描述符,否则会耗尽系统文件描述符资源,导致新连接无法建立。
  • accept 的阻塞特性accept默认是阻塞的,当没有客户端连接时会一直阻塞,可通过 IO 多路复用(select/poll/epoll)实现非阻塞的连接管理,支持更高的并发量。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:本文从 TCP 协议的核心特性出发,通过演进式的思路,从单进程基础版到多线程高并发版,完整拆解了 TCP Echo 回显服务器的实现全流程,同时深度解读了每个版本的设计思路、源码细节与优化方案。Echo 服务器虽小,却包含了 TCP 网络编程的所有核心知识点,是后续开发 HTTP 服务器、RPC 框架等复杂网络服务的基础。基于本文的内容,还可以进一步扩展线程池版本、IO 多路复用、应用层协议定制、粘包处理、心跳检测等进阶功能,逐步构建工业级的 TCP 网络服务。

✨把这些内容吃透超牛的!放松下吧✨
ʕ˘ᴥ˘ʔ
づきらど

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐