深入了解linux网络—— 自定义协议(上)
序列化和反序列化
我们知道,协议是一种约定;且在调用soccket
相关通信接口时,都是以字符串的形式发送信息。
如果,要传输一些结构化数据呢?协议也是双方约定好的一种结构化数据
例如,现在要实现一个网络版本的计算器,客户端就要将要计算的数字和运算符发送给服务端,由服务端处理完毕之后再返回给客户端。
在客户端就势必会存在
Request
结构化字段,其中存储在要运算的数字x
、y
和运算符oper
。而客户端要发送一个类似于
1+1
的字符串给服务端,为了保证服务端在接收到消息字符串时知道如何去处理;就要做好约定:字符串中存在两个操作数,两个数字之间存在一个操作符,操作符可能是
+
、-
、*
、/
。
这里在客户端和服务端就存在结构化字段Request
、Responce
;
在发送信息时将结构化字段转化为字符串信息。在接受到字符串信息后,也能将字符串转化为结构化数据。
序列化:将结构化信息转化为字符串信息
反序列化:将字符串信息转化为结构化信息
这里无论如何去实现,只要保证一端发送的数据,在另一端能够正确的进行解析即可。
而这种约定,就是 应用层协议
所以,在协议当中就势必要存在序列化和反序列化的相关方法
理解read
、write
、recv
、send
和TCP
支持全双工
我们知道TCP
在进行通信时是支持全双工的(可以同时读写)
读写相关接口read
、write
、recv
和send
都是支持全双工的;如何理解呢?
- 这里,在任何一台主机上,
TCP
连接既有发送缓冲区,也有接受缓冲区;所以就支持全双工(发送信息的同时,也可以接受信息)write
、send
接口,本质上就是将数据拷贝到TCP
的发送缓冲区中;而read
、recv
接口本质上就是从TCP
的接受缓冲区中将数据拷贝到内核中。- 对于数据什么时候发送、发送多少、出错了怎么办都由
TCP
控制;TCP
传输控制协议。
那也就是说,我们之前使用的read
、write
接口都是将数据交给了操作系统,也都是从操作系统中读取数据。
我们找直到
TCP
是面向字节流的,而之前的文件也是面向字节流的。之前在使用
write
和read
进行文件读写时,写端可以调用了多次write
,而读端可能一次调用read
就将写端写的所有信息都读取出来了;也可以写端写了一半的数据被读取上来了。所以,协议不仅要提供序列化和反序列化的方法;还要保证读取到的报文的完整性。
socket封装
这里简单对socket
进行封装,使用模版设计模式:
这里设计一个基类Socket
,其中包含纯虚函数:对socket
、bind
、connect
等的封装。
而基类中还存在CreateTcpServerSocket
方法,其中调用对socket
、bind
等封装好的纯虚方法。
class Socket
{
public:virtual void SocketOrDie() = 0;virtual void BindOrDie() = 0;virtual void ListenOrDie() = 0;virtual void AcceptOrDie() = 0;
public:void CreateTcpServerSocket(){SocketOrDie();BindOrDie();ListenOrDie();}
};
这里只罗列出了部分方法,在后续实现中进一步完善其中方法。
有了Socket
,现在就要实现TcpSocket
,而TcpSocket
类就要继承Socket
类,实现Socket
中的纯虚方法。
而Tcp
创建套接字:socket
、bind
、listen
这里就不详细介绍了。
class TcpSocket : public Socket
{
public:void SocketOrDie() override{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(Level::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;}void BindOrDie(int port) override{InetAddr addr(port);int n = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());if (n < 0){LOG(Level::FATAL) << "bind error";exit(SOCKET_ERR);}LOG(Level::DEBUG) << "bind success, sockfd : " << _sockfd;}void ListenOrDie(int backlog) override{int n = listen(_sockfd, backlog);if (n < 0){LOG(Level::FATAL) << "listen error";exit(SOCKET_ERR);}LOG(Level::DEBUG) << "listen success, sockfd : " << _sockfd;}
private:int _sockfd;
};
要绑定端口号,服务端就只需要端口号,这里端口号就通过参数传递;
而
listen
的第二个参数backlog
,这里也设置也可以通过参数传递,且也设置了缺省参数。这些参数都由调用用
CreateTcpServerSocket
来传递。
//Socket类
class Socket
{
protected:virtual void SocketOrDie() = 0;virtual void BindOrDie(int port) = 0;virtual void ListenOrDie(int backlog) = 0;virtual int AcceptOrDie() = 0;
public:void CreateTcpServerSocket(int port, int backlog = 6){SocketOrDie();BindOrDie(port);ListenOrDie(backlog);}
};
有了上述这些,服务端就只需要调用CreateTcpServerSocket
,传递端口号和backlog
(可以不传);即可创建套接字、绑定端口号和设置监听状态。
TcpServer
封装实现
有了上述实现的TcpSocket
,现在先来完善一点TcpServer
;
对于TcpServer
成员,这里就设置成智能指针对象;基类Socket
智能指针执行派生类对象。
对于TcpServer
构造函数,只需要调用Socket
类中的CreateTcpSeverSocket
方法将端口号传递进去即可。
class TcpServer
{
public:TcpServer() {}TcpServer(int port) : _socket(std::make_unique<TcpSocket>()){_socket->CreateTcpServerSocket(port);}private:std::unique_ptr<Socket> _socket;
};
这里简单测试一下,创建
TcpServer
对象,然后程序休眠,查看一下日志和listen
状态即可。
可以看到,创建套接字、绑定端口号和设置监听状态都是成功的。
那现在就要让服务器运行起来,就要有accept
获取连接请求。
accept
封装
TcpSocket
实现AcceptOrDie
,对accept
的封装。(AcceptOeDie
返回值暂时设置为int
)
accept
除了获取连接请求外,还会获取对方的struct sockaddr_in
和长度,这里就通过输出性参数见对方的sockaddr
传递出去(这里使用封装好的InetAddr
即可)
//Socket
class Socket
{
protected:virtual int AcceptOrDie(InetAddr *addr) = 0;
public:
};class TcpSocket : public Socket
{
public:int AcceptOrDie(InetAddr *addr) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (fd < 0){LOG(Level::FATAL) << "accept error";return -1;}LOG(Level::DEBUG) << "accept success";addr->Set(peer);return fd;}
private:int _sockfd;
};
这里如果获取连接请求失败,就直接返回-1
,由调用方去处理accept
的情况。
上述中返回的是
accept
返回的,用来通信的文件描述符;但是这里我们都对
socket
进行了封装,这里就可以直接返回一个std::shared_ptr<TcpSocket>
的智能指针对象;这样在调用读写操作时,就可以面向对象式调用。(统一化,
TcpServer
包含的就是指向Socket
的智能指针对象)
//Socket
class Socket
{
protected:// virtual int AcceptOrDie(InetAddr *addr) = 0;virtual std::shared_ptr<Socket> AcceptOrDie(InetAddr *addr) = 0;
public:
};
class TcpSocket : public Socket
{
public:TcpSocket(){}TcpSocket(int fd) : _sockfd(fd) {}std::shared_ptr<Socket> AcceptOrDie(InetAddr *addr) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (fd < 0){LOG(Level::FATAL) << "accept error";return nullptr;}LOG(Level::DEBUG) << "accept success";addr->Set(peer);return std::make_shared<TcpSocket>(fd);}private:int _sockfd;
};
实现了对accept
的封装,那在TcpServer
中,运行时,只需要通过智能指针对象调用AcceptOrDie
既可以获得用来通信的TcpSocket
。
获取连接请求成功之后,就要进行通信,这里使用多进程版;
子进程创建子进程,让孙子进程去执行。
子进程和父进程都要关闭不要的文件描述符,那对应的
Socket
就还需要提供一个Close
方法来关闭文件描述符。
对于孙子进程如何进行服务:
这里,就通过回调函数,由上层去决定如何进行服务。(这里要自定义协议,就先使用回调函数)
using func_t = std::function<void(std::shared_ptr<Socket> fd, InetAddr &client)>;
class TcpServer
{
public:TcpServer() {}TcpServer(int port, func_t func) : _socket(std::make_unique<TcpSocket>()), _func(func){_socket->CreateTcpServerSocket(port);}void Start(){while (true){InetAddr peer;auto fd = _socket->AcceptOrDie(&peer);if (fd == nullptr){exit(ACCEPT_ERR);}// 通信int id = fork();if (id < 0){LOG(Level::FATAL) << "fork error";exit(FORK_ERR);}else if (id == 0){_socket->Close();if (fork() > 0)exit(OK);_func(fd, peer);}else{// 父进程fd->Close();waitpid(id, nullptr, 0);}}}private:std::unique_ptr<Socket> _socket;func_t _func;
};//Socket
class Socket
{
public:virtual void Close() = 0;
};
class TcpSocket : public Socket
{
public:void Close() override{close(_sockfd);}
private:int _sockfd;
};
//TcpServer
using func_t = std::function<void(std::shared_ptr<Socket> fd, InetAddr &client)>;
class TcpServer
{
public:TcpServer() {}TcpServer(int port, func_t func) : _socket(std::make_unique<TcpSocket>()), _func(func){_socket->CreateTcpServerSocket(port);}void Start(){while (true){InetAddr peer;auto fd = _socket->AcceptOrDie(&peer);if (fd == nullptr){exit(ACCEPT_ERR);}// 通信int id = fork();if (id < 0){LOG(Level::FATAL) << "fork error";exit(FORK_ERR);}else if (id == 0){_socket->Close();if (fork() > 0)exit(OK);_func(fd, peer);}else{// 父进程fd->Close();waitpid(id, nullptr, 0);}}}
private:std::unique_ptr<Socket> _socket;func_t _func;
};
这样,服务器在获取连接请求成功后,就让孙子进程(孤儿进程),去完成服务,父进程继续等待连接请求。
至于如何进行服务,就由上层传递的回调函数来决定。
自定义协议
这里要实现网页版计算器,我们就要制定相关协议(结构化数据、序列化反序列化等等)
要自定义协议,首先要有结构化数据,这里定义Request
(请求结构化字段)、Responce
(结果结构化字段)以及协议字段protocol
class Request
{
public:Request() {}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) {}private:int _x;int _y;char _oper;
};class Responce
{
public:Responce() {}Responce(int result, int code) : _result(result), _code(code) {}private:int _result; // 结果int _code; // 标识计算是否出错
};
class protocol
{public:
};
到这里,本篇文章大致内容就结束了
简答总结:
- 序列化和反序列化
- 理解
TCP
面向字节流,支持全双工。- 协议需要提供对应的序列化和反序列化方法、并且要保证读取到报文的完整性。
socket
的封装、TcpServer
的封装实现- 自定义协议:结构化数据
Resquest
和Responce
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws