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

【C++】深入浅出“图”——最短路径算法

【C++】深入浅出“图”——最短路径算法

文章目录 * 一、Dijkstra算法 * 二、Bellman_Ford算法 * 三、Floyd_Warshall算法 一、Dijkstra算法 最短路径问题是指,从在带权的有向图中从某一顶点出发,找到通往另一顶点的最短路径,“最短”指的是沿路径各边的权值总和最小。 Dijkstra算法是单源最短路径的经典贪心算法,只能用于没有负权的图。它从起点出发,每次选当前距离最小且未确定最短路径的节点,用它去松弛(更新)所有邻接点的最短路径估计值,标记该节点为 “已确定”,重复此过程直到所有节点处理完毕,最终得到起点到图中所有节点的最短路径。 // src是选定的起点,dist记录起点到各点的最短路径,pPath记录到每个点的最短路径的前驱顶点下标voidDijkstra(const V& src, vector<W>& dist, vector<int>& pPath){ size_t srci =GetVertexIndex(

By Ne0inhk
备战蓝桥杯----C/C++组 (一)所需C++基础知识(上)

备战蓝桥杯----C/C++组 (一)所需C++基础知识(上)

个人主页: wengqidaifeng ✨永远在路上,永远向前走 个人专栏: 数据结构 C语言 嵌入式小白启动! 重要OJ算法题详解 文章目录 * 前言 * 一. 分析大纲,了解所需 * 1. 大纲显示内容 * 2、组别划分与难度关系 * 3、知识点结构分析(按组别) * 3.1 大学C组:基础入门阶段 * 3.2 大学B组:中级提高阶段 * 3.3 大学A组 / 研究生组:高级挑战阶段 * 4.难度系数说明 * 二. C++基础语法(上):从零开始的编程基石 * 1.前言 * 2.开发环境搭建 - DevC++的安装与使用 * 2.1

By Ne0inhk
2025年9月GESP真题及题解(C++八级): 选择题和判断题(题解)

2025年9月GESP真题及题解(C++八级): 选择题和判断题(题解)

2025年9月GESP真题及题解(C++八级): 选择题和判断题(题解) 第 1 题 小杨想点一杯奶茶外卖,但还差 5 元起送。于是,小杨决定点一些小料。可选的小料包括:珍珠 1 元、椰果 2 元、奶冻 3 元、奶盖 4 元。每种小料最多点 1 份。请问共有多少种满足起送条件的点小料方案?( )。 A. 16 B. 10 C. 9 D. 7 答案:C 题解:小料有4种,每种可选或不选,总共有 24=1624=16 种组合。总价分别为:

By Ne0inhk
【C++从零实现Json-Rpc框架】第六弹——服务端模块划分

【C++从零实现Json-Rpc框架】第六弹——服务端模块划分

一、前言         在第五弹中我们对项目的功能进行了讲解,接下来我们就服务端的角度,来设计对应模块,帮助服务端实现项目的三个功能: ●  rpc调用 ● 服务的注册与发现以及服务的下线/上线通知 ● 消息的发布订阅 二、正文 1. 服务端的功能需求 在进行服务端具体的模块划分之前,我们先来明确下服务端的功能需求 ● 基于网络通信接受客户端的请求,提供rpc服务 ● 基于网络通信接受客户端的请求,提供服务注册与发现,上线&下线通知 ●  基于网络通信接受客户端的请求,提供主题操作(创建/删除/订阅/取消),消息发布 2. 服务端的模块划分  基于上述的功能,我们对服务端可以划分出这么几个模块 ● Network:网络通信模块 ● Protocol: 应用层通信协议模块 ● Dispatcher:消息分发处理模块 ● RpcRouter: 远端调用路由功能模块 ● Publish-Subcriber: 发布订阅模块 ● Registry-Discovery:服务注册/发现/上线/下线功能模块 ● Server:

By Ne0inhk