
我们写的⼀个个解决我们实际问题,满⾜我们⽇常需求的⽹络程序,都是在 应⽤层 。
协议是⼀种 "约定" ,是双⽅约定好的 结构化的数据 ,在读写数据时,都是按 "字符串" 的⽅式来发送接收的。如果我们要传输⼀些 "结构化的数据" 怎么办呢?
1.序列化和反序列化
简单来说,序列化就是把消息由多变一,反序列化就是把消息一变多,方便上层处理。多,就是结构化数据,其实就是class或struct;一,就是一个大的字符串。

这种有客户端(client)和服务端(server)的模式就叫cs模式。
为什么不直接发结构体数据过去?
服务器和客户端的实现语言可能不同,有可能是python,Java等,如果直接发结构体过去,各个语言对结构体的解析不一样,直接传容易出错。
有了序列化,把结构转成字符串,如果结构里的数据发生了变更,序列化后统一都是字符串,不影响网络的发送。网络通信的过程可以不关心发送的具体是什么,只要知道是字节流就行,应用层反序列化的时候转化成合法的结构体就可以。
所以,如果我们要进行网络协议式的通信,在应用层,强烈建议使用序列化和反序列化的方案。直接传结构体的话除非特殊场景需要,否则并不建议。
2.重谈write和read
在TCP层,TCP协议会存在一个发送缓冲区和接收缓冲区,对于对端主机也是一样,有自己的发送缓冲区和接收缓冲区。
以TCP为例,我们之前收发消息用到的write和read,write的时候并不是把数据直接写到网络里,我们只是把数据写到了TCP的发送缓冲区里,所以我们之前用的write不是发送接口,而是拷贝接口,也就是write接口将数据拷贝到了发送缓冲区里。数据什么时候发、发多少、出错了怎么办由TCP协议自主决定,所以TCP叫做传输控制协议,我们只是把数据拷贝到缓冲区里,也就是操作系统内核里,由OS内核决定什么时候发。

OS把数据发送到网络里,本质也就是拷贝到网络里。
数据会被对端的OS收到,也就是网卡先收到,收到之后把数据拷贝到接收缓冲区里,所以我们用read接收数据的时候,也不是从网络里读,其实是检测接收缓冲区有没有数据,如果接收缓冲区里没数据,read就会阻塞,有数据就会将数据拷贝到用户。
所以主机间通信的本质就是:把发送方的发送缓冲区的数据拷贝到对端的接收缓冲区。
TCP通信的时候有两对发送缓冲区和接收缓冲区。我们用一个文件描述符sockfd,读的时候就从接收缓冲区读,写的时候往发送缓冲区写,如果把缓冲区写满了write会阻塞。对一个主机来说,接受和发送是分离的,这也说明TCP通信的时候是全双工的。
内核往缓冲区写/读,用户从缓冲区读/写,这就是内核和用户之间的生产者消费者模型(绿框),有四组生产者消费者模型。

所以TCP通信就可以看作4组内核和用户的生产者消费者模型。
我们将结构化数据序列化之后的字符串,写的时候将字符串整个写入到发送缓冲区里,但是在TCP中,对方读的时候,不一定会读到整个串。
TCP面向字节流,数据什么时候发、发多少、出错了怎么办由TCP协议自主决定,相当于write写完之后就直接返回了,就好比我们寄快递的时候,把东西交给快递员我们就不用管了,快递什么时候发、怎么发等问题由快递公司决定。
我们的操作系统不关心write拷贝下来的字符串是什么,OS因为各种原因,会根据实际情况,有可能把write拷贝下来的报文只给对方发一部分,对方从接收缓冲区读的时候也就可能拿不到完整报文。这就叫TCP数据报的粘报问题。

所以在TCP中,要读到完整的报文,由接收方应用层自己确定。当只发送的报文不完整,就要等,等OS把剩下的发过来再反序列化。
在UDP中,不会存在上述问题,因为UDP面向数据报,一定是完整的报文,而TCP面向字节流。
所以我们需要定制协议,协议中要有结构化字段,并且提供序列化和反序列化的方案;要解决因为字节流问题,导致的读取报文不完整问题(只处理读取)。
3.网络版本计算器
我们在写自定义协议的时候,基于一个网络版本的计算器的实现来理解协议。
3.1 socket封装
模板方法模式
首先我们分装一下socket,我们封装这个socket的时候要基于一种设计模式,叫做模板方法模式。
模板方法模式在一个抽象类中定义了一个算法的骨架(即模板方法),将算法中某些步骤的具体实现延迟到其子类中。抽象类负责固定算法的执行流程和公共步骤,子类无需改变算法整体结构,仅需按需实现可变步骤,以此实现代码复用和流程标准化。
大概样子如下,具体参数后面边写边修改,纯虚函数后面再具体实现。
cpp
// Socket.hpp文件 封装套接字
// 设计模式:模版方法模式
#pragma once
#include <iostream>
#include "Common.hpp"
namespace MySocket
{
class Socket
{
public:
virtual void CreateSocket() = 0; // 创建套接字
virtual void BindSocket(uint16_t port) = 0; // bind套接字信息
virtual void ListenSocket(int backlog) = 0; // 设置listen状态
virtual Socket *Accept() = 0; // 建立连接
public:
// 公共方法:建立listen状态时的固定步骤就是如下
void BuildListenSocketMethod(uint16_t port, int backlog)
{
CreateSocket(); // 步骤一:创建套接字
BindSocket(port); // 步骤二:bind套接字信息
ListenSocket(backlog); // 步骤三:设置listen状态
}
};
// 具体子类,继承抽象Socket类,实现纯虚函数
class TcpSocket : public Socket
{
void CreateSocket() override // 实现纯虚函数:创建套接字
{
}
void BindSocket(uint16_t port) // 实现纯虚函数:bind套接字信息
{
}
void ListenSocket(int backlog) // 实现纯虚函数:设置listen状态
{
}
Socket *Accept() // 实现纯虚函数:建立连接
{
}
// ...
};
}
如果说要创建UDP,直接加一个公共方法,子类UDP里自己去实现,如下。
cpp
// Socket.hpp文件 封装套接字
// 设计模式:模版方法模式
#pragma once
#include <iostream>
#include "Common.hpp"
namespace MySocket
{
class Socket
{
public:
virtual void CreateSocket() = 0; // 创建套接字
virtual void BindSocket(uint16_t port) = 0; // bind套接字信息
virtual void ListenSocket(int backlog) = 0; // 设置listen状态
virtual void Accept() = 0; // 建立连接
public:
// 公共方法:建立listen状态时的固定步骤就是如下
void BuildListenSocketMethod(uint16_t port, int backlog)
{
CreateSocket(); // 步骤一:创建套接字
BindSocket(port); // 步骤二:bind套接字信息
ListenSocket(backlog); // 步骤三:设置listen状态
}
// 公共方法:建立UDP套接字时的固定步骤就是如下
void BuildUDPSocketMethod(uint16_t port, int backlog)
{
CreateSocket(); // 步骤一:创建套接字
BindSocket(port); // 步骤二:bind套接字信息
}
};
// 具体子类,继承抽象Socket类,实现纯虚函数
class TcpSocket : public Socket
{
void CreateSocket() override // 实现纯虚函数:创建套接字
{
}
void BindSocket(uint16_t port) // 实现纯虚函数:bind套接字信息
{
}
void ListenSocket(int backlog) // 实现纯虚函数:设置listen状态
{
}
void Accept() // 实现纯虚函数:建立连接
{
}
// ...
};
// 具体子类,UDP的
class UdpSocket : public Socket
{
void CreateSocket() override // 实现纯虚函数:创建套接字
{
}
void BindSocket(uint16_t port) // 实现纯虚函数:bind套接字信息
{
}
// ...
};
}
上面就是模板设计模式的例子,这里用不到UDP,后面就不实现了,前面只是举个例子。
大体框架
下面的"Common.hpp" "InetAddr.hpp" "MyLog.hpp"都是之前封装过的。
cpp
// Socket.hpp文件 封装套接字
// 设计模式:模版方法模式
#pragma once
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "MyLog.hpp"
#define default_backlog 8
using namespace MyLog;
namespace MySocket
{
class Socket
{
public:
virtual void CreateSocket() = 0; // 创建套接字
virtual void BindSocket(uint16_t port) = 0; // bind套接字信息
virtual void ListenSocket(int backlog) = 0; // 设置listen状态
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0; // 建立连接
virtual void Close() = 0; // 关闭文件描述符
virtual int Recv(std::string *out) = 0; // 接收
virtual int Send(const std::string &message) = 0; // 发送
public:
// 公共方法:建立listen状态时的固定步骤就是如下
void BuildListenSocketMethod(uint16_t port, int backlog = default_backlog)
{
CreateSocket(); // 步骤一:创建套接字
BindSocket(port); // 步骤二:bind套接字信息
ListenSocket(backlog); // 步骤三:设置listen状态
}
};
// 具体子类,继承抽象Socket类,实现纯虚函数
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(-1)
{}
void CreateSocket() override // 实现纯虚函数:创建套接字
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(ExitCode::SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
void BindSocket(uint16_t port) // 实现纯虚函数:bind套接字信息
{
InetAddr addr(port);
int n = ::bind(_sockfd, addr.NetAddrPtr(), addr.NetAddrLen());
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(ExitCode::BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
void ListenSocket(int backlog) // 实现纯虚函数:设置listen状态
{
int listen_socket = ::listen(_sockfd, backlog);
if(listen_socket < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(ExitCode::LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
void Close() override
{
if (_sockfd >= 0)
::close(_sockfd);
}
int Recv(std::string *out) override
{
// 从文件描述符里读,流式读取,不关心读到的是什么
char buffer[1024];
// ssize_t n = ::read(_sockfd, buffer, sizeof(buffer) - 1);
ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0; // 当作字符串
*out += buffer; // 是 +=,直接拼接到上层的缓冲区里
}
return n;
}
int Send(const std::string &message) override
{
// 写
return ::send(_sockfd, message.c_str(), message.size(), 0);
}
virtual std::shared_ptr<Socket> Accept(InetAddr *client) // 实现纯虚函数:建立连接
{
//...
}
private:
int _sockfd;
};
}
cpp
// TcpServer.hpp文件
#include <iostream>
#include <memory>
#include "Socket.hpp"
using namespace MySocket;
class TcpServer
{
public:
TcpServer(uint16_t port)
: _port(port),
_isrunning(false),
_listensocketptr(std::make_unique<TcpSocket>())
{
_listensocketptr->BuildListenSocketMethod(port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
_listensocketptr->Accept(); //
std::cout << "服务器启动..." << std::endl;
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
bool _isrunning;
std::unique_ptr<Socket> _listensocketptr;
};
cpp
//TcpServer.cc文件
#include <iostream>
#include "TcpServer.hpp"
#include "MyLog.hpp"
using namespace MyLog;
// ./server port
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "Usage: " << argv[0] << " port" << std::endl;;
exit(ExitCode::USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> ts = std::make_unique<TcpServer>(port);
ts->Start();
return 0;
}
3.2 完善其他模块
在accept的时候,我们会获得client的套接字信息,所以需要一个输出型参数。
并且accept的时候会返回一个新的文件描述符,我们要用这个新的文件描述符构造出一个新的TcpSocket,所以返回值设置为一个基类指针。
cpp
// Socket.hpp文件 TcpSocket类中实现 如下
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_sockfd, CONV(peer), &len);
if (fd < 0)
{
LOG(LogLevel::WARNING) << "accept warning...";
return nullptr;
}
client->SetAddr(peer);
return std::make_shared<TcpSocket>(fd);
}
在外部就可以像下面这样调用。
cpp
// TcpServer.hpp文件
class TcpServer
{
public:
// ...
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
// 传入client,获取client的相关信息,返回一个套接字对象
auto sock = _listensocketptr->Accept(&client);
if(nullptr == sock) //智能指针可以和空指针比较
{
continue;
}
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
bool _isrunning;
std::unique_ptr<Socket> _listensocketptr;
};
accept获取到链接之后,这里选择就用多进程来操作了。这个部分的代码逻辑详细解释在【Linux网络】Socket编程TCP-实现Echo Server(下)的2.2节。
cpp
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
// 传入client,获取client的相关信息,返回一个套接字对象
auto sock = _listensocketptr->Accept(&client);
if (nullptr == sock) // 智能指针可以和空指针比较
{
continue;
}
pid_t id = fork();
if (id > 0) // 父进程
{
sock->Close(); // 关闭不要的文件描述符,就是刚accept得到的
pid_t rid = ::waitpid(id, nullptr, 0); // 等待子进程退出
(void)rid;
}
else if (id == 0) // 子进程
{
_listensocketptr->Close(); // 关闭listen文件描述符
if (fork() > 0)
exit(ExitCode::normal); // 子进程创建出孙子进程,子进程自己正常退出
// 到这里是孙子进程在执行,孙子进程此时是孤儿进程
}
else // <0创建失败
{
LOG(LogLevel::FATAL) << "fork error";
exit(ExitCode::FORK_ERR);
}
}
_isrunning = false;
}
孙子进程要执行的任务由上层传递,我们对代码做层状设计。
cpp
// TcpServer.hpp文件
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>
#include "Socket.hpp"
#include "InetAddr.hpp"
using namespace MySocket;
// 回调函数参数要传套接字和客户端地址
using ioservic_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
TcpServer(uint16_t port, ioservic_t ioservic) //构造时传要调用的方法
: _port(port),
_isrunning(false),
_listensocketptr(std::make_unique<TcpSocket>()),
_ioservic(ioservic)
{
_listensocketptr->BuildListenSocketMethod(port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
// 传入client,获取client的相关信息,返回一个套接字对象
auto sock = _listensocketptr->Accept(&client);
if (nullptr == sock) // 智能指针可以和空指针比较
{
continue;
}
pid_t id = fork();
if (id > 0) // 父进程
{
sock->Close(); // 关闭不要的文件描述符,就是刚accept得到的
pid_t rid = ::waitpid(id, nullptr, 0); // 等待子进程退出
(void)rid;
}
else if (id == 0) // 子进程
{
_listensocketptr->Close(); // 关闭listen文件描述符
if (fork() > 0)
exit(ExitCode::normal); // 子进程创建出孙子进程,子进程自己正常退出
// 到这里是孙子进程在执行,孙子进程此时是孤儿进程
_ioservic(sock, client); // 回调,上层处理任务
}
else // <0创建失败
{
LOG(LogLevel::FATAL) << "fork error";
exit(ExitCode::FORK_ERR);
}
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
bool _isrunning;
std::unique_ptr<Socket> _listensocketptr;
ioservic_t _ioservic;
};
3.2 自定义协议(重点)
整体框架思路
我们把协议定制在Protocol.hpp文件内。
我们要实现的是一个计算器,对于client端,计算数和计算操作符要有,对于server端,计算完成后的结果要有。但是只有结果不满足需求,还要区分返回的是结果还是异常。
对于请求和响应来说可以提供无参构造,有参构造,还要有序列化和反序列化的接口。
我们还需要一个协议的类,里面包含请求和响应的方法。
cpp
// Protocol.hpp 自定义网络版本计算器
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"
using namespace MySocket;
// Request是client向server发起请求时的协议/报文
class Request // client -> server
{
public:
Request() {}
Request(int x, int y, int oper) : _x(x), _y(y), _oper(oper) {}
std::string Serialization()
{
}
bool Deserialization(std::string &in)
{
}
~Request() {}
private:
int _x; // 运算数
int _y; // 运算数
char _oper; // 运算符
};
// Response是server端处理完之后给client端的协议/报文
class Response // server -> client
{
public:
Response() {}
Response(int result, int code) : _result(result), _code(code) {}
std::string Serialization()
{
}
bool Deserialization(std::string &in)
{
}
~Response() {}
private:
int _result; // 运算结果
int _code; // success:0 其他不同运算异常情况:1、2、3、4...
};
class Protocol
{
public:
Protocol() {}
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
}
~Protocol() {}
private:
Request _req;
Response _rsp;
};
这个code为0的时候,result里的结果才是我们要关心的,不为0的时候,就是运算出错了,不用管result里面是多少。
Protocol里面就要提供获取请求的get方法,这个Get方法的参数和回调函数ioservic_t 的要一致。
在外部我们调用时,就可以像下面这样调用。
cpp
//TcpServer.cc文件
#include <iostream>
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "MyLog.hpp"
using namespace MyLog;
// ./server port
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "Usage: " << argv[0] << " port" << std::endl;;
exit(ExitCode::USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(); //获取协议
//将协议模块和TcpServer模块联系起来
std::unique_ptr<TcpServer> ts = std::make_unique<TcpServer>(port,
[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){
protocol->GetRequest(sock, client);
});
ts->Start();
return 0;
}
序列化和反序列化
序列化和反序列化的时候我们可以用Json,c++中用到的库是jsoncpp。用法如下。
序列化
序列化常用的接口如下。
cpp
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <sstream>
#include <memory>
int main()
{
Json::Value root;
root["name"] = "张三";
root["sex"] = "男";
root["age"] = 20;
/*用法1*/
// Json::StyledWriter sw;
// std::string s = sw.write(root);
/*用法2*/
Json::FastWriter fw;
std::string s = fw.write(root);
/*用法3*/
// Json::StreamWriterBuilder swb;
// std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
// std::stringstream ss;
// sw->write(root, &ss);
// std::string s = ss.str();
std::cout << s << std::endl;
return 0;
}
用法2是用逗号分割。

用法1和3打印的结果有\n分割,整体还是一个字符串。

反序列化
反序列化用到的是Json::Reader类里的parse接口
cpp
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <sstream>
#include <memory>
int main()
{
Json::Value root;
root["name"] = "张三";
root["sex"] = "男";
root["age"] = 20;
// 序列化成一个字符串s
Json::FastWriter fw;
std::string s = fw.write(root);
std::cout << s << std::endl;
// 对s反序列化
Json::Value t;
Json::Reader reader;
bool ret = reader.parse(s, t); //将s字符串反序列化,放到t里
// t是一个Json::Value对象,直接用key获取value
std::string name = t["name"].asString(); //不是直接t["name"]
int age = t["age"].asInt();
std::string sex = t["sex"].asString();
std::cout << name << std::endl;
std::cout << age << std::endl;
std::cout << sex << std::endl;
}

我们Protocol.hpp文件里的序列化和反序列化接口就可以像下面这样实现。
保证报文完整性
前面我们说过,读取方用read在读数据的时候,可能读到一个完整的json请求,也可能是半个、一个半、三个四个等等,都有可能,read自身并不能保证读取到的报文的完整性,只能说有数据就读上来。
那么保证读取到的报文是一个完整的这个事情就需要应用层程序员自己来做。
我们在定制协议的时候,规定发过来的报文里不能只有内容,还要有效内容的长度,就是整个json串的长度,就相当于加了一个报头,大概就是像下面这样。
cpp
50{"x":10, "y":20, "oper":'+'} //假设长度为50
然后我们还要在长度和内容之间加上\r\n,在json串的结尾,也要加上\r\n(增加可读性,意义不大)。这个长度是不包括\r\n的,就是{"x":10, "y":20, "oper":'+'}的长度。
cpp
50\r\n{"x":10, "y":20, "oper":'+'}\r\n //假设长度为50
所以这个请求打印出来的时候应该是下面这个样子。
cpp
50
{"x":10, "y":20, "oper":'+'}
这才是一个完整的报文。
将来再读取的时候,第一个字段一定是一个数字len,然后一直读,直到读到\r\n,此时前面的就是报文的长度,如果读不到\r\n,就下一次再读;如果读到了,我就知道再往后读len个长度就能拿到报文内容了。
所以在协议里我们还需要实现增加报头、去掉报头的操作。
增加报头
cpp
const std::string sep = "\r\n";
std::string Encode(const std::string &json_str) // 添加报头
{
// 1.获取得到的字符串长度,并转成string
std::string len = std::to_string(json_str.size());
// 2.添加报头,构建报文
std::string package = len + sep + json_str + sep; //应用层封装
return package;
}
增加报头之后就长这个样子: 50\r\n{"x":10, "y":20, "oper":'+'}\r\n ,函数返回这个报文就行。
解包
但是我们一定会得到一个完整的报文吗?前面说过,不一定,下面的情况都有可能。
cpp
5
50
50\r
50\r\n
50\r\n{"x":
50\r\n{"x":10, "
50\r\n{"x":10, "y":20,
50\r\n{"x":10, "y":20, "oper":'+
50\r\n{"x":10, "y":20, "oper":'+'}\r
50\r\n{"x":10, "y":20, "oper":'+'}\r\n 50\r\n{"x":10, "y":20, "oper":'+'}\r\n
...
所以我们解包的函数必须包括以下功能:
- 判断报文完整性
- 如果包含至少一个完整请求,要提取出来,并从接受缓冲区里移除,方便处理下一个
这里函数参数的buffer就是已经把数据从缓冲区里读到了用户层,buffer里就是读到的字符串
cpp
bool Decode(std::string &buffer, std::string *message) // 解包
{
auto pos = buffer.find(sep); // 先找\r\n
if (pos == std::string::npos) // 没找到,这个报文不可能是完整报文
return false; // 直接返回false,让调用方继续从内核中读取数据
std::string messagelen_str = buffer.substr(0, pos); //提取出报头,报文的长度
int messagelen_int = std::stoi(messagelen_str); //长度从string转成int
}
到这里的时候,我们就要确定buffer里是否至少有一个完整的请求,确定方法就是计算长度,进行对比,直接看代码。
cpp
bool Decode(std::string &buffer, std::string *message) // 解包
{
// 这里的buffer就是已经把数据从缓冲区里读到了用户层,buffer里就是读到的字符串
auto pos = buffer.find(sep); // 先找\r\n
if (pos == std::string::npos) // 没找到,这个报文不可能是完整报文
return false; // 直接返回false,让调用方继续从内核中读取数据
std::string messagelen_str = buffer.substr(0, pos); //提取出报头,报文的长度
int messagelen_int = std::stoi(messagelen_str); //长度从string转成int
int total = messagelen_str.size() + messagelen_int + 2 * sep.size();
if (buffer.size() < total) //一定有一个完整的报文的话,长度必须 >=total
return false; // 如果小,就没有一个完整报文
//到这里,一定至少有一个完整的报文
}

total是一个完整报文的长度,buffer里的长度要大于等于total才能证明buffer里至少有一个完整报文。
然后我们就可以把完整报文提取出来,并且还要把提取过的从buffer里删除。
cpp
bool Decode(std::string &buffer, std::string *message) // 解包
{
// 这里的buffer就是已经把数据从缓冲区里读到了用户层,buffer里就是读到的字符串
auto pos = buffer.find(sep); // 先找\r\n
if (pos == std::string::npos) // 没找到,这个报文不可能是完整报文
return false; // 直接返回false,让调用方继续从内核中读取数据
std::string messagelen_str = buffer.substr(0, pos); //提取出报头,报文的长度
int messagelen_int = std::stoi(messagelen_str); //长度从string转成int
int total = messagelen_str.size() + messagelen_int + 2 * sep.size();
if (buffer.size() < total) //一定有一个完整的报文的话,长度必须 >=total
return false; // 如果小,就没有一个完整报文
//到这里,一定至少有一个完整的报文
*message = buffer.substr(pos + sep.size(), messagelen_int); //提取完整报文
buffer.erase(0, total); //从缓冲区删除提取过的
return true;
}

删除的时候不能只把json串删除,这整个都要删。
这个代码即判断了报文完整性,又提取了完整报文。