应用层自定义协议与序列化实战:从 TCP 粘包问题到 JSON 通信

应用层自定义协议 将数据结构或对象状态转换为可以存储或传输的格式(通常是字节流)的过程。反序列化(Deserialization) 阅读详情

在 TCP 网络编程中,“发送字符串” 只能满足简单需求,而实际业务(如网络计算器、用户信息传输)需要传递结构化数据。这就需要我们自定义应用层协议,解决 “数据边界识别” 和 “结构化数据传输” 两大核心问题。本文将以 “网络计算器” 为案例,从协议设计、序列化 / 反序列化到代码实现,完整讲解应用层协议的落地过程,同时深入解析 TCP 粘包问题的解决方案。

一、为什么需要自定义应用层协议?

TCP 是 “面向字节流” 的协议,它会将数据无边界地存入发送 / 接收缓冲区,这就导致两个关键问题:

  1. 粘包问题:多次发送的数据可能被合并接收(如 “1+1” 和 “2+3” 被拼成 “1+12+3”),或单次发送的数据被拆分接收(如 “12345” 被拆成 “12” 和 “345”),接收方无法识别数据边界。
  2. 结构化数据传输:业务中需要传递的 “操作数 + 运算符”“结果 + 状态码” 等结构化信息,无法直接通过字符串传递(如 “1+1” 需要解析出1、+、1三个字段,解析逻辑脆弱且易出错)。

应用层协议的本质是双方约定的结构化数据格式,它需要同时解决:

        如何识别数据边界(解粘包);

        如何将结构化数据转换为可传输的字节流(序列化);

        如何将字节流还原为结构化数据(反序列化)。

二、核心概念:序列化与反序列化

在设计协议前,需先理解 “序列化” 和 “反序列化” 的作用 —— 它们是连接 “内存中的结构化数据” 和 “网络传输的字节流” 的桥梁。

2.1 基本定义

        序列化:将内存中的结构化数据(如 C++ 的struct/class)转换为连续的字节流(如字符串、JSON),便于网络传输或存储。

        反序列化:将接收到的字节流还原为原始的结构化数据,供业务逻辑使用。

2.2 常见序列化方案对比

本文选择JSON(通过 Jsoncpp 库)作为序列化方案,其优势与其他方案对比如下:

方案优点缺点适用场景
自定义字符串格式(如 “x op y”)实现简单,无需第三方库扩展性差(新增字段需修改解析逻辑),不支持复杂结构(如嵌套对象)极简单场景(如仅传输两个整数 + 运算符)
JSON(Jsoncpp)人类可读,支持复杂结构(对象、数组),跨语言兼容序列化后体积略大,性能低于二进制方案大多数业务场景(如 API 接口、中小型数据传输)
二进制协议(如 Protobuf)体积小、性能高可读性差,需定义.proto 文件,学习成本高高并发、大数据量场景(如游戏、物联网)

三、应用层协议设计:解决粘包与结构化传输

以 “网络计算器” 为例,我们设计一套完整的应用层协议,需包含 “数据边界标识” 和 “结构化数据字段” 两部分。

3.1 协议核心设计思路

为解决粘包问题,我们采用 “长度前缀法”:在每个数据包前添加 “有效数据长度” 字段,接收方先读取长度,再根据长度读取完整的有效数据。同时,有效数据部分使用 JSON 格式存储结构化信息,便于序列化 / 反序列化。

3.2 协议格式定义

最终约定的数据包格式如下(\r\n为分隔符,不属于有效数据):

[有效数据长度]\r\n[JSON格式的有效数据]\r\n

        有效数据长度:表示后续 JSON 数据的字节数(如 JSON 数据 “{"datax":1,"datay":1,"oper":"+"}” 长度为 32,则前缀为 “32”);

        JSON 有效数据:存储结构化业务数据(如请求中的 “操作数 + 运算符”,响应中的 “结果 + 状态码”);

        分隔符\r\n:用于区分 “长度字段” 和 “有效数据”,避免长度字段与有效数据混淆(如长度 “123” 和有效数据 “456” 不会被误判为 “123456”)。

3.3 业务结构体定义

针对 “网络计算器” 场景,定义两个核心结构体:

  1. 请求结构体(Request):客户端向服务器发送的 “计算请求”,包含两个操作数和一个运算符;
  2. 响应结构体(Response):服务器向客户端返回的 “计算结果”,包含结果值和状态码(如 0 表示成功,1 表示除零错误)。

结构体字段定义如下:

结构体字段类型说明
Request_data_xint第一个操作数(如 1)
Request_data_yint第二个操作数(如 1)
Request_operchar运算符(如 +、-、*、/、%)
Response_resultint计算结果(如 2)
Response_codeint状态码(0 = 成功,1 = 除零错误,2 = 非法运算符)

四、基础封装:Socket 与协议工具类

        为简化代码,先封装通用的 Socket 类(TCP)和协议工具类(编码 / 解码、序列化 / 反序列化)。

4.1 Socket 封装(Socket.hpp)

        基于 TCP 协议封装 Socket 基类和实现类,提供 “创建、绑定、监听、接受连接、收发数据” 等核心接口,采用 “模板方法模式” 统一流程:

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

// 地址结构转换宏(struct sockaddr_in* → struct sockaddr*)
#define Convert(addrptr) ((struct sockaddr *)addrptr)

namespace Net_Work {
// 错误码定义
enum ErrorCode {
    SocketError = 1,
    BindError,
    ListenError
};
const static int defaultsockfd = -1; // 默认无效socket描述符
const static int backlog = 5;        // 默认最大等待连接数

// Socket基类(接口类)
class Socket {
public:
    virtual ~Socket() {}
    // 创建socket(失败则退出)
    virtual void CreateSocketOrDie() = 0;
    // 绑定端口(失败则退出)
    virtual void BindSocketOrDie(uint16_t port) = 0;
    // 监听socket(失败则退出)
    virtual void ListenSocketOrDie(int backlog) = 0;
    // 接受连接(返回新的通信socket,传出客户端IP和端口)
    virtual Socket* AcceptConnection(std::string* peerip, uint16_t* peerport) = 0;
    // 连接服务器(返回是否成功)
    virtual bool ConnectServer(std::string& serverip, uint16_t serverport) = 0;
    // 获取socket描述符
    virtual int GetSockFd() = 0;
    // 设置socket描述符(用于接受连接后创建新socket)
    virtual void SetSockFd(int sockfd) = 0;
    // 关闭socket
    virtual void CloseSocket() = 0;
    // 接收数据(指定缓冲区大小,返回是否成功)
    virtual bool Recv(std::string* buffer, int size) = 0;
    // 发送数据
    virtual void Send(std::string& send_str) = 0;

    // 模板方法:创建监听socket(统一流程)
    void BuildListenSocketMethod(uint16_t port, int backlog = Net_Work::backlog) {
        CreateSocketOrDie();
        BindSocketOrDie(port);
        ListenSocketOrDie(backlog);
    }

    // 模板方法:创建客户端连接socket(统一流程)
    bool BuildConnectSocketMethod(std::string& serverip, uint16_t serverport) {
        CreateSocketOrDie();
        return ConnectServer(serverip, serverport);
    }

    // 模板方法:通过已有的sockfd创建socket(用于accept后)
    void BuildNormalSocketMethod(int sockfd) {
        SetSockFd(sockfd);
    }
};

// TCP Socket实现类
class TcpSocket : public Socket {
public:
    TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd) {}
    ~TcpSocket() { CloseSocket(); }

    // 创建TCP socket(SOCK_STREAM表示TCP)
    void CreateSocketOrDie() override {
        if (_sockfd < 0) {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0) {
                perror("socket create failed");
                exit(SocketError);
            }
        }
    }

    // 绑定端口(INADDR_ANY表示绑定所有网卡IP)
    void BindSocketOrDie(uint16_t port) override {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port); // 主机字节序转网络字节序

        if (::bind(_sockfd, Convert(&local), sizeof(local)) < 0) {
            perror("bind failed");
            exit(BindError);
        }
    }

    // 设为监听状态
    void ListenSocketOrDie(int backlog) override {
        if (::listen(_sockfd, backlog) < 0) {
            perror("listen failed");
            exit(ListenError);
        }
    }

    // 接受客户端连接(返回新的TcpSocket用于通信)
    Socket* AcceptConnection(std::string* peerip, uint16_t* peerport) override {
        struct sockaddr_in peer;
        socklen_t peer_len = sizeof(peer);
        int new_sockfd = ::accept(_sockfd, Convert(&peer), &peer_len);
        if (new_sockfd < 0) {
            perror("accept failed");
            return nullptr;
        }

        // 传出客户端IP和端口
        *peerip = inet_ntoa(peer.sin_addr);
        *peerport = ntohs(peer.sin_port);
        return new TcpSocket(new_sockfd);
    }

    // 连接服务器
    bool ConnectServer(std::string& serverip, uint16_t serverport) override {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 字符串IP转网络字节序
        server.sin_port = htons(serverport);

        if (::connect(_sockfd, Convert(&server), sizeof(server)) < 0) {
            perror("connect failed");
            return false;
        }
        return true;
    }

    // 接收数据(将数据追加到buffer,返回是否成功)
    bool Recv(std::string* buffer, int size) override {
        char in_buf[size];
        ssize_t n = ::recv(_sockfd, in_buf, size - 1, 0); // 留1字节存'\0'
        if (n > 0) {
            in_buf[n] = '\0';
            *buffer += in_buf; // 追加接收,避免拆分数据丢失
            return true;
        } else if (n == 0) {
            // 对端关闭连接
            return false;
        } else {
            // 接收错误
            perror("recv failed");
            return false;
        }
    }

    // 发送数据
    void Send(std::string& send_str) override {
        ::send(_sockfd, send_str.c_str(), send_str.size(), 0);
    }

    // 获取/设置socket描述符、关闭socket(实现略)
    int GetSockFd() override { return _sockfd; }
    void SetSockFd(int sockfd) override { _sockfd = sockfd; }
    void CloseSocket() override {
        if (_sockfd > defaultsockfd) {
            ::close(_sockfd);
            _sockfd = defaultsockfd;
        }
    }

private:
    int _sockfd; // socket描述符
};
} // namespace Net_Work

4.2 协议工具类(Protocol.hpp)

封装 “编码(添加长度前缀)”“解码(解粘包)”“序列化(结构体转 JSON)”“反序列化(JSON 转结构体)” 四大核心功能,同时提供 “请求 / 响应结构体” 和 “工厂类”(简化对象创建)。

4.2.1 依赖库:Jsoncpp 安装与使用

本文使用 Jsoncpp 实现 JSON 序列化,需先安装:

        Ubuntu:sudo apt-get install libjsoncpp-dev

        CentOS:sudo yum install jsoncpp-devel

4.2.2 完整协议工具类代码
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <jsoncpp/json/json.h>

namespace Protocol {
// 协议分隔符(\r\n用于区分长度和有效数据)
const std::string LineBreakSep = "\r\n";

// 1. 编码:给有效数据(如JSON字符串)添加长度前缀,生成完整数据包
std::string Encode(const std::string& message) {
    std::string len_str = std::to_string(message.size()); // 有效数据长度
    // 格式:[长度]\r\n[有效数据]\r\n
    return len_str + LineBreakSep + message + LineBreakSep;
}

// 2. 解码:从接收缓冲区中提取完整数据包(解粘包)
// package:接收缓冲区(需持续累加数据),message:输出的完整有效数据
bool Decode(std::string& package, std::string* message) {
    // 步骤1:查找第一个\r\n,确定长度字段的结束位置
    size_t len_sep_pos = package.find(LineBreakSep);
    if (len_sep_pos == std::string::npos) {
        // 未找到分隔符,数据不完整
        return false;
    }

    // 步骤2:解析长度字段(字符串转整数)
    std::string len_str = package.substr(0, len_sep_pos);
    int message_len = std::stoi(len_str); // 有效数据长度

    // 步骤3:计算完整数据包的总长度(长度字段+2个\r\n+有效数据)
    int total_len = len_str.size() + LineBreakSep.size() + message_len + LineBreakSep.size();
    if (package.size() < total_len) {
        // 接收缓冲区数据不足,等待后续数据
        return false;
    }

    // 步骤4:提取有效数据,并从缓冲区中删除已处理的数据包
    *message = package.substr(len_sep_pos + LineBreakSep.size(), message_len);
    package.erase(0, total_len); // 清除已处理数据,避免重复解析
    return true;
}

// 3. 请求结构体(客户端→服务器:操作数+运算符)
class Request {
public:
    Request() : _data_x(0), _data_y(0), _oper(0) {}
    Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op) {}

    // 序列化:Request→JSON字符串
    bool Serialize(std::string* out) {
        Json::Value root;
        root["datax"] = _data_x;   // 第一个操作数
        root["datay"] = _data_y;   // 第二个操作数
        root["oper"] = _oper;      // 运算符(JSON中char以int存储)
        Json::FastWriter writer;   // 无格式JSON(体积小,适合传输)
        *out = writer.write(root);
        return true;
    }

    // 反序列化:JSON字符串→Request
    bool Deserialize(std::string& in) {
        Json::Value root;
        Json::Reader reader;
        if (!reader.parse(in, root)) {
            // JSON解析失败
            std::cerr << "Request deserialize failed: " << reader.getFormattedErrorMessages() << std::endl;
            return false;
        }

        // 提取字段(需判断字段是否存在,避免崩溃)
        if (root.isMember("datax") && root["datax"].isInt() &&
            root.isMember("datay") && root["datay"].isInt() &&
            root.isMember("oper") && root["oper"].isInt()) {
            _data_x = root["datax"].asInt();
            _data_y = root["datay"].asInt();
            _oper = static_cast<char>(root["oper"].asInt());
            return true;
        }
        return false;
    }

    // 获取字段(实现略)
    int GetX() const { return _data_x; }
    int GetY() const { return _data_y; }
    char GetOper() const { return _oper; }

private:
    int _data_x;  // 第一个操作数
    int _data_y;  // 第二个操作数
    char _oper;   // 运算符(+、-、*、/、%)
};

// 4. 响应结构体(服务器→客户端:结果+状态码)
class Response {
public:
    Response() : _result(0), _code(0) {}
    Response(int result, int code) : _result(result), _code(code) {}

    // 序列化:Response→JSON字符串
    bool Serialize(std::string* out) {
        Json::Value root;
        root["result"] = _result; // 计算结果
        root["code"] = _code;     // 状态码(0=成功,1=除零错误,2=非法运算符)
        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }

    // 反序列化:JSON字符串→Response
    bool Deserialize(std::string& in) {
        Json::Value root;
        Json::Reader reader;
        if (!reader.parse(in, root)) {
            std::cerr << "Response deserialize failed: " << reader.getFormattedErrorMessages() << std::endl;
            return false;
        }

        if (root.isMember("result") && root["result"].isInt() &&
            root.isMember("code") && root["code"].isInt()) {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
            return true;
        }
        return false;
    }

    // 设置/获取字段(实现略)
    void SetResult(int result) { _result = result; }
    void SetCode(int code) { _code = code; }
    int GetResult() const { return _result; }
    int GetCode() const { return _code; }

private:
    int _result; // 计算结果
    int _code;   // 状态码
};

// 5. 工厂类(简化Request/Response对象创建,避免重复new/delete)
class Factory {
public:
    // 创建空Request
    std::shared_ptr<Request> BuildRequest() {
        return std::make_shared<Request>();
    }

    // 创建带参数的Request
    std::shared_ptr<Request> BuildRequest(int x, int y, char op) {
        return std::make_shared<Request>(x, y, op);
    }

    // 创建空Response
    std::shared_ptr<Response> BuildResponse() {
        return std::make_shared<Response>();
    }

    // 创建带参数的Response
    std::shared_ptr<Response> BuildResponse(int result, int code) {
        return std::make_shared<Response>(result, code);
    }
};
} // namespace Protocol

五、业务实现:网络计算器(服务器与客户端)

基于上述封装,实现 “网络计算器” 的服务器和客户端,完整流程为:

  1. 客户端:构造 “操作数 + 运算符” 请求 → 序列化→编码→发送;
  2. 服务器:接收数据→解码→反序列化→计算→构造响应→序列化→编码→发送;
  3. 客户端:接收响应→解码→反序列化→显示结果。

5.1 服务器实现(TcpServerMain.cc)

服务器采用 “多线程” 模型(复用之前的线程池逻辑),每个客户端连接由独立线程处理,核心是 “协议解析” 和 “计算逻辑”。

#include <iostream>
#include <memory>
#include <pthread.h>
#include "Socket.hpp"
#include "Protocol.hpp"

// 线程数据:传递通信socket和客户端信息
struct ThreadData {
    Net_Work::Socket* sock;
    std::string peerip;
    uint16_t peerport;
};

// 计算逻辑:根据Request计算结果,生成Response
Protocol::Response Calculate(Protocol::Request& req) {
    Protocol::Response resp;
    int x = req.GetX();
    int y = req.GetY();
    char op = req.GetOper();

    switch (op) {
        case '+':
            resp.SetResult(x + y);
            resp.SetCode(0); // 成功
            break;
        case '-':
            resp.SetResult(x - y);
            resp.SetCode(0);
            break;
        case '*':
            resp.SetResult(x * y);
            resp.SetCode(0);
            break;
        case '/':
            if (y == 0) {
                resp.SetResult(0);
                resp.SetCode(1); // 除零错误
            } else {
                resp.SetResult(x / y);
                resp.SetCode(0);
            }
            break;
        case '%':
            if (y == 0) {
                resp.SetResult(0);
                resp.SetCode(1); // 除零错误
            } else {
                resp.SetResult(x % y);
                resp.SetCode(0);
            }
            break;
        default:
            resp.SetResult(0);
            resp.SetCode(2); // 非法运算符
            break;
    }
    return resp;
}

// 线程执行函数:处理单个客户端的通信
void* HandleClient(void* args) {
    pthread_detach(pthread_self()); // 线程分离,无需主线程回收
    ThreadData* td = static_cast<ThreadData*>(args);
    if (td == nullptr || td->sock == nullptr) {
        return nullptr;
    }

    std::cout << "New client connected: " << td->peerip << ":" << td->peerport << std::endl;
    std::string recv_buf; // 接收缓冲区(累加数据,用于解粘包)
    Protocol::Factory factory;

    while (true) {
        // 1. 接收数据(追加到缓冲区)
        if (!td->sock->Recv(&recv_buf, 1024)) {
            std::cout << "Client disconnected: " << td->peerip << ":" << td->peerport << std::endl;
            break;
        }

        // 2. 解码:提取完整的有效数据(解粘包)
        std::string message;
        while (Protocol::Decode(recv_buf, &message)) {
            // 3. 反序列化:JSON→Request
            auto req = factory.BuildRequest();
            if (!req->Deserialize(message)) {
                std::cerr << "Request deserialize failed from " << td->peerip << ":" << td->peerport << std::endl;
                continue;
            }

            // 4. 计算:根据Request生成Response
            Protocol::Response resp = Calculate(*req);

            // 5. 序列化:Response→JSON
            std::string resp_json;
            resp.Serialize(&resp_json);

            // 6. 编码:添加长度前缀,发送响应
            std::string resp_package = Protocol::Encode(resp_json);
            td->sock->Send(resp_package);
            std::cout << "Handled request from " << td->peerip << ":" << td->peerport 
                      << " -> " << req->GetX() << req->GetOper() << req->GetY() 
                      << " = " << resp.GetResult() << " (code: " << resp.GetCode() << ")" << std::endl;
        }
    }

    // 释放资源
    td->sock->CloseSocket();
    delete td->sock;
    delete td;
    return nullptr;
}

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]);

    // 创建监听socket
    std::unique_ptr<Net_Work::Socket> listen_sock(new Net_Work::TcpSocket());
    listen_sock->BuildListenSocketMethod(port);
    std::cout << "Server started, listening on port " << port << std::endl;

    // 循环接受客户端连接
    while (true) {
        std::string peerip;
        uint16_t peerport;
        // 接受连接,返回新的通信socket
        Net_Work::Socket* conn_sock = listen_sock->AcceptConnection(&peerip, &peerport);
        if (conn_sock == nullptr) {
            continue;
        }

        // 创建线程数据,启动线程处理客户端
        ThreadData* td = new ThreadData{conn_sock, peerip, peerport};
        pthread_t tid;
        if (pthread_create(&tid, nullptr, HandleClient, td) != 0) {
            perror("pthread_create failed");
            delete td;
            conn_sock->CloseSocket();
            delete conn_sock;
        }
    }

    return 0;
}

5.2 客户端实现(TcpClientMain.cc)

客户端逻辑简单:读取用户输入的 “操作数 + 运算符”,构造请求并发送,接收响应后显示结果。

#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"
#include "Protocol.hpp"

// 解析用户输入:如“1+1”→x=1, y=1, op='+'
bool ParseInput(const std::string& input, int* x, int* y, char* op) {
    // 查找运算符位置(仅支持单个运算符)
    size_t op_pos = input.find_first_of("+-*/%");
    if (op_pos == std::string::npos || op_pos == 0 || op_pos == input.size() - 1) {
        return false;
    }

    // 提取操作数和运算符
    *x = std::stoi(input.substr(0, op_pos));
    *y = std::stoi(input.substr(op_pos + 1));
    *op = input[op_pos];
    return true;
}

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]);
    Protocol::Factory factory;

    // 创建客户端socket并连接服务器
    std::unique_ptr<Net_Work::Socket> client_sock(new Net_Work::TcpSocket());
    if (!client_sock->BuildConnectSocketMethod(server_ip, server_port)) {
        std::cerr << "Connect to server failed" << std::endl;
        return 1;
    }
    std::cout << "Connected to server: " << server_ip << ":" << server_port << std::endl;

    std::string recv_buf; // 接收缓冲区(用于解粘包)
    std::string input;

    while (true) {
        // 1. 读取用户输入
        std::cout << "Please enter expression (e.g. 1+1, q to quit): ";
        std::getline(std::cin, input);
        if (input == "q" || input == "Q") {
            break;
        }

        // 2. 解析输入(如“1+1”→x=1, y=1, op='+')
        int x, y;
        char op;
        if (!ParseInput(input, &x, &y, &op)) {
            std::cerr << "Invalid expression! Example: 1+1" << std::endl;
            continue;
        }

        // 3. 构造Request→序列化→编码
        auto req = factory.BuildRequest(x, y, op);
        std::string req_json;
        req->Serialize(&req_json);
        std::string req_package = Protocol::Encode(req_json);

        // 4. 发送请求
        client_sock->Send(req_package);

        // 5. 接收响应并解码
        recv_buf.clear();
        while (true) {
            if (!client_sock->Recv(&recv_buf, 1024)) {
                std::cerr << "Server disconnected" << std::endl;
                return 1;
            }

            std::string resp_json;
            if (Protocol::Decode(recv_buf, &resp_json)) {
                // 6. 反序列化→显示结果
                auto resp = factory.BuildResponse();
                if (resp->Deserialize(resp_json)) {
                    int result = resp->GetResult();
                    int code = resp->GetCode();
                    switch (code) {
                        case 0:
                            std::cout << "Result: " << result << std::endl;
                            break;
                        case 1:
                            std::cout << "Error: division by zero" << std::endl;
                            break;
                        case 2:
                            std::cout << "Error: invalid operator" << std::endl;
                            break;
                        default:
                            std::cout << "Error: unknown code " << code << std::endl;
                            break;
                    }
                }
                break;
            }
        }
    }

    // 关闭连接
    client_sock->CloseSocket();
    std::cout << "Disconnected from server" << std::endl;
    return 0;
}

六、测试与验证

6.1 编译与运行

需链接 Jsoncpp 库(编译时加-ljsoncpp),Makefile 示例:

CC = g++
CFLAGS = -std=c++11 -Wall
LDFLAGS = -ljsoncpp -lpthread

TARGETS = tcp_server tcp_client

all: $(TARGETS)

tcp_server: TcpServerMain.cc
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

tcp_client: TcpClientMain.cc
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

clean:
	rm -f $(TARGETS)

6.2 测试步骤

  1. 启动服务器:./tcp_server 8888;
  2. 启动客户端:./tcp_client 127.0.0.1 8888;
  3. 客户端输入表达式(如 “10+20”“15/3”“7%2”),查看结果;
  4. 测试异常场景(如 “5/0”“10#3”),验证错误处理。

6.3 预期结果

        正常输入:10+20 → 输出 “Result: 30”;

        除零错误:5/0 → 输出 “Error: division by zero”;

        非法运算符:10#3 → 输出 “Error: invalid operator”。

七、总结与扩展

本文通过 “网络计算器” 案例,完整讲解了应用层协议的设计与实现,核心收获包括:

  1. 协议设计:用 “长度前缀法” 解决 TCP 粘包问题,用 JSON 实现结构化数据传输;
  2. 核心工具:封装 Socket 类简化网络操作,封装协议类统一编码 / 解码、序列化 / 反序列化;
  3. 业务落地:从请求构造到响应处理,形成完整的网络通信闭环。

扩展方向

  1. 协议优化:
    • 增加 “协议版本号” 字段,支持协议兼容升级;
    • 增加 “校验和” 字段,防止数据传输损坏。
  2. 性能优化:
    • 替换 JSON 为 Protobuf,减少数据体积和序列化耗时;
    • 采用 IO 多路复用(epoll/select)替代多线程,支持更高并发。
  3. 功能扩展:
    • 支持更多运算(如浮点数、复杂表达式);
    • 增加身份认证(如 Token),提升安全性。
如何利用CN3791实现太阳能电池板对锂电池的高效充电管理 本文详细介绍了如何利用CN3791芯片实现太阳能电池板对锂电池的高效充电管理。CN3791集成了MPPT(最大功率点跟踪)功能,能智能调节太阳能板的输出,确保在不同光照条件下获得最大功率。文章涵盖芯片工作原理、外围电路设计、充电全过程管理及实际应用经验,帮助开发者优化太阳能充电系统效率。 阅读详情

相关推荐

ABB AC500 系列 PLC 与上位机iFix 的通讯配置

在 软件中,有 3 种方式可以进入 IP config tool 的配置界面 双击 IP_settings,点击 IP config tool,即可进入 IP config tool 界面点击菜单栏的 Tool→IP config如果单独安装了 ABB IP Configration,则可以通过开始→程序→ABB→ABB IPConfigration 启动 IP 配置工具启动 IP config tool 后,即可进入 IP 配置界面。

xdpcxq的专栏 4029

自定义TCP通讯协议

大家都清楚最基础的TCP通讯机制是不能满足我们复杂业务需求的,遇到黏包,大量数据传输,丢包的时候怎么应对呢,针对我们自己的业务可以定义出一个通用的交互协议

Pawn_lixl的博客 1456

AXI4接口说明

ACLK:全局AXI接口时钟输入(建议与系统时钟保持一致)ARESET_n:全局AXI接口复位信号,低电平有效。PS:在HBM AXI接口应用中,此复位端口仅应在数据传输开始前使用。若在传输开始后使用,会导致AXI接口与内存控制器失步(out-of-sync)。当开关启用时,IP核会将所有ARESET_n引脚内部连接在一起——因为内存作为一个整体工作。

rhtul的博客 1522

TCP 协议设计入门:自定义消息格式与粘包解决方案

TCP 是流式传输(无消息边界概念),必须设计一套【消息结构】,让接收端能准确拆分出完整的消息。

moton2017的博客 1223

浙政钉(专有钉钉)门户免登(超详细)

钉钉家族 介绍浙政钉门户免登之前,首先搞清楚几个软件的关系,软件分别是:钉钉,专有钉钉,浙政钉 钉钉 是阿里巴巴集团专为中小企业打造的沟通和协同的多端平台。 专有钉钉 原名 政务钉钉,有更开放的设计能力、集成能力、更结构化协同产品、更定制化开发平台。支持专有化部署,数据落本地,更安全、客户可对该产品持牌运营、结构化编排,协助构建本地应用中心、提供移动应用全面安全保障体系。 浙政钉 为规范浙政钉整体架构体系,按照统分结合原则,由省政府办公厅统一设计整体工作界面和系统框架,统筹指导全省统建应用建设,各单

一缕南风的博客 2万+

【问题解决】解决 Python `PermissionError` 权限不足:文件/进程/端口全场景修复

本文总结了Python程序中常见的权限问题及解决方案,包括文件、进程和端口权限错误。文件权限问题通常由文件占用、权限不足或路径错误引起;进程问题多因权限不足或系统限制;端口问题则与端口占用、防火墙或特权端口相关。解决方案提供了详细的代码实现:文件权限管理器类(FilePermissionManager)包含检查权限(check_permission)、修复权限(fix_permission)、安全写入(safe_write)和安全删除(safe_delete)等方法,通过异常处理和临时文件等技术确保操作安全

博客 357

TCP自定义通信协议

Tcp 服务端自定义协议,客户端解析协议的处理 我们想想,为什么要定义协议: 回答:因为我们在传输的过程中会出现分包(没有接受到一个完整的包)与黏包(收到比一包多的数据,除去完整的一包,首尾有数据)的问题,所以我这里我是服务端自定义通信协议,客户端协议解析。由于我这里是要实现一个下位机给我发数据,我要将他的数据拿到并以图的形式显示在app界面。 接下来我们看下面,协议是参考这个的,图片是网上找...

wjj的博客 5677

【FISCO BCOS】二十、多机部署区块链

实际开发中,为了满足高可用性和高容错性的需求,往往会采用多级部署区块链的方式,今天介绍一下FISCO BCOS的多机部署流程。

naihe_fish的博客 3648

【网络原理】TCP协议 | TCP的报文格式 | 三次握手 | 四次挥手 | 可靠传输 | 确认应答 | 超时重传 | 滑动窗口 | 流量控制 | 拥塞控制 | 延时应答 | 捎带应答 | 粘包问题

上述重传中,没有额外的操作,哪个数据丢了,就重传哪个(快速重传),是滑动窗口下,超时重传的变种。让A先按照比较慢的速度发送数据(小的窗口),如果数据传输过程非常顺利,没有丢包,再一步步尝试使用更大的窗口,更高的速度进行发送。​ 假设当前的接收方缓冲区的大小是4000,拿到1000的数据后,通过ACK报文,把剩余的3000大小,返回给发送方。​ 此时,暂停发送数据后,A虽然不传输业务数据了,仍然会周期性的发送一个“窗口探测包”,并不携带业务数据,只是为了触发ACK,从而查询接收方的接收缓冲区剩余空间。

WengJM的博客 2552

Open3D——RANSAC拟合球面【2026最新版】

RANSAC三维点云球面拟合,博客长期更新,本文最新更新时间为:2026年3月5日。

点云侠的博客 4084

基于Costas环的载波同步实现及Matlab仿真

其中,第一个相位环接收带噪声的待同步载波,并产生一个相位误差信号,这个误差信号再通过第二个相位环进行反馈控制,使得两个相位环的相位差保持为90度,最终输出同步载波信号。接下来,我们可以编写Costas环的仿真程序,并将上面生成的信号输入到Costas环中进行同步,最终输出同步后的载波信号。综上所述,本文介绍了如何使用Matlab实现基于Costas环的载波同步,并给出相应的源代码。上述代码中,我们初始化了Costas环的各个参数,然后通过循环迭代实现了相位估计和相位误差控制,最终输出了同步后的载波信号。

2301_78484069的博客 1048

ros1通过tcp+json进行多ros主机通信

本文提供了一种在多个设备运行ros1系统,需要互相通信的一种解决方案。虽然可以多个设备共同运行一个ros主机,但对ros主机运行的设备压力通信压力较大,且不能对设备进行单独调试。

weixin_45706621的博客 545

目标跟踪与视频时序分析基础

两帧均有目标,却因快速移动导致检测框无重叠,仅靠交并比(IoU)无法维持身份关联。本文通过手算与纯Python实现最小跟踪流程,揭示IoU仅为位置线索,不能应对快速位移或遮挡。实验表明,即使目标真实存在,低重叠也会触发新编号。强调跟踪需结合运动预测、外观特征与阈值验证,避免误判。

Mr数据杨 2335

【Linux】深入理解网络编程:应用层自定义协议、序列化、TCP粘包问题与Socket封装

深入理解网络编程:应用层协议、序列化、TCP粘包问题与Socket封装

可涵的博客 4208

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

【Linux之旅】Linux 应用层自定义协议与序列化:从粘包问题到网络计算器

本文将围绕"网络版计算器"这个具体需求,从协议设计思路出发,引出序列化/反序列化的必要性,用 jsoncpp 实现数据格式转换,再深入 TCP 流式数据的粘包问题并给出 Encode/Decode 方案,最后封装一个完整的通用 Socket 框架。读完本文,你将掌握从零设计一个应用层协议并写成一个完整网络服务的全流程。

2302_80243065的博客 2050

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

应用层自定义协议与序列化

toStyledString、StreamWriter 和FastWriter 提供了不同的序列化选项, 你可以根据具体需求选择使用。Json::Reader 和parseFromStream 函数是Jsoncpp中主要的反序列化工具, 它们提供了强大的错误处理机制。在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输出的有效性。

jingfeng514的博客 4918

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

【Linux网络】彻底搞懂应用层自定义协议与序列化:从底层原理到工业级实战

很多 Linux 后端开发的新手,在学完 TCP Socket 基础 API 后,都能轻松写出一个 Echo 回显服务器,但一到真实业务场景就频频踩坑:想传输用户信息、计算请求等结构化数据,却不知道如何封装;客户端和服务端数据收发频繁出现解析错乱;明明 TCP 是可靠传输,却还是会出现 “粘包” 问题。这些问题的根源,在于只掌握了 Socket 的 API 调用,却没有理解应用层协议的核心价值,以及序列化与反序列化的底层逻辑。TCP 协议只负责字节流的可靠传输,却不关心字节流的业务含义,而应用层协议与序列化

会当凌绝顶,一览众山小 2645

【Linux网络】Linux 网络编程:应用层自定义协议与序列化(2)序列化与反序列化

摘要:本文从 TCP/UDP 通信本质、Linux 内核报文管理入手,系统讲解应用层自定义协议设计、序列化 / 反序列化原理与工程实践,并以网络计算器为例完整实现,彻底解决 TCP 粘包问题,适合后端开发、网络编程学习者系统掌握网络编程不只调用 socket API,业务数据需要结构化传输、跨平台 / 跨语言互通、可靠解析。传输层只负责数据搬运,不理解业务语义 ——必须在应用层约定协议、做序列化、处理报文边界。协议 = 通信双方约定的结构化数据structclass),包含字段含义、顺序、解析规则。序列化。

艾莉丝努力练剑的博客,感谢大家一直以来的对艾莉丝的支持和鼓励,uu们如果对C/C++方向感兴趣的话,可以通过艾莉丝主页的微信名片加艾莉丝的好友,和努力练剑的艾莉丝一起加油! 1万+

解决TCP粘包问题,使用C++实现TCP通信的自定义协议设计

这篇笔记记录了我用C++做TCP通信时,自定义协议的落地思路和实操细节。核心围绕协议层、业务层、网络层三层拆分,重点解决TCP粘包拆包、跨平台数据传输一致问题,如果未学习网络部分知识,可以看我之前的文章。

2301_79969685的博客 1025

Linux网络学习【应用层自定义协议与序列化实战:从手写序列化与反序列化到深入理解进程间关系与守护进程】

本文围绕 Linux 网络编程中的应用层自定义协议与序列化实战展开,从 TCP 字节流的消息边界问题入手,系统讲解自定义协议设计、报文格式、数据封装与解析,并通过手写序列化和反序列化,深入理解结构化数据与网络字节流之间的转换过程。随后进一步分析父子进程、孤儿进程、僵尸进程、进程组与会话等核心概念,介绍守护进程的创建原理、脱离终端及后台持续运行机制。通过原理分析与实践结合,帮助读者建立从网络通信、协议解析到进程管理与后台服务开发的完整知识体系,为后续学习高性能网络服务器及复杂网络应用开发打下扎实基础。

承于所学,探技术之渊;以代码求政,寻成长之道。 1万+

【Linux】应用层自定义协议与序列化

在应用层中,我们解释了序列化和反序列化的操作,接下来,我们需要将请求和响应进行封装。根据我们自定义协议来进行序列化。下面,我们就以一个有关计算器的请求和响应的数据结构,

加油,考上985的研究生 1805

应用层自定义协议以及序列化和反序列化

应用层自定义协议以及序列化和反序列化

Xpccccc的博客 1400

TCP协议的粘包问题&解决方式

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

qq_48811377的博客 1213

【Linux网络编程】应用层自定义协议与序列化

摘要:本文介绍了网络编程中结构化数据的传输方法,重点讲解了自定义协议与序列化的实现。通过构建网络计算器示例,详细说明了如何解决TCP粘包问题、使用Jsoncpp库进行序列化/反序列化,以及应用层协议的定制。文章包含服务端和客户端的完整代码实现,展示了从字节流处理到业务逻辑的完整流程,特别强调了协议报文格式设计(如"长度\r\n内容\r\n")和分层架构思想(网络通信层与业务处理层解耦)。

2401_82551617的博客 1087

Linux网络(七):从 Socket 封装到应用层协议:手写网络版计算器,彻底搞懂 TCP 通信、序列化与反序列化

本文我们将开始讲解自主设计一个“应用层协议”。并再这个基础上设计一个“网络计算器”。好。我们废话不多说,直接开始吧。

小此方的博客 1583
上一篇: TCP Socket 编程实战:从单连接到线程池,打造高可用网络服务
下一篇: HTTP 协议从入门到实战:理解核心原理与实现简易服务器
cpparf
博客等级 码龄2年 48粉丝 65原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值