手写一个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

Flutter 组件 jaspr_serverpod 适配鸿蒙 HarmonyOS 实战:前后端同构,构建全栈式组件渲染与高性能后端集成架构

Flutter 组件 jaspr_serverpod 适配鸿蒙 HarmonyOS 实战:前后端同构,构建全栈式组件渲染与高性能后端集成架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 jaspr_serverpod 适配鸿蒙 HarmonyOS 实战:前后端同构,构建全栈式组件渲染与高性能后端集成架构 前言 在鸿蒙(OpenHarmony)生态迈向全栈式开发、涉及跨端组件复用及高性能服务端逻辑集成的背景下,如何实现前端 UI 组件与后端业务逻辑的“无缝类型对齐”,已成为提升全栈研发效率与系统稳定性的关键议题。在鸿蒙设备这类强调分布式架构与端云协同的环境下,如果前端应用(Jaspr)与后端引擎(Serverpod)依然依赖碎片的 REST 协议驱动,由于由于接口契约的离散性,极易由于由于“前后端模型失致”导致线上环境的数据解析崩溃或并发冲突。 我们需要一种能够支持全栈 Dart 编写、具备自动代码生成且支持服务器端渲染(SSR)的同构映射方案。 jaspr_serverpod 为 Flutter/Dart 开发者引入了“全栈闭环”开发模式。

By Ne0inhk

Stable Diffusion数据集标签编辑器完整使用指南

Stable Diffusion数据集标签编辑器完整使用指南 【免费下载链接】stable-diffusion-webui-dataset-tag-editorExtension to edit dataset captions for SD web UI by AUTOMATIC1111 项目地址: https://gitcode.com/gh_mirrors/st/stable-diffusion-webui-dataset-tag-editor 你是否正在为这些AI训练数据问题而困扰? 当你在准备Stable Diffusion模型训练数据时,是否经常遇到这样的困境: * 成百上千张图片的标签需要批量修改,手动操作耗时耗力? * 数据集标签格式不统一,影响模型训练效果? * 需要快速筛选特定标签的图片,却缺乏高效工具? 这款强大的数据集标签编辑器正是为你量身打造的解决方案!它不仅简化了标签管理流程,更提供了专业级的批量处理能力。 核心功能解析:从单张编辑到批量处理 🔍 智能标签筛选系统 通过智能筛选功能,你可以: * 使用正向/负向过滤精准定位

By Ne0inhk
Flutter for OpenHarmony:Flutter 三方库 country — 为鸿蒙应用提供完善的国家/地区数据支持(适配鸿蒙 HarmonyOS Next ohos)

Flutter for OpenHarmony:Flutter 三方库 country — 为鸿蒙应用提供完善的国家/地区数据支持(适配鸿蒙 HarmonyOS Next ohos)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net。 Flutter for OpenHarmony:Flutter 三方库 country — 为鸿蒙应用提供完善的国家/地区数据支持(适配鸿蒙 HarmonyOS Next ohos) 前言 随着鸿蒙(OpenHarmony)系统在全球范围内的快速扩张,构建具备国际化能力的应用已成为开发者的必修课。无论是在注册流程中选择手机号冠字码(+86, +1, 等)、在个人中心修改地区,还是在物流应用中选择邮寄目的地,都需要一套权威、完整且易于查询的国家/地区数据库。 在 Flutter for OpenHarmony 开发中,我们不需要手动维护庞大的 JSON 字典。country 库提供了一套极其严谨的、遵循 ISO 标准的国家数据模型。今天,我们将探索如何让鸿蒙应用秒变“地理通”。 一、为什么集成 country

By Ne0inhk

Flutter 三方库 at_server_status 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、实时的 @protocol 去中心化身份服务器状态感知与鉴权监控引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 at_server_status 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、实时的 @protocol 去中心化身份服务器状态感知与鉴权监控引擎 在鸿蒙(OpenHarmony)系统的隐私保护应用、去中心化身份管理工具(基于 @protocol 协议)或需要实时监控全球分布式节点健康状况的场景中,如何判定一个 @sign(电子签名标识)背后的 Root 服务器或 Secondary 服务器是否在线、配置是否由于由于由于由于已就绪?at_server_status 为开发者提供了一套工业级的、基于协议栈的状态审计与自检方案。本文将深入实战其在鸿蒙 Web3 身份安全底座中的应用。 前言 什么是 atServer Status?它是 @protocol(一种旨在让用户完全掌控数据的去中心化协议)官方生态的核心组件。

By Ne0inhk