序列化和反序列化
我们知道,协议是一种约定;且在调用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
等封装好的纯虚方法。
cpp
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
这里就不详细介绍了。
cpp
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
来传递。
cpp
//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
方法将端口号传递进去即可。
cpp
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
即可)
cpp
//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
的智能指针对象)
cpp
//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
方法来关闭文件描述符。
对于孙子进程如何进行服务:
这里,就通过回调函数,由上层去决定如何进行服务。(这里要自定义协议,就先使用回调函数)
cpp
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
cpp
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