【网络】协议与网络版计算器
文章目录
- 1.协议的概念
-
- 1.1序列化与反序列化
- 2.网络版计算器
-
- 2.1封装套接字
- 2.2协议定制
-
- 2.2.1Jsoncpp
- 2.2.2报文处理
- 2.3会话层:TcpServer
- 2.4应用层:Calculate
- 2.5表示层:Service
- 2.6应用层、表示层和会话层->应用层
1.协议的概念
为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
通俗的讲,协议本质就是双方约定好的结构化的数据。
假设我们此时要实现一个网络版计算器,那么很明显我们需要“操作数1”、“操作符”、“操作数2”等多个信息,为了减小服务器的压力,我们一定不能将这些信息分批发送,一定是打包好成为一个数据发送给对端,那么此时就有了以下方案:
定制结构体+序列化和反序列化
- 定制结构体来表示需要交互的信息。
- 发送数据时将这个结构体按照一个规则转换成字符串,接收数据时再按照相同的规则把接收到的字符串转化为结构体。
- 这个过程叫做**“序列化”和“反序列化”**。
1.1序列化与反序列化
在之前实现的聊天室服务器中,用户发送数据实际上发送的不仅仅有“message”,还可能包括用户名、发送时间等信息,很明显这就是一个结构化的数据,那序列化就是将该结构化的数据转化为字符串方便网络发送,反序列化就是把信息一变多,方便上层读取处理。
那么read
、write
或者是recv
、send
函数是在网络中是如何工作起来的呢?
在系统中,我们是知道read
、write
函数的工作过程的,比如write
函数将用户缓冲区中的数据拷贝到文件描述符所指向的文件结构体的内核缓冲区,操作系统会在合适的时间将内核缓冲区的内容刷新到磁盘上。
那在网络中也是一样的,只不过内核缓冲区变成了传输层维护的发送缓冲区和接收缓冲区(实际上也是内核级缓冲区,这块是方便理解),那么什么时候发?怎么发?出错了怎么办?这些问题就是TCP协议需要考虑解决的问题,所以TCP协议即(Transmission Control Protocol)传输控制协议,这个控制就体现在这了。
所以read
、write
或者是recv
、send
函数本质上是拷贝函数,他们完成的工作无非就是将一块区域的数据拷贝到另一块区域,即发送数据的本质就是将自己的发送缓冲区中的数据拷贝到接收方的接收缓冲区,所以也可以说通信的本质就是拷贝,也证明双发的主机通信本质是双方操作系统在进行通信。
TCP协议支持全双工的原因也可以找到了:TCP协议拥有两块缓冲区:发送缓冲区、接收缓冲区,这两块缓冲区互不干扰,仅需一个文件描述符fd
就可以实现,因为写是向发送缓冲区写,读是在接收缓冲区读。
而且这些过程像极了系统中学习的管道、文件部分,比如read
函数为什么会阻塞,因为接收缓冲区中无数据,所以系统对于网络学习是十分重要的。
但既然TCP是面向字节流的,那我们该如何解决数据传输过程中数据缺失的问题呢?毕竟在传输层眼里数据都是字节流,无法识别出字段含义的。
为什么不能将结构体直接发送呢?还需要转化成字符串发送?
- 技术上:跨平台问题,不同操作系统结构体内存对齐方式不同,甚至可能有大小端的问题。
- 业务上:结构体成员可能会随着业务变化而变化,如果在通信过程不转化为字符串,那么在后期维护上会面临诸多问题,每次修改都可能会牵一发而动全身。
2.网络版计算器
2.1封装套接字
首先我们先将套接字进行封装,封装的主要目的是为了简化操作,让我们仅需调用几个函数就可以完成一些对于套接字的初始化工作。
我们可以设计一个父类Socket,内部包含有若干接口,然后再根据具体套接字对父类Socket进行实现,比如TcpSocket继承父类Socket,实现父类Socket的接口。
// 模板方法模式
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
};
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 Connector(InetAddr &addr) = 0;
virtual int SockFd() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &in) = 0;
// virtual void Other()=0;
public:
void BuildListenSocket(InetAddr &addr)
{
CreateSocketOrDie();
BindSocketOrDie(addr);
ListenSocketOrDie();
}
bool BuildClientSocket(InetAddr &addr)
{
CreateSocketOrDie();
return Connector(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", _sockfd);
}
void BindSocketOrDie(InetAddr &addr) override
{
// 2. 绑定
struct sockaddr_in local; // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。
bzero(&local, sizeof(local)); // 将从&local开始的sizeof(local)大小的内存区域置零
local.sin_family = AF_INET; // 设置网络通信方式
local.sin_port = htons(addr.Port()); // 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");
exit(BIND_ERROR);
}
LOG(DEBUG, "bind success,sockfd is : %d", _sockfd);
}
void ListenSocketOrDie() override
{
// 3. tcp是面向连接的,所以通信之前,必须先建立连接,服务器是被链接的
// tcpserver启动,未来首先要一直等待客户端的连接,listen
int n = listen(_sockfd, gbacklog);
if (n < 0)
{
LOG(FATAL, "listen error");
exit(LISTEN_ERROR);
}
LOG(DEBUG, "listen success,sockfd is : %d", _sockfd);
}
socket_sptr Accepter(InetAddr *addr) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept会阻塞等待,直到有客户端连接
int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error");
return nullptr;
};
*addr = peer;
socket_sptr sock = std::make_shared(sockfd);
return sock;
}
bool Connector(InetAddr &addr) override
{
// 构建目标主机的socket信息
struct sockaddr_in server;
memset(&ser