【计网】从零开始掌握序列化与反序列化 --- 基础知识储备与程序重构
从零开始掌握序列化与反序列化
- 1 初识序列化与反序列化
- 2 再谈Tcp协议
- 3 程序重构
- 3.1 Socket类
- 3.2 回调函数设计
- 3.3 最终的Tcp服务器类
1 初识序列化与反序列化
在刚学习计算机网络时,我们谈到过网络协议栈,其中最上层的就是应用层,那么这个应用层到底是什么呢?
前几篇文章中编写的程序就是应用层!但是应用层协议应该有一个双方都认识的结构啊?我们之前写的双方都是以字符串结构进行的通信!所以我们写的应用层协议的基础就是双方都可以读取识别字符串!!!而我们使用的socket
等函数是传输层!
协议就是双方都认识的结构化的数据!
前面我们通过字符串来实现协议,那么以后如果想要传输结构体这样结构化的数据应该如何传递?
假设我们想要实现一个网络计算器,那么用户需要传递两个数字和一个运算符。
此时我们需要自己设计协议struct request {int x ;int y ;char oper ; } struct result {int result ;int code ;//退出码 }
双方需要同时认识这样的一个结构体,也就是确定协议!
对于这样的协议应该如何传输呢?有两个方案:
- 约定方案一:
- 客户端发送一个形如"1+1"的字符串;
- 这个字符串中有两个操作数, 都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
- 数字和运算符之间没有空格;
- …
- 约定方案二:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
因为C语言支持二进制读写,那么我们如果直接通过
write
将结构体写入到sockfd
文件中,对方再以同样方式读取出来可不可以呢?
不可以!因为你无法保证对方是和自己一样的系统!Linux64位与Linux32位的对齐方式就不一样,更何况一些移动端系统或者其他语言了!!!这样就不可能保证可以正常读取!!!技术上就不推荐这样操作!!!再来说应用层面上,这样相当于把写入与读取写死了,如果产品需求改变,那么将会是一场改代码的大灾难!!!
所以不能直接传输结构体!就需要使用方案二,方案二这个过程叫做 “序列化” 和 “反序列化”!为什么要转换成字符串在发送呢?
那么什么是序列化和反序列化呢?
在群聊中,小明现在发送了一条消息,那么发送的消息不单单是这单独的消息,还会带着小明的昵称,发送的时间等信息一并打包发送!这样才能保证其他人知道是何人何时发的消息。这就叫序列化 !
其他人收到消息,会从这一串字符串中进行解析,将时间,昵称,信息都读取出来。这就叫反序列化!
向上通过反序列化读取消息,向下通过序列化包装消息。而TCP/UDP不关心发送的是什么,都按照字符串进行传输!
2 再谈Tcp协议
在这里我们重新探讨一下 read、 write、 recv、 send 和 tcp 为什么支持全双工?
客户端与服务端进行通信时,双方需要使用套接字。当使用Tcp套接字时,传输层会创建两个缓冲区:发送缓冲区和接收缓冲区。
缓冲区我们在学习文件时详细讲过,当我们打开一个文件时,会创建一个文件描述符fd,指向
struct file
结构体。其中就维护了一个文件缓冲区。当用户向fd
写入时会先将数据拷贝到缓冲区,再按照一定的刷新策略刷新到磁盘中去!
那么传输层的缓冲区也是一样:一个fd,代表一个链接;一个链接有两个缓冲区!每当应用层写入数据时(write,send…)本质是将数据拷贝到发送缓冲区中,读取数据时(read, recv…)本质上也是从读取缓冲区中进行读取。所以read , write , send , recv
本质上都是拷贝函数!
网络传输的本质:从发送方的发送缓冲区把数据通过网络协议栈和网络拷贝发送给接收方的接收缓冲区!
以上就是tcp支持全双工通信的本质原因!!!
接下来Tcp就需要解决发送缓冲区的一些问题:
- 什么时候将数据发送?
- 一次发送多少数据?
- 发送出错了怎么办?
这些都是由Tcp协议来决定的!使用时不需要管这些问题,因为传输层是属于操作系统的,传输层的问题都是由OS自主来决定的!那么这不就是相当于文件吗!
Tcp设计也是符合生产者消费者模型!因为发送缓冲区和接收缓冲区都是属于操作系统的,所以一定是临界资源!会有多个生产者,多个消费者!而IO发生阻塞也就是为了维护同步关系,保证缓冲区的正确使用!
传输层什么时候发,发多少,出错怎么办都是由OS决定,有没有一种可能 :对方的接收缓冲区写满了,对方一种不读,那么我们的发送缓冲区就积压了很多同样的请求,如果一次性刷新过去,对方就读取到多条信息;又或者只发送了一条请求的一半过去,那么接受方读取就读取一半了,就不可能进行反序列化!这个过程就叫面向字节流!!!客户端发的不一定是服务端收的!!!
所以怎么保证读取的是一个完整的请求呢???
所以对序列化和反序列化中还需要进行特别处理
3 程序重构
在我们将序列化与发序列化加入我们的程序中之前我们先来将我们的代码进行一个重构。之前我们编写的Tcp代码的服务器类并没有做到绝对的解耦:
- 服务器类中进行了Socket套接字的创建,bind绑定服务器端口号,进入监听模式。都是通过初始化函数来进行
- 服务器类中的在工作中需要做到从套接字文件中获取链接,然后通过sockfd获取数据,也要向客户端发送数据
- 服务类类中还需要进行回调函数的处理!
服务器类的工作是比较冗杂的,我们可以将对于套接字文件的操作提取出来,封装为一个Socket类来完成对于套接字的操作。而服务器类只负责建立连接和执行回调函数(回调函数由上层传递),不用在处理套接相关的操作!!!
3.1 Socket类
- 将socket系列操作分类封装,设计为基类,派生出Tcp和Udp两种具体的Socket!基类都需要进行创建socket文件 、进行绑定、 进入listen 、获取链接、 申请链接…由于两种类的操作方式不一致,所以基类只需要进行一个声明就可以,具体实现在派生类中完成!
- TcpSocket继承Socket类 成员变量 sockfd(可以是listensockfd 也可以是普通套接字)
- 构造与析构
- 创建套接字 直接CV原本的代码就可以
- 进行绑定 需要本地端口号 uint16_t port
- 进行监听 需要gblcklog
- 获取链接 输出型参数客户端InetAddr
using SockSPtr = std::shared_ptr;进行封装,返回一个指针 - 进行链接 通过远端IP 端口号进行链接
- Recv 接口负责读
- Send 接口负责发送
- 通过这些操作的组合,可以进行建立监听链接 ,建立客户端连接等操作,十分方便!这种设计模式是模版方法设计模式!!!
整体的框架如下:
namespace socket_ns
{class Socket;using SockSPtr = std::shared_ptr<Socket>;const int gblocklog = 8;enum{SOCKET_FD = 1,SOCKET_BIND,SOCKET_LISTNE};using namespace log_ns;// 模版方法类!class Socket{public:virtual void CreateSocketOrDie() = 0;virtual void CreateBindOrDie(uint16_t port) = 0;virtual void CreateListenOrDie(int blocklog = gblocklog) = 0;virtual SockSPtr Accepter(InetAddr *addr) = 0;virtual bool Connector(const std::string &peerip, uint16_t peerport) = 0;virtual int GetSockfd() = 0;virtual void Close() = 0;virtual ssize_t Recv(std::string *out) = 0;virtual ssize_t Send(std::string &in) = 0;public://将若干接口合并,完成复杂任务!void BuildListenSocket(uint16_t port){CreateSocketOrDie();CreateBindOrDie(port);CreateListenOrDie();}void BuildClientSocket(const std::string &peerip, uint16_t peerport){CreateSocketOrDie();Connector(peerip, peerport);}void BuildUdpSocket(){}};class TcpSocket : public Socket{public:TcpSocket() {}TcpSocket(int sockfd) : _sockfd(sockfd) {}~TcpSocket() {}void CreateSocketOrDie() override{}void CreateBindOrDie(uint16_t port) override{}void CreateListenOrDie(int blocklog = gblocklog) override{}SockSPtr Accepter(InetAddr *addr) override{}bool Connector(const std::string &peerip, uint16_t peerport) override{}ssize_t Recv(std::string *out) override{}ssize_t Send(std::string &in) override{ }int GetSockfd(){return _sockfd;}void Close(){if (_sockfd > 0){::close(_sockfd);}}private:int _sockfd = -1;//可以是listensSockfd 也可以是 Sockfd};// class UdpSocket :public Socket// {// };}
我们从原本的代码中可以拆分出三个部分:
- 创建套接字 直接CV原本的代码就可以
- 进行绑定 需要本地端口号 uint16_t port
- 进行监听 需要gblcklog
void CreateSocketOrDie() override{// 创建socket文件 --- 字节流方式_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "socket error!!!\n");return;}LOG(INFO, "socket create success!!! _listensockfd: %d\n", _sockfd);}void CreateBindOrDie(uint16_t port) override{// 建立server结构体struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY; // 服务器IP一般设置为0local.sin_port = htons(port);// 进行绑定if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error!!!\n");return;}LOG(INFO, "bind success!!!\n");}void CreateListenOrDie(int blocklog = gblocklog) override{// 将_listensockfd文件转换为listening状态!!!if (::listen(_sockfd, blocklog) < 0){LOG(FATAL, "listen error!!!\n");exit(SOCKET_LISTNE);}LOG(INFO, "listen success!!!\n");}
注意添加具体的参数,让操作更加顺畅!
然后就是加入获取链接与进行链接,这也可以从之前的服务器端和客户端代码中拆分出来,之后服务端和客户端只需要调用对应的接口即可,非常方便 !!!
SockSPtr Accepter(InetAddr *addr) override{// accept接收sockfdstruct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");return nullptr;}*addr = InetAddr(client);// 读取数据return std::make_shared<TcpSocket>(sockfd);}bool Connector(const std::string &peerip, uint16_t peerport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 数据归零server.sin_family = AF_INET;server.sin_port = htons(peerport); // 端口号::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);// 进行发送数据int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect socket error" << std::endl;return false;}return true;}
最后是包装一下发送与接收数据,这个比较简单:
ssize_t Recv(std::string *out) override{char buffer[4096];ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;*out += buffer;}return n;}ssize_t Send(std::string &in) override{return ::send(_sockfd, in.c_str(), in.size(), 0);}
这样我们的Socket类就编写好了,之后的Tcp服务器就只需要进行调用类内的接口即可,这样Tcp服务器的逻辑更加直观!!!
3.2 回调函数设计
对于回调函数我们也要单独设计一下,主要是实现IO的功能,这以后就作为回调函数来进行操作!
#include "Socket.hpp"
#include "InetAddr.hpp"using namespace socket_ns;class Service
{
public:Service(){}void IOExecute(SockSPtr sock, InetAddr &addr){LOG(INFO, "service start!!!\n");while (true){std::string message;ssize_t n = sock->Recv(&message);if (n > 0){LOG(INFO, "sockfd read success!!! buffer: %s\n", message.c_str());std::string hello = "hello";sock->Send(hello);}else if (n == 0){LOG(INFO, "client %s quit!\n", addr.AddrStr().c_str());break;}else{LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}}}~Service(){}
};
目前先简单的进行写一下,后续添加业务逻辑!
3.3 最终的Tcp服务器类
我们将Socket类封装好,IO也单独封装好之后,我们的服务器类就会变为这样简洁的形式:
class TcpServer
{
public:// 模版方法模式TcpServer(service_io_t service, int port = gport) : _port(port),_listensock(std::make_shared<TcpSocket>()),_isrunning(false),_service(service){_listensock->BuildListenSocket(_port);}void Loop(){_isrunning = true;while (_isrunning){// accept接收sockfdInetAddr client;SockSPtr newsock = _listensock->Accepter(&client);if (newsock == nullptr)continue;LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->GetSockfd());pthread_t tid;ThreadData *td = new ThreadData(newsock, client, this);pthread_create(&tid, nullptr, Execute, td);}_isrunning = false;}class ThreadData{public:SockSPtr _sockfd;InetAddr _addr;TcpServer *_this;public:ThreadData(SockSPtr sockfd, InetAddr addr, TcpServer *p) : _sockfd(sockfd),_this(p),_addr(addr){}};// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!static void *Execute(void *args){pthread_detach(pthread_self()); // 线程分离!!!// 执行Service函数TcpServer::ThreadData *td = static_cast<TcpServer::ThreadData *>(args);td->_this->_service(td->_sockfd, td->_addr);td->_sockfd->Close();delete td;return nullptr;}~TcpServer(){}
private:uint16_t _port; // 服务器端口SockSPtr _listensock;bool _isrunning;service_io_t _service;
};
我们可以测试运行一下:
可以完成基础工作!!!
后面我们就来加入序列化与反序列化!!!