【Linux】深入理解网络编程:应用层自定义协议、序列化、TCP粘包问题与Socket封装_自定义报文分隔符避开常规
目录
1.应用层
1.1.再谈应用层
1.2.再谈“协议”
2.初识序列化与反序列化
2.1.基本概念
为什么要转换成字符串在发送呢?
2.2.重新理解 read、write、recv、send 和 tcp 为什么支持全双工
我们的read或者recv为什么会阻塞?
2.3.如何解决粘包问题
2.3.1编码操作:
2.3.2.解码操作
2.4.如何使用Json进行序列化和反序列化
2.4.1.序列化
2.4.2.反序列化
3.封装socket类
3.1.封装思想
3.2.基本框架
3.3.代码实现
1.应用层
1.1.再谈应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
1.2.再谈“协议”
根据前面的知识我们知道了协议是一种 \"约定\"。
socket api 的接口, 在读写数据时, 都是按 \"字符串\" 的方式来发送接收的。 如果我们要传输一些 \"结构化的数据\" 怎么办呢?
协议的真正概念:
其实,协议就是双方约定好的结构化的数据!
2.初识序列化与反序列化
2.1.基本概念
- 序列化:把信息由多变一,方便网络发送。
- 反序列化:把信息由一变多,方便上层处理。
举个例子:
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
约定方案:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
这就是序列化和反序列化!
为什么要转换成字符串在发送呢?
向上通过反序列化读取消息,向下通过序列化包装消息。而TCP/UDP不关心发送的是什么,都按照字符串进行传输!
2.2.重新理解 read、write、recv、send 和 tcp 为什么支持全双工
- 在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
- 这就是为什么一个 tcp sockfd 读写都是它的原因
- 实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议。这就体现控制!传输层的问题都是由OS自主来决定的!
tcp能接受全双工的本质原因是因为TCP连接各有一对发送缓冲区和接受缓冲区。
tcp发送数据的本质:将自己的发送缓冲区拷贝到接收方的接受缓冲区中!
通信的本质就是拷贝!!!
read、write、recv、send本质是拷贝函数!
我们的read或者recv为什么会阻塞?
因为缓冲区中没有数据,阻塞的本质就是用户层在同步!
Tcp设计也是符合生产者消费者模型!因为发送缓冲区和接收缓冲区都是属于操作系统的,所以一定是临界资源!会有多个生产者,多个消费者!而IO发生阻塞也就是为了维护同步关系,保证缓冲区的正确使用!
接受的时候,我们还需要解决一个问题,保证我们拿到的是一个完整请求!
举个例子:
对方的接收缓冲区写满了,对方一直不读,那么我们的发送缓冲区就积压了很多同样的请求,如果一次性刷新过去,对方就读取到多条信息;又或者只发送了一条请求的一半过去,那么接受方读取就读取一半了,就不可能进行反序列化!这个过程就叫面向字节流!!!
所以怎么保证读取的是一个完整的请求呢???
这就是经典的TCP的粘包问题了!
2.3.如何解决粘包问题
需要我们自定义协议出马了!(协议的再理解)
我们自己规定协议如下:
报文 = 报头+有效载荷
\"有效载荷的长度\"\\r\\n\"有效载荷\"\\r\\n
\"len\"\\r\\n\"_x _op _y\"\\r\\n -> len: 有效载荷的长度,约定\\r\\n是分隔符,不参与统计
2.3.1编码操作:
我们自己利用自定义协议将报文封装起来,方便解码判断是否是一个完整的报文。
编码后的报文:
有效载荷的长度 + 分隔符 + 有效载荷 + 分隔符
const std::string SEP = \"\\r\\n\"; // 我们把tcp中读到的报文,可能读到半个,也可能读到1个半个, TCP 粘报问题 // 解决TCP的粘报问题 std::string Encode(const std::string &json_str) { int json_str_len = json_str.size(); std::string proto_str = std::to_string(json_str_len); proto_str += SEP; proto_str += json_str; proto_str += SEP; return proto_str; }
2.3.2.解码操作
根据我们自己定义的协议,分离报头,分隔符后,看是否是一个完整的报头。
如果输入流还有内容,我们只需要从输入流中提取出一个完整的请求,剩余留着下一次处理!
std::string Decode(std::string &inbuffer) { auto pos = inbuffer.find(SEP); if (pos == std::string::npos) return std::string(); std::string len_str = inbuffer.substr(0, pos); if (len_str.empty()) return std::string(); int packlen = std::stoi(len_str); int total = packlen + len_str.size() + 2 * SEP.size(); if (inbuffer.size() < total) return std::string(); std::string package = inbuffer.substr(pos + SEP.size(), packlen); inbuffer.erase(0, total); return package; }
2.4.如何使用Json进行序列化和反序列化
下面主要是主要代码
2.4.1.序列化
bool Serialize(std::string *out) { // 转换成为字符串 Json::Value root; root[\"result\"] = _result; root[\"code\"] = _code; Json::FastWriter writer; // Json::StyledWriter writer; *out = writer.write(root); return true; }
2.4.2.反序列化
bool Deserialize(const std::string &in) { Json::Value root; Json::Reader reader; bool res = reader.parse(in, root); if (!res) return false; _result = root[\"result\"].asInt(); _code = root[\"code\"].asInt(); return true; }
3.封装socket类
3.1.封装思想
将socket系列操作分类封装,设计为基类,派生出Tcp和Udp两种具体的Socket!基类都需要进行创建socket文件 、进行绑定、 进入listen 、获取链接、 申请链接…由于两种类的操作方式不一致,所以基类只需要进行一个声明就可以,具体实现在派生类中完成!
以后直接调用相应接口即可,非常优雅!TcpSocket继承Socket类 成员变量 sockfd
3.2.基本框架
通过这些操作的组合,可以进行建立监听链接 ,建立客户端连接等操作,十分方便!这种设计模式是模版方法设计模式!!!
3.3.代码实现
#pragma once#include #include #include #include /* See NOTES */#include #include #include #include #include #include #include #include #include \"InetAddr.hpp\"#include \"Log.hpp\"// 模版方法模式namespace socket_ns{ class Socket; const static int gbacklog = 8; using socket_sptr = std::shared_ptr; enum { SOCKET_ERROR = 1, BIND_ERROR, LISTEN_ERROR, USAGE_ERROR }; // std::unique_ptr listensock = std::make_unique(); // listensock->BuildListenSocket(); // std::unique_ptr clientsock = std::make_unique(); // clientsock->BuildClientSocket(); // clientsock->send(); // clientsock->Recv(); class Socket { public: virtual void CreateSocketOrDie() = 0; virtual void BindSocketOrDie(InetAddr &addr) = 0; virtual void ListenSocketOrDie() = 0; virtual socket_sptr Accepter(InetAddr *addr) = 0; virtual bool Connetcor(InetAddr &addr) = 0; virtual void SetSocketAddrReuse() = 0; virtual int SockFd() = 0; virtual int Recv(std::string *out) = 0; virtual int Send(const std::string &in) = 0; virtual void Close() = 0; // virtual void Recv() = 0; // virtual void Send() = 0; // virtual void other() = 0; public: void BuildListenSocket(InetAddr &addr) { CreateSocketOrDie(); SetSocketAddrReuse(); BindSocketOrDie(addr); ListenSocketOrDie(); } bool BuildClientSocket(InetAddr &addr) { CreateSocketOrDie(); return Connetcor(addr); } // void BuildUdpSocket() // { // CreateSocketOrDie(); // BindSocketOrDie(); // } }; class TcpSocket : public Socket { public: TcpSocket(int fd = -1) : _sockfd(fd) { } void CreateSocketOrDie() override { // 1. 创建流式套接字 _sockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) { LOG(FATAL, \"socket error\"); exit(SOCKET_ERROR); } LOG(DEBUG, \"socket create success, sockfd is : %d\\n\", _sockfd); } void BindSocketOrDie(InetAddr &addr) override { // 2. bind struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(addr.Port()); local.sin_addr.s_addr = inet_addr(addr.Ip().c_str()); int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); if (n < 0) { LOG(FATAL, \"bind error\\n\"); exit(BIND_ERROR); } LOG(DEBUG, \"bind success, sockfd is : %d\\n\", _sockfd); } void ListenSocketOrDie() override { int n = ::listen(_sockfd, gbacklog); if (n < 0) { LOG(FATAL, \"listen error\\n\"); exit(LISTEN_ERROR); } LOG(DEBUG, \"listen success, sockfd is : %d\\n\", _sockfd); } socket_sptr Accepter(InetAddr *addr) override { struct sockaddr_in peer; socklen_t len = sizeof(peer); int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len); if (sockfd < 0) { LOG(WARNING, \"accept error\\n\"); return nullptr; } *addr = peer; socket_sptr sock = std::make_shared(sockfd); return sock; } virtual bool Connetcor(InetAddr &addr) { // tcp client 要bind,不要显示的bind. struct sockaddr_in server; // 构建目标主机的socket信息 memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(addr.Port()); server.sin_addr.s_addr = inet_addr(addr.Ip().c_str()); int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server)); if (n < 0) { std::cerr << \"connect error\" < 0) { inbuffer[n] = 0; *out = inbuffer; // ??? += } return n; } int Send(const std::string &in) override { int n = ::send(_sockfd, in.c_str(), in.size(), 0); return n; } int SockFd() override { return _sockfd; } void Close() override { if (_sockfd > -1) ::close(_sockfd); } private: int _sockfd; }; // class SocketFactor // { // public: // void Build // }} // namespace socket_ns