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

在网络编程的世界里,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. 一次收到了1+12+2(两个报文粘在一起,粘包);
  2. 第一次收到1+12,第二次收到+2(一个报文分多次到达,半包);
  3. 其他任意组合的字节流拆分。

TCP 不会帮我们做业务报文的拆分与合并,这个工作必须由应用层来完成,这就是我们需要自定义应用层协议的核心原因。

2:什么是应用层协议

本质就是通信双方的约定。

定义:只要一端发送的结构,另一端能正确解析,这个约定就是应用层协议。

我们以网络计算器 为实战场景,看看两种最常见的协议约定方案,对比优劣就能明白为什么工业界都用结构化 + 序列化的方案。

1:纯字符串简单约定

约定客户端发送形如1+1的字符串,规则为:两个整型操作数 + 单个运算符(+-*/%),无空格、无其他字符。

  • 优点:实现简单,入门易上手;
  • 缺点:扩展性极差(新增字段要重写解析逻辑)、容错性低(多一个空格就解析失败)、全量字符串处理易出 bug。

这种方案只适合入门 demo,完全无法应对实际开发的复杂场景。

2:结构化数据+序列化,反序列化

这是工业界的标准方案,核心三步:

  1. 定义结构化的类 / 结构体,描述通信的业务数据(比如请求的操作数、运算符,响应的结果、状态码);
  2. 序列化:将内存中的结构化对象,转换成网络可传输的字符串 / 字节流;
  3. 反序列化:将网络接收到的字符串 / 字节流,还原成内存中的结构化对象。

这个方案的核心优势是结构化清晰、扩展性强、解析容错性高 ,新增业务字段只需在结构体中添加,对原有解析逻辑几乎无侵入。而我们要设计的应用层自定义协议,就是这套 "结构化定义 + 序列化规则 + 报文边界约定" 的完整规范。

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 一致,只需将字段替换为resultcode即可。

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;
		}
	};
}
相关推荐
handler011 小时前
算法:Trie树(字典树)
c语言·数据结构·c++·笔记·算法·深度优先
坚定学代码2 小时前
qt c++ 局域网聊天小工具
c++·qt·个人开发
Lost_in_the_woods2 小时前
Java程序员的Linux之路——命令篇
linux·运维·服务器
IpdataCloud2 小时前
在线IP查询API与本地离线库,速度与安全如何选型?
运维·服务器·网络
志栋智能2 小时前
超自动化巡检,如何成为业务稳定的“压舱石”?
大数据·运维·网络·人工智能·自动化
晓纪同学2 小时前
EffctiveC++_01第一章
java·开发语言·c++
困惑阿三2 小时前
全栈服务器运维终极备忘录
运维·服务器·nginx·pm2
SVIP111592 小时前
Vue3 WebSocket 封装通关指南:心跳 + 重连 + 全局状态管理,复制即用!
网络·websocket·网络协议
2401_846341652 小时前
C++动态链接库开发
开发语言·c++·算法