news 2026/10/9 12:12:11

自定义协议实现网络计算器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自定义协议实现网络计算器

目录

1.直接传递struct --- 有限场景

2.自定义协议 --- 自己做序列化和反序列化

3.引入成熟的序列和反序列化协议


定义结构体来表示我们需要交互的信息,发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体,这个过程叫做"序列化" 和 "反序列化"。所以只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析,就是ok的。 这种约定,就是应用层协议。

  • 序列化:将协议对应的结构化数据,转换成为“字符串”字节流,方便网络发送。
  • 反序列化:将“字符串”字节流,转换成为结构化数据,为了方便上层业务随时提取有效字段

为什么我们不直接传 struct 的方式,而在应用层以序列化方式传递字节流,因为应用层协议变化会非常快,在不同平台下,比如信息存储空间变化,字段变多,或者语言java c++不同,就有可能读错,而序列化后数据可以更好的解决平台性的差异。Linux 内核使用的是结构体,因为它是不经常改变的,约定好的。本文我们将通过自定义序列和反序列化协议和成熟的序列和反序列化协议模拟实现网络计算器,即客户端服务端包含同样的协议。

1.直接传递struct --- 有限场景

TCP可以可以使用的一些发送接收端口

#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd,const void* buf,size_t len, int flags); ssize_t recv(int sockfd,void *buf, size_t len,int flags);

send前几个参数与write相同,flags表示我们发送的方式,一般用不到,设置为0。成功返回接收到了数据。当客户端正常关闭时,服务端的 recv 会返回 0(表示对端关闭了连接)。返回值==-1含义:接收失败,发生了错误。处理:需要检查全局变量 errno,根据错误码决定后续动作:errno =EINTR:被信号中断,通常应该重试(continue)。errno =EAGAIN或EWOULDBLOCK:非阻塞模式下,当前没有数据可读,不是致命错误,可以稍后再试或继续等待。errno =ECONNRESET:对端强制重置了连接(比如客户端按了Ctrl+C且处于异常状态),应该退出循环并清理资源。其他错误:属于致命错误,打印strerror(errno)并退出。

recv与read相似,也是多一个flags。返回值> 0:(大于 0)含义:成功发送了数据。返回值== 0:含义:在 send 中,返回 0 通常极少见(除非len本身就是 0)。返回值== -1:含义:发送失败,发生错误。处理:检查 errno:errno ==EINTR:被信号中断,重试。errno ==EAGAIN/EWOULDBLOCK:非阻塞模式下,发送缓冲区已满,稍后重试。errno ==EPIPE:连接已断开(对端已关闭)。此时进程会收到SIGPIPE信号,默认行为是直接杀死进程!解决办法:在代码开头设置signal(SIGPIPE, SIG_IGN)忽略该信号,或者在send时加上MSG_NOSIGNAL标志。errno ==ECONNRESET:连接被重置,退出。

在单机、同编译器、同平台的简单演示中可能没问题,但在实际的网络编程中,这是一种极不推荐的做法。核心原因可以总结为:网络通信两端是两个独立的进程,可能运行在不同机器、不同操作系统、不同编译器上。直接传结构体内存违背了“跨平台数据交换”的基本原则。简要总结如下:

  • 内存对齐:编译器会插入填充字节,不同平台/编译器下 sizeof(结构体)可能不同,导致读取错位。
  • 字节序(大小端):x86 是小端,网络是大端。不发 htonl/ntohl 转换,数字全乱。
  • 类型大小不固定:long在 Linux 64 位是 8 字节,Windows 64 位是 4 字节,跨平台直接崩。
  • TCP 粘包/半包:TCP 无消息边界,recv 一次不一定读到一个完整结构体,会拆包或拼包。
  • 指针与 C++ 对象:结构体里含指针、std::string 、虚函数表,接收方直接段错误。

2.自定义协议 --- 自己做序列化和反序列化

TCP 一旦建立连接,client/server 他们的身份是对等的,可以互发消息,都有发送缓冲区和接收缓冲区,发送数据时 write/send 并没有将数据发送到网络中,只是拷贝到内核发送缓冲区,本质是拷贝函数,发送缓冲区中的数据,发多少,出错了怎么办是由内核决定的,具体比如TCP协议决定。TCP然后通过网络将数据拷贝到对方的接收缓冲区,所以实际上是双方操作系统之间进行通信。而 read/recv 就是将接收缓冲区的内容拷贝到用户空间,也只是拷贝函数,没有数据就会被阻塞。

像是一个生产者消费者模型,client 写数据,然后TCP才能发送,而 server 读数据时需要缓冲区中有数据。而 server 也可以写数据让 client 来读,这就是为什么TCP协议叫做全双工协议。在发送用户空间:你认为你发了多少字节,但是不一定我要收多少字节,是面向字节流的,所以需要上层解析这些字节。而UDP要求报文必须是完整的,它是面向数据报的。

对于字节流,我们要明确报文与报文之间的边界,然后才能序列和反序列化。UDP不需要处理就是因为报文和报文之间的边界是明确的。

我们约定"len\nx op y\n"为client向server发送的信息,有效载荷为x op y,长度为len,"len\result code\n"为 server 向 client 发送的计算结果,我们对有效载荷 message 进行打包和解包返回,方式如下,package为收到的信息。

const std::string ProtSep = " "; const std::string LineBreakSep = "\n"; // 这里code只为加头和尾,序列化是调整有效载荷 std::string Encode(std::string &message) { std::string len = std::to_string(message.size()); std::string package = len + LineBreakSep + message + LineBreakSep; return package; } // 解析时无法保证是完整报文 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; *message = package.substr(pos + LineBreakSep.size(), total - LineBreakSep.size()); package.erase(0, total); return true; }

client 发送请求序列化:

bool Serialize(std::string *out) { *out = std::to_string(_data_x) + ProtSep + _oper + ProtSep + std::to_string(_data_y); return true; }

client 收到信息反序列化:

bool DeSerialize(std::string &in) //"x op y" { auto left = in.find(ProtSep); if (left == std::string::npos) return false; auto right = in.rfind(ProtSep); if (right == std::string::npos) return false; std::string x = in.substr(0, left); std::string op = in.substr(left + ProtSep.size(), right - (left + ProtSep.size())); std::string y = in.substr(right + ProtSep.size()); // std::stoi 会跳过前导空白,然后解析数字,遇到第一个非数字字符就停止,后面的 \n 直接被忽略。 _data_x = std::stoi(x); _data_y = std::stoi(y); if (op.size() != 1) return false; _oper = op.at(0); return true; }

server 发送响应序列化:

bool Serialize(std::string *out)//"_result _code" { *out = std::to_string(_result) + ProtSep + std::to_string(_code); return true; }

server 接收请求反序列化:

bool DeSerialize(std::string &in) //_result _code { auto pos = in.find(ProtSep); if (pos == std::string::npos) return false; _result = std::stoi(in.substr(0, pos)); _code = std::stoi(in.substr(pos + ProtSep.size())); return true; }

3.引入成熟的序列和反序列化协议

json 序列和反序列化最常见的方式之一,是{key,value}格式。

JSON 中值 value 只能是这几种:

类型示例
字符串"hello"
数字123、3.14、-10、1.5e3
布尔值true、false
nullnull
对象{ "name": "Alice" }
数组[1, 2, 3]

JSON 中{ ... }表示对象,所以这段内容叫做“一个 JSON 对象”。也可以多组json串

{key0: [{ “key1","value1"}, { “key2","value2}, { “key3","value3}}];

JSON全称JavaScript Object Notation,中文常叫“JavaScript 对象表示法”。虽然名字里有 JavaScript,但它现在已经是一种语言无关的文本数据交换格式,几乎所有编程语言都支持。我们要使用需要下载相应的SDK

查看库所在目录

Json::Value 万能类型,可以收受任意类型,Json::FastWriter 和 Json::StyledWriter 都是 jsoncpp 里用来把 Json::Value 序列化成字符串的类。区别就一个:输出格式不同。

类输出风格适合场景
Json::FastWriter紧凑,没有多余空格和换行网络传输、存储,省空间
Json::SytledWriter美化,带缩进和换行给人看、调试、配置文件

序列化与反序列化

bool Serialize(std::string *out) { #ifdef SelfDefine #else Json::Value root; root["datax"] = _data_x; root["datay"] = _data_y; root["oper"] = _oper;//字符本质是整数,可以传入 Json::FastWriter writer; *out = writer.write(root); return true; #endif } bool DeSerialize(std::string &in) //"x op y" { #ifdef SelfDefine #else 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();//没有char类型 return true; } return false; #endif } int GetX() { return _data_x; } int GetY() { return _data_y; } char GetOper() { return _oper; } private: //_data_x _oper _data_y // "len\nx op y\n" ; \n 不属于报文的一部分 约定 // len表长度 自描述字段 保证完整 int _data_x = 0; // 第一个参数 int _data_y = 0; // 第二个参数 char _oper = 0; //+ - * / % }; class Response { public: Response() {}; Response(int result, int code) : _result(result), _code(code) {}; bool Serialize(std::string *out) { #ifdef SelfDefine #else Json::Value root; root["result"] = _result; root["code"] = _code; Json::FastWriter writer; *out = writer.write(root); return true; #endif } bool DeSerialize(std::string &in) //_result _code { #ifdef SelfDefine #else Json::Value root; Json::Reader reader;//反序列化 bool res = reader.parse(in,root); if(res) { _result = root["result"].asInt(); _code = root["code"].asInt(); return true; } return false; #endif }

总结:我们定义了几组协议呢?可以同时存在多个协议吗?当然可以我们可以定义“protocol_code\nlen\nx op y\n",就可以通过protocol_code选择协议簇,先选择使用的哪种协议,然后再读取有效载荷内容。

OSI 7层网络结构中,会话层管理通信,负责建立与断开连接,就是connect 和 accept;表示层扶着数据格式的转换,即序列化反序列化,添加报头解包;应用层就是完成想要的功能,如计算器完成计算。为什么TCP/IP中被压缩成立一层,因为这三层要完成的功能方式可能会一直改变,不能写入内核,所以只能压缩到应用层,这三层其实自己写代码时还是都要具体完成的。

本篇结束!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 12:10:54

Simple Live:3 步把 4 个平台的直播聚合进一个播放器

Simple Live&#xff1a;3 步把 4 个平台的直播聚合进一个播放器 【免费下载链接】dart_simple_live 简简单单的看直播 项目地址: https://gitcode.com/GitHub_Trending/da/dart_simple_live 晚上想看 B 站的游戏主播&#xff0c;平时追的斗鱼主播也刚好开播&#xff0c…

作者头像 李华
网站建设 2026/10/9 12:10:53

体系3.枚举、查找算法讲义

目录 第一部分 知识与方法1.1 枚举算法1.2 查找算法1.3 二分答案第二部分 例题精讲第三部分 分类拓展第四部分 总结梳理 壹 第一部分 知识与方法 1.1 枚举算法&#xff08;穷举法 Enumeration&#xff09; ① 基本思想 枚举就是把问题所有可能的解&#xff0c;按照某…

作者头像 李华
网站建设 2026/10/9 12:10:34

机器学习预测钢管混凝土柱承载力:高精度代理模型实战

简介&#xff1a;本资源是一套面向土木工程与人工智能交叉领域研究者的机器学习建模实践项目&#xff0c;聚焦于内配型钢钢管混凝土柱承载力的高精度预测问题&#xff0c;适用于结构工程方向的研究生、科研人员及具备Python基础的算法实践者。压缩包共5个文件&#xff0c;含4个…

作者头像 李华
网站建设 2026/10/9 12:09:12

VS Code 国际化插件 i18n Ally 配置到 TaoToken 的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华