手写一个C++ TCP服务器实现自定义协议(顺便解决粘包问题)

手写一个C++ TCP服务器实现自定义协议(顺便解决粘包问题)

在之前的博客中,我们了解了关于UDP和TCP的网络编程,直观的感受了一下网络套接字是如何使用的,并且成功的完成了客户端与服务端的网络通信,但是其中还有一个小细节我们可能会忽略,就是UDP是基于数据报进行传输的,一下子就将所有我们要发送的信息传送给对方,但是我们的TCP可是基于字节流进行传输的,我们如何保证读取上来的数据,是一个完整的报文呢?

我们在进行TCP网络通信的时候,通过调用connec函数调用,使客户端可以和服务端保持链接之后,客户端将自己想要发送的数据通过write系统调用写进对应的socket函数调用给我们返回的文件描述符所对应的文件中。

现在有一个问题就是我们向文件中写入的时候,直接将其放入即可,但是想要往出拿的时候就有点困难了,想要往出拿的人如果不知道放的人是如何放的,就会造成一系列的错误,这就好比放数据时先放了一个整形,又放了一个浮点数,还放了一个字符串,然而拿的人按照字符串,整形,浮点数这样的方式进行获取,这就会导致数据不一致的现象,所以一旦我们要发送一些带有结构化的数据时,就必须再次制定——协议,这样才能满足我们想要返送一些结构化数据的需求。

TCP是传输控制协议,它主要负责的内容有:

  • 什么时候发送数据
  • 一次发送多少数据
  • 发送过程中出错了该怎么办

我们平时使用的read和write这些系统调用接口是从用户空间拷贝到内核空间,也就是我们在应用层将需要发送的数据拷贝到TCP的发送缓冲区中,这就结束了,至于到底什么时候发送这些数据给对方,全权由TCP进行控制,这样进行数据的传输。

但是正因如此,这就会倒是在接收端就会在接收的时候十分的困难,比如发送方发送了一个长报文,但是接收方只接收一点点数据就给用户返回了,这就会倒是接收方接收的数据时,会有不确定的情况。

这就好比假如现在和女朋友吵架了,这个时候你通过微信进行道歉,原本你想说的是我不后悔我爱你,结果由于接收方接收数据的问题,变成了我不后悔,这就会导致你女朋友意味你不后悔和她吵架,结果你就无辜的恢复了单身汉。

所以同理为了避免TCP在接收数据时的差异,我们就必须做好应用层的协议,这样才能保证发送的数据全部接收到并返回给用户,保证了接收方可以完整的接收到全部的数据。

那么应用层协议应该如何定制呢?我们通过一个列子进行理解。

现在假如我们在一个聊天群里进行聊天,大家彼此之间畅谈甚欢,现在假如我发送了一句哈哈哈,大家在群里看到的肯定不止只有哈哈哈这三个字,还会有我们的昵称,我们的头像,我什么时候发送的等等信息,所以看似我只是发送了一个哈哈哈三个字,但其实还有很多的附加数据同时进行了发送。

而我们的数据是通过字节流的方式进行传输的,所以我们必须将这些数据(昵称-头像-时间-信息)都转换为一个字符串,然后一起传送给对方,当对方接收到这个数据时,再将这一个字符串信息的内容进行分别拆解,最后显示到我们的显示器上。

所以这种方式就是序列化和反序列化。

所以现在我们实现一个简单的网络版本的计算器进行理解序列化和反序列化

TCP服务器整体结构

Client
   │
   │  request
   ▼
TcpServer
   │
   │  callback
   ▼
CalculatorServer
   │
   │  result
   ▼
response


主要模块:

模块作用
Socket封装 socket API
TcpServerTCP服务器框架
Protocol协议封装
CalculatorServer计算逻辑

自定义应用层协议设计

由于 TCP 是 字节流协议,一次 read() 可能:

  • 读到半个数据
  • 读到多个数据

就比如:

客户端发送两个请求

10 + 20
5 * 6

服务器可能读到:

10 + 20\n5 * 6\n

或者:

10 + 

正是因为如此,所以我们要自定义协议,接下来就是我们网络版本的计算器的协议设计:

协议格式

协议的格式设计如下:

len\n
content\n

举个例子就是:

6
10 + 5

编码表示就是:

6\n10 + 5\n

总之就是如下的格式:

|len| \n |content| \n

协议封装实现

Encode(封装报文)

std::string Encode(std::string &content) { std::string s; size_t len = content.size(); s += std::to_string(len); s += "\n"; s += content; s += "\n"; return s; }

例如:

10 + 20

就会变为:

7\n10 + 20\n

Decode(解析报文)

bool Decode(std::string &s, std::string *content) { size_t left_pos = s.find("\n"); if (left_pos == std::string::npos) return false; std::string content_len = s.substr(0, left_pos); int len = std::stoi(content_len); if (s.size() < content_len.size() + len + 2) return false; *content = s.substr(left_pos + 1, len); s.erase(0, content_len.size() + len + 2); return true; }

Decode 做了三件事:

  1. 判断是否有完整头部(长度)
  2. 判断数据是否完整
  3. 解析 + 从缓冲区移除

请求与响应设计

请求 request

客户端发送:

10 + 5

结构:

class request { public: int x_; int y_; char op_; };

序列化:

"10 + 5"

反序列化:

string -> request

响应 response

服务器返回:

"15 0"

结构:

class response { public: int result_; int code_; };

code的含义:

code含义
0成功
1除0
2取模0
3非法操作

核心难点:解决 TCP 粘包问题

❓ 什么是粘包?

TCP 是面向字节流的协议:

👉 发送:

请求1 + 请求2 + 请求3

👉 接收:

可能变成: 请求1请求2 | 请求3

解码函数 Decode(重点)

bool Decode(std::string &s, std::string *content) { size_t pos = s.find("\n"); if (pos == std::string::npos) return false; int len = std::stoi(s.substr(0, pos)); if (s.size() < pos + 1 + len + 1) return false; *content = s.substr(pos + 1, len); s.erase(0, pos + 1 + len + 1); return true; }

服务器的处理方法Calculator(重点)

 std::string Calculator(std::string& s) { std::string content; if (Decode(s, &content) == false) { return ""; } request req; bool r = req.Deserialization(content); if (!r) { return ""; } response res = CalculatorHandler(req); std::string ret = res.serialization(); ret = Encode(ret); return ret; }

服务器核心代码:(重点)

while (true) { int sockfd = listenfd_.Accept(&client_port, &client_ip); if (fork() == 0) { while (1) { char buffer[1280]; ssize_t s = read(sockfd, buffer, sizeof buffer - 1); if (s > 0) { inbuffer_stream += buffer; while (true) { std::string info = callback_(inbuffer_stream); if (info.empty()) break; write(sockfd, info.c_str(), info.size()); } } } } }

可以看到我们的服务器在收到一个报文之后,首先会调用服务器的处理方法Calculator,在处理方法中会进行解码,如果收到的报文不能够分解为类似(6\n10 + 5\n)这样的格式,我们就会返回一个空字符串,而一旦返回的是一个空字符串,我们的服务器就知道这个报文不完整,就会继续接收新的报文,直到接收到一个完整的报文且可以通过Decode解码成功之后,我们的程序才会继续进行,这样就保证了我们每次服务器处理的肯定是一个正确格式的报文。

完整代码

自定义协议

#include <string> #define blank_sep " " #define protocol_sep "\n" class request { public: request(int x, int y, char op) : x_(x), y_(y), op_(op) { } request() { } ~request() { } std::string serialization() { std::string str; str += std::to_string(x_); str += blank_sep; str += op_; str += blank_sep; str += std::to_string(y_); return str; } bool Deserialization(std::string &in) { size_t leftpos = in.find(blank_sep); if (leftpos == std::string::npos) { return false; } std::string str_x = in.substr(0, leftpos); x_ = std::stoi(str_x); op_ = in[leftpos + 1]; size_t rightpos = in.rfind(blank_sep); if (rightpos == std::string::npos) { return false; } std::string str_y = in.substr(rightpos + 1); y_ = std::stoi(str_y); return true; } void DebugPrint() { std::cout << "新请求构建完成: " << x_ << op_ << y_ << "=?" << std::endl; } public: int x_; int y_; char op_; }; class response { public: response() { } response(int result, int code) : result_(result), code_(code) { } ~response() { } std::string serialization() { std::string str; str += std::to_string(result_); str += blank_sep; str += std::to_string(code_); return str; } bool Deserialization(std::string &in) { size_t pos = in.find(blank_sep); if (pos == std::string::npos) { return false; } std::string str_result = in.substr(0, pos); result_ = std::stoi(str_result); std::string str_code = in.substr(pos + 1); code_ = std::stoi(str_code); return true; } void DebugPrint() { std::cout << "结果响应完成, result: " << result_ << ", code: " << code_ << std::endl; } public: int result_; int code_; }; std::string Encode(std::string &content) { std::string s; size_t len = content.size(); s += std::to_string(len); s += protocol_sep; s += content; s += protocol_sep; return s; } bool Decode(std::string &s, std::string *content) { size_t left_pos = s.find(protocol_sep); if (left_pos == std::string::npos) { return false; } std::string content_len = s.substr(0, left_pos); int len = std::stoi(content_len); if (s.size() < content_len.size() + len + 2) { return false; } *content = s.substr(left_pos + 1, len); s.erase(0, content_len.size() + len + 2); return true; }

服务端

#include <iostream> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string> enum error { SocketErr = 2, BindErr, ListenErr, ConnectErr, }; class Socket { public: Socket() { sockfd_ = socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ < 0) { std::cout << "socket fail" << std::endl; exit(SocketErr); } } void Bind(uint16_t &port, std::string &ip) { struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &server.sin_addr); if (bind(sockfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { std::cout << "server bind fail!" << std::endl; exit(BindErr); } std::cout << "server bind successful" << std::endl; } void Listen() { if (listen(sockfd_, 10) < 0) { std::cout << "server listen fail!" << std::endl; exit(ListenErr); } std::cout << "server listen successful" << std::endl; } int Accept(uint16_t *client_port, std::string *client_ip) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(sockfd_, (struct sockaddr *)&client, &len); if (sockfd < 0) { std::cout << "accept fail!" << std::endl; return -1; } std::cout << "accept successful" << std::endl; *client_port = ntohs(client.sin_port); char ip[64]; inet_ntop(AF_INET, &client.sin_addr, ip, sizeof ip); *client_ip = ip; return sockfd; } void Connect(uint16_t &server_port, std::string &server_ip) { struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(server_port); inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); if (connect(sockfd_, (struct sockaddr *)&server, sizeof server) < 0) { std::cout << "connect fail!" << std::endl; exit(ConnectErr); } std::cout << "connect successful!" << std::endl; } void Close() { close(sockfd_); } int fd() { return sockfd_; } ~Socket() { close(sockfd_); } private: int sockfd_; }; class CalculatorServer { public: CalculatorServer() { } response CalculatorHandler(const request &req) { response res(0, 0); switch (req.op_) { case '+': res.result_ = req.x_ + req.y_; break; case '-': res.result_ = req.x_ - req.y_; break; case '*': res.result_ = req.x_ * req.y_; break; case '/': if (req.y_ == 0) { res.code_ = 1; break; } res.result_ = req.x_ / req.y_; break; case '%': if (req.y_ == 0) { res.code_ = 2; break; } res.result_ = req.x_ % req.y_; break; default: res.code_ = 3; break; } return res; } std::string Calculator(std::string& s) { std::string content; if (Decode(s, &content) == false) { return ""; } request req; bool r = req.Deserialization(content); if (!r) { return ""; } response res = CalculatorHandler(req); std::string ret = res.serialization(); ret = Encode(ret); return ret; } ~CalculatorServer() { } }; using func_t = std::function<std::string(std::string &)>; class TcpServer { public: TcpServer(uint16_t port, std::string ip, func_t callback) : port_(port), ip_(ip), callback_(callback) { } void InitServer() { listenfd_.Bind(port_, ip_); listenfd_.Listen(); std::cout << "init server successful!" << std::endl; } void start() { signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); while (true) { uint16_t client_port; std::string client_ip; int sockfd = listenfd_.Accept(&client_port, &client_ip); if (sockfd < 0) { continue; } if (fork() == 0) { // 子进程 listenfd_.Close(); std::string inbuffer_stream; while (1) { char buffer[1280]; ssize_t s = read(sockfd, buffer, sizeof buffer - 1); if (s > 0) { buffer[s] = 0; // std::cout << buffer << std::endl; inbuffer_stream += buffer; while (true) { std::string info = callback_(inbuffer_stream); std::cout << info << std::endl; if (info.empty()) { break; } write(sockfd, info.c_str(), info.size()); } } else if (s == 0) { break; } else { break; } } close(sockfd); exit(0); } close(sockfd); } } ~TcpServer() { } private: Socket listenfd_; uint16_t port_; std::string ip_; func_t callback_; }; int main(int argc,char* argv[]) { if(argc != 3) { exit(0); } uint16_t server_port = std::atoi(argv[2]); std::string server_ip = argv[1]; CalculatorServer cal; TcpServer* ser = new TcpServer(server_port,server_ip,std::bind(&CalculatorServer::Calculator, &cal, std::placeholders::_1)); ser->InitServer(); ser->start(); return 0; } 

客户端

#include <iostream> #include <cassert> #include <unistd.h> #include "Protocol.hpp" #include "Socket.hpp" static void Usage(const std::string &proc) { std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl; } // ./clientcal ip port int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); Socket sockfd; sockfd.Connect(serverport, serverip); srand(time(nullptr) ^ getpid()); int cnt = 1; const std::string opers = "+-*/%=-=&^"; std::string inbuffer_stream; while (cnt <= 10) { std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl; int x = rand() % 100 + 1; usleep(1234); int y = rand() % 100; usleep(4321); char oper = opers[rand() % opers.size()]; request req(x, y, oper); req.DebugPrint(); std::string package; package = req.serialization(); package = Encode(package); std::cout << package << std::endl; write(sockfd.fd(), package.c_str(), package.size()); char buffer[128]; ssize_t n = read(sockfd.fd(), buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = 0; inbuffer_stream += buffer; // "len"\n"result code"\n std::cout << inbuffer_stream << std::endl; std::string content; bool r = Decode(inbuffer_stream, &content); // "result code" assert(r); response resp; r = resp.Deserialization(content); assert(r); resp.DebugPrint(); } std::cout << "=================================================" << std::endl; sleep(1); cnt++; } sockfd.Close(); return 0; }

到这里,我们已经完整实现了一个基于 C++ 的 TCP 计算器服务器,到现在,我们应该建立这样一个认知:

TCP编程的本质不是收发数据,而是如何正确解析数据的边界

总而言之,核心思想就一句话,就是TCP没有消息边界,所以我们必须设计应用层协议。

Read more

RK3588+AI算力卡替代英伟达jetson方案,大算力,支持FPGA自定义扩展

RK3588+AI算力卡替代英伟达jetson方案,大算力,支持FPGA自定义扩展

RK3588+AI算力卡替代英伟达Jetson方案的技术对比与实施路径 1. ‌硬件性能与算力配置‌ * ‌RK3588核心优势‌:采用8nm工艺,集成6TOPS NPU,支持INT4/INT8混合精度计算,搭配PCIe 3.0接口可扩展Hailo-8等AI加速卡,实现32TOPS总算力‌12。 ‌Jetson Thor对比‌:英伟达新一代平台提供2070 FP4 TFLOPS算力(约5168 TOPS),是RK3588+扩展方案的160倍,但功耗高达130W,远超RK3588的5W典型功耗‌34。 2. ‌边缘AI场景适配性‌ * ‌实时性需求‌:RK3588在1080P视频结构化分析中延迟低于50ms,满足工业质检、安防监控等场景;Jetson Thor虽支持毫秒级多模态推理,但成本过高(量产模组2999美元)‌24。 ‌能效比‌:RK3588方案能效达1.2 TOPS/W,优于Jetson Orin的4.5 TOPS/W,适合电池供电的移动机器人‌14。

By Ne0inhk
AstrBot+NapCat 一键部署 5 分钟搞定智能 QQ 机器人!cpolar解决公网访问 :cpolar 内网穿透实验室第 777 个成功挑战

AstrBot+NapCat 一键部署 5 分钟搞定智能 QQ 机器人!cpolar解决公网访问 :cpolar 内网穿透实验室第 777 个成功挑战

这篇教程会带你用最简单的方式:**只用一份 docker-compose,一次命令,5 分钟以内完成 AstrBot + NapCat 部署,把 DeepSeekAI 接入你的 QQ。**AstrBot 本身就是为 AI 而生的现代化机器人框架,插件丰富、支持 DeepSeek/OpenAI 等大模型、带 WebUI、可扩展性强,真正做到"搭好就能用"。照着做,你马上就能拥有属于自己的 QQ AI 机器人。 1 项目介绍 1.1 AstrBot是什么? GitHub 仓库:https://github.com/AstrBotDevs/AstrBot AstrBot 是一个专为 AI 大模型设计的开源聊天机器人框架,

By Ne0inhk

KaiwuDB+CodeArts 智能体,让ai快速构建一个智能家居本地化数据处理系统

针对智能家居云端数据处理模式的网络依赖、低延迟性差、隐私泄露三大痛点,基于 KaiwuDB(KWDB)多模时序数据库 + 华为 CodeArts 代码智能体的本地化数据处理解决方案。从环境搭建、KWDB 自动化部署,到系统全模块开发、接口测试实现全流程落地,打造零云端依赖、低延迟、高隐私的智能家居本地化数据处理系统,方案基于开源技术栈与自动化开发工具,降低技术门槛,适配新手开发者与实际家庭场景需求。         随着智能家居设备渗透率持续提升,家庭中温湿度传感器、智能灯、空调、门锁等设备呈规模化增长,设备运行产生的时序数据(温湿度、能耗、设备状态)与关系型数据(设备信息、规则配置)呈爆发式增长,对数据的存储、处理与利用提出更高要求。 本文选择KaiwuDB作为本地化数据存储与计算核心,华为 CodeArts 代码智能体作为自动化研发引擎,二者结合实现智能家居本地化数据处理系统的高效构建,核心优势如下: 1.1 KaiwuDB:适配 AIoT 场景的多模数据库基座 KaiwuDB(开源版本简称

By Ne0inhk

实测|龙虾机器人(OpenClaw)Windows系统部署全攻略(含避坑指南)

作为一名热衷于折腾新技术的ZEEKLOG博主,最近被一款名为「龙虾机器人」的开源AI工具圈粉了!它还有个更正式的名字——OpenClaw(曾用名Clawdbot、MoltBot),不同于普通的对话式AI,这款工具能真正落地执行任务,比如操作系统命令、管理文件、对接聊天软件、自动化办公,而且支持本地部署,数据隐私性拉满。 不过调研发现,很多小伙伴反馈龙虾机器人在Windows系统上部署容易踩坑,官方文档对Windows的适配细节描述不够细致。今天就结合自己的实测经历,从环境准备、分步部署、初始化配置,到常见问题排查,写一篇保姆级攻略,不管是新手还是有一定技术基础的同学,都能跟着一步步完成部署,少走弯路~ 先简单科普下:龙虾机器人本质是一款开源AI代理框架,核心优势是“能行动、可本地、高灵活”——它不内置大模型,需要对接第三方AI接口(如GPT、Claude、阿里云百炼等),但能将AI的指令转化为实际的系统操作,相当于给AI配了一个“能动手的身体”,这也是它和普通对话大模型的核心区别。另外要注意,它还有一种“生物混合龙虾机器人”的概念,是利用龙虾壳改造的柔性机器人,本文重点分享的是可本

By Ne0inhk