一、认识协议
TCP是面向字节流,write发了五次,对端可能read一次就收完了,也可能要多次才收完。对端读多少次,和发了多少次无关。此时如果读到的数据是多次发送的数据,就需要由我们自己来讲这些数据分开。那我如何知道要怎么分开这些数据呢?它们之间由有边界吗?想要解决,就需要协议。
协议就是大家认同的一种“约定”。
想象给朋友发送消息的常见,:
通过上面这张图,我们初步认识了序列化与反序列化。
可是这和协议有啥关系呢?如果把昵称、时间、消息内容封装为一个结构体,发送方与接收方都使用这个结构体来处理数据。这个结构体,就可以说是协议,这是发送放与接收方的约定嘛,协议,就是约定。
但是,这种结构体的类型来制定应用层协议并不推荐,因为接收方和发送方的操作系统可能存在差异,结构体在不同的运行环境下可能存在差异,跨平台性太差。所以应用层协议一般不这么定义。
所以我们不能把这些结构体类型直接通过网络发送给对端,而是要通过序列化把结构体数据变为一个字符串,再通过网络发送给对端,对端收到后,把字符串转化成结构体数据。
- 序列化:将协议对应的结构体数据转化为“字符串”(字节流)。
- 反序列化:将字节流转化为结构体数据。
为什么要进行序列化?最重要的就是方便网络发送。
为什么要进行反序列化呢?为了上层业务能更方便的提取有效数据。
二、实现网络版计算器
接下来,我们要写一个网络版计算器,并且在其中实现自定义协议。
1、v1版本
这个版本直接用结构体来进行网络传输。
(1)自定义协议
//Protocol.hpp
#pragma once
#include <iostream>
#include <memory>
class Request//请求
{
public:
Request()
{}
Request(int x,int y,int oper)
:_x(x),_y(y),_oper(oper)
{}
void Debug()
{
std::cout<<_x<<_oper<<_y<<std::endl;
}
void Inc()
{
++_x;
++_y;
}
private:
int _x;//第一个参数
int _y;//第二个参数
char _oper;//运算符,+-*/
};
class Response//回应
{
public:
Response()
{}
Response(int result,bool code)
:_result(result),_code(code)
{}
private:
int _result;//运算结果
bool _code;//运算状态
};
//工厂模式
class Factory
{
public:
std::shared_ptr<Request> BuildRequest()
{
std::shared_ptr<Request> req=std::make_shared<Request>();
return req;
}
std::shared_ptr<Request> BuildRequest(int x,int y,char oper)
{
std::shared_ptr<Request> req=std::make_shared<Request>(x,y,oper);
return req;
}
std::shared_ptr<Response> BuildResponse()
{
std::shared_ptr<Response> res=std::make_shared<Response>();
return res;
}
std::shared_ptr<Response> BuildResponse(int result,bool code)
{
std::shared_ptr<Response> res=std::make_shared<Response>(result,code);
return res;
}
};
既然是协议,那么server和client都要能使用Request和Response。那么以后写的serevr和client必然都需要包含Protocol.hpp这个头文件。
(2)实现Socket.hpp
就是把套接字操作封装起来。
//Socket.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
#include <string>
namespace Net_Work
{
enum
{
CreateErr = 1,
BindErr,
ListenErr,
};
class Socket
{
public:
Socket(){};
virtual ~Socket() {}
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie(int backlog) = 0;
virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
virtual bool ConnectServer(std::string serverip, uint16_t serverport) = 0;
virtual int GetSockfd() = 0;
virtual void SetSockfd(int sockfd) = 0;
virtual void CloseSockfd() = 0;
virtual bool Recv(std::string *out, int size) = 0;
virtual bool Send(const std::string &in) = 0;
public:
void BuildListenSocketMethod(uint16_t port, int backlog)
{
CreateSocketOrDie(); // 创建套接字
BindSocketOrDie(port); // 绑定套接字
ListenSocketOrDie(backlog); // 设置为监听状态
}
bool BuildConnectSocketMethod(std::string serverip, uint16_t serverport)
{
CreateSocketOrDie(); // 创建套接字
return ConnectServer(serverip, serverport);
}
void BuildNormalSocketMethod(int sockfd)
{
SetSockfd(sockfd);
}
};
class Tcp_Socket : public Socket
{
public:
Tcp_Socket(int sockfd = -1)
: _sockfd(sockfd){};
void CreateSocketOrDie() override // 创建套接字
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0) // 创建失败
{
exit(CreateErr);
}
}
void BindSocketOrDie(uint16_t port) override // 绑定套接字
{
// 解决绑定失败问题
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 2.绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
exit(BindErr);
}
}
void ListenSocketOrDie(int backlog) override // 设置套接字为监听状态
{
int m = listen(_sockfd, backlog);
if (m < 0)
{
exit(ListenErr);
}
}
Tcp_Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override // 获取连接
{
sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (n < 0)
{
return nullptr;
}
*peerip = inet_ntoa(peer.sin_addr);
*peerport = ntohs(peer.sin_port);
Tcp_Socket *ret = new Tcp_Socket(n);
return ret;
}
bool ConnectServer(std::string serverip, uint16_t serverport) override//连接服务器
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
if (n == 0)
return true;
else
return false;
}
int GetSockfd() override//获取Sockfd
{
return _sockfd;
}
void SetSockfd(int sockfd) override//设置Sockfd
{
_sockfd = sockfd;
}
void CloseSockfd() override//关闭Sockfd
{
if (_sockfd > -1)
close(_sockfd);
}
bool Recv(std::string *out, int size) override//读取
{
char buffer[size];
ssize_t n = recv(_sockfd, buffer, size - 1, 0);
if (n <= 0)
{
return false;
}
buffer[n] = '\0';
*out += buffer;
return true;
}
bool Send(const std::string &in) override//写入
{
ssize_t n = send(_sockfd, in.c_str(), in.size(), 0);
if (n < 0)
return false;
else
return true;
}
~Tcp_Socket() {}
private:
int _sockfd;
};
}
(4)完整代码
protocol_v1 · 吕世雄/Linux_test - 码云 - 开源中国 (gitee.com)
(5)测试
经过我们的测试,server和client在直接传输结构体时没有出现任何问题啊。这是因为此时测试的server和client都是在同一台机器上,运行环境都是相同的,如果在不同的运行环境上,是可能会出问题的。
2、v2版本
TCP建立连接后,server可以给client发消息,client也可以给server发消息,二者都是有发送缓冲区与接收缓冲区的。当我们把数据从client调用send/write发送给server时,并不是直接就发送过去,而是拷贝到发送缓冲区,此时send/write的工作就结束了。也就是说,其实我们调用的send/write并么有把数据发送到网络中,只是把数据从应用层拷贝到了内核的缓冲区中。同理,recv/read也只是执行拷贝工作。如果发送缓冲区满了,send/write就会出错,接收缓冲区满了,recv/read就会出错。
TCP之所以是全双工的,就是因为发送缓冲区和接收缓冲区是分离的,互不影响。
缓冲区中的数据,什么时候发?发多少?出错了怎么办?完全由内核决定,具体一点就是完全由TCP协议决定。
TCP实际通信时,其实是双方操作系统之间进行通信。
假设此时client想要向server发送一个helloworld,当send/write把helloworld拷贝到发送缓冲区后,发现对方的接受缓冲区只剩下五个字节的空间了,于是就只能发hello过去。
所以TCP下发送方发了多少字节,接收方不一定就要接收这么多字节。UDP则不一样,如果发,那就一定发完整的报文,否则就干脆不发了。
在假设client要发送两条报文,一条是helloworld,一条是good,这两条消息都会被拷拷贝到发送缓冲区,再发出去。当server收到了helloworldgood时,他该如何知道这是两条报文,还是一条报文,或者三条报文呢?
UDP就不会有这个问题,因为它没有发送缓冲区。
如何确定收到的报文是否是完整的呢?如何分离多条报文呢?可以通过协议,确定报文的边界,如果读到边界了,也就能确认读完了一条报文。同时也能区分开多条报文(解决粘包问题)。
重新回到网络版计算器:
(1)自定义协议
相比于v1版本,现在我们要解决两个问题:1.结构化数据的序列化与反序列化问题。2.用户区分报文边界的问题。
先解决第一个问题,现在把第一个参数x,第二个参数y,操作符op序列化为 “x op y” ,用空格分开参数与操作符。
接下来解决第二个问题,op的长度一定是1个字节,但是x和y是变长的。为了方便定义报文边界,于是在前面再加一个len,代表有效载荷的长度, "len""x op y" 。这个len就相当于报文的报头,可以通先读到len,确定接下来的有效载荷要读多少个字节能读完。这个len就是报文的自描述字段。
但是。如何保证读到了一个完整的len呢?有点鸡生蛋蛋生鸡的问题。在len和有效载荷中间加一个\n, "len"\n"x op y" 。读到\n了,就一定读到了一个完整的len。
诶?问题又来了,既然\n能保证读到一个完整的len,那为什么不用 "x op y"\n 呢,这样也能保证读到完整的报文了。在写网络版计算器中,确实不要len也没问题,因为现在我们的有效载荷里没有\n。但是以后我们的报文的有效载荷如果是文本呢?图片呢?视频呢?之所以要加入len是为了保证通用性,我能保证len里面一定不包含\n,但是不能保证有效载荷里一定没有\n呀。如果报文的有效载荷里本就含有\n,那就会影响到我们判断报文边界,此时就需要用len来确实报文长度了。
我们一个报文的格式应该是"len\nx op y"。
收到多个报文时就是"len\nx op y""len\nx op y""len\nx op y""len\nx op y"。也没问题,也能区分报文边界。但是,为了方便打印收到的报文,于是决定在一个报文的末尾再加一个\n。\n是不会纳入有效载荷长度的。
所以,一个报文的最终格式应该是"len\nx op y\n"。
//Protocol.hpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
namespace ProtocolNS
{
const std::string ProtSep = " ";
const std::string MessageSep = "\n";
//message可能是 "x op y"或者"resulet code"
std::string Encode(const std::string &message) // 封装
{
std::string len = std::to_string(message.size());//len
std::string package = len + MessageSep + message + MessageSep;
//拼接 "len\nmessage\n"
return package;
}
//我无法保证package是一个完整的报文,它可能是如下的多种的情况:
//"l"
//"le"
//"len"
//"len\n"
//"len\nx"
//"len\nx "
//"len\nx op"
//"len\nx op "
//"len\nx op y"
//"len\nx op y\n"
//"len\nx op y\n""len\n"
bool Decode(std::string &package, std::string *message) // 解包
{
int pos = package.find(MessageSep);//寻找第一个\n
if (pos == std::string::npos)
return false;//没找着,那就不用解包了,连一个完整的len都没有
std::string lens = package.substr(0, pos);//获取len
int messagelen = std::stoi(lens);//有效载荷的长度
int total = messagelen + lens.size() + 2 * MessageSep.size(); //一整条报文的长度
if (package.size() < total)//没有一条完整的报文
return false;
*message = package.substr(pos + 1, messagelen);//从报文中提取有效载荷可能是"x op y"或者"resulet code"
package.erase(0, total);//删掉已经处理完的报文
return true;
}
class Request // 请求
{
public:
Request()
: _x(0), _y(0), _oper('+')
{}
Request(int x, int y, int oper)
: _x(x), _y(y), _oper(oper)
{}
void Debug()
{
std::cout << _x << _oper << _y << std::endl;
}
void Inc()
{
++_x;
++_y;
}
int Getx()//获取第一个参数
{
return _x;
}
int Gety()//获取第二个参数
{
return _y;
}
char Getoper()//获取运算符
{
return _oper;
}
//"x op y"
bool Serialize(std::string *out) // 序列化
{
//把结构体数据转化为字符串
*out = std::to_string(_x) + ProtSep + _oper + ProtSep + std::to_string(_y);
return true;
}
//"x op y"
bool Deserialize(const std::string &in) // 反序列化
{
//把字符串转化为结构体数据
int left = in.find(ProtSep, 0);
if (left == std::string::npos)
return false;
int right = in.find(ProtSep, left + 1);
if (right == std::string::npos)
return false;
_x = std::stoi(in.substr(0, left));
_y = std::stoi(in.substr(right + ProtSep.size()));
std::string oper = in.substr(left + ProtSep.size(), right - (left + ProtSep.size()));
if (oper.size() != 1)
return false;
_oper = oper[0];
return true;
}
private:
int _x;//第一个参数
int _y;//第二个参数
char _oper;//操作符,+-*/
};
class Response // 回应
{
public:
Response()
: _result(0), _code(true)
{}
Response(int result, bool code)
: _result(result), _code(code)
{}
void Setresult(int result)//设置运算结果
{
_result = result;
}
void SetCode(bool code)//设置运算状态
{
_code = code;
}
//"result code"
bool Serialize(std::string *out) //序列化
{
//把结构体数据转化为字符串
*out = std::to_string(_result) + ProtSep + std::to_string(_code);
return true;
}
//"result code"
bool Deserialize(const std::string &in) // 反序列化
{
//把字符串转化为结构体数据
int pos = in.find(ProtSep, 0);
if (pos == std::string::npos)
return false;
_result = std::stoi(in.substr(0, pos));
_code = std::stoi(in.substr(pos + ProtSep.size()));
return true;
}
private:
int _result;//运算结果
bool _code;//运算状态
};
//工厂模式
class Factory
{
public:
std::shared_ptr<Request> BuildRequest()
{
std::shared_ptr<Request> req = std::make_shared<Request>();
return req;
}
std::shared_ptr<Request> BuildRequest(int x, int y, char oper)
{
std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);
return req;
}
std::shared_ptr<Response> BuildResponse()
{
std::shared_ptr<Response> res = std::make_shared<Response>();
return res;
}
std::shared_ptr<Response> BuildResponse(int result, bool code)
{
std::shared_ptr<Response> res = std::make_shared<Response>(result, code);
return res;
}
};
}
(2)业务处理
收到报文,并从中提取有效载荷生成Request,此时就需要对Request处理了,出完完毕后,生成Response,再把Response发回去。
#include "Protocol.hpp"
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc,char* args[])
{
if(argc!=3)
{
std::cout<<"请按照格式使用:./server ip port"<<std::endl;
return 1;
}
std::string serverip=args[1];
uint16_t serverport=atoi(args[2]);
Net_Work::Tcp_Socket socket;
auto n=socket.BuildConnectSocketMethod(serverip,serverport);
if(!n)
{
std::cout<<"连接失败"<<std::endl;
return 1;
}
ProtocolNS::Factory f;
std::shared_ptr<ProtocolNS::Request> req=f.BuildRequest(10,10,'+');
while(true)
{
//1.序列化
std::string message;
auto r=req->Serialize(&message);
if(!r)
std::cout<<"序列化失败"<<std::endl;
std::cout<<"序列化成功,message--->"<<message<<std::endl;
//2.封装
std::string package=ProtocolNS::Encode(message);
std::cout<<"封装成功,package--->"<<package<<std::endl;
//3.发送
ssize_t n=send(socket.GetSockfd(),package.c_str(),sizeof(*req),0);
if(n<0)
{
std::cout<<"发送失败,errno="<<errno<<std::endl;
continue;
}
req->Inc();
socket.CloseSocket();
sleep(1);
}
std::cout<<"client 退出"<<std::endl;
return 0;
}
(3)完整代码
protocol_v2 · 吕世雄/Linux_test - 码云 - 开源中国 (gitee.com)
(4)测试
3、v3版本
相比v2版本的自定义序列反序列化,v3版本将引入更成熟的序列反序列化方案——json。
用条件编译让使用者可以选择使用自定义序列反序列化方案或者json方案。相比v2版本的代码,变化集中在Protocol.hpp中。