一、TCP 是字节流协议
之前学习 TCP 通信时,我们通常会这样写:
cpp
send(sockfd, ...);
recv(sockfd, ...);
发送方调用 send 发送数据,接收方调用 recv 接收数据。
刚开始可能会觉得,发送方发送一条消息,接收方调用一次 recv 就能收到这条消息。
但实际上并不是这样。
TCP 是一种面向字节流的传输协议,它只保证数据能够按照顺序可靠地到达对端,并不保证发送方的一次发送操作与接收方的一次接收操作对应。
举个例子:
发送方连续发送两条消息:
text
hello
world
接收方可能出现下面几种情况。
情况一:一次收到完整的两条消息。
text
helloworld
情况二:第一次只收到部分数据。
text
hel
后面再收到:
text
loworld
情况三:先收到第一条消息,之后再收到第二条消息。
text
hello
text
world
这些情况都是有可能出现的。
这就是我们常说的粘包和半包问题。
二、什么是粘包和半包?
1. 粘包
发送方发送了多条消息,但是接收方一次读取时,可能将多条消息一起读取出来。
例如:
text
发送方:
消息1:hello
消息2:world
接收方一次读取:
helloworld
此时,如果接收方不知道消息之间的边界,就无法确定哪里是第一条消息的结束位置。
2. 半包
发送方发送了一条完整的消息,但是接收方一次读取时,只读取到了其中一部分。
例如:
text
发送方:
hello world
第一次接收:
hello
第二次接收:
world
接收方第一次读取到的数据并不是一条完整的消息。
因此,我们不能简单地认为一次 recv 就能得到一个完整的请求。
TCP 只负责传输字节流,不负责维护应用层消息的边界。
这就需要我们在应用层自己解决。
三、如何解决粘包和半包?
既然 TCP 不会帮我们区分消息边界,那么我们就需要自己设计一种规则,让接收方能够判断一条消息什么时候结束。
常见的思路之一就是在报文中增加长度字段。
例如这样的报文格式:
text
有效载荷长度\r\n
有效载荷内容\r\n
例如:
text
5\r\n
hello\r\n
这里:
5表示有效载荷的长度;- 第一个
\r\n用来分隔长度和内容; hello是真正需要传输的数据;- 最后的
\r\n是结束分隔符。
这样,接收方就可以先读取长度,再根据长度判断当前是否已经收到完整的报文。
四、实现 Encode:给报文增加长度信息
首先实现一个编码函数:
cpp
const std::string LineBreakSep = "\r\n";
std::string Encode(const std::string &message)
{
std::string len = std::to_string(message.size());
std::string package =
len + LineBreakSep + message + LineBreakSep;
return package;
}
这个函数主要做了三件事:
- 获取原始消息的长度;
- 将长度转换成字符串;
- 按照约定的格式拼接完整报文。
例如:
cpp
std::string message = "hello";
std::string package = Encode(message);
最终得到:
text
5\r\nhello\r\n
这样,原本没有边界信息的字符串就变成了一条具有明确格式的报文。
需要注意,长度字段记录的是有效载荷的长度,而不是整个报文的长度。
五、实现 Decode:判断报文是否完整
编码完成之后,接收方还需要实现对应的解码函数。
cpp
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(),
messagelen
);
// 删除已经处理过的报文
package.erase(0, total);
return true;
}
这段代码是整个协议处理中比较重要的部分,需要理解每一步的作用。
1. 找到长度字段的结束位置
cpp
auto pos = package.find(LineBreakSep);
我们约定长度和消息内容之间使用 \r\n 分隔。
因此,只要找到第一个 \r\n,就能知道长度字符串在哪里结束。
如果没有找到,说明目前收到的数据还不够,暂时无法解析。
cpp
if(pos == std::string::npos)
return false;
2. 获取消息长度
cpp
std::string lens = package.substr(0, pos);
int messagelen = std::stoi(lens);
例如:
text
5\r\nhello\r\n
此时:
text
lens = "5"
messagelen = 5
3. 判断报文是否完整
cpp
int total = lens.size()
+ messagelen
+ 2 * LineBreakSep.size();
为什么要这样计算?
因为完整报文由三部分组成:
text
长度字段 + 分隔符 + 有效载荷 + 结束分隔符
如果当前缓冲区中的数据长度还小于 total,就说明报文还没有接收完整。
cpp
if(package.size() < total)
return false;
此时不能急着解析,也不能删除缓冲区中的数据。
应该保留已经收到的内容,等待后续数据到来。
4. 提取消息并删除已处理的数据
确认报文完整之后,就可以提取有效载荷:
cpp
*message = package.substr(
pos + LineBreakSep.size(),
messagelen
);
这里从第一个分隔符之后开始读取,读取长度为 messagelen 的内容。
最后:
cpp
package.erase(0, total);
删除已经处理完成的报文。
为什么要删除?
因为缓冲区中可能不止一条消息。
例如:
text
5\r\nhello\r\n
5\r\nworld\r\n
如果一次读取到了两条完整报文,那么解析第一条之后,还需要保留第二条,等待下一次解析。
因此这里只删除已经处理的部分,而不是直接清空整个缓冲区。
六、为什么 Recv 需要拼接数据?
前面讲过,TCP 接收数据时可能出现半包。
因此,我们不能每次 recv 完就直接解析,而应该先把收到的数据保存起来。
cpp
bool Recv(std::string *buffer, int size)
{
char inbuffer[size];
ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);
if(n > 0)
{
inbuffer[n] = 0;
*buffer += inbuffer;
return true;
}
else if(n == 0)
return false;
else
return false;
}
这里最重要的是:
cpp
*buffer += inbuffer;
它不是直接覆盖原来的缓冲区,而是把新收到的数据拼接到原有数据后面。
例如:
第一次收到:
text
5\r\nhe
第二次收到:
text
llo\r\n
拼接之后:
text
5\r\nhello\r\n
这时候再调用 Decode,就能够得到完整的消息。
如果一次收到多条消息,解码函数也可以逐条提取,每次处理完成后删除对应的数据,直到缓冲区中剩下不完整的报文。
七、完整的数据处理流程
结合前面的内容,整个 TCP 应用层数据处理流程可以整理成下面这样。
发送方:
- 创建 Request 对象;
- 将 Request 序列化成 JSON 字符串;
- 调用 Encode 添加报文长度和分隔符;
- 使用 Send 发送完整报文。
接收方:
- 使用 Recv 接收数据;
- 将数据拼接到接收缓冲区;
- 调用 Decode 判断报文是否完整;
- 如果完整,就提取有效载荷;
- 将 JSON 字符串反序列化成 Request 对象;
- 处理业务逻辑;
- 生成 Response,并按照相同的方式返回。
整个流程可以表示为:
text
客户端
|
创建 Request
|
序列化
|
Encode
|
Send
|
v
TCP 网络传输
|
v
服务器
|
Recv
|
拼接接收缓冲区
|
Decode
|
判断报文完整性
|
反序列化
|
处理业务逻辑
|
创建 Response
|
序列化
|
Encode
|
Send
|
v
客户端
八、代码示例
cpp
// Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 模板方法模式(基类控制流程,派生类实现具体步骤)
class Socket
{
public:
virtual ~Socket() {}
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
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;
virtual int Connect(const std::string &server_ip, uint16_t port) = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
void BuildTcpClientSocketMethod()
{
SocketOrDie();
}
};
const static int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultfd)
{
}
TcpSocket(int fd) : _sockfd(fd)
{
}
~TcpSocket() {}
void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
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);
}
int Recv(std::string *out) override
{
char buffer[1024];
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);
}
void Close() override
{
if (_sockfd >= 0)
::close(_sockfd);
}
int Connect(const std::string &server_ip, uint16_t port) override
{
InetAddr server(server_ip, port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}
private:
int _sockfd;
};
}
cpp
// Protocol.hpp
#pragma once
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
#include <functional>
// 实现一个自定义的网络版本的计算器
using namespace SocketModule;
// client -> server
class Request
{
public:
Request() {}
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{
}
std::string Serialize()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
}
return ok;
}
~Request() {}
int X() { return _x; }
int Y() { return _y; }
char Oper() { return _oper; }
private:
int _x;
int _y;
char _oper; // + - * / %
};
// server -> client
class Response
{
public:
Response() {}
Response(int result, int code) : _result(result), _code(code)
{
}
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
return writer.write(root);
}
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return ok;
}
~Response() {}
void SetResult(int res)
{
_result = res;
}
void SetCode(int code)
{
_code = code;
}
void ShowResult()
{
std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;
}
private:
int _result; // 运算结果
int _code; // 0:success 1,2,3,4->不同的运算异常的情况
};
const std::string sep = "\r\n";
using func_t = std::function<Response(Request &req)>;
class Protocol
{
public:
Protocol()
{
}
Protocol(func_t func) : _func(func)
{
}
std::string Encode(const std::string &jsonstr)
{
std::string len = std::to_string(jsonstr.size());
return len + sep + jsonstr + sep; // 应用层封装报头
}
bool Decode(std::string &buffer, std::string *package)
{
size_t pos = buffer.find(sep);
if (pos == std::string::npos)
return false;
std::string package_len_str = buffer.substr(0, pos);
int package_len_int = std::stoi(package_len_str);
int target_len = package_len_str.size() + package_len_int + 2 * sep.size();
if (buffer.size() < target_len)
return false;
// buffer至少有一个完整的报文
*package = buffer.substr(pos + sep.size(), package_len_int);
buffer.erase(0, target_len);
return true;
}
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
std::string buffer_queue;
while (true)
{
int n = sock->Recv(&buffer_queue);
if (n > 0)
{
std::string json_package;
// 1. 解析报文, 提取完整的json请求, 如果不完整就让服务器继续读取
while (Decode(buffer_queue, &json_package))
{
LOG(LogLevel::DEBUG) << client.StringAddr() << " 请求: " << json_package;
// 2. 一定拿到了一个完整的报文. 对请求json串反序列化
Request req;
bool ok = req.Deserialize(json_package);
if (!ok)
continue;
// 3. 一定得到了一个内部属性已经被设置了的req, req->resp完成计算功能
Response resp = _func(req);
// 4. 序列化
std::string json_str = resp.Serialize();
// 5. 添加自定义长度
std::string send_str = Encode(json_str); // 携带长度的应答报文
// 6. 直接发送
sock->Send(send_str);
}
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";
break;
}
else
{
LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";
break;
}
}
}
bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buffer, Response *resp)
{
while (true)
{
int n = client->Recv(&resp_buffer);
if (n > 0)
{
std::string json_package;
while (Decode(resp_buffer, &json_package))
{
resp->Deserialize(json_package);
}
return true;
}
else if (n == 0)
{
std::cout << "server quit" << std::endl;
return false;
}
else
{
std::cout << "recv error" << std::endl;
return false;
}
}
}
std::string BuildRequestString(int x, int y, char oper)
{
// 1. 构建一个完整的请求
Request req(x, y, oper);
// 2. 序列化
std::string json_req = req.Serialize();
// 3. 添加长度报头
return Encode(json_req);
}
~Protocol()
{
}
private:
// Request _req;
// Response _resp;
func_t _func;
};
cpp
// NetCal.hpp
#pragma once
#include "Protocol.hpp"
#include <iostream>
class Cal
{
public:
Response Execute(Request &req)
{
Response resp(0, 0); // code: 0表示成功
switch (req.Oper())
{
case '+':
resp.SetResult(req.X() + req.Y());
break;
case '-':
resp.SetResult(req.X() - req.Y());
break;
case '*':
resp.SetResult(req.X() * req.Y());
break;
case '/':
{
if (req.Y() == 0)
{
resp.SetCode(1); // 1表示除零错误
}
else
{
resp.SetResult(req.X() / req.Y());
}
}
break;
case '%':
{
if (req.Y() == 0)
{
resp.SetCode(2); // 2表示 mod 0错误
}
else
{
resp.SetResult(req.X() % req.Y());
}
}
break;
default:
resp.SetCode(3); // 非法操作
break;
}
return resp;
}
};
cpp
// TcpServer.hpp
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>
using namespace SocketModule;
using namespace LogModule;
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
TcpServer(uint16_t port, ioservice_t service) : _port(port),
_listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false),
_service(service)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
auto sock = _listensockptr->Accept(&client);
if (sock == nullptr)
{
continue;
}
LOG(LogLevel::DEBUG) << "accept success..." << client.StringAddr();
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if (id == 0)
{
// 子进程
_listensockptr->Close();
if (fork() > 0)
exit(OK);
// 孙子进程在执行任务, 已经是孤儿了
_service(sock, client);
sock->Close();
exit(OK);
}
else
{
// 父进程
sock->Close();
pid_t rid = ::waitpid(id, nullptr, 0);
(void)rid;
}
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
ioservice_t _service;
};
cpp
// main.cc
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "Daemon.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " port" << std::endl;
}
// ./tcpserver port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::cout << "服务器已经启动, 已经是一个守护进程了" << std::endl;
Daemon(0, 0);
// daemon(1, 1);
// Enable_Console_Log_Strategy();
Enable_File_Log_Strategy();
// 1. 顶层
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 2. 协议层
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{
return cal->Execute(req);
});
// 3. 服务器层
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){
protocol->GetRequest(sock, client);
});
tsvr->Start();
return 0;
}
九、总结
这部分让我对 TCP 通信有了更深入的理解。
之前写网络程序时,很容易把一次 send 和一次 recv 当成对应的操作。
但实际上,TCP 传输的是连续的字节流,并不会保留应用层消息的边界。
因此,我们需要自己设计应用层协议,通过长度字段和分隔符判断一条消息是否完整。
这篇主要掌握三个内容:
- TCP 是字节流协议,存在粘包和半包问题。
- 可以通过在报文中添加长度字段,帮助接收方确定消息边界。
- 接收方需要使用缓冲区保存数据,只有收到完整报文之后才能进行解析。
另外,序列化和报文封装也不是一回事:
- 序列化负责把结构化数据转换成字符串;
- Encode 负责给字符串增加报文边界信息;
- Decode 负责从 TCP 接收缓冲区中提取完整报文;
- 反序列化负责把提取出的字符串还原成结构化数据。
把这几个过程区分开,整个应用层协议的设计思路就清楚多了。