在网络编程的世界里,Socket 为我们打通了进程间通信的通道,但原生的 Socket API 仅支持字节流 / 数据报的读写,当我们需要传输结构化数据 时,直接传输字符串会面临解析混乱、粘包等问题。这时候就需要我们在应用层定义专属协议,并通过序列化与反序列化实现结构化数据和字节流的双向转换。于此我们还有两个难题:
1:TCP 是无边界的字节流,接收端如何区分一个完整的业务报文?
2:内存中的结构体 / 对象,如何转换成网络可传输的字节流,且能被对端正确还原?
这两个问题,正是应用层自定义协议 与序列化 / 反序列化要解决的核心痛点。本文将基于 Socket 网络编程的底层原理,从零到一讲透自定义协议的设计逻辑、核心实现与落地实战,所有内容均基于 Linux C++ 技术栈展开。
1:Socket和TCP的字节流本质
在谈自定义协议之前,我们必须要先理解TCP的传输的核心特性
1:TCP全双工的底层原理
任何一台主机上的TCP连接,在内核中都同时拥有发送缓冲区和接收缓冲区。
- 当我们调用
send/write时,只是把数据从应用层拷贝到了内核的 TCP 发送缓冲区,什么时候发、发多少,由 TCP 协议本身控制; - 当我们调用
recv/read时,只是把数据从内核的 TCP 接收缓冲区拷贝到了应用层,而非直接从网络中读取。
正是因为收发的缓冲区相互独立,我们可以在同一个socket文件描述符上同时进行读写操作,这就是TCP支持全双工通信的核心原因。
2:字节流带来的粘/半包问题
TCP的核心定位是从面向连接,可靠的字节流传输协议,他只负责把字节流有序,无差错的从一段传输到另一端,但是它完全不关心上层业务的报文边界。
举个最典型的例子:客户端分两次调用send,第一次发送1+1,第二次发送2+2,服务端调用recv时,可能出现以下情况:
- 一次收到了
1+12+2(两个报文粘在一起,粘包); - 第一次收到
1+12,第二次收到+2(一个报文分多次到达,半包); - 其他任意组合的字节流拆分。
TCP 不会帮我们做业务报文的拆分与合并,这个工作必须由应用层来完成,这就是我们需要自定义应用层协议的核心原因。
2:什么是应用层协议
本质就是通信双方的约定。
定义:只要一端发送的结构,另一端能正确解析,这个约定就是应用层协议。
我们以网络计算器 为实战场景,看看两种最常见的协议约定方案,对比优劣就能明白为什么工业界都用结构化 + 序列化的方案。
1:纯字符串简单约定
约定客户端发送形如1+1的字符串,规则为:两个整型操作数 + 单个运算符(+-*/%),无空格、无其他字符。
- 优点:实现简单,入门易上手;
- 缺点:扩展性极差(新增字段要重写解析逻辑)、容错性低(多一个空格就解析失败)、全量字符串处理易出 bug。
这种方案只适合入门 demo,完全无法应对实际开发的复杂场景。
2:结构化数据+序列化,反序列化
这是工业界的标准方案,核心三步:
- 定义结构化的类 / 结构体,描述通信的业务数据(比如请求的操作数、运算符,响应的结果、状态码);
- 序列化:将内存中的结构化对象,转换成网络可传输的字符串 / 字节流;
- 反序列化:将网络接收到的字符串 / 字节流,还原成内存中的结构化对象。
这个方案的核心优势是结构化清晰、扩展性强、解析容错性高 ,新增业务字段只需在结构体中添加,对原有解析逻辑几乎无侵入。而我们要设计的应用层自定义协议,就是这套 "结构化定义 + 序列化规则 + 报文边界约定" 的完整规范。
3:序列化和反序列化:结构化数据的"网络适配器"
很多新手会问:为什么不直接把结构体强转成 char * 通过 Socket 发送?答案是存在三个致命问题:内存对齐(不同编译器 / 架构规则不同)、字节序(网络大端 / 主机小端)、指针无效(对端无法访问本地内存地址)。
序列化就是专门解决这些问题的技术,它能将结构化数据转换成跨平台、可传输、可解析 的格式。本文选用Jsoncpp(C++ 主流 JSON 解析库)实现序列化,JSON 是轻量级文本格式,可读性强、调试方便,非常适合入门学习。
1:先定义结构化的通信数据
cpp
// 客户端请求类:包含两个操作数和运算符
class Request
{
private:
int _data_x; // 操作数1
int _data_y; // 操作数2
char _oper; // 运算符 +-*/%
public:
// 序列化/反序列化方法、getter方法后续实现
};
// 服务端响应类:包含计算结果和状态码(0成功,非0异常)
class Response
{
private:
int _result; // 计算结果
int _code; // 状态码
public:
// 序列化/反序列化方法、getter/setter方法后续实现
};
2:Jsoncpp实现序列化和反序列化
序列化的核心是将对象字段映射为 JSON 键值对 ,反序列化则是将 JSON 键值对还原为对象字段 。Jsoncpp 提供了Json::Value(存储 JSON 数据)、Json::FastWriter(快速序列化)、Json::Reader(反序列化解析)三个核心工具。
1:序列化实现(以Request为例)
将 Request 对象转换成无格式的 JSON 字符串(FastWriter 比格式化的 StyledWriter 更节省传输体积):
cpp
bool Request::Serialize(std::string *out)
{
Json::Value root;
// 字段映射为JSON键值对
root["datax"] = _data_x;
root["datay"] = _data_y;
root["oper"] = _oper;
// 快速序列化生成JSON字符串
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
比如Request(1,1,'+')序列化后,会得到{"datax":1,"datay":1,"oper":43}(43 是 '+' 的 ASCII 码)。
2:反序列化实现(以Request为例)
将收到的JSON字符串,还原为Request对象的字段:
cpp
bool Request::Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
// 解析JSON字符串,失败则返回false
bool res = reader.parse(in, root);
if(res)
{
// 从JSON键值对还原字段
_data_x = root["datax"].asInt();
_data_y = root["datay"].asInt();
_oper = root["oper"].asInt();
}
return res;
}
Response 类的序列化 / 反序列化逻辑与 Request 一致,只需将字段替换为result和code即可。
4:自定义协议的核心:设计报文格式,解决粘包,半包问题
序列化解决了 "结构化数据怎么转成可传输格式" 的问题,而报文格式设计则是解决 "TCP 字节流怎么区分报文边界" 的关键,也是应用层协议设计的核心。
本文采用工业界最通用、最稳定 的报文格式方案:长度字段 + 分隔符 + 有效载荷,具体约定如下:
有效载荷长度\]\\r\\n\[有效载荷内容\]\\r\\n
- 有效载荷长度:序列化后的 JSON 字符串的字节数,告诉对端 "需要接收多少字节的业务数据";
- 分隔符
\r\n:分隔长度字段和有效载荷,方便调试和报文完整性校验; - 有效载荷内容:序列化后的 JSON 字符串(真正的业务数据)。
1:报文编解码:Encode(封装)和Decode(解析)
基于上述格式,实现两个核心函数,完成报文的封装和解析,这是处理粘包/半包的关键
1:Encode:将有效载荷封装成报文
cpp
// 定义分隔符
const std::string LineBreakSep = "\r\n";
// 入参:序列化后的JSON字符串(有效载荷);出参:完整协议报文
std::string Encode(const std::string &message)
{
// 1. 获取有效载荷长度并转字符串
std::string len = std::to_string(message.size());
// 2. 按协议格式拼接报文
std::string package = len + LineBreakSep + message + LineBreakSep;
return package;
}
示例:有效载荷是{"datax":1,"datay":1,"oper":43}(长度 33),封装后报文为33\r\n{"datax":1,"datay":1,"oper":43}\r\n。
2:Decode从TCP字节流中解析出完整的有效载荷
这是最核心的逻辑,需要处理半包和粘包,关键是应用层维护一个接收缓冲区,缓存未处理的字节流,直到解析出完整报文。
cpp
// 入参:应用层接收缓冲区(引用,需持续维护);出参:解析出的有效载荷
bool Decode(std::string &package, std::string *message)
{
// 1. 查找第一个分隔符,判断是否拿到长度字段
auto pos = package.find(LineBreakSep);
if (pos == std::string::npos)
return false; // 无分隔符,半包,等待后续数据
// 2. 提取长度字段并转换为整数
std::string lens = package.substr(0, pos);
int messagelen = std::stoi(lens);
// 3. 计算完整报文的总长度:长度字段+2个分隔符+有效载荷
int total = lens.size() + messagelen + 2 * LineBreakSep.size();
if (package.size() < total)
return false; // 缓冲区数据不足,半包,等待后续数据
// 4. 提取完整的有效载荷
*message = package.substr(pos + LineBreakSep.size(), messagelen);
// 5. 从缓冲区删除已处理的完整报文,保留剩余数据(处理粘包)
package.erase(0, total);
return true;
}
核心要点:应用层接收缓冲区必须是全局/持久化的,本次recv的半包数据要和下次recv的数据拼接,才能正确解析出完整报文。
5:socket封装与协议实现
有了协议设计和序列化实现,接下来结合 Socket 编程完成实战落地。首先对 Linux 原生 Socket API 进行面向对象封装,屏蔽底层细节,提供统一的创建、绑定、连接、收发接口,然后梳理服务端和客户端的核心业务流程。
1:socket的底层封装,TCPSoket类
封装 Linux 的 socket、bind、listen、accept、connect、recv、send 等 API,实现一个可复用的 TcpSocket 类,核心核心接口如下
cpp
class TcpSocket
{
private:
int _sockfd; // Socket文件描述符
public:
void CreateSocketOrDie(); // 创建socket
void BindSocketOrDie(uint16_t port); // 绑定端口
void ListenSocketOrDie(int backlog); // 监听
// 接受连接,返回新的TcpSocket对象,带出客户端IP和端口
TcpSocket* AcceptConnection(std::string *peerip, uint16_t *peerport);
// 连接服务端
bool ConnectServer(std::string &serverip, uint16_t serverport);
// 接收数据(拼接到应用层缓冲区)
bool Recv(std::string *buffer, int size);
// 发送数据
void Send(std::string &send_str);
void CloseSocket(); // 关闭socket
};
具体实现
cpp
#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>
#define Convert(addrptr) ((struct sockaddr *)addrptr)
namespace Net_Work
{
const static int defaultsockfd = -1;
const int backlog = 5;
enum
{
SocketError = 1,
BindError,
ListenError,
};
// 封装一个基类,Socket 接口类
// 设计模式:模版方法类
class Socket
{
public:
virtual ~Socket() {}
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie(int backlog) = 0;
virtual Socket* AcceptConnection(std::string* peerip,uint16_t* peerport) = 0;
virtual bool ConnectServer(std::string& serverip, uint16_tserverport) = 0;
virtual int GetSockFd() = 0;
virtual void SetSockFd(int sockfd) = 0;
virtual void CloseSocket() = 0;
virtual bool Recv(std::string* buffer, int size) = 0;
virtual void Send(std::string& send_str) = 0;
// TODO
public:
void BuildListenSocketMethod(uint16_t port, int backlog)
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie(backlog);
}
bool BuildConnectSocketMethod(std::string& serverip,uint16_t serverport)
{
CreateSocketOrDie();
return ConnectServer(serverip, serverport);
}
void BuildNormalSocketMethod(int sockfd)
{
SetSockFd(sockfd);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
{
}
~TcpSocket()
{
}
void CreateSocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
exit(SocketError);
}
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);
int n = ::bind(_sockfd, Convert(&local),
sizeof(local));
if (n < 0)
exit(BindError);
}
void ListenSocketOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
exit(ListenError);
}
Socket* AcceptConnection(std::string* peerip, uint16_t *peerport) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = ::accept(_sockfd, Convert(&peer),&len);
if (newsockfd < 0)
return nullptr;
*peerport = ntohs(peer.sin_port);
*peerip = inet_ntoa(peer.sin_addr);
Socket* s = new TcpSocket(newsockfd);
return s;
}
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());
server.sin_port = htons(serverport);
int n = ::connect(_sockfd, Convert(&server),
sizeof(server));
if (n == 0)
return true;
else
return false;
}
int GetSockFd() override
{
return _sockfd;
}
void SetSockFd(int sockfd) override
{
_sockfd = sockfd;
}
void CloseSocket() override
{
if (_sockfd > defaultsockfd)
::close(_sockfd);
}
bool Recv(std::string* buffer, int size) override
{
char inbuffer[size];
ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);
if (n > 0)
{
inbuffer[n] = 0;
*buffer += inbuffer; // 故意拼接的
return true;
}
else if (n == 0) return false;
else return false;
}
void Send(std::string& send_str) override
{
// 多路转接我们在统一说
send(_sockfd, send_str.c_str(), send_str.size(), 0);
}
private:
int _sockfd;
};
}
2:定制协议
cpp
#pragma once
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>
namespace Protocol
{
// 问题
// 1. 结构化数据的序列和反序列化
// 2. 还要解决用户区分报文边界 --- 数据包粘报问题
// 讲法
// 1. 自定义协议
// 2. 成熟方案序列和反序列化
// 总结:
// 我们今天定义了几组协议呢??我们可以同时存在多个协议吗???可以
// "protocol_code\r\nlen\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
const std::string ProtSep = " ";
const std::string LineBreakSep = "\r\n";
// "len\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
std::string Encode(const std::string& message)
{
std::string len = std::to_string(message.size());
std::string package = len + LineBreakSep + message +
LineBreakSep;
return package;
}
// "len\nx op y\n" : \n 不属于报文的一部分,约定
// 我无法保证 package 就是一个独立的完整的报文
// "l
// "len
// "len\r\n
// "len\r\nx
// "len\r\nx op
// "len\r\nx op y
// "len\r\nx op y\r\n"
// "len\r\nx op y\r\n""len
// "len\r\nx op y\r\n""len\n
// "len\r\nx op
// "len\r\nx op y\r\n""len\nx op y\r\n"
// "len\r\nresult code\r\n""len\nresult code\r\n"
bool Decode(std::string& package, std::string* message)
{
// 除了解包,我还想判断报文的完整性, 能否正确处理具有"边界"的报文
auto pos = package.find(LineBreakSep);
if (pos == std::string::npos)
return false;
std::string lens = package.substr(0, pos);
int messagelen = std::stoi(lens);
int total = lens.size() + messagelen + 2 *
LineBreakSep.size();
if (package.size() < total)
return false;
// 至少 package 内部一定有一个完整的报文了!
*message = package.substr(pos + LineBreakSep.size(), messagelen);
package.erase(0, total);
return true;
}
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)
{
}
void Debug()
{
std::cout << "_data_x: " << _data_x << std::endl;
std::cout << "_data_y: " << _data_y << std::endl;
std::cout << "_oper: " << _oper << std::endl;
}
void Inc()
{
_data_x++;
_data_y++;
}
// 结构化数据->字符串
bool Serialize(std::string* out)
{
Json::Value root;
root["datax"] = _data_x;
root["datay"] = _data_y;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string& in) // "x op y" [)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if (res)
{
_data_x = root["datax"].asInt();
_data_y = root["datay"].asInt();
_oper = root["oper"].asInt();
}
return res;
}
int GetX() { return _data_x; }
int GetY() { return _data_y; }
char GetOper() { return _oper; }
private:
// _data_x _oper _data_y
// 报文的自描述字段
// "len\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
// 很多工作都是在做字符串处理!
int _data_x; // 第一个参数
int _data_y; // 第二个参数
char _oper; // + - * / %
};
class Response
{
public:
Response() : _result(0), _code(0)
{
}
Response(int result, int code) : _result(result), _code(code)
{
}
bool Serialize(std::string* out)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string& in) // "_result _code" [)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if (res)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return res;
}
void SetResult(int res) { _result = res; }
void SetCode(int code) { _code = code; }
int GetResult() { return _result; }
int GetCode() { return _code; }
private:
// "len\r\n_result _code\r\n"
int _result; // 运算结果
int _code; // 运算状态
};
// 简单的工厂模式,建造类设计模式
class Factory
{
public:
std::shared_ptr<Request> BuildRequest()
{
std::shared_ptr<Request> req =
std::make_shared<Request>();
return req;
}
std::shared_ptr<Request> BuildRequest(int x, int y, char
op)
{
std::shared_ptr<Request> req =
std::make_shared<Request>(x, y, op);
return req;
}
std::shared_ptr<Response> BuildResponse()
{
std::shared_ptr<Response> resp =
std::make_shared<Response>();
return resp;
}
std::shared_ptr<Response> BuildResponse(int result, int
code)
{
std::shared_ptr<Response> req =
std::make_shared<Response>(result, code);
return req;
}
};
}