本篇笔记记录五种IO模型的基本概念,重点针对多路转接IO模型对我们的网络服务器进行设计,从而达到提升效率的效果。
我的上一篇Linux笔记:【Linux】网络基础(3)_柒海啦的博客-CSDN博客
让我们开始吧~
目录
一、五种IO模型
1.阻塞IO
2.非阻塞IO
3.信号驱动IO
4.多路转接IO
5.异步IO
IO模型之间的联系与区别
fcntl设置非阻塞IO
二、select与poll多路转接方案
1.select接口介绍
2.编写select的一般模式
3.总结一下select的优缺点:
4.poll接口介绍:
5.poll的优缺点
三、epoll多路转接方案
epoll_create
1.epoll的工作原理
epoll_ctl
epoll_wait
2.epoll的工作模式
四、基于epoll的Reactor模式
在本机通信的时候,实际上进程间通信就是一个IO的过程。
学习了网络通信后,我们知道网络实际上也是一个进程向另一个进程进行通信,也是一个IO的过程。
那么不妨重新介绍一下IO的概念:
IO = input && output
依据于冯诺依曼结构来说,就是访问外设的过程。输入和输出,所以一般情况就是先等数据就绪,就绪完后就对齐进行拷贝。
所以,IO还可以如下的进行表示:
IO = 等 + 数据拷贝
对于输入来说,等就是等待对应层将数据放入接收缓冲区的过程,等待完成后在拷贝给应用层。
对于输出来说,等就是等待发送缓冲区为空或者有剩余的过程,等待完成后从应用层拷贝到缓冲区内。
对于上面的等 + 数据拷贝,我们可以以网络套接字编程中recv/read为例子。
1.当我们read/recv的时候,如果底层缓冲区没有数据怎么办?阻塞进行等待。
2.当我们read/recv的时候,如果底层缓冲区有数据,read/recv会怎么办?拷贝。
但是往往拷贝就是一瞬间的事情,所以IO的时间取决于等待的过程。一个最简单的例子:C/C++使用scanf或者cin的时候就是输入IO操作,此时运行会发现运行窗口一直再等待阻塞。阻塞什么?等待输入数据!
所以,我们想要提高IO的效率的话,就要从等这方面下手脚。这样也就存在下面的五种IO模型进行优化操作。
阻塞IO就是最常见的IO。我们之前的使用的IO绝大多数都是使用的阻塞IO。
阻塞IO就是自己去等,一旦等待完成就完成拷贝。
非阻塞IO就不同了,它会在等待数据的过程中不去阻塞等待,而是直接返回,下一次再次调用再去检查释放准备好,没有准备好也是直接返回.....直到数据等待好了就进行拷贝。
我们将等待数据的过程交给专门的信号处理程序,一旦等待完成信号处理程序通知上层(SIGIO),上层便就调用接口直接进行拷贝。
通过一个系统调用,我们一次性批量等待大量的IO接口。一旦存在数据等待完成,上层一次性将批量等待的IO接口等待完成的完成拷贝数据。
核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
内核帮助应用程序对特定文件描述符进行等待IO资源,当等待成功,内核拷贝数据完成后,在通知应用程序并且拷贝到用户空间。
首先,我们需要理解一下异步IO和同步IO之间的关系。
这里的同步IO不同于线程中讲解的线程同步。此同步表示在IO过程中参与了其中的一个步骤(阻塞IO:等 + 拷贝,信号驱动IO:拷贝),在发起调用后,最终是自己获取的结果。
但是异步IO完全是交给别人去做,自己不参与IO的过程。也就是说它发起调用,会立即返回但是不会存在结果,是由别人-真正的被调用者通过状态、通知来通知调用者,处理别人获取的结果。
对于阻塞IO和非阻塞IO来说,阻塞IO在等待的过程中,是会被cpu链入对于资源的等待队列中的,所以当前线程实际上是会被挂起的。但是非阻塞IO如果不能得到结果,该调用是不会阻塞当前进程的。
针对非阻塞IO的这种特性,我们代码书写的时候一般是循环来写的。那么我们有没有一种统一的方法对文件描述符设置为非阻塞IO的形式呢?(通过各个接口设置的方式不一样,并且都默认是阻塞IO,所以非阻塞IO是需要自己手动设置的)并且在实际操作中,我们如何对非阻塞IO究竟是出错还是-输入已经空了、输出已经满了进行判断呢?
我们可以使用fcntl函数对文件描述符的属性进行设置。
man fcntl
头文件:
#include
#include
函数描述:int fcntl(int fd, int cmd, ... /* arg */ );
fcntl()执行一个操作下面描述的fd打开的文件描述符。操作由cmd决定。
函数参数:
fd:操作的文件描述符。
cmd:
F_GETFL 获取文件访问模式和文件状态标志;Arg被忽略。
F_SETFL 将文件状态标志设置为arg指定的值。文件访问方式(O_RDONLY, O_WRONLY, O_RDWR)
......
arg:
arg中的文件创建标志(即O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC)被忽略。在Linux上这个命令只能修改O_APPEND、O_ASYNC、O_DIRECT、O_NOATIME和O_NONBLOCK标志。
其中O_NONBLOCK标志就是非阻塞标志。
返回值:
出错了返回-1,并且设置error。
设置了非阻塞IO后,由于是如果失败返回-1,设置错误码,这个失败包含两种情况:
1.等待失败(比如in的时候接收缓冲区为空,或者out的时候发送缓冲区满了),这种情况是正常的。errno会被设置为EWOULDBLOCK或者EAGAIN。
2.IO中断。这个调用被一个信号给中断。也就是说可能在等文件资源的时候,被系统唤醒,等待中断了。errno会被设置为EINTR。这也是正常的,重新进行即可,但是概率非常小。
3.真正的错误。进行差错处理。
我们下面利用我们经常使用的标准输入描述符进行实验,查看设置非阻塞IO的基本流程以及如何去使用非阻塞IO接口。
#include
#include
#include
#include
int main()
{
// 注意标准输入描述符是默认打开的,并且是0号fd哦,当然也可以直接stdin
int fl = fcntl(0, F_GETFL); // 首先获取原本的文件标志状态,保证修改的时候不动原来的状态
int flag = fcntl(0, F_SETFL, fl | O_NONBLOCK); // 设置此文件的文件标志状态,增加非阻塞状态
printf("请输入: ");
char buffer[1024];
while (true)
{
ssize_t n = read(0, buffer, sizeof(buffer) - 1); // 0此时没有作用了,因为标准输入文件描述符标志状态设置为非阻塞了
if (n > 0)
{
// 此时是读取成功的,进行打印
buffer[n] = 0;
std::cout << buffer << std::endl;
}
else if (n < 0)
{
// 读取失败,判断两种情况
if (errno == EWOULDBLOCK || errno == EAGAIN)
{
// 此时只是读取缓冲区为空而已
std::cout << "null" << std::endl;
sleep(1);
}
else if (errno == EINTR)
{
// 此时小概率被IO中断了
continue;
}
else{
// 此时才是真正的错误
std::cout << "read error:" << errno << strerror(errno) << std::endl;
break;
}
}
}
return 0;
}
现象果然如上面所说。
那么,现在我们需要在我的自定义组件MySock文件中加入设置非阻塞的选项,供之后使用,就需要增加setNonBlock接口。针对于MySock文件,详细介绍可以在我的这两篇文章寻找答案,实际上就只是对套接字编程进行一个基本封装而已,加上了log日志打印文件而已。
【Linux】网络基础(1)_柒海啦的博客-CSDN博客
【Linux】网络套接字编程_柒海啦的博客-CSDN博客
源文件可以在我的仓库中提出哦:linux日常练习/EpollTcpTest/ET/MySock.hpp · 刘凌宇/Linux_Test - Gitee.com
增加接口代码:
// 设置对此资源设置非阻塞IO,出错会设置致命出错日志
static bool setNonBlock(int sock)
{
int fl = fcntl(sock, F_GETFL); // 获取当前sock底层的读写标记位 - 保持当前的不变
if (fl > 0)
{
int flag = fcntl(sock, F_SETFL, fl | O_NONBLOCK); // 在不动原来的基础上进行一个非阻塞
if (flag != -1) return true;
}
// 这里都是出错了
logMessage(FATAL, "fcntl error %d:%s", errno, strerror(errno));
return false;
}
观察上面的五种IO模型,在等待一批量的文件描述符的时候,除开多路转接IO外,其余都是一个一个进行等待的,但是多路转接IO是同时等待。这样的话它就在单位时间,等的比重就会非常低,所以效率就会很高。
所以,下面我们针对多路转接,操作系统为我们提供了select、poll、epoll等接口实现,我们逐步用这些方案实现一个成熟的服务器出来。使用多路转接后,就不像之前服务器一样需要开多线程或者多进程执行,减少操作系统的压力。
前面我们已经提到过多路转接方案了。它这种IO模型是一次性等待一批文件描述符,从而缩短整体等待的时间,达到提高效率的效果。
select接口就是系统给我们的一个多路转接方案,select只解决等的问题。所以它会:1.帮用户一次等待多个文件sock。2.当哪些文件sock就绪了,select通知用户,就绪的sock有哪些,用户调用io接口读取即可。
man select
头文件:
#include
函数描述:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);select()允许程序监视多个文件描述符,直到一个或多个文件描述符为某些类型的I/O操作(例如,可能的输入)“准备好了”。如果一个文件描述符能够在不阻塞的情况下执行相应的I/O操作(例如,read(2)),那么它就被认为是就绪的。
函数参数:
nfds:监听中最大的文件描述符+1。
readfds、writefds、exceptfds:
操作系统提供的类型:fd_set。本质是一个位图,表示文件描述符集,并且存在上限:1024个。这三种,分别将fd设进去表示操作系统需要帮助我们关心什么IO操作:(读、写、异常是否就绪),然后返回的时候,对应fd_set如果存在对应的fd表示事件就绪了。所以它是一个输入输出参数。
对于fd_set这个位图结构,我们不能直接操作,需要利用系统提供的接口对位图进行扩展操作。
void FD_CLR(int fd, fd_set *set); // 删除位图中对应的fd。
int FD_ISSET(int fd, fd_set *set); //检查对应fd是否存在位图中,存在返回1否则0.
void FD_SET(int fd, fd_set *set); // 将对应fd添加到为位图中去
void FD_ZERO(fd_set *set); // 将此位图结构清空timeout:是一个结构体,用来描述时间 - 阻塞的时长:
struct timeval-获取当前时区的时间。
{time_t tv_sec - 时间戳
suseconds_t tv_usec; // 微妙级别的}
输入参数的时候,表示阻塞一次需要多少时间。输出参数的时候表示基于需要的时间还剩下多少时间。
返回值:
如果成功,select()和pselect()返回三个返回的描述符中包含的文件描述符的数量。如果出错,返回-1并且设置errno。
通过上面的接口介绍,我们可以总结一下通过select编写IO的一般模式:
1、nfds,随着获取的sock的越来越多,注定了nfds都可能发生变化。-最大文件描述符+1。
2、rfds、wfds、efds都是输入输出型参数。输入输出不一定是一样的。每一次对rfds重新添加。
3、timeout也是输入输出,每一次需要重置,前提是你需要的话。
通过1、2将我们自己合法的文件描述符全部保存起来,用来支持:1-更新最大fd,2.更新fds-位图结构。
所以我们就需要需要一个第三方数组,保存合法的fd。针对这个数组:1、遍历数组,更新最大值。2、遍历数组,添加关心的fd到set中。3、事件检测。4、遍历数组,找到就绪的事件,完成对应的动作:连接、读取。扩展:引入写入。(现在不好改写,epoll解决。)数组需要1024个即可。对于数组的处理:只要获取了sock,添加到数组里即可。如果满了,表示已经超过select监管个数了,满了,你没有位置了,丢弃此链接!如果存在,就添加到我们管理的文件描述表中即可。
处理就绪套接字资源的时候,需要串行执行:原本是-1,跳过 ,如果FD_ISSET(fd, &fd_set)就是判断合法文件描述符是否事件就绪了。读事件就绪了就一定是read嘛,自然还有监听套接字。判断是否是监听套接字,监听套接字就获取链接添加sock,否则就是read!
对于IO事件的处理,就read来说,分为三种情况,针对这三种情况分别要做不同的处理,编码的时候需要特别注意。
下面,我们首先基于之前的log、mysock文件进行编写一个select实现的多路转接服务器,但是只处理读的事件,也就是说利用客户端进行测试的时候,服务器只需要打出信息即可,不做其他的处理。
#ifndef _SELECT_SERVER_H_
#define _SELECT_SERVER_H_
#include "MySock.hpp"
#include
#include
#include
const int NUM = (sizeof(fd_set) * 8); // 定义fd_set最多接收的fd个数 - fd_set是一个位图 一个字节是八个比特位(一个比特位从右到左表示0123.....)
const int FD_NONE = -1; // 设定fd的初始值为-1,表示此处fd空缺,可以填入或者跳过
class SelectServer
{
public:
SelectServer(const uint16_t port = 8080)
{
_listenSock = QiHai::Sock::socket();
QiHai::Sock::bind(_listenSock, port); // 服务器默认绑定0.0.0.0 ip
QiHai::Sock::listen(_listenSock);
// 首先对_fdsArray进行初始化,好存放我们需要处理的fd,并且处理后的结果
for (int i = 0; i < NUM; ++i) _fdsArray[i] = FD_NONE;
_fdsArray[0] = _listenSock; // 默认监听套接字为第一个
}
void Start()
{
// 首先,将通过监听套接字获取链接视为一种IO资源,我们是读取其中的链接的
fd_set rfds;
while(true)
{
// 因为rfd为输入输出参数,所以每次循环需要对其进行更新
FD_ZERO(&rfds); // 将文件集清空 - 本质是一个位图
// 每次需要重新将输入的fd_set位图结果进行更新
int maxFd = _listenSock;
for(int i = 0; i < NUM; ++i)
{
if (_fdsArray[i] != FD_NONE)
{
FD_SET(_fdsArray[i], &rfds);
if (_fdsArray[i] > maxFd) maxFd = _fdsArray[i];
}
}
int n = select(maxFd + 1, &rfds, nullptr, nullptr, nullptr); // 最后一个参数nullptr为阻塞进行处理
switch(n)
{
case 0:
// 表示当前链接的所有fd没有就绪的 - 还需要进行等待 但是阻塞等待的话就不存在了
logMessage(DEBUG, "select timeout......");
break;
case -1:
logMessage(WARNING, "select error:%d-%s", errno, strerror(errno));
break;
default:
// 出现文件就绪成功 IO = 等待 + 拷贝,等待成功!
HandlerEvent(rfds);
break;
}
}
}
~SelectServer()
{
if (_listenSock >= 0) close(_listenSock);
}
private:
// 此时select负责的多个fd中存在就绪的了,需要对其进行处理
void HandlerEvent(fd_set& rfds)
{
//明确rfds是一个输入输出型参数,此时是输出型参数,如果对应fd等待资源就绪,内核会进行一个设置
for (int i = 0; i < NUM; ++i)
{
if (_fdsArray[i] == FD_NONE) continue;
if (FD_ISSET(_fdsArray[i], &rfds)) // 表示此时对应的就绪了
{
if (_fdsArray[i] == _listenSock) Accepter(); // 表示连接服务
else Recver(i); // 正常的读服务
}
}
}
void Accepter()
{
// 连接服务
int sock = QiHai::Sock::accept(_listenSock); // 此时是就绪的,不用阻塞进行等待了 debug模式里面会打印客户端ip和port
int pos = 1; //从1开始
for (; pos < NUM; ++pos)
{
if (_fdsArray[pos] == FD_NONE) break;
}
if (pos == NUM)
{
// 此时select托管的fd满了,不好意思,只能抛弃此连接了
close(sock);
logMessage(WARNING, "accept error,_fdsArray overload......");
}
else _fdsArray[pos] = sock;
}
void Recver(int pos)
{
// 读取服务
// 当前应该配合协议进行读取,但是当前为了实现简单没有对数据进行处理
char buffer[1024];
int n = recv(_fdsArray[pos], buffer, sizeof(buffer) - 1, 0); // 应该也是无需阻塞等待,直接读取
if (n > 0)
{
buffer[n] = '\0';
logMessage(DEBUG, "fds[%d]# %s", pos, buffer);
}
else if (n == 0){
// 对方关闭连接
close(_fdsArray[pos]);
_fdsArray[pos] = FD_NONE;
logMessage(WARNING, "fds[%d] close, me too ......", pos);
}
else{
// 读取出现错误
logMessage(ERROR, "recv error %d-%s", errno, strerror(errno));
close(_fdsArray[pos]);
_fdsArray[pos] = FD_NONE;
}
}
private:
int _listenSock;
int _fdsArray[NUM];
};
#endif
运行结果:
优点:-任何一个多路转接都是这样
a、效率高!-和之前的比。单位事件内,等的比重小了。
b、应用场景:有大量的连接,但是只有少量是活跃的!
缺点:
a、为了维护第三方数组,select服务器充满了大量的遍历 - On OS底层关心的时候也是存在大量的遍历的。
b、每一次都要对select输出参数重新设定。
c、能够同时管理的fd的个数是存在上限的。-fd_set。-1024bit
d、因为几乎每一个参数都是输入输出型的,决定了select一定频繁的用户到内核,内核到用户参数数据拷贝。
e、编码比较复杂。(前四个缺点导致的)
针对于select缺点中的fd的上限个数,以及参数是输入输出型的,poll函数对齐进行了优化。
man poll
头文件:
#include
函数描述:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
Poll()执行与select(2)类似的任务:它等待一组文件描述符中的一个准备好执行I/O。
函数参数:
fds:类型为pollfd,为内核提供:
struct pollfd{
int fd; // 不会对fd 用户和内核都不会改其
short events; // 请求事件 -- 用户写的,内核只对其读取
short revents; // 响应事件,内核告诉用户哪些事件就绪了。};
基于select,实际上把之前fd_set三个位图fd表示的两种意思分离了。就使用一个数组进行搞定。由于fd_set是固定的大小所以只能是固定的fd个数。而现在数组个数完全由用户决定,理论上没有上限的。并且由于输入输出参数的分离,就不用每次对参数进行重新设定了。
其中请求和响应事件存在对应的宏作为读、写、异常的关心:
POLLIN、POLLOUT、POLLERR、POLLPRI、 POLLRDHUP....
(可读 可写 错误 高优先级数据可读 TCP对方关闭连接)
nfds:决定等待的文件描述符个数。
timeout:输入型参数,单位为毫秒,表示阻塞一次的时间。(同理,0为非阻塞,-1为阻塞,其余就是阻塞一次的时间)
返回值:
-1表示错误。否则就是等待就绪的文件描述符个数。
poll的工作模式和select类似,这里不在过多赘述,我们可以直接在之前那份代码上简单修改几处就能修改为poll的工作模式。
#ifndef _POLL_SERVER_H_
#define _POLL_SERVER_H_
#include "MySock.hpp"
#include "poll.h"
#include
#include
const int FD_NONE = -1; // 设定fd的初始值为-1,表示此处fd空缺,可以填入或者跳过
class PollServer
{
public:
PollServer(const uint16_t port = 8080, int nfds = 1024):_nfds(nfds)
{
_listenSock = QiHai::Sock::socket();
QiHai::Sock::bind(_listenSock, port); // 服务器默认绑定0.0.0.0 ip
QiHai::Sock::listen(_listenSock);
_pollFds = new pollfd[_nfds];
for (int i = 0; i < _nfds; ++i)
{
_pollFds[i].fd = FD_NONE;
_pollFds[i].events = _pollFds[i].revents = 0; // 置空
}
_pollFds[0].fd = _listenSock;
_pollFds[0].events = POLLIN; // 只读事件
}
void Start()
{
// 首先,将通过监听套接字获取链接视为一种IO资源,我们是读取其中的链接的
while(true)
{
int n = poll(_pollFds, _nfds, -1); // 最后一个参数-1为阻塞的进行读取哦
switch(n)
{
case 0:
// 表示当前链接的所有fd没有就绪的 - 还需要进行等待 但是阻塞等待的话就不存在了
logMessage(DEBUG, "poll timeout......");
break;
case -1:
logMessage(WARNING, "poll error:%d-%s", errno, strerror(errno));
break;
default:
// 出现文件就绪成功 IO = 等待 + 拷贝,等待成功!
HandlerEvent();
break;
}
}
}
~PollServer()
{
if (_listenSock >= 0) close(_listenSock);
delete[] _pollFds;
}
private:
// 此时select负责的多个fd中存在就绪的了,需要对其进行处理
void HandlerEvent()
{
//明确rfds是一个输入输出型参数,此时是输出型参数,如果对应fd等待资源就绪,内核会进行一个设置
for (int i = 0; i < _nfds; ++i)
{
if (_pollFds[i].fd == FD_NONE) continue;
if (_pollFds[i].revents & POLLIN) // 表示此时对应的就绪了
{
if (_pollFds[i].fd == _listenSock) Accepter(); // 表示连接服务
else Recver(i); // 正常的读服务
}
}
}
void Accepter()
{
// 连接服务
int sock = QiHai::Sock::accept(_listenSock); // 此时是就绪的,不用阻塞进行等待了 debug模式里面会打印客户端ip和port
int pos = 1; //从1开始
for (; pos < _nfds; ++pos)
{
if (_pollFds[pos].fd == FD_NONE) break;
}
if (pos == _nfds)
{
// 此时select托管的fd满了,不好意思,只能抛弃此连接了
close(sock);
logMessage(WARNING, "accept error,_fdsArray overload......");
}
else{
_pollFds[pos].fd = sock;
_pollFds[pos].events = POLLIN;
}
}
void Recver(int pos)
{
// 读取服务
// 当前应该配合协议进行读取,但是当前为了实现简单没有对数据进行处理
char buffer[1024];
int n = recv(_pollFds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 应该也是无需阻塞等待,直接读取
if (n > 0)
{
buffer[n] = '\0';
logMessage(DEBUG, "fds[%d]# %s", pos, buffer);
}
else if (n == 0){
// 对方关闭连接
close(_pollFds[pos].fd);
_pollFds[pos].fd = FD_NONE;
logMessage(WARNING, "fds[%d] close, me too ......", pos);
}
else{
// 读取出现错误
logMessage(ERROR, "recv error %d-%s", errno, strerror(errno));
close(_pollFds[pos].fd);
_pollFds[pos].fd = FD_NONE;
}
}
private:
int _listenSock;
int _nfds;
struct pollfd* _pollFds;
};
#endif
优点:
1.效率高
2.有大量的链接,只有少量的是活跃的-节省资源
3.输入输出参数分离,不需要进行大量的重置。
4.poll参数级别,没有可以管理fd的上限。
缺点:
***1.poll仍然避免不了结构体数组,还是需要进行遍历的,在用户从检测事件就绪,内核检测fd就绪,都是一样。
2.poll需要内核到用户的拷贝。 -- 少不了
*3.poll编码也不容易。--比select容易。
可以看到,虽然在一定程度上做了简化,参数分离,提高上限等操作,但是还是避免不大量的遍历。这些问题的原因就是用户在对这一批fd进行组织管理的。
那么我们能不能将管理这一批fd也交给操作系统呢?并且操作系统做相关的回调设置,减少遍历的操作吗?当然可以,epoll就是这样完美解决这些问题的方案。
epoll是为了处理大批量句柄而改进的poll的接口。
epoll针对poll的缺点,-目的是为了解决用户来维护数组,并且不断循环拷贝(内核->用户)的过程,解决这些只能在内核层自己处理。
所以首先在操作系统内部创建这个管理系统加了epoll_create进行创建,epoll_ctl提供了是否添加、修改、删除对应fd的一个监控模式,而epoll_wait则提供了拿出就绪文件描述符的功能。
man epoll_create
头文件:
#include
函数描述:
int epoll_create(int size);
epoll_create()创建一个epoll实例。从Linux 2.6.8开始,size参数被忽略,但必须大于零;
epoll_create()返回指向新的epoll实例的文件描述符。该文件描述符用于对epoll接口的所有后续调用。当不再需要时,该文件epoll_create()返回的描述符应该使用close关闭。当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放用于重用的关联资源。
如果失败返回-1,并且设置errno。
因为操作系统需要在内核为我们维护,那么自然需要先组织。
针对于用户想要关心的fd是否就绪,内核采用的是红黑树进行组织。红黑树是自平衡的搜索二叉树,可以通过此链接进行了解哦(红黑树的插入实现)~
所以,这样就避免了像select、poll,利用用户的数组进行遍历,内核查看哪些需要进行关心,而是用户直接到内核进行注册,这样OS每次就对这颗树进行检查,避免频繁的遍历以及切换状态。
另外,用户曾经在红黑树注册的fd的事件,会在底层网卡的驱动程序中设置一个回调函数(OS是如何直到网卡内存在数据呢?--TCP/IP协议栈,想传给上层网络层、传输层的时候 采用硬件中断的方式。网卡-中断通知>cpu-执行void(*hanlder[])()-中断向量表->read_netcard()此时就可以从网卡内搬到操作系统内部了。-此时在经过网络协议栈进行一步步,最后通知上层。转化为一个值保存到寄存器里-索引),设置了回调函数后,如果对应的事件就绪,此时不用关心红黑树,插入进就绪队列。此时不用再关心红黑树,直接队列送上去即可。-内核告诉用户哪些事件就绪了.
针对于poll、select,他们的工作原理如下,可以做一个更好的比对:
1.无论是select、poll都是需要用户自己维护数组进行保存fd和特定事件的。--成本、程序员承担
2.select、poll都需要遍历。(内核和用户都要遍历)
3.poll、select工作模式:
a、通过系统调用,用户告诉内核哪些fd上的哪些事件
b、通过系统的返回,内核告诉用户哪些fd的哪些事件已经发生了。
所以,我们往epoll模型中的红黑树进行插入、修改、删除对应fd关心时,使用的接口就是epoll_ctl。
man epoll_ctl
头文件:
#include
函数描述:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
这个系统调用对文件描述符epfd引用的epoll实例执行控制操作。它请求对目标文件描述符fd执行操作op。
函数参数:
epfd:epoll_create获得的epoll模型的文件描述符。
op:对对应的fd如何操作。
EPOLL_CTL_ADD // 添加fd关心事件
EPOLL_CTL_MOD //更改更改与目标文件描述符fd关联的事件事件
EPOLL_CTL_DEL //从epfd引用的epoll实例中删除(取消注册)目标文件描述符fd。(需要注意,一般时先从epoll模型中删除fd,然后在close,否则可能存在bug)
fd:添加对应关心的事件描述符。
event:关心的事件。
struct epoll_event // 系统创建的类型
{
uint32_t events; // epoll事件
epoll_data_t data; // 用户数据变量
};
epoll事件:
EPOLLIN 读事件
EPOLLOUT 写事件
EPOLLHUP 关联文件描述符挂起。没有必要等它。(网络中对方关闭连接)
EPOLLERR 相关文件描述符发生错误。
EPOLLET 为关联的文件描述符设置边缘触发行为。(默认为水平触发行为,此行为为多路转接的工作模式,后续会讲)
epoll_data_t 为一个联合体,用户自己设定想要返回什么信息:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;返回值:
成功返回0,失败返回-1,设置errno。
当监视的fd存在关心事件就绪的时候,从epoll模型维护的就绪队列中提出来的方法时epoll_wait。
man epoll_wait
头文件:
#include
函数描述:
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);epoll_wait()系统调用等待文件描述符epfd引用的epoll实例上的事件。由事件指向的内存区域将包含调用者可用的事件。epoll_wait()最多返回maxevents。maxevents参数必须大于零。
函数参数:
epfd:epoll模型的文件描述符。
events:用户保存就绪事件的数组。
maxevents:期望就绪事件个数。(返回来的事件个数最多不超过期望个数)
timeout:和poll类似,大于0设置阻塞时间,等于0表示非阻塞,小于0表示阻塞等待。
返回值:
当成功时,epoll_wait()返回为请求的I/O准备好的文件描述符的数量,如果在请求的超时毫秒内没有文件描述符准备好,则返回零。当一个发生错误时,epoll_wait()返回-1,并适当设置errno。(这个数量就是存在在数组的整个个数,从0下标开始到此返回数 - 1)
实际上,人们针对于这个内核等待fd就绪事件发生,如何通知用户有着不同的操作模式。这里就涉及到了多路转接的工作模式了。
epoll针对事件就绪后通知用户的行为,增加了其工作模式,以便应对不同的IO模式。
我们以快递小哥发送快递为例。
假设快递小哥A拿到多个人的包裹,他在发送快递的时候,每一个快递都是等别人真正从他手中拿过的时候,他才进行下一个快递的发放。
但是快递小哥B不同,他拿到多个包裹后,不同的快递通知不同的人时,每个只是通知一遍,通知后他会放到一个位置走了,继续下一个快递的发放。如果同一个人的快递临时多了一个,他会在通知这个人一遍。
实际上,快递小哥A的工作模式就是水平触发模式(Level Triggered),简称LT。而快递小哥B的工作模式就是边缘触发模式(Edge Triggered),简称ET。
默认条件下,epoll模型设置的是LT模式,对于poll和select的工作模式也是LT模式。就是说一旦存在关心的事件就绪了,会不断的通知你(通过wait接口返回的就绪个数)。但是ET只有数据首次到达或者发生变化的时候才会通知。
针对于这两种模式,很明显,显然ET模式的工作效率会很高。但是即使是LT模式,我每次拿数据的时候也一次性拿完,效率是和ET模式下差不多的。
LT模式 VS ET模式原则上谁更高效?
ET模式。
1.更少的返回次数。
2.ET模式会倒逼程序员将接收缓冲区中的数据全部取走,应用层就更快的取走了缓冲区的数据,单位时间下,此服务器在一定程度上会给发送方一个更大的接收窗口,对方就可以有更大的滑动窗口,提高IO吞吐。
ET模式的代码复杂度比较高。
所以,我们通过ET模式可以发现一个问题,它每次通知我们对应的fd关心事件的时候,如果是读取。没有读完的话,那么下次就读不到了,获取不到完整的数据(LT模式下下次还能继续)。所以在ET模式下,我们需要读取一次完整的读完。
那么,这里就需要对IO的特性做出区别了。由于读取的时候,可能因为用户的接收缓冲区过小或者发生信号中断的话,就不能一次性的从接收缓冲区中读取完全,所以我们需要循环的去读。但是,如果是阻塞IO的话,读取完后发现接收缓冲区为空会发生阻塞,此时此进程就会被挂起。这怎么可以呢?所以需要非阻塞IO进行轮询读取,最后根据错误码进行判断是缓冲区空了还是失败了。
所以,基于ET的工作环境,我们需要非阻塞IO进行读取。
那么,现在我们综合实现一个完善的服务器。利用epoll的多路转接接口和ET工作模式。
需求如下:
1.实现网络版的计算器。
2.服务器对于超时连接-比如超时20s没有发送任何请求,需要主动断开。
3.采用epoll的多路转接,不出现任何多执行流。
综合上述需求,我们实际编码的时候,需要自己定制协议(协议可以采用现成的Json,自己解决粘包即可)。之前的代码中,因为只解决了读的需求,每个fd都是共享一个自己的用户缓冲区,现在在ET、并且要处理读和写事件下,需要每个fd都能维护两个缓冲区,并且采用非阻塞IO的方式,实现TCP网络版的计算器。
参考代码如下:(注:MySock、log头文件没有提供编码)
// Epoll.hpp
#ifndef __EPOLL_HPP__
#define __EPOLL_HPP__
#include
#include
#include "log.hpp"
// 创建epoll模型,使用此epoll模型进行操作
class Epoll
{
const static int gsize = 256;
const static int gtimeout = 10000; // 10s
private:
int _epfd;
public:
Epoll():_epfd(-1)
{}
// 创建epoll模型
bool EpollCreate()
{
_epfd = epoll_create(gsize); // gsize需要注意是一个废弃的参数,这里随便设置即可
if (_epfd < 0) logMessage(FATAL, "epoll_create error %d:%s", errno, strerror(errno));
return _epfd != -1;
}
// 增添监听对象到epoll模型中来,并且说明需要关心的事件 如果需要使用ET模式进行监听,需要在event选项添加EPOLLET,需要注意,默认就是LT模式
bool AddSockToEpoll(int sock, int event)
{
struct epoll_event epev;
epev.events = event;
epev.data.fd = sock;
int flag = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &epev);
if (flag < 0) logMessage(FATAL, "epoll_ctl error %d:%s", errno, strerror(errno));
return flag != -1;
}
// 修改sock在epoll所关心的状态
bool CtlSockToEpoll(int sock, int event)
{
// 进来先添加ET模式
event |= EPOLLET;
struct epoll_event epev;
epev.events = event;
epev.data.fd = sock;
int flag = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &epev);
if (flag < 0) logMessage(FATAL, "epoll_ctl error %d:%s", errno, strerror(errno));
return flag != -1;
}
// 删除sock在epoll对应的监听
bool DelFromEpoll(int sock)
{
int flag = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
if (flag < 0) logMessage(FATAL, "epoll_ctl error %d:%s", errno, strerror(errno));
return flag != -1;
}
// 从epoll模型的就绪队列取出num个(num是期望个数),并且以timeout毫秒的时候进行一次阻塞。-1为阻塞,0非阻塞,默认5s一次阻塞
// 需要注意的是,如果是ET模型,只有等待对象的缓存区出现变化后才会通知,并且只通知一次,如果通知后没有取走或者取完,此就绪队列就不会存储其相关的就绪事件了
int WaitEpoll(epoll_event events[], int num, int timeout = gtimeout) // 默认5s进行进行一次阻塞
{
return epoll_wait(_epfd, events, num, timeout);
}
~Epoll()
{
if (_epfd >= 0) close(_epfd);
}
};
#endif
// Protocal.hpp 自己定制协议
#ifndef __PROTOCAL_HPP__
#define __PROTOCAL_HPP__
#include
#include
#include
#include
// 自定义协议 - 使用json封装一下
namespace QiHai
{
static const char* SEP_BEGIN = "{";
static const char* SEP_END = "}";
static const int SEP_LEN = strlen(SEP_END);
// 请求
class Request
{
public:
Request() = default;
Request(int x, int y, char op)
:_x(x), _y(y), _op(op)
{}
// 序列化
std::string serialization()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
Json::FastWriter writer;
return writer.write(root);
}
// 反序列化
bool deserialization(std::string request)
{
Json::Reader read;
Json::Value root;
if (!read.parse(request, root)) return false;
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asInt();
return true;
}
int _x;
int _y;
char _op;
};
// 响应
class Response
{
public:
Response() = default;
Response(int state, int result)
:_state(state), _result(result)
{}
// 序列化
std::string serialization()
{
Json::Value root;
root["state"] = _state;
root["result"] = _result;
Json::FastWriter writer;
return writer.write(root);
}
// 反序列化
bool deserialization(std::string request)
{
Json::Reader read;
Json::Value root;
if (!read.parse(request, root)) return false;
_state = root["state"].asInt();
_result = root["result"].asInt();
return true;
}
int _state; // 状态:1正确 0异常
int _result; // 结果:正常就是计算结果,否则就是错误编号
};
// 解决粘包以及不是独立的一个报文问题:这里采用的是Json的格式{"1":1}...
void SpliteMessage(std::string& message, std::vector& messages)
{
while (true)
{
// 首先找到第一个{的位置
auto begin = message.find(SEP_BEGIN);
if (begin == std::string::npos) break; // 没有找到一个完整报文
auto end = message.find(SEP_END);
if (end == std::string::npos) break; // 同理
std::cout << "DEBUG: " << message.substr(0, end + 1) << std::endl;
messages.push_back(message.substr(0, end + 1)); // {}
message.erase(0, end + SEP_LEN);
}
}
}
#endif
// TCPServer.hpp
#ifndef __TCPSERVER_HPP__
#define __TCPSERVER_HPP__
#include
#include
#include
#include "MySock.hpp"
#include "log.hpp"
#include "Epoll.hpp"
#include "Protocal.hpp"
namespace QiHai
{
// 用户管理
class User
{
public:
std::string _ip;
std::uint16_t _port;
User(std::string ip, uint16_t port)
:_ip(ip), _port(port)
{}
};
class TcpServer;
class Connection;
using func_t = std::function; // 注意前面需要声明哦
using service_logic = std::function;
static const time_t MAX_TIME = 20; // 最大时间间隔
// 每个sock对应一个连接类进行管理,里面存在其输入输出缓冲区,以及对应读取、写入、异常的回调方法
class Connection
{
public:
int _sock;
// 三个回调函数
func_t _recv_cb;
func_t _send_cb;
func_t _except_cb;
// 接收缓冲却和发送缓冲区
std::string _inbuffer; // 当前存在bug,不能处理二进制流,但是文本是可以的
std::string _outbuffer;
TcpServer* _tsvr; // 对服务器的一个回调指针,后续会使用到
public:
time_t _time; // 当前的时间戳
Connection(int sock, TcpServer* tsvr)
:_sock(sock), _tsvr(tsvr)
{}
// 设置三种回调函数
void SetCallBack(func_t recv_cb, func_t send_cb, func_t except_cb)
{
_recv_cb = recv_cb;
_send_cb = send_cb;
_except_cb = except_cb;
}
~Connection()
{}
};
class TcpServer
{
const static uint16_t gport = 8080;
const static int gnum = 128;
private:
int _listensock;
uint16_t _port;
Epoll _poll;
std::unordered_map _connections; // 组织链接
std::unordered_map _users; // 组织用户
struct epoll_event* _revs; // 读取就绪事件的缓冲区
int _revs_num; // 每次读就绪事件的大小,这里初始化的时候固定大小即可
service_logic _call; // 处理业务逻辑的函数,由上层提供
private:
// 将此sock建立一个连接对象描述起来,并且连接到map。设置三个回调函数。默认关心的事件是读取,写入和异常后续设置
// 任何多路转接的服务器,一般默认只会打开对读取事件的关心,写入事件按需打开
void AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb)
{
if(!QiHai::Sock::setNonBlock(sock)) exit(2); // 设置其属性为非阻塞
Connection* connection = new Connection(sock, this);
connection->_time = time(nullptr); // 初始时间戳
connection->SetCallBack(recv_cb, send_cb, except_cb);
_connections.insert(std::make_pair(sock, connection));
// 添加入epoll模型中,默认关系读取事件
if (!_poll.AddSockToEpoll(sock, EPOLLIN | EPOLLET)) exit(6); // 主动设置为ET模式对其进行监听
}
// 专门处理监听套接字的读取回调函数
void Accepter(Connection* conn)
{
logMessage(DEBUG, "Accepter wait......");
std::string client_ip;
uint16_t client_port;
int err_num = 0;
while(true)
{
int sock = QiHai::Sock::accept(conn->_sock, &err_num, &client_ip, &client_port);
// 注意,sock设置的是非阻塞读取,所以循环的进行绑定,一次性读完
if (sock < 0)
{
if (err_num == EAGAIN || err_num == EWOULDBLOCK) break; // 此时只是没有链接了,读完了
else if(err_num == EINTR) continue; // 此时是被中断了
else{
logMessage(WARNING, "accept error...... %d:%s", err_num, strerror(err_num));
break; // 此时错误了,但是影响不大,break即可
}
}
_users.insert(std::make_pair(sock, new User(client_ip, client_port)));
AddConnection(sock, std::bind(&TcpServer::Recver, this, std::placeholders::_1), \
std::bind(&TcpServer::Sender, this, std::placeholders::_1), \
std::bind(&TcpServer::Excpeter, this, std::placeholders::_1));
logMessage(DEBUG, "is %d connection! add Connection success!", sock);
}
}
// 三大回调方法
// 读回调
void Recver(Connection* conn)
{
// 此链接读的事件响应,处理读的事件
// 首先保证将本次从底层的读取缓冲区拿完到用户的缓冲区
conn->_time = time(nullptr); // 更新时间戳
bool err = false;
const int nums = 1024;
while(true)
{
char buffer[nums];
int n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0); // 此时最后一个0没有多大意义。因为sock始终保证的就是非阻塞进行读取
if(n > 0)
{
// 读取成功
buffer[n] = '\0';
conn->_inbuffer += buffer;
}
else if(n == 0)
{
// 对方关闭连接
logMessage(DEBUG, "sock-%d[%s:%d] close connection, server close connection!", \
conn->_sock, _users[conn->_sock]->_ip.c_str(), _users[conn->_sock]->_port);
conn->_except_cb(conn); // 调用异常处理方法
err = true;
break;
}
else
{
// 小于0判断是否读取完毕
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 读取完毕正常退出
else if (errno == EINTR) continue; // 中断了,在继续读取
else
{
err = true;
// 此时真的出错了
logMessage(FATAL, "recv error %d:%s", errno, strerror(errno));
conn->_except_cb(conn); // 调用异常处理方法
break;
}
}
}
if (!err)
{
logMessage(DEBUG, "sock:%d[%s:%d]# %s", \
conn->_sock, _users[conn->_sock]->_ip.c_str(), _users[conn->_sock]->_port, conn->_inbuffer.c_str());
// 此时需要保证读取的数据是一个一个独立完整的报文,然后传递给上层逻辑进行处理
std::vector messages;
QiHai::SpliteMessage(conn->_inbuffer, messages); // test:{"x":1,"y":2,"op":43}{"x":2,"y":3,"op":47}{"x":0,"y":0,"op":37}{"x":1,
for (auto& msg : messages)
{
// 调用上层逻辑进行处理,服务器只是帮你提取出报文出来
_call(conn, msg);
}
}
}
void Sender(Connection* conn)
{
// 写也是非阻塞的写,也就是说,如果写入缓冲区满了也是会直接返回
while(true)
{
int n = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
if (n > 0)
{
// 写入成功
conn->_outbuffer.erase(0, n);
if (conn->_outbuffer.empty()) break; // 写完了,退出
}
else{
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 此时底层的发送缓冲区可能满了,不可发送,等下次机会 - 只要空了就会在执行这里
else if (errno == EINTR) continue; // IO中断了,重新试一次
else{
// 此处出错了
logMessage(FATAL, "send error %d:%s", errno, strerror(errno));
conn->_except_cb(conn);
break;
}
}
}
// 注意,如果将缓冲区内的数据发完了,需要关闭epoll对其写入事件的关心 - 因为一旦发送缓冲区变化了-变空 会通知的
if (conn->_outbuffer.empty()) EnableReadWrite(conn, true, false);
else EnableReadWrite(conn, true, true);
// 保险起见,没发完的下次还要继续发
}
void Excpeter(Connection* conn)
{
User* user = _users[conn->_sock];
_users.erase(conn->_sock);
delete user;
if (!_poll.DelFromEpoll(conn->_sock)) exit(6); // 从epoll 模型中移除
_connections.erase(conn->_sock);
close(conn->_sock); // 关闭此文件描述符
delete conn;
}
bool IsConnectionExists(int sock)
{
auto res = _connections.find(sock); // 可能后续因为异常,存在没有映射到此管理的表中的,这里需要检测一下
return res != _connections.end();
}
void LoopOnce()
{
// 具体去就epoll就绪队列中获取
int n = _poll.WaitEpoll(_revs, _revs_num); // 默认10s阻塞一次
if (n == 0)
{
logMessage(DEBUG, "events wait......");
}
else if (n < 0)
{
logMessage(FATAL, "epoll_wait error %d:%s", errno, strerror(errno));
exit(7);
}
else{
logMessage(DEBUG, "have events!");
for (int i = 0; i < n; ++i)
{
int sock = _revs[i].data.fd;
int event = _revs[i].events;
if (event & EPOLLERR) _revs[i].events |= (EPOLLIN | EPOLLOUT); // 直接掉用recv、send报错后调用异常处理
if (event & EPOLLHUP) _revs[i].events |= (EPOLLIN | EPOLLOUT); // 对方断开连接同理
if (event & EPOLLIN)
{
if(IsConnectionExists(sock) && _connections[sock]->_recv_cb) // 首先是否找得到,然后判断是否注册了读取回调函数
_connections[sock]->_recv_cb(_connections[sock]);
}
if (event & EPOLLOUT)
{
if (IsConnectionExists(sock) && _connections[sock]->_send_cb)
_connections[sock]->_send_cb(_connections[sock]);
}
}
}
}
void Survivallink() // 生存链接时间,发现大于MAX_TIME,主动断开连接
{
auto res = _connections.begin();
while (res != _connections.end())
{
time_t t = time(nullptr);
Connection* conn = res->second;
++res;
if (t - conn->_time >= MAX_TIME && conn->_except_cb) // 排除了sock
{
logMessage(DEBUG, "sock:%d[%s:%d] No communication for a long time,close sock!",\
conn->_sock, _users[conn->_sock]->_ip.c_str(), _users[conn->_sock]->_port);
conn->_except_cb(conn);
}
}
}
public:
TcpServer(uint16_t port = gport)
:_port(port), _revs(nullptr), _revs_num(gnum)
{
_listensock = QiHai::Sock::socket();
if (_listensock < 0) exit(1);
// 绑定
if (!QiHai::Sock::bind(_listensock, _port)) exit(3);
// 设置监听状态
if (!QiHai::Sock::listen(_listensock)) exit(4);
logMessage(DEBUG, "_listensock init success!");
// 创建epoll模型
if (!_poll.EpollCreate()) exit(5);
logMessage(DEBUG, "epoll init success!");
// 此时能否向以前那样,所有sock共享一个缓冲区呢(即裸的sock)?自然不能,可能存在多个sock是阶段性发送的,报文数据不完全。
// 所以我们需要维护sock的每一个缓冲区,先描述在组织 使用类connection进行描述,使用哈希表实现的map进行一个组织
// 在添加监听套接字对象到epoll之前,首先维护好一个就绪缓冲区
_revs = new epoll_event[_revs_num];
// 正式添加,我们增加一个接口,后续sock添加到里面都通过此接口 - 要建立连接类之类的,解耦一下
AddConnection(_listensock, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
logMessage(DEBUG, "listensock connection add success!");
}
// 修改连接在epoll模型的读写状态
void EnableReadWrite(Connection* conn, bool readable, bool writeable)
{
int event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0);
if (!_poll.CtlSockToEpoll(conn->_sock, event)) exit(6);
}
// 服务器开始对特定事件等待并且进行一个派发
void Dispather(service_logic func)
{
_call = func; // 业务处理函数
while(true)
{
Survivallink(); // 每次循环检查一次
LoopOnce(); // 一次
}
}
~TcpServer()
{
close(_listensock);
if (_revs != nullptr) delete[] _revs;
}
};
}
#endif
// main.cc 程序入口
#include "TcpServer.hpp"
#include
void business(QiHai::Request& req, QiHai::Response& res)
{
res._state = 1;
switch (req._op)
{
case '+':
res._result = req._x + req._y;
break;
case '-':
res._result = req._x - req._y;
break;
case '*':
res._result = req._x * req._y;
break;
case '/':
if (req._y == 0)
{
res._state = 0;
res._result = 1; // 除0异常
}
else res._result = req._x / req._y;
break;
case '%':
if (req._y == 0)
{
res._state = 0;
res._result = 2; // 模0异常
}
else res._result = req._x % req._y;
break;
default:
res._state = 0;
res._result = 3; // 未知运算符
break;
}
}
void Natcb(QiHai::Connection* conn, std::string message)
{
// 上层业务处理
// 接收到一个报文后,因为是一个结构化的东西,首先进行反序列化
QiHai::Request req;
if (!req.deserialization(message))
{
logMessage(WARNING, "Request deserialization error!");
return;
}
QiHai::Response res;
business(req, res);
conn->_outbuffer += res.serialization();
conn->_tsvr->EnableReadWrite(conn, true, true); // 修改一次写的状态,剩下由服务器进行处理
}
int main()
{
std::unique_ptr server(new QiHai::TcpServer);
server->Dispather(Natcb);
// QiHai::Response res;
// res.deserialization("{\"state\":1,\"result\":2}");
// std::cout << res.serialization() << std::endl;
// QiHai::Request req;
// std::string msg = "{\"op\":43,\"x\":1,\"y\":2}";
// req.deserialization(msg);
// std::cout << req.serialization() << std::endl;
// QiHai::Request req(1, 2, '+');
// std::cout << req.serialization() << std::endl;
return 0;
}
实现效果:
由于没有实现此服务器的客户端,所以使用telnet模拟发送一个自己定制协议的报文,查看结果,并且在创建一个进行连接,长时间不请求,是否会断开: