自定义网络协议与序列化/反序列化

自定义网络协议与序列化/反序列化

如果你做过网络编程,可能会遇到这样的问题:用TCP发了一个"1+1"的计算请求,对方却只收到了"1";或者一次收到了"1+12+3"这种拼接的内容------这时候怎么判断哪个是完整的请求?又怎么把这些字符串解析成能计算的操作数和运算符?这篇文章就从TCP的"天生缺陷"说起,带你搞懂自定义协议的必要性、序列化与反序列化的核心逻辑,最后通过一个网络计算器实例,把这些知识点落地成可运行的代码。

一、TCP通信的字节流边界问题

先问一个问题:TCP是"面向字节流"的协议,这意味着什么?

你可能知道,发送端调用write,只是把数据拷贝到操作系统的内核缓冲区,什么时候发、发多少,全由TCP协议自己决定(比如根据网络带宽、接收方缓冲区大小调整);接收端调用read,可能只读到部分数据(比如对方只发了一半),也可能读到多个拼接的数据(比如对方连续发了两个请求,被TCP打包成一次发送)。

举个例子:你发了两次请求"1+1"和"2+3",接收端read时可能拿到"1+12+3"------这时候你怎么区分这是两个请求,而不是一个"1+12+3"的请求?又怎么保证每次处理的都是一个完整的请求?

这就是TCP字节流的"边界问题"------它没法保证应用层能读到"完整的报文"。要解决这个问题,光靠TCP协议不够,必须在应用层自定义协议

二、应用层协议定制的必要性

协议本质是"通信双方的约定"------比如约定"数据格式是什么""每个字段代表什么意思""怎么判断一个报文结束"。没有约定,接收端根本不知道怎么解析数据。

以"网络计算器"为例,我们需要客户端给服务器发"计算请求",服务器返回"计算结果"。这时候协议该怎么定?

至少要明确两件事:

  1. 请求格式(Request) :需要包含两个操作数(比如xy)、一个运算符(比如op,支持+ - * / %)。
  2. 响应格式(Response) :需要包含计算结果(result)、状态码(code,比如0表示成功,1表示除0错误,2表示非法运算符)。

如果没有这个约定,客户端发个"1+1",服务器可能以为是字符串"1+1",而不是"x=1,op=+,y=1"------根本没法计算。

所以,应用层协议的核心作用是:给字节流"贴标签",让接收端能正确识别数据结构和字段含义

三、关键技术:序列化与反序列化

协议定好了Request和Response的结构,接下来的问题是:怎么把这些结构化数据(比如C++的类/结构体)通过网络发送?

直接发结构体行不行?比如客户端定义一个struct Request { int x; int y; char op; },然后把结构体直接write到socket------这在同平台(比如都是Linux)可能暂时能用,但跨平台(比如客户端Windows,服务器Linux)一定会出问题。

为什么?因为结构体的内存对齐 。不同编译器(比如VS和GCC)对结构体的内存对齐规则可能不同:比如一个struct { char a; int b; },VS可能按4字节对齐(总大小8字节),GCC可能按自然对齐(总大小5字节)。直接传结构体,接收端解析时字段会错位(比如把a的内存当成b的一部分),结果完全错误。

这时候就需要"序列化"和"反序列化":

  • 序列化(Serialization) :把结构化数据(比如Request类对象)转换成通用的字符串或字节流(比如"10 + 20"),消除平台差异。
  • 反序列化(Deserialization) :把接收到的字符串/字节流还原成结构化数据(比如把"10 + 20"还原成x=10op=+y=20)。

举个生活中的例子:你在QQ发消息,消息里包含"昵称、时间、内容"------这其实是一个结构化数据。QQ会把这三个字段打包成一个字符串(比如"小明|2024-05-20 10:00|你好"),这个过程就是序列化;接收方拿到字符串后,按"|"分割,还原出昵称、时间、内容,这就是反序列化。

四、实战:网络计算器的协议设计与实现

下面我们从0实现一个网络计算器,把"协议定制""序列化/反序列化"落地。整个过程分三步:设计协议结构、实现序列化/反序列化、保障报文完整性。

4.1 第一步:定义协议结构(Request/Response)

首先,我们用C++类定义请求和响应的结构(代替结构体,方便封装方法):

cpp 复制代码
// protocol.hpp
#pragma once
#include <string>
#include <iostream>

// 计算请求:x op y(比如10 + 20)
class Request {
public:
    int x;       // 左操作数
    int y;       // 右操作数
    char op;     // 运算符:+ - * / %
    std::string sep = " ";  // 字段分隔符(空格)

    // 构造函数
    Request(int x_ = 0, int y_ = 0, char op_ = 0) 
        : x(x_), y(y_), op(op_) {}

    // 序列化:把Request对象转成字符串(比如"10 + 20")
    bool Serialize(std::string& out_str);

    // 反序列化:把字符串转成Request对象
    bool Deserialize(const std::string& in_str);
};

// 计算响应:result(结果) + code(状态码)
class Response {
public:
    int result;  // 计算结果(仅当code=0时有效)
    int code;    // 状态码:0=成功,1=除0错误,2=模0错误,3=非法运算符
    std::string sep = " ";  // 字段分隔符(空格)

    // 构造函数
    Response(int res_ = 0, int code_ = 0) 
        : result(res_), code(code_) {}

    // 序列化:把Response对象转成字符串(比如"30 0")
    bool Serialize(std::string& out_str);

    // 反序列化:把字符串转成Response对象
    bool Deserialize(const std::string& in_str);
};

4.2 第二步:实现序列化与反序列化

接下来实现SerializeDeserialize方法,核心是"按约定格式处理字符串"。

4.2.1 Request的序列化与反序列化
  • 序列化:把xopy用空格拼接成字符串(比如x=10op='+'y=20 → "10 + 20")。
  • 反序列化:把字符串按空格分割,提取xopy(比如"10 + 20" → x=10op='+'y=20)。
cpp 复制代码
// Request序列化
bool Request::Serialize(std::string& out_str) {
    // 拼接格式:x + " " + op + " " + y
    out_str = std::to_string(x) + sep + op + sep + std::to_string(y);
    return true;  // 简单场景,暂不处理异常
}

// Request反序列化
bool Request::Deserialize(const std::string& in_str) {
    // 1. 找第一个分隔符(空格),提取x
    size_t first_sep = in_str.find(sep);
    if (first_sep == std::string::npos) {
        return false;  // 没找到分隔符,格式错误
    }
    std::string x_str = in_str.substr(0, first_sep);
    x = std::stoi(x_str);  // 字符串转整数

    // 2. 找第二个分隔符,提取op
    size_t second_sep = in_str.find(sep, first_sep + 1);
    if (second_sep == std::string::npos) {
        return false;
    }
    op = in_str[first_sep + 1];  // 分隔符后第一个字符是op

    // 3. 提取y
    std::string y_str = in_str.substr(second_sep + 1);
    y = std::stoi(y_str);

    return true;
}
4.2.2 Response的序列化与反序列化

逻辑类似,只是字段变成resultcode

cpp 复制代码
// Response序列化
bool Response::Serialize(std::string& out_str) {
    // 拼接格式:result + " " + code
    out_str = std::to_string(result) + sep + std::to_string(code);
    return true;
}

// Response反序列化
bool Response::Deserialize(const std::string& in_str) {
    // 找分隔符,分割result和code
    size_t sep_pos = in_str.find(sep);
    if (sep_pos == std::string::npos) {
        return false;
    }
    std::string res_str = in_str.substr(0, sep_pos);
    std::string code_str = in_str.substr(sep_pos + 1);

    result = std::stoi(res_str);
    code = std::stoi(code_str);

    return true;
}

4.3 第三步:保障报文完整性------加"长度头"和"分隔符"

现在我们能把Request/Response转成字符串了,但还有个问题:接收端怎么知道一个报文的结束?比如客户端发了"10 + 20",接收端可能只读到"10 + "(半包),或者读到"10 + 2020 - 5"(粘包)。

解决方案:给每个报文加一个"长度头",格式约定为"长度\n有效载荷"------比如"5\n10+20"(长度5表示后面的有效载荷是5个字符)。接收端先读"长度",再按长度读"有效载荷",就能保证拿到完整的报文。

我们写两个通用函数Encode(加长度头)和Decode(解长度头):

cpp 复制代码
// protocol.hpp 中添加
#include <cstring>

// 协议分隔符:用于分割长度头和有效载荷(换行符\n)
const std::string PROTOCOL_SEP = "\n";

// Encode:给有效载荷加长度头(格式:长度\n有效载荷)
std::string Encode(const std::string& payload) {
    int len = payload.size();
    return std::to_string(len) + PROTOCOL_SEP + payload;
}

// Decode:从报文中提取有效载荷(输入是接收缓冲区,输出是有效载荷)
// 返回值:true=解析成功,false=报文不完整
bool Decode(std::string& in_buf, std::string& out_payload) {
    // 1. 找长度头的分隔符\n
    size_t sep_pos = in_buf.find(PROTOCOL_SEP);
    if (sep_pos == std::string::npos) {
        return false;  // 没找到分隔符,报文不完整
    }

    // 2. 提取长度头,转成整数
    std::string len_str = in_buf.substr(0, sep_pos);
    int payload_len = std::stoi(len_str);

    // 3. 检查缓冲区是否包含完整的有效载荷
    int total_len = len_str.size() + PROTOCOL_SEP.size() + payload_len;
    if (in_buf.size() < total_len) {
        return false;  // 有效载荷不完整
    }

    // 4. 提取有效载荷,并从缓冲区中移除已处理的报文
    out_payload = in_buf.substr(sep_pos + 1, payload_len);
    in_buf.erase(0, total_len);  // 移除已处理的部分(长度头+有效载荷)

    return true;
}

五、工程化优化:套接字封装与服务端框架

现在协议和序列化都搞定了,接下来要实现网络通信。为了避免重复写socket代码,我们先封装一个Socket类;再写一个TCP服务器,用多进程处理并发连接。

5.1 套接字封装(Socket类)

把socket的创建、绑定、监听、accept、connect等操作封装成类,方便客户端和服务器复用:

cpp 复制代码
// socket.hpp
#pragma once
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <iostream>

class Socket {
private:
    int sock_fd_;  // 套接字文件描述符

public:
    Socket() : sock_fd_(-1) {}
    ~Socket() { if (sock_fd_ != -1) close(sock_fd_); }

    // 1. 创建流式套接字(TCP)
    bool Create() {
        sock_fd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sock_fd_ < 0) {
            std::cerr << "Socket create failed: " << strerror(errno) << std::endl;
            return false;
        }
        return true;
    }

    // 2. 绑定端口(服务器用,IP默认INADDR_ANY)
    bool Bind(uint16_t port) {
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);  // 主机字节序转网络字节序
        addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定所有网卡IP

        if (bind(sock_fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
            std::cerr << "Socket bind failed: " << strerror(errno) << std::endl;
            return false;
        }
        return true;
    }

    // 3. 监听(服务器用)
    bool Listen(int backlog = 10) {
        if (listen(sock_fd_, backlog) < 0) {
            std::cerr << "Socket listen failed: " << strerror(errno) << std::endl;
            return false;
        }
        return true;
    }

    // 4. 接受连接(服务器用,返回新的通信套接字)
    int Accept(std::string& client_ip, uint16_t& client_port) {
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
        int conn_fd = accept(sock_fd_, (struct sockaddr*)&client_addr, &client_addr_len);
        if (conn_fd < 0) {
            std::cerr << "Socket accept failed: " << strerror(errno) << std::endl;
            return -1;
        }

        // 提取客户端IP和端口
        client_ip = inet_ntoa(client_addr.sin_addr);  // 网络字节序转IP字符串
        client_port = ntohs(client_addr.sin_port);    // 网络字节序转主机字节序

        return conn_fd;
    }

    // 5. 连接服务器(客户端用)
    bool Connect(const std::string& server_ip, uint16_t server_port) {
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(server_port);
        // IP字符串转网络字节序
        if (inet_pton(AF_INET, server_ip.c_str(), &server_addr.sin_addr) <= 0) {
            std::cerr << "Invalid server IP: " << server_ip << std::endl;
            return false;
        }

        if (connect(sock_fd_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
            std::cerr << "Socket connect failed: " << strerror(errno) << std::endl;
            return false;
        }
        return true;
    }

    // 获取套接字文件描述符
    int GetFd() const { return sock_fd_; }
};

5.2 多进程TCP服务器实现

服务器的逻辑:主进程监听端口,接受新连接;每收到一个连接,fork一个子进程处理业务(计算请求),主进程继续监听。同时忽略SIGCHLD信号,避免僵尸进程。

cpp 复制代码
// server.cpp
#include "socket.hpp"
#include "protocol.hpp"
#include <signal.h>
#include <sys/wait.h>
#include <functional>

// 忽略SIGCHLD信号,避免僵尸进程
void IgnoreSigchld() {
    signal(SIGCHLD, SIG_IGN);
}

// 处理客户端请求:读取请求→计算→返回响应
void HandleClient(int conn_fd) {
    std::string in_buf;  // 接收缓冲区(累积数据,处理粘包/半包)
    char buf[1024] = {0};

    while (true) {
        // 1. 读取客户端数据
        ssize_t n = read(conn_fd, buf, sizeof(buf) - 1);
        if (n < 0) {
            std::cerr << "Read failed: " << strerror(errno) << std::endl;
            break;
        } else if (n == 0) {
            std::cout << "Client closed connection" << std::endl;
            break;
        }

        // 2. 把读取到的字节流追加到缓冲区
        buf[n] = '\0';  // 确保字符串结束
        in_buf += buf;
        memset(buf, 0, sizeof(buf));

        // 3. 解析缓冲区中的完整报文(可能有多个)
        std::string payload;
        while (Decode(in_buf, payload)) {  // 循环解析,直到没有完整报文
            // 4. 反序列化:payload→Request对象
            Request req;
            if (!req.Deserialize(payload)) {
                std::cerr << "Invalid request format: " << payload << std::endl;
                continue;
            }

            // 5. 执行计算,生成Response
            Response resp;
            switch (req.op) {
                case '+':
                    resp.result = req.x + req.y;
                    resp.code = 0;
                    break;
                case '-':
                    resp.result = req.x - req.y;
                    resp.code = 0;
                    break;
                case '*':
                    resp.result = req.x * req.y;
                    resp.code = 0;
                    break;
                case '/':
                    if (req.y == 0) {
                        resp.code = 1;  // 除0错误
                    } else {
                        resp.result = req.x / req.y;
                        resp.code = 0;
                    }
                    break;
                case '%':
                    if (req.y == 0) {
                        resp.code = 2;  // 模0错误
                    } else {
                        resp.result = req.x % req.y;
                        resp.code = 0;
                    }
                    break;
                default:
                    resp.code = 3;  // 非法运算符
                    break;
            }

            // 6. 序列化Response→字符串,加长度头
            std::string resp_str;
            resp.Serialize(resp_str);
            std::string encoded_resp = Encode(resp_str);

            // 7. 发送响应给客户端
            write(conn_fd, encoded_resp.c_str(), encoded_resp.size());
            std::cout << "Handled request: " << payload 
                      << " → Response: " << resp_str << std::endl;
        }
    }

    close(conn_fd);  // 关闭通信套接字
}

// TCP服务器类
class TcpServer {
private:
    Socket listen_sock_;
    uint16_t port_;

public:
    TcpServer(uint16_t port) : port_(port) {}

    // 初始化服务器:创建→绑定→监听
    bool Init() {
        if (!listen_sock_.Create()) return false;
        if (!listen_sock_.Bind(port_)) return false;
        if (!listen_sock_.Listen()) return false;
        std::cout << "Server init success, listening on port " << port_ << std::endl;
        return true;
    }

    // 启动服务器:接受连接→fork子进程处理
    void Start() {
        IgnoreSigchld();  // 忽略SIGCHLD,避免僵尸进程

        while (true) {
            std::string client_ip;
            uint16_t client_port;
            int conn_fd = listen_sock_.Accept(client_ip, client_port);
            if (conn_fd < 0) continue;

            // fork子进程处理客户端请求
            pid_t pid = fork();
            if (pid < 0) {
                std::cerr << "Fork failed: " << strerror(errno) << std::endl;
                close(conn_fd);
                continue;
            } else if (pid == 0) {
                // 子进程:处理请求(关闭监听套接字,只保留通信套接字)
                listen_sock_.~Socket();  // 子进程不需要监听套接字,手动析构
                HandleClient(conn_fd);
                exit(0);  // 处理完请求,子进程退出
            } else {
                // 父进程:关闭通信套接字(子进程会复制一份)
                close(conn_fd);
            }
        }
    }
};

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);

    TcpServer server(port);
    if (!server.Init()) return 1;
    server.Start();

    return 0;
}

5.3 客户端实现

客户端逻辑:创建套接字→连接服务器→构造请求→发送→接收响应→解析。

cpp 复制代码
// client.cpp
#include "socket.hpp"
#include "protocol.hpp"
#include <random>
#include <unistd.h>

// 随机生成计算请求(模拟用户输入)
Request GenerateRandomRequest() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> num_dist(1, 100);  // 1~100的随机数
    std::uniform_int_distribution<> op_dist(0, 4);     // 0~4对应5种运算符

    int x = num_dist(gen);
    int y = num_dist(gen);
    char ops[] = "+-*/%";
    char op = ops[op_dist(gen)];

    // 避免除0/模0(简化测试)
    if ((op == '/' || op == '%') && y == 0) {
        y = 1;
    }

    return Request(x, y, op);
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建套接字并连接服务器
    Socket sock;
    if (!sock.Create()) return 1;
    if (!sock.Connect(server_ip, server_port)) return 1;
    std::cout << "Connected to server: " << server_ip << ":" << server_port << std::endl;

    // 2. 发送10个随机请求
    for (int i = 0; i < 10; ++i) {
        // 生成请求
        Request req = GenerateRandomRequest();
        std::string req_str;
        req.Serialize(req_str);
        std::string encoded_req = Encode(req_str);

        // 发送请求
        write(sock.GetFd(), encoded_req.c_str(), encoded_req.size());
        std::cout << "Sent request: " << req_str << std::endl;

        // 接收响应
        char buf[1024] = {0};
        ssize_t n = read(sock.GetFd(), buf, sizeof(buf) - 1);
        if (n < 0) {
            std::cerr << "Read response failed: " << strerror(errno) << std::endl;
            break;
        }
        buf[n] = '\0';
        std::string in_buf = buf;

        // 解析响应
        std::string payload;
        if (Decode(in_buf, payload)) {
            Response resp;
            if (resp.Deserialize(payload)) {
                if (resp.code == 0) {
                    std::cout << "Received response: " << req_str << " = " << resp.result << std::endl;
                } else {
                    std::string err_msg;
                    switch (resp.code) {
                        case 1: err_msg = "division by zero"; break;
                        case 2: err_msg = "mod by zero"; break;
                        case 3: err_msg = "invalid operator"; break;
                    }
                    std::cout << "Received error: " << req_str << " → " << err_msg << std::endl;
                }
            }
        }

        sleep(1);  // 每隔1秒发一次,避免请求太密集
    }

    // 3. 关闭套接字
    close(sock.GetFd());
    std::cout << "Disconnected from server" << std::endl;

    return 0;
}

六、从手写到底层到通用方案------引入JSON

手写序列化/反序列化虽然能帮我们理解原理,但实际项目中很繁琐(比如字段多了要写大量字符串分割代码)。工业界常用JSON作为通用序列化方案------它是跨语言的文本格式,支持各种数据类型(整数、字符串、数组、对象),还有成熟的库(比如C++的jsoncpp)。

6.1 为什么用JSON?

  • 跨语言:Python、Java、Go都支持JSON,不同语言的服务能轻松通信。
  • 易读易调试 :JSON格式直观(比如{"x":10,"y":20,"op":"+"}),方便打印日志和调试。
  • 无需手写代码:库会自动处理序列化/反序列化,减少重复工作。

6.2 用jsoncpp替换手写序列化

首先安装jsoncpp库(Ubuntu为例):

bash 复制代码
sudo apt-get install libjsoncpp-dev

然后修改protocol.hpp,用JSON实现序列化/反序列化(保留原手写代码,用条件编译切换):

cpp 复制代码
// protocol.hpp 中添加
#include <json/json.h>  // jsoncpp头文件

// 条件编译:定义USE_JSON则用JSON,否则用手写
#ifdef USE_JSON
// JSON版Request序列化
bool Request::Serialize(std::string& out_str) {
    Json::Value root;
    root["x"] = x;
    root["y"] = y;
    root["op"] = op;  // jsoncpp会自动把char转成字符串

    Json::FastWriter writer;  // 快速序列化(无格式)
    out_str = writer.write(root);
    // 去掉JSON字符串末尾的换行符(FastWriter会加)
    if (!out_str.empty() && out_str.back() == '\n') {
        out_str.pop_back();
    }
    return true;
}

// JSON版Request反序列化
bool Request::Deserialize(const std::string& in_str) {
    Json::Value root;
    Json::Reader reader;
    if (!reader.parse(in_str, root)) {
        std::cerr << "JSON parse failed: " << in_str << std::endl;
        return false;
    }

    // 检查字段是否存在
    if (!root.isMember("x") || !root["x"].isInt()) return false;
    if (!root.isMember("y") || !root["y"].isInt()) return false;
    if (!root.isMember("op") || !root["op"].isString()) return false;

    x = root["x"].asInt();
    y = root["y"].asInt();
    op = root["op"].asString()[0];  // 字符串转char
    return true;
}

// JSON版Response序列化
bool Response::Serialize(std::string& out_str) {
    Json::Value root;
    root["result"] = result;
    root["code"] = code;

    Json::FastWriter writer;
    out_str = writer.write(root);
    if (!out_str.empty() && out_str.back() == '\n') {
        out_str.pop_back();
    }
    return true;
}

// JSON版Response反序列化
bool Response::Deserialize(const std::string& in_str) {
    Json::Value root;
    Json::Reader reader;
    if (!reader.parse(in_str, root)) {
        std::cerr << "JSON parse failed: " << in_str << std::endl;
        return false;
    }

    if (!root.isMember("result") || !root["result"].isInt()) return false;
    if (!root.isMember("code") || !root["code"].isInt()) return false;

    result = root["result"].asInt();
    code = root["code"].asInt();
    return true;
}
#else
// 原手写序列化/反序列化代码(略)
#endif

6.3 编译与测试

-DUSE_JSON启用JSON模式,链接jsoncpp库:

bash 复制代码
# 编译服务器
g++ server.cpp -o server -DUSE_JSON -ljsoncpp
# 编译客户端
g++ client.cpp -o client -DUSE_JSON -ljsoncpp

启动服务器和客户端,会看到请求格式变成JSON:

复制代码
# 服务器输出
Handled request: {"op":"+","x":10,"y":20} → Response: {"code":0,"result":30}

七、总结

这篇文章从TCP的边界问题出发,讲了自定义协议、序列化/反序列化的核心逻辑,最后用实战代码落地。复习时可以重点关注这几个点:

  1. TCP边界问题:面向字节流导致读不完整/粘包,必须在应用层解决。
  2. 协议定制:约定数据格式(Request/Response)和报文结构(长度头+有效载荷)。
  3. 序列化/反序列化
    • 作用:消除跨平台差异,把结构化数据转通用格式。
    • 手写实现:按约定分隔符处理字符串,加长度头保障完整性。
    • 通用方案:用JSON等成熟格式,减少重复代码。
  4. 工程化:套接字封装复用代码,多进程处理并发,忽略SIGCHLD避免僵尸进程。
相关推荐
泽02025 小时前
Linux之环境变量
java·linux·redis
正在努力的小河5 小时前
Linux 自带的 LED 灯驱动实验
linux·运维·服务器
Pafey5 小时前
MFC中一个类的成员变量值自动被篡改:多重继承带来的问题
c++·mfc
hsjkdhs5 小时前
C++之多层继承、多源继承、菱形继承
开发语言·c++·算法
立志成为大牛的小牛5 小时前
数据结构——十七、线索二叉树找前驱与后继(王道408)
数据结构·笔记·学习·程序人生·考研·算法
阳光雨滴5 小时前
使用wpf用户控件编程落石效果动画
c++·wpf
檀越剑指大厂6 小时前
【Linux系列】Vim 中删除当前光标到行尾
linux·运维·vim
小贾要学习6 小时前
【数据结构】C++实现红黑树
数据结构·c++
菠萝吹雪ing6 小时前
GUI 自动化与接口自动化:概念、差异与协同落地
运维·笔记·程序人生·自动化·接口测试·gui测试