欢迎光临
我们一直在努力

Linux之应用层层协议认知(TCP协议编程模块化demo) —— 兼论守护进程机制

一、TCP/IP 协议栈与 Socket 编程基础

1.1 TCP/IP 分层模型

TCP/IP 是现代互联网的核心协议栈,其官方标准为四层结构,自下而上分别为网络接口层、网际层(IP 层)、传输层、应用层。教学场景中常结合 OSI 七层模型将其扩展为五层结构,即把网络接口层拆分为数据链路层与物理层,该拆分仅为教学简化,并非 TCP/IP 协议族的标准分层。

各层核心职责如下:

  • 应用层:面向业务逻辑,由 HTTP、FTP、DNS 等具体协议组成,负责定义数据的业务语义与交互规则。
  • 传输层:提供端到端的进程间通信能力,核心协议为 TCP(面向连接、可靠字节流)与 UDP(无连接、不可靠数据报)。
  • 网络层:负责跨网络的路由选择与寻址,核心协议为 IP 协议,实现主机到主机的数据包投递。
  • 数据链路层 + 物理层:负责相邻节点间的帧封装、差错校验与物理介质上的比特传输。

协议栈遵循 “下层为上层提供透明传输服务” 的设计原则:发送端数据自上而下传递时,每一层都会为上层数据添加本层协议首部(数据链路层还会添加尾部校验),形成对应层的协议数据单元(PDU);接收端数据自下而上传递时,逐层剥去对应首部,最终将业务数据交付给应用层,该过程称为数据的封装与解封装。

1.2 Socket 接口的本质

read/write、recvfrom/sendto等接口是操作系统内核暴露给应用层的系统调用,是应用进程操作内核网络协议栈的通用编程入口,而非仅能操作传输层:原始套接字(SOCK_RAW)可直接操作网络层 IP 协议,PF_PACKET类型套接字可直接操作数据链路层帧数据。

应用层开发者无需关心内核协议栈的实现细节,只需通过 Socket 接口以类似操作文件的方式读写数据;接口之下,内核会自动完成数据封装、流量控制、丢包重传、路由转发等复杂工作。

从内核视角看,Socket 是一个抽象的通信端点:每个 Socket 对应内核中的struct socket结构体,并关联一个struct file实例;文件描述符(fd)是进程文件描述符表的索引,指向该file实例。这是 Linux “一切皆文件” 设计思想的体现,但需注意:Socket 并非普通文件,lseek、ftruncate等文件专属操作对套接字无效,仅支持通用的读写与关闭接口。


二、TCP 套接字的内核机制:缓冲区与读写语义

2.1 收发缓冲区与全双工通信

每个 TCP 套接字在内核创建时,都会分配两片独立的内核缓冲区:

  • 发送缓冲区:暂存应用层写入、尚未被内核协议栈封装发出的数据。
  • 接收缓冲区:暂存内核从网络中收到、尚未被应用层读取的数据。

两片缓冲区相互独立、互不干扰,因此 TCP 可以在同一时刻双向传输数据,这是 TCP 全双工特性的底层支撑机制。缓冲区的大小可通过setsockopt调整,也受内核全局参数tcp_wmem、tcp_rmem的约束。

2.2 write/read 系统调用的真实行为

应用层调用的读写接口,本质是用户态内存与内核态缓冲区之间的内存拷贝,其行为与业务直觉存在显著差异:

  • write 操作:将用户态内存中的数据拷贝到内核发送缓冲区。调用成功返回仅代表数据已进入内核缓冲区,不代表数据已发送到网络,更不代表对端应用已接收。当发送缓冲区剩余空间不足时,会出现 “短写” 现象:write仅写入部分数据,返回已写入的字节数(大于 0 但小于预期长度),而非报错。
  • read 操作:将内核接收缓冲区中的数据拷贝到用户态内存。阻塞模式下,若缓冲区为空则线程挂起等待;非阻塞模式下则立即返回EAGAIN错误。

2.3 连接建立过程与内核连接队列

TCP 三次握手过程中,内核会维护两个独立的队列,与数据收发缓冲区无关联:

  • 半连接队列(SYN 队列):服务端收到客户端 SYN 报文后,连接处于SYN_RCVD状态,暂存于此队列。其最大长度由内核参数/proc/sys/net/ipv4/tcp_max_syn_backlog控制,SYN 洪水攻击正是通过耗尽该队列实现拒绝服务,防御手段为启用tcp_syncookies。
  • 全连接队列(accept 队列):完成三次握手后,连接移入此队列,等待accept()系统调用取出。
  • 关于listen函数的backlog参数,在 Linux 2.2 及以上内核中,其仅控制全连接队列的最大长度,且实际上限为min(backlog, net.core.somaxconn),若backlog超过系统somaxconn,内核会自动截断为系统上限值。当全连接队列满时,内核默认丢弃客户端的第三次握手 ACK 报文,触发客户端重传;也可通过tcp_abort_on_overflow参数修改为直接回复 RST 复位连接。

    accept()的作用是从全连接队列中取出一个已就绪的连接,返回一个全新的已连接套接字;该套接字自带独立的收发缓冲区,用于后续与客户端的数据通信。


    三、应用层协议设计核心问题

    3.1 序列化与反序列化

    网络传输的本质是字节序列,而应用层通常以结构体、对象等结构化形式组织数据。直接传输内存中的结构体存在四大核心问题,跨平台、跨语言通信时尤为突出:

  • 字节序差异:不同 CPU 架构存在大端 / 小端字节序区别,直接传输多字节数值会出现解析错误。
  • 内存对齐差异:不同编译器、不同语言、不同编译选项下,结构体的内存对齐规则不同,内存布局不一致。
  • 数据类型长度差异:不同平台下int、long等基础类型的字节长度可能不同。
  • 指针成员不可传输:结构体中若包含指针,直接拷贝内存仅会传输指针地址,而对端进程地址空间完全独立,该地址无任何意义。
  • 因此跨端通信时,必须先将结构化数据转换为统一格式的字节序列,这一过程称为序列化;接收方将字节序列还原为结构化数据,称为反序列化。

    主流序列化方案可分为两大类,核心对比如下:

    类型代表方案核心优点核心缺点典型适用场景
    文本序列化 JSON、XML、自定义文本 跨语言兼容好、人类可读、调试成本低 编解码性能差、数据冗余度高 对外开放接口、调试友好的轻量场景
    二进制序列化 Protobuf、Thrift、自定义二进制 编解码效率高、数据体积小、格式严谨 不可读、需要 IDL 或协议定义 内部高性能通信、RPC 框架、高并发场景

    3.2 TCP 字节流特性与粘包问题

    TCP 是面向字节流的协议,其核心特性是:不维护应用层报文边界,只保证字节按序、无差错送达。内核只关心缓冲区中有多少字节可用,不理解也不关心应用层 “一条消息” 的逻辑边界。

    由此产生 “粘包” 现象:发送方连续发送的多条消息,可能被内核合并后发出;接收方一次read可能读到多条完整消息,也可能只读到半条消息(半包)。粘包是 TCP 字节流特性的必然结果,不属于程序 Bug。

    粘包的成因同时存在于收发两端:

    • 发送端:Nagle 算法会自动合并小数据包批量发送以减少网络报文数量;即使关闭 Nagle 算法,若应用层写入速度快于内核发送速度,发送缓冲区也会累积多个报文。
    • 接收端:内核将收到的数据存入接收缓冲区,应用层读取时机不确定,可能一次读取到多个已到达的报文,也可能只读取到部分报文。

    关闭 Nagle 算法仅能减少发送端粘包,无法解决接收端粘包,因此粘包问题必须由应用层协议通过界定消息边界来解决。

    3.3 粘包问题的主流解决方案

    业界主流的消息边界界定方案有三种,各有优劣与适用场景:

  • 固定长度法:每条消息长度固定,长度不足时用填充字节补齐。实现最简单,但灵活性极差,带宽浪费严重,仅适用于报文长度恒定的特定场景。
  • 分隔符法:用特殊字符或字符序列(如\\r\\n、换行符)作为消息边界。文本类协议广泛使用,但正文中若出现分隔符需要做转义处理,且解析需逐字节扫描,性能较差。
  • 长度前缀法:在每条消息头部的固定位置,存放消息体的长度值。接收方先读取长度字段,再按长度读取完整报文。该方案精确高效,是工业界最主流的方案,也是本文实现采用的方案。工业级实现中通常使用固定字节数的二进制长度字段(如 4 字节大端整数),比文本格式的长度字段解析效率更高、格式更严谨。

  • 四、模块化 TCP 网络编程实现

    本文以简易计算器服务为例,采用面向对象思想与模块化设计实现 TCP 服务器与客户端,包含地址封装、Socket 封装、线程封装、日志、应用层协议、服务端与客户端七大模块。

    4.1 整体设计

    • 应用层采用长度前缀 + 文本正文的协议格式,解决 TCP 粘包问题;
    • 服务器采用per-connection per-thread模型,每个连接分配一个独立线程处理业务;
    • 采用 RAII 思想封装底层系统细节,上层业务只需关注协议解析与业务逻辑。

    4.2 地址封装模块(TcpAddr.hpp)

    对sockaddr_in进行 RAII 封装,屏蔽原生 Socket 地址的类型转换与字节序转换细节,采用线程安全的inet_pton/inet_ntop替代过时的非线程安全函数,提供统一的地址操作接口。

    #pragma once
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <string>
    #include <cstring>
    #include <stdexcept>

    #define SOCK_CAST(addr) reinterpret_cast<struct sockaddr*>(addr)

    namespace InaddrModule {
    class Inaddr {
    struct sockaddr_in _sin;
    public:
    Inaddr(const std::string& ip, uint16_t port) {
    memset(&_sin, 0, sizeof(_sin));
    _sin.sin_family = AF_INET;
    if (inet_pton(AF_INET, ip.c_str(), &_sin.sin_addr) <= 0) {
    throw std::invalid_argument("无效的IP地址格式");
    }
    _sin.sin_port = htons(port);
    }

    explicit Inaddr(uint16_t port) {
    memset(&_sin, 0, sizeof(_sin));
    _sin.sin_family = AF_INET;
    _sin.sin_addr.s_addr = htonl(INADDR_ANY);
    _sin.sin_port = htons(port);
    }

    Inaddr() { memset(&_sin, 0, sizeof(_sin)); }

    struct sockaddr_in* get() { return &_sin; }
    const struct sockaddr_in* get() const { return &_sin; }
    socklen_t size() const { return sizeof(_sin); }

    std::string get_ip() const {
    char buf[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &_sin.sin_addr, buf, sizeof(buf));
    return std::string(buf);
    }

    uint16_t get_port() const {
    return ntohs(_sin.sin_port);
    }
    };
    }

    4.3 Socket 封装模块(Socket.hpp)

    抽象 Socket 基类,封装 TCP 套接字的创建、绑定、监听、接受连接、读写、关闭等操作。

    #pragma once
    #include <unistd.h>
    #include <sys/socket.h>
    #include <string>
    #include <memory>
    #include <cerrno>
    #include "TcpAddr.hpp"
    #include "Log.hpp"
    #include "Common.hpp"

    namespace SocketModule {
    using namespace InaddrModule;

    // 循环写,解决TCP短写问题
    static ssize_t writen(int fd, const char* buf, size_t len) {
    size_t total = 0;
    while (total < len) {
    ssize_t n = ::write(fd, buf + total, len – total);
    if (n < 0) {
    if (errno == EINTR) continue; // 被信号中断,重试
    return -1;
    }
    total += n;
    }
    return static_cast<ssize_t>(total);
    }

    class Socket {
    public:
    Socket() = default;
    virtual ~Socket() = default;

    virtual void create() = 0;
    virtual void bind(uint16_t port) = 0;
    virtual void listen(int backlog) = 0;
    virtual std::shared_ptr<Socket> accept() = 0;
    virtual std::string read() = 0;
    virtual void write(const std::string&) = 0;
    virtual void close() = 0;
    virtual void build_server(uint16_t port, int backlog) = 0;
    };

    class TcpSocket : public Socket {
    int _fd = -1;
    Inaddr _peer_addr;

    void set_fd(int fd) { _fd = fd; }

    public:
    TcpSocket() = default;
    TcpSocket(const Inaddr& peer, int fd) : _peer_addr(peer), _fd(fd) {}

    ~TcpSocket() override {
    if (_fd >= 0) {
    ::close(_fd);
    _fd = -1;
    }
    }

    // 禁止拷贝,允许移动
    TcpSocket(const TcpSocket&) = delete;
    TcpSocket& operator=(const TcpSocket&) = delete;

    void build_server(uint16_t port, int backlog) override {
    create();
    bind(port);
    listen(backlog);
    }

    void create() override {
    _fd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (_fd < 0) {
    LOG(bksw::Loglevel::ERROR) << "socket创建失败, errno=" << errno;
    exit(EXITCODE::SOCKET_ERROR);
    }
    // 开启地址复用,解决TIME_WAIT导致的bind失败
    int opt = 1;
    setsockopt(_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    LOG(bksw::Loglevel::INFO) << "socket创建成功, fd=" << _fd;
    }

    void bind(uint16_t port) override {
    Inaddr addr(port);
    int ret = ::bind(_fd, SOCK_CAST(addr.get()), addr.size());
    if (ret < 0) {
    LOG(bksw::Loglevel::ERROR) << "bind失败, errno=" << errno;
    exit(EXITCODE::BIND_ERROR);
    }
    LOG(bksw::Loglevel::INFO) << "bind成功, 端口=" << port;
    }

    void listen(int backlog) override {
    int ret = ::listen(_fd, backlog);
    if (ret < 0) {
    LOG(bksw::Loglevel::ERROR) << "listen失败, errno=" << errno;
    exit(EXITCODE::LISTEN_ERROR);
    }
    LOG(bksw::Loglevel::INFO) << "listen成功, backlog=" << backlog;
    }

    std::shared_ptr<Socket> accept() override {
    Inaddr peer;
    socklen_t len = peer.size();
    int cfd = ::accept(_fd, SOCK_CAST(peer.get()), &len);
    if (cfd < 0) {
    if (errno != EINTR) {
    LOG(bksw::Loglevel::ERROR) << "accept失败, errno=" << errno;
    }
    return nullptr;
    }
    LOG(bksw::Loglevel::INFO) << "新连接接入: " << peer.get_ip() << ":" << peer.get_port();
    return std::make_shared<TcpSocket>(peer, cfd);
    }

    std::string read() override {
    char buff[1024];
    ssize_t n = ::read(_fd, buff, sizeof(buff));
    if (n == 0) {
    LOG(bksw::Loglevel::INFO) << _peer_addr.get_ip() << " 连接正常关闭";
    return "";
    } else if (n < 0) {
    if (errno != EINTR) {
    LOG(bksw::Loglevel::ERROR) << "读取失败, errno=" << errno;
    }
    return "";
    }
    return std::string(buff, n);
    }

    void write(const std::string& str) override {
    ssize_t n = writen(_fd, str.c_str(), str.size());
    if (n < 0) {
    LOG(bksw::Loglevel::ERROR) << "发送数据失败";
    }
    }

    void close() override {
    if (_fd >= 0) {
    ::close(_fd);
    _fd = -1;
    }
    }

    int get_fd() const { return _fd; }
    };
    }

    4.4 线程封装模块(Thread.hpp)

    对原生pthread进行 RAII 封装,支持绑定任意函数与参数,自动管理线程生命周期。

    #ifndef _THREAD_HPP
    #define _THREAD_HPP
    #include <pthread.h>
    #include <functional>
    #include <memory>
    #include <string>
    #include <stdexcept>

    namespace bksw {
    enum class ThreadStatus {
    NEW = 0,
    RUNNING,
    STOPPED
    };

    class Thread {
    std::string _name;
    pthread_t _tid = 0;
    ThreadStatus _status = ThreadStatus::NEW;
    bool _detached = false;

    static void* run(void* arg) {
    auto* task = static_cast<std::function<void()>*>(arg);
    try {
    (*task)();
    } catch (const std::exception& e) {
    // 捕获线程异常,避免进程终止
    // 实际项目中可接入日志系统
    } catch (…) {}
    delete task;
    return nullptr;
    }

    public:
    Thread() = default;
    explicit Thread(std::string name) : _name(std::move(name)) {}
    ~Thread() = default;

    // 禁止拷贝
    Thread(const Thread&) = delete;
    Thread& operator=(const Thread&) = delete;

    template<typename Func, typename… Args>
    int start(Func&& func, Args&&… args) {
    if (_status == ThreadStatus::RUNNING) return -1;

    auto* task = new std::function<void()>(
    std::bind(std::forward<Func>(func), std::forward<Args>(args)…)
    );

    int ret = pthread_create(&_tid, nullptr, run, task);
    if (ret != 0) {
    delete task; // 创建失败释放内存,避免泄漏
    return ret;
    }

    _status = ThreadStatus::RUNNING;
    if (_detached) {
    pthread_detach(_tid);
    }
    return 0;
    }

    int join() {
    if (_status != ThreadStatus::RUNNING || _detached) return -1;
    int ret = pthread_join(_tid, nullptr);
    if (ret == 0) _status = ThreadStatus::STOPPED;
    return ret;
    }

    int detach() {
    if (!_detached && _status == ThreadStatus::RUNNING) {
    pthread_detach(_tid);
    }
    _detached = true;
    return 0;
    }

    ThreadStatus get_status() const { return _status; }
    };
    }
    #endif

    4.5 日志模块(Log.hpp)

    采用策略模式实现控制台 / 文件两种日志输出,支持分级日志与时间戳。

    #pragma once
    #include <sstream>
    #include <iostream>
    #include <string>
    #include <memory>
    #include <filesystem>
    #include <unistd.h>
    #include <ctime>
    #include <fcntl.h>
    #include "mutex.hpp"

    namespace bksw {
    #define LOG(level) bksw::global_log(level, __FILE__, __LINE__)
    #define LOG_TO_FILE() bksw::global_log.UseFileLogStrategy()
    #define LOG_TO_CONSOLE() bksw::global_log.UseConsoleLogStrategy()

    constexpr const char* PATH_SEP = "/";
    constexpr const char* DEFAULT_LOG_DIR = "./Log/";
    constexpr const char* DEFAULT_LOG_FILE = "log.txt";

    class LogStrategy {
    public:
    virtual void output(const std::string& message) = 0;
    virtual ~LogStrategy() = default;
    protected:
    mutex _mutex;
    };

    class ConsoleLogStrategy : public LogStrategy {
    public:
    void output(const std::string& msg) override {
    std::lock_guard<mutex> lock(_mutex);
    std::cout << msg << std::endl;
    }
    };

    class FileLogStrategy : public LogStrategy {
    int _fd = -1;
    public:
    FileLogStrategy(const std::string& dir = DEFAULT_LOG_DIR,
    const std::string& filename = DEFAULT_LOG_FILE) {
    if (!std::filesystem::exists(dir)) {
    std::filesystem::create_directories(dir);
    }
    std::string full_path = std::string(dir) + filename;
    _fd = open(full_path.c_str(), O_APPEND | O_WRONLY | O_CREAT, 0644);
    }

    void output(const std::string& msg) override {
    std::lock_guard<mutex> lock(_mutex);
    write(_fd, msg.c_str(), msg.size());
    write(_fd, "\\n", 1);
    }

    ~FileLogStrategy() override {
    if (_fd >= 0) close(_fd);
    }
    };

    enum class Loglevel {
    INFO, DEBUG, WARNING, ERROR, FATAL
    };

    static std::string level_to_string(Loglevel level) {
    switch(level) {
    case Loglevel::INFO: return "INFO";
    case Loglevel::DEBUG: return "DEBUG";
    case Loglevel::WARNING: return "WARNING";
    case Loglevel::ERROR: return "ERROR";
    case Loglevel::FATAL: return "FATAL";
    default: return "UNKNOWN";
    }
    }

    static std::string get_current_time() {
    time_t now = time(nullptr);
    struct tm tm_buf;
    localtime_r(&now, &tm_buf);
    char buf[64];
    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
    return std::string(buf);
    }

    class Log {
    std::shared_ptr<LogStrategy> _strategy;
    std::string _log_path;
    bool _is_console;

    class LogStream {
    std::stringstream _ss;
    Log& _owner;
    public:
    LogStream(Loglevel level, const std::string& file, int line, Log& owner)
    : _owner(owner) {
    _ss << '[' << get_current_time() << ']'
    << '[' << level_to_string(level) << ']'
    << '[' << getpid() << ']'
    << '[' << file << ':' << line << "] – ";
    }

    template<typename T>
    LogStream& operator<<(T&& t) {
    _ss << t;
    return *this;
    }

    ~LogStream() {
    _owner._strategy->output(_ss.str());
    }
    };

    public:
    Log(const std::string& path = std::string(DEFAULT_LOG_DIR) + DEFAULT_LOG_FILE,
    bool console = true)
    : _log_path(path), _is_console(console) {
    if (_is_console) {
    _strategy = std::make_shared<ConsoleLogStrategy>();
    } else {
    size_t idx = _log_path.rfind(PATH_SEP);
    std::string dir, file;
    if (idx == std::string::npos) {
    dir = "./";
    file = _log_path;
    } else {
    dir = _log_path.substr(0, idx + 1);
    file = _log_path.substr(idx + 1);
    }
    _strategy = std::make_shared<FileLogStrategy>(dir, file);
    }
    }

    void UseConsoleLogStrategy() {
    _strategy = std::make_shared<ConsoleLogStrategy>();
    _is_console = true;
    }

    void UseFileLogStrategy() {
    size_t idx = _log_path.rfind(PATH_SEP);
    std::string dir, file;
    if (idx == std::string::npos) {
    dir = "./";
    file = _log_path;
    } else {
    dir = _log_path.substr(0, idx + 1);
    file = _log_path.substr(idx + 1);
    }
    _strategy = std::make_shared<FileLogStrategy>(dir, file);
    _is_console = false;
    }

    LogStream operator()(Loglevel level, std::string file, int line) {
    return LogStream(level, std::move(file), line, *this);
    }
    };

    // 全局日志实例,定义在Log.cpp中
    extern Log global_log;
    }

    4.6 应用层协议模块(Protocol.hpp)

    采用长度前缀法界定消息边界,正文采用空格分隔的文本格式,包含请求与响应两类报文。

    #pragma once
    #include <iostream>
    #include <sstream>
    #include <string>
    #include <functional>
    #include <stdexcept>

    constexpr const char* MSG_SEP = "\\r\\n";
    constexpr size_t SEP_LEN = 2;

    // 计算请求报文
    class Request {
    int _x = 0;
    int _y = 0;
    char _op = 0;
    public:
    Request() = default;
    Request(int x, int y, char op) : _x(x), _y(y), _op(op) {}

    // 序列化:长度前缀 + 正文
    std::string Serialize() const {
    std::stringstream body;
    body << _x << " " << _y << " " << _op << MSG_SEP;
    std::string body_str = body.str();
    size_t body_len = body_str.size();

    std::stringstream header;
    header << body_len << MSG_SEP;
    return header.str() + body_str;
    }

    // 反序列化:仅解析正文部分
    bool Deserialize(const std::string& body) {
    size_t p1 = body.find(' ');
    if (p1 == std::string::npos) return false;
    size_t p2 = body.find(' ', p1 + 1);
    if (p2 == std::string::npos) return false;

    try {
    _x = std::stoi(body.substr(0, p1));
    _y = std::stoi(body.substr(p1 + 1, p2 – p1 – 1));
    _op = body[p2 + 1];
    } catch (…) {
    return false;
    }
    return true;
    }

    int X() const { return _x; }
    int Y() const { return _y; }
    char Op() const { return _op; }
    };

    // 计算响应报文
    class Response {
    int _result = 0;
    int _code = 0; // 0表示成功,非0表示错误
    public:
    Response() = default;
    Response(int res, int code) : _result(res), _code(code) {}

    std::string Serialize() const {
    std::stringstream body;
    body << _result << " " << _code << MSG_SEP;
    std::string body_str = body.str();
    size_t body_len = body_str.size();

    std::stringstream header;
    header << body_len << MSG_SEP;
    return header.str() + body_str;
    }

    bool Deserialize(const std::string& body) {
    size_t p1 = body.find(' ');
    if (p1 == std::string::npos) return false;
    size_t p2 = body.find(' ', p1 + 1);
    if (p2 == std::string::npos) return false;

    try {
    _result = std::stoi(body.substr(0, p1));
    _code = std::stoi(body.substr(p1 + 1, p2 – p1 – 1));
    } catch (…) {
    return false;
    }
    return true;
    }

    int Result() const { return _result; }
    int Code() const { return _code; }
    };

    using HandlerFunc = std::function<Response(const Request&)>;

    class Protocol {
    HandlerFunc _handler;
    public:
    explicit Protocol(HandlerFunc func) : _handler(std::move(func)) {}

    Response Process(const std::string& full_msg) const {
    // 解析长度前缀,提取正文
    size_t sep_pos = full_msg.find(MSG_SEP);
    if (sep_pos == std::string::npos) {
    return {0, -1};
    }

    size_t body_len = 0;
    try {
    body_len = std::stoul(full_msg.substr(0, sep_pos));
    } catch (…) {
    return {0, -1};
    }

    size_t body_start = sep_pos + SEP_LEN;
    if (full_msg.size() < body_start + body_len) {
    return {0, -1};
    }

    std::string body = full_msg.substr(body_start, body_len);
    Request req;
    if (!req.Deserialize(body)) {
    return {0, -1};
    }

    return _handler(req);
    }
    };

    4.7 服务器端实现

    采用 Meyers 单例模式封装服务器,使用长度前缀法正确处理粘包,每个连接启动一个分离线程处理业务。

    // server.hpp
    #pragma once
    #include <iostream>
    #include <string>
    #include <memory>
    #include <signal.h>
    #include "Socket.hpp"
    #include "Thread.hpp"
    #include "Protocol.hpp"

    namespace TcpCalServer {
    using namespace SocketModule;
    using namespace bksw;

    class Server {
    std::unique_ptr<Socket> _listen_sock;
    Protocol _protocol;

    Server(HandlerFunc func, uint16_t port, int backlog)
    : _protocol(std::move(func)) {
    // 忽略SIGPIPE,防止向已关闭连接写数据导致进程崩溃
    signal(SIGPIPE, SIG_IGN);

    _listen_sock = std::make_unique<TcpSocket>();
    _listen_sock->build_server(port, backlog);
    }

    public:
    // Meyers单例,线程安全
    static Server& get_instance(HandlerFunc func, uint16_t port, int backlog = 10) {
    static Server instance(std::move(func), port, backlog);
    return instance;
    }

    void run() {
    LOG(Loglevel::INFO) << "计算器服务器启动成功,开始监听连接";
    while (true) {
    auto conn = _listen_sock->accept();
    if (!conn) continue;

    auto* th = new Thread();
    th->start(&Server::handle_connection, this, conn);
    th->detach();
    }
    }

    private:
    void handle_connection(std::shared_ptr<Socket> conn) {
    std::string recv_buffer;
    while (true) {
    std::string chunk = conn->read();
    if (chunk.empty()) break; // 连接关闭
    recv_buffer += chunk;

    // 长度前缀法解粘包
    while (true) {
    size_t sep_pos = recv_buffer.find(MSG_SEP);
    if (sep_pos == std::string::npos) break;

    size_t body_len = 0;
    try {
    body_len = std::stoul(recv_buffer.substr(0, sep_pos));
    } catch (…) {
    recv_buffer.clear();
    break;
    }

    size_t total_len = sep_pos + SEP_LEN + body_len;
    if (recv_buffer.size() < total_len) break; // 半包,等待更多数据

    // 提取完整报文并处理
    std::string full_msg = recv_buffer.substr(0, total_len);
    Response rep = _protocol.Process(full_msg);
    conn->write(rep.Serialize());

    // 移除已处理报文
    recv_buffer.erase(0, total_len);
    }
    }
    }

    Server(const Server&) = delete;
    Server& operator=(const Server&) = delete;
    };
    }

    cpp

    // tcpserver.cpp
    #include <iostream>
    #include "server.hpp"
    #include "Cal.hpp" // 计算器业务逻辑实现

    int main(int argc, char** argv) {
    if (argc != 2) {
    std::cerr << "用法: ./server 端口号" << std::endl;
    return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    Cal calculator;

    auto& srv = TcpCalServer::Server::get_instance(
    [&calculator](const Request& req) -> Response {
    return calculator.Execute(req);
    }, port
    );

    srv.run();
    return 0;
    }

    4.8 客户端实现

    复用地址封装逻辑,实现完整的粘包处理与循环写入.

    // tcpclient.cpp
    #include <iostream>
    #include <string>
    #include <sys/socket.h>
    #include <unistd.h>
    #include <signal.h>
    #include "TcpAddr.hpp"
    #include "Protocol.hpp"
    #include "Log.hpp"

    // 循环写
    static ssize_t writen(int fd, const char* buf, size_t len) {
    size_t total = 0;
    while (total < len) {
    ssize_t n = ::write(fd, buf + total, len – total);
    if (n < 0) {
    if (errno == EINTR) continue;
    return -1;
    }
    total += n;
    }
    return static_cast<ssize_t>(total);
    }

    int main(int argc, char** argv) {
    if (argc != 3) {
    std::cerr << "用法: ./client 服务器IP 端口号" << std::endl;
    return 1;
    }

    signal(SIGPIPE, SIG_IGN);

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
    perror("socket");
    return 1;
    }

    InaddrModule::Inaddr server_addr(argv[1], std::stoi(argv[2]));
    if (connect(fd, SOCK_CAST(server_addr.get()), server_addr.size()) < 0) {
    perror("connect");
    close(fd);
    return 1;
    }

    std::cout << "连接服务器成功" << std::endl;
    std::string recv_buffer;
    char buff[1024];

    while (true) {
    int x, y;
    char oper;
    std::cout << "请输入 x: "; std::cin >> x;
    std::cout << "请输入 y: "; std::cin >> y;
    std::cout << "请输入运算符(+ – * /): "; std::cin >> oper;

    Request req(x, y, oper);
    std::string msg = req.Serialize();
    if (writen(fd, msg.c_str(), msg.size()) < 0) {
    std::cerr << "发送数据失败,连接断开" << std::endl;
    break;
    }

    // 循环读取直到拿到完整响应
    Response rep;
    bool got_response = false;
    while (!got_response) {
    ssize_t n = read(fd, buff, sizeof(buff));
    if (n <= 0) {
    std::cerr << "连接断开" << std::endl;
    close(fd);
    return 1;
    }
    recv_buffer.append(buff, n);

    size_t sep_pos = recv_buffer.find(MSG_SEP);
    if (sep_pos == std::string::npos) continue;

    size_t body_len = 0;
    try {
    body_len = std::stoul(recv_buffer.substr(0, sep_pos));
    } catch (…) {
    recv_buffer.clear();
    break;
    }

    size_t total_len = sep_pos + SEP_LEN + body_len;
    if (recv_buffer.size() < total_len) continue;

    std::string full_msg = recv_buffer.substr(0, total_len);
    // 提取正文并反序列化
    std::string body = full_msg.substr(sep_pos + SEP_LEN, body_len);
    rep.Deserialize(body);
    recv_buffer.erase(0, total_len);
    got_response = true;
    }

    if (rep.Code() == 0) {
    LOG(bksw::Loglevel::INFO) << "计算结果: " << rep.Result();
    } else {
    LOG(bksw::Loglevel::ERROR) << "计算失败,错误码: " << rep.Code();
    }
    }

    close(fd);
    return 0;
    }


    五、守护进程机制与服务器后台化

    常规终端启动的服务器会随 SSH 连接断开而终止,无法满足服务长期运行的需求。守护进程(Daemon Process,俗称精灵进程)是脱离控制终端、在后台独立运行的服务进程,是后端服务的标准运行形态。理解守护进程的实现原理,需要先掌握 Linux 三级进程组织模型、伪终端内核机制与终端信号管控逻辑。

    5.1 进程组织层级:进程组与会话

    Linux 系统中,进程以「进程 → 进程组 → 会话」的三级层级化方式组织,该层级结构是终端作业控制、信号批量管理、守护进程实现的核心基础。

    5.1.1 进程组的定义与核心特性

    进程组(Process Group)是一个或多个具有关联关系的进程的集合,核心作用是实现信号的批量投递与终端资源的统一管理,对应 Shell 中的「作业(Job)」概念。

    每个进程组拥有唯一的进程组标识号 PGID,其数值与进程组组长(Group Leader) 的 PID 完全相等,组长进程是创建该进程组的首个进程,核心特征为 PID == PGID。

    进程组具备以下核心规则:

  • 创建与继承:进程通过 setpgid() 系统调用可将自身或同会话内的子进程加入指定进程组,或创建全新进程组;fork() 创建的子进程默认继承父进程的进程组。
  • 生命周期:进程组不会因组长进程退出而立即消亡,只有当组内最后一个进程退出或转移至其他进程组后,该进程组才会被内核销毁,PGID 保持不变。
  • 会话约束:同一进程组内的所有进程必须隶属于同一个会话,不允许跨会话迁移进程组。
  • 信号批量投递:信号可按进程组批量发送,例如 kill -PGID 可向组内所有进程同时发送信号。Shell 中的一条管道命令(如 ls | grep test | wc -l)包含多个进程,Shell 会将它们放入同一个进程组,作为一个作业统一启停。
  • 5.1.2 会话的定义与控制终端绑定

    会话(Session)是一个或多个进程组的集合,是 Linux 最高层级的进程组织单位,通常与一个控制终端(Controlling Terminal) 绑定,对应用户的一次完整登录交互周期。

    每个会话拥有唯一的会话标识号 SID,其数值与会话首进程(Session Leader) 的 PID 完全相等,特征为 PID == SID,会话首进程是调用 setsid() 系统调用创建会话的进程。

    会话的核心机制如下:

  • 终端绑定规则:一个会话最多只能关联一个控制终端;反之,一个控制终端也只能归属一个会话。终端的输入、终端硬件产生的信号,仅作用于该会话内部。
  • 前后台进程组划分:每个拥有控制终端的会话,同一时间只能有一个前台进程组,其余进程组均为后台进程组。终端输入的独占权、终端信号的接收权,仅归前台进程组所有。
  • 生命周期管理:当控制终端断开连接(如 SSH 掉线、终端窗口关闭)时,内核会向会话内的所有进程发送 SIGHUP 信号,实现登录退出时的批量资源回收。
  • 5.1.3 登录会话的建立流程:bash 作为会话首进程

    bash 作为用户登录后的交互 Shell,天然是会话的首进程与默认前台进程组组长,其完整建立流程由登录守护进程完成:

  • 系统级守护进程(SSH 场景下为 sshd、本地图形化场景下为终端模拟器)接收用户登录请求后,调用 fork() 创建子进程;
  • 该子进程调用 setsid() 系统调用,创建全新会话,自身成为会话首进程;同时自动创建新进程组,自身成为进程组组长;
  • 子进程打开终端设备,将其设置为当前会话的控制终端;
  • 子进程完成用户身份降权后,通过 exec() 系统调用加载并启动 bash 程序。 由于 exec() 仅替换进程的代码段与数据段,不改变 PID、PGID、SID 等身份属性,bash 完整继承了会话首进程的身份,成为该会话的第一个用户态交互进程。
  • 5.2 伪终端机制与终端访问控制

    SSH 远程登录场景下,服务器没有连接物理终端硬件,此时系统通过伪终端(Pseudo Terminal, PTY) 软件模拟真实终端的全部行为,为 Shell 与业务进程提供兼容的终端接口。

    5.2.1 伪终端对与 /dev/pts 文件系统

    伪终端以成对的形式存在,称为「伪终端对」,包含两个功能互补的双向字符设备,行为与物理终端完全一致,进程无法感知二者区别。

    • 主设备(PTY Master):面向终端模拟器 / 守护进程的控制端。持有主设备的进程可通过写入主设备模拟用户键盘输入,通过读取主设备获取终端输出内容。主设备没有公开的设备文件节点,仅以文件描述符的形式存在于持有它的进程中。
    • 从设备(PTY Slave):面向业务进程的终端接口。Shell、vim 等用户进程将从设备视为标准终端,读取从设备即可获取终端输入,写入从设备即可输出内容。

    伪终端对与普通管道的核心差异在于:主从设备之间存在一层终端行规程(Line Discipline),内置了回显、行编辑、特殊字符转信号、终端窗口尺寸控制等完整的终端语义,这是纯字节流管道不具备的能力。

    /dev/pts 是 devpts 伪文件系统的挂载点,所有文件均由内核动态生成于内存中,不占用磁盘存储空间,目录内仅包含两类文件:

  • ptmx(伪终端主设备多路复用器) 该文件是主设备号 5、次设备号 2 的字符设备,是系统中所有伪终端对的统一创建入口。当进程打开 /dev/ptmx 时,内核会分配一个未使用的终端编号,生成对应的从设备文件,并返回主设备的文件描述符。 该文件权限通常设置为 000(无任何读写权限),属于安全设计:Linux 中 root 用户不受文件权限位限制,可正常打开该设备创建伪终端;普通用户无法直接打开,避免非授权用户滥用伪终端进行权限逃逸或信息窃取。

  • 数字编号文件(如 /dev/pts/0) 每个数字文件对应一个活跃的伪终端从设备,文件名即为终端编号,也是该设备的次设备号,主设备号固定为 136(符合 UNIX 98 伪终端标准)。每新建一个终端会话就会生成一个对应编号的文件,会话关闭后由内核自动回收删除。 从设备默认权限为 crw–w—-,所有者为当前登录用户,所属组为 tty 组,仅允许所有者读取终端输入,防止其他用户窃取键盘输入内容。

  • 挂载点(Mount Point)是 Linux 目录树中的一个普通目录,它是外部文件系统接入系统统一目录树的 “接口目录”。 将一个文件系统挂载到该目录后,访问这个目录就等价于访问被挂载的文件系统内部的所有内容;原目录里的文件会被临时隐藏,直到卸载(umount)后恢复。

    2. 为什么需要 “挂载” 这个设计

    Linux 遵循「一切皆文件」的设计哲学,所有文件、设备、资源都统一组织在一棵以 / 为根的目录树中。但系统的文件来源是多样的:

    • 磁盘上的 ext4/xfs 分区

    • U 盘、光盘等外部存储

    • proc、sysfs、devpts 这类内核虚拟出来的伪文件系统(完全在内存中,不占磁盘)

    这些文件系统各自独立、结构不同,不能直接拼进根目录。挂载就是把这些独立的文件系统,“挂接” 到根目录树的某个目录节点上,让用户可以通过统一的路径访问它们。

    3. 挂载的本质效果

  • 挂载前:挂载点只是一个普通的空目录,里面的文件都存在于根目录所在的磁盘分区上。

  • 执行 mount 操作后:内核将目标文件系统的根目录,映射到这个挂载点目录上。用户 cd 进这个目录,看到的就不再是原磁盘分区的内容,而是被挂载文件系统的内部内容。

  • 卸载(umount)后:挂载点恢复为普通目录,原目录的内容重新可见。

  • 4. 结合你熟悉的 /dev/pts 举例

    /dev/pts 就是一个典型的挂载点,对应 devpts 伪文件系统:

    • devpts 是内核维护的、完全存在于内存中的文件系统,用来动态生成伪终端从设备文件;

    • 系统启动时,内核执行挂载操作,把 devpts 文件系统挂载到 /dev/pts 这个目录上;

    • 所以你执行 ll /dev/pts 看到的 0、ptmx 这些文件,都不是磁盘里存的,而是内核实时生成的内存文件;

    • 你可以用 mount | grep devpts 命令看到这条挂载记录。

    再举一个更直观的例子:插入 U 盘后,执行 mount /dev/sdb1 /mnt/usb,其中 /mnt/usb 就是挂载点。访问 /mnt/usb 目录,就是直接读写 U 盘里的文件。

    5. 挂载点的关键特性

    • 挂载点必须是已经存在的目录,不能是文件;

    • 理论上可以挂载到任意目录,不局限于 /mnt、/media;

    • 同一个文件系统可以挂载到多个挂载点(多入口访问);

    • 挂载点目录下原本的文件会被 “覆盖隐藏”,不会被删除,卸载后恢复。

    5.2.2 伪终端的内核缓冲队列与数据流

    每一对伪终端都会由 Linux 内核的 tty 子系统在内核空间中维护独立的缓冲队列,队列不属于任何用户态进程,由内核统一管理,用户进程只能通过系统调用间接操作。该设计与第二章 TCP 套接字的收发缓冲区思想一致,均为内核态与用户态之间的数据缓冲层。

    每套缓冲结构包含两个独立的环形队列:

    • 输入队列(读队列):缓存待被业务进程读取的终端输入数据;
    • 输出队列(写队列):缓存业务进程写入的终端输出数据。

    以 SSH 场景为例,完整的双向数据流如下:

  • 输入链路(键盘→业务进程):本地键盘输入由操作系统捕获后,经 SSH 客户端加密通过网络传输至服务器;服务器对应的 sshd 子进程接收解密后,调用 write() 写入自身持有的伪终端主设备;数据进入内核空间,经过行规程处理(回显复制、特殊字符解析、行编辑处理)后,存入对应从设备的输入队列;最终由前台进程组的进程调用 read() 读取。
  • 输出链路(业务进程→屏幕):服务器业务进程调用 write() 写入标准输出 / 标准错误,数据进入从设备的输出队列;内核自动将输出队列的数据同步转发至配对主设备的可读缓冲区;sshd 子进程读取主设备数据,加密后通过网络回传至本地 SSH 客户端,最终渲染至终端窗口。
  • 进程的标准输入(fd 0)、标准输出(fd 1)、标准错误(fd 2)通常都指向同一个从设备文件,但不会出现字节流紊乱,核心保障机制有三点:

  • 读写通道物理隔离:终端是字符流设备,无文件位置指针,读操作仅消费输入队列,写操作仅追加输出队列,不存在指针偏移冲突;输入回显是行规程主动复制的结果,不属于通道串扰。
  • 输入侧独占机制:通过前台进程组规则,仅前台进程可读取终端输入,从根源避免多进程争抢输入导致的数据拆分。
  • 输出侧原子性保障:输出队列遵循 FIFO 规则,单次写入长度不超过 PIPE_BUF(Linux 下为 4096 字节)时,内核保证写入操作的原子性,数据不会被其他进程的写入穿插。
  • 5.2.3 前后台进程组的终端权限管控

    同一个会话内,只能有一个前台进程组,其余均为后台进程组。二者的终端访问权限差异由内核终端驱动直接管控,并非 Shell 主动限制。

    读取权限管控:内核在终端输入队列的读取出口处做了强制校验,当进程调用 read() 尝试读取控制终端时:

  • 内核获取当前进程的进程组 ID(PGID);
  • 将其与终端设备记录的前台进程组 ID 做比对;
  • 若匹配,则判定为前台进程,正常从输入队列返回数据;
  • 若不匹配,则判定为后台进程,不返回任何数据,直接向该进程所在的整个进程组发送 SIGTTIN 信号。 SIGTTIN 信号的默认行为是暂停进程执行,直到进程被移至前台、收到 SIGCONT 信号恢复后,才能重新获得终端读取权限。
  • 写入权限管控:与读取的严格限制不同,Linux 默认允许后台进程向终端写入输出,仅当用户通过 stty tostop 开启终端选项后,后台进程写入才会触发 SIGTTOU 信号被暂停。因此常规场景下,后台进程的输出可以正常打印到终端屏幕,仅可能出现多进程并发输出的行级交错。

    Shell 的作业控制功能,本质就是通过系统调用管理进程组与终端的前台归属关系:

    • 前台命令启动时,bash 调用 setpgid() 将新命令进程放入独立进程组,再调用 tcsetpgrp() 将其设为终端前台进程组,bash 自身退居后台等待;
    • 后台命令(& 后缀)启动时,bash 仅将命令放入独立进程组,不修改终端前台归属;
    • fg 命令调用 tcsetpgrp() 将指定后台进程组设为前台,同时发送 SIGCONT 恢复进程运行;
    • Ctrl+C / Ctrl+Z 等终端快捷键,由终端行规程识别后,直接向前台进程组的所有进程统一发送 SIGINT 或 SIGTSTP 信号,实现批量终止或暂停。
    5.2.4 终端断开的终止链路:SIGHUP 信号传递

    SSH 连接断开时,服务器端的伪终端主设备会被关闭,内核检测到终端载波丢失,会触发完整的信号传递链路:

  • 内核向控制终端所属会话的首进程(通常为 bash)发送 SIGHUP 信号;
  • bash 作为会话首进程与作业管理者,收到 SIGHUP 后,会向其管理的所有作业进程组转发 SIGHUP 信号;
  • 进程收到 SIGHUP 信号后默认执行终止操作。 这就是 “关闭 SSH 终端后服务器进程自动退出” 的根本原因:进程并非被网络断开直接杀死,而是通过会话→终端的信号机制被批量终止。
  • 5.3 守护进程的核心设计思路

    让服务器脱离终端、长期运行的核心方法是:创建新的独立会话,彻底脱离原控制终端,从根源上规避终端断开的信号影响。

    POSIX 提供了 setsid() 系统调用,调用该函数的进程会创建一个新会话并成为会话首进程,同时创建一个新进程组并成为组长。但该调用有一个强制前提:调用进程不能是当前进程组的组长,否则调用失败。这是为了防止组长进程创建新会话后,原进程组出现 PGID 与进程 PID 冲突的异常。

    因此守护进程的基础实现流程为:

  • 父进程 fork 出子进程,父进程直接退出;
  • 子进程不是原进程组组长,满足 setsid() 调用前提,调用 setsid() 创建新会话,彻底脱离原控制终端;
  • 重定向标准输入、输出、错误到 /dev/null,避免终端 IO 操作报错。
  • 5.4 标准守护进程的工程实现

    实现规范步骤
  • 第一次 fork,父进程退出,子进程继续,保证子进程不是原进程组组长;
  • 子进程调用 setsid(), 创建新会话、新进程组,彻底脱离原控制终端;
  • 第二次 fork,父进程(会话首进程)退出,子进程继续。此时子进程不再是会话首进程,永远无法重新打开控制终端,彻底断绝与终端的关联;
  • 调用 umask(0) 清除文件权限掩码,避免继承父进程的权限掩码导致文件权限不符合预期;
  • 调用 chdir("/") 将工作目录切换到根目录,避免占用可卸载文件系统,同时消除对启动目录的依赖;
  • 关闭所有继承的文件描述符,将标准输入、标准输出、标准错误重定向到 /dev/null。
    • 第一次 fork:为了能调用 setsid,脱离原终端;
    • 第二次 fork:为了放弃会话首进程身份,永远不能再重新获取终端

    这里要解释一下:Linux规定只有会话首进程有权限为会话打开一个终端,也就是在挂载点(/dev/pts设置一个新的设备文件)

    标准实现代码

    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <stdlib.h>

    int daemonize(int nochdir, int noclose) {
    pid_t pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(0); // 父进程退出

    // 第一步:创建新会话,脱离原控制终端
    if (setsid() < 0) return -1;

    // 第二步:第二次fork,彻底脱离控制终端,禁止重新打开
    pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(0);

    // 清除文件权限掩码
    umask(0);

    // 切换工作目录
    if (!nochdir) {
    chdir("/");
    }

    // 关闭并重定向标准文件描述符
    if (!noclose) {
    int fd = open("/dev/null", O_RDWR);
    if (fd < 0) return -1;
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    if (fd > STDERR_FILENO) {
    close(fd);
    }
    }

    return 0;
    }

    Linux 系统也提供了标准库函数 daemon(int nochdir, int noclose),参数与功能与上述实现一致:

    • nochdir:为 0 时将工作目录切换到根目录,为 1 时不切换;
    • noclose:为 0 时关闭并重定向标准文件描述符,为 1 时不处理。
    概念边界澄清
    • 守护进程必然是孤儿进程(父进程退出后被 init 进程收养),但孤儿进程不等于守护进程。孤儿进程仅表示父进程已终止,可能仍关联控制终端;守护进程的核心特征是脱离控制终端、独立会话、长期后台运行。
    • 前台 / 后台是针对拥有控制终端的会话内进程组的划分,守护进程已脱离控制终端,不存在前台、后台的属性。

    使用时只需在服务器 main 函数初始化阶段调用 daemonize(0, 0),即可将服务器转为守护进程运行。


    六、总结与扩展方向

    本文系统梳理了 TCP 网络编程的核心原理,从协议栈分层、Socket 内核机制到应用层协议设计,层层递进地解释了网络通信的底层逻辑;并通过 C++ 模块化编程实现了完整的 TCP 计算器服务,覆盖了地址封装、套接字管理、线程封装、日志系统、协议设计等工程核心模块,修复了短写、信号安全、线程安全等常见工程缺陷。最后深入讲解了守护进程的实现原理,修正了进程组、会话、伪终端相关的常见认知误区,给出了符合标准的工程化实现。

    赞(0)
    未经允许不得转载:171主机测评 » Linux之应用层层协议认知(TCP协议编程模块化demo) —— 兼论守护进程机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址