一、五种IO模型与非阻塞IO
1.1、高效IO的高效体现在哪?
根据之前的学习,直到IO无非就是input和ouput数据,就是输入输出数据,但是有这样一个问题,难道想要IO的时候,就一定有数据吗?比如读的时候,对方数据没有准备好,那么在IO之前就需要去进行等待,等待有数据之后再进行IO,这就说明了IO实际上分为两部分 IO = 等待 + 拷贝。
比如进行网络通信的场景:必须要等待对方网卡准备就绪,有数据之后才会进行通信,将数据拷贝到本地主机的网卡然后向上交付给用户,实际上等待的过程在IO的整体过程中占比是远大于拷贝的,拷贝很简单,很快。
所以之前写的代码中调用的系统调用比如:read()和write()实际上都是分为等待和拷贝两个过程。
为了提高IO的效率,应该将重点放在如何减少等待时间的关键点上面。
1.2、五种IO模型
阻塞式IO
在数据准备好之前,系统调用会一直等待,所有的系统调用默认都是阻塞方式IO。在等待过程中进行系统调用来IO的进程会阻塞,做不了其他的事情。

非阻塞式IO
进行调用系统调用并且设置非阻塞的参数,代表此时是非阻塞式IO,并且会采用轮询的方式去查看数据有没有准备好,若是没有准备好会返回相应的错误码。
轮询的含义:程序员通过循环的方式去尝试读写文件描述符,这对CPU是较大的浪费,只会在特定的情况下使用

非阻塞式的IO的IO效率相较于阻塞式IO实际上并没有提升,NIO只是在每次尝试读写文件描述符失败之后转而去做其他的事情,而IO并没有实际的进展。
信号驱动IO
调用信号驱动IO的进程会调用系统接口sigaction,此时内核会等待数据,在等待的过程中进程可以去做其他的事情,所以是非阻塞的,当内核发现数据就绪,那么就会给调用系统调用的进程发送信号SIGIO,此时触发中断,对应进程就会来读写拷贝数据,调用recvfrom()等接口。

多路转接IO
核心是可以同时等待多个文件描述符的就绪状态到来
多个等待中的每一个等待实际上就相当于阻塞式IO(可以设置为非阻塞)。但是多路转接IO是五种IO中效率最高的,这是因为多路转接IO是广撒网,在相同时间内有更大的概率能够等待到就绪的文件描述符,进而使得相同时间内多路转接IO等待的时间减少了。

异步IO
在内核数据拷贝完成时通知进程进行数据的处理,这个进程也仅仅只需要进行数据的处理,而等待和拷贝的过程进程不关系,进程只需要发起IO,异步IO没有参与IO过程

五种IO总结
IO永远包含等待和拷贝两个过程,并且等待的时间远远大于拷贝的时间,在实际过程中为了提高IO的效率就需要减少等待的时间。
前四种IO都是同步IO,都参与了等待或者拷贝的过程(这里的同步不是进程同步表示先后顺序的,而是表示进程有没有参与这个IO过程,是同步的就说明进程参与了IO过程),而异步IO也应证了这个概念,不关心IO过程,完全交给内核进行等待和拷贝,自己只需要处理即可。
1.3、深刻认识非阻塞IO(代码实例)
在实际过程中使用到多路转接IO最多,但是一般甚至可以说多路转接IO会配合非阻塞式IO一起使用,所以要先深刻了解一下非阻塞式IO。
在之前学习过程中read、write这样的接口实际上也可以设置非阻塞式IO的参数来达到非阻塞式读写,但是这样会增加记忆成本,在这里使用一种新的接口来实现非阻塞式IO。
首先介绍一下这个接口
#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );
fd表示要设置的文件描述符,后面的cmd是一个可变参数列表,可以设置
- 复制一个现有的描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
- 获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)
- 获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
- 获得/设置记录锁(cmd=F_GETLK、F_SETLK 或 F_SETLKW)
在这里只需要使用到第三行的设置文件状态标记,F_GETFL表示获取文件的状态标识,这个标识是一个整数,用位图表示,F_SETFL就是设置文件状态标识,可以和O_NONBLOCK和状态标识进行或运算,然后设置给这个文件,这样就改成了非阻塞IO。
具体代码:
具体代码中有很多细节也会展示并且学习
首先看一下阻塞式IO

可以看到从标准输入也就是0号文件描述符也就是键盘中读取数据的时候,需要从键盘先输入数据,否则阻塞,输入数据之后发现每次多出来一个空行,如果是换行那么下一次输入应该在打印的aa后面,但是明显出现了空行,这是因为每次输入完之后都按了一下回车键,这个回车键也是一个字符,打印的时候因为调用的是系统调用接口,所以会进行处理也就打印出来了一个空行,为了解决这个问题,将buffer[n-1] = 0即可。之前写的程序没有这个问题,这是因为一些函数进行了忽略,而read是系统调用,不会忽略。

当这样写的时候

结果是
不会输出任何数据,这是因为用户级缓冲区的存在,需要使用std::endl来进行缓冲区的刷新。
代码:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
void SetNonBlock(int fd)
{
int f1 = fcntl(fd, F_GETFL);
if (f1 < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, f1 | O_NONBLOCK);
}
int main()
{
// 设置为非阻塞
SetNonBlock(0);
char buffer[1024];
while (true)
{
int n = read(0, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n - 1] = 0;
std::cout << buffer << std::endl;
}
else if (n < 0)
{
}
else
{
}
sleep(1);
std::cout << "n : " << n << std::endl;
}
return 0;
}
文件读取有三种情况,读取成功,读取失败,读取结束,上述代码是成功的代码;读取结束返回值为0,在Linux中可以使用Ctrl+d标识输入结束,类似读取到文件结尾;
n==0的时候直接break结束文件读取过程即可

正常输入三个lll的不按回车,那么就还是-1-1,因为有缓冲区的存在,按一次回车刷新缓冲区,并且此时n是4,回车算一个字符

返回值为-1,在阻塞情况下就是读取失败也就是小于0,但是先来看一组现象,当设置为非阻塞的时候,不输入数据,看返回值n是什么:

可以发现非阻塞的场景下,不输入数据也就是数据没有准备好默认的返回值就是-1,那么应该如何区分呢,当返回值是-1的时候使用错误码来标识此时是不是数据没有准备好或者说赖标识是什么原因:
当错误码errno是EAGAIN或者是EWOULDBLOCK就表明此时-1是数据没有准备好,当错误码是EINTR的时候表示系统调用 / 库函数还没执行完,就被一个信号打断了,函数直接失败返回(这不是读取错误的情况,而是被打断了失败返回的):
完整代码:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
void SetNonBlock(int fd)
{
int f1 = fcntl(fd, F_GETFL);
if (f1 < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, f1 | O_NONBLOCK);
}
int main()
{
// 设置为非阻塞
SetNonBlock(0);
char buffer[1024];
while (true)
{
int n = read(0, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n - 1] = 0;
std::cout << buffer << std::endl;
}
else if (n < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
std::cout << "数据没有准备好……" << std::endl;
sleep(1);
// 做进程自己的事情,等待数据就绪
continue;
}
else if (errno == EINTR)
{
continue;
}
else
{
// 真正的错误
}
}
else
{
break;
}
// sleep(1);
// std::cout << "n : " << n << endl;
}
return 0;
}
errno 是系统全局变量,系统头文件自动声明,你不用定义,系统调用失败 → 自动设置 errno

当读取失败返回值为-1的时候,会设置错误码,当是数据没有准备好的时候进程可以做其他的事情,其他事情做完再次尝试read,并且整个过程一直是轮询进行的,一旦有数据n就大于0。
轮询的主动权在进程,只有进程一直去尝试read才会得知数据有没有准备好。
二、多路转接IO
1.1、select
select接口介绍

select系统调用就是等待多个fd,一旦这多个fd中有一个或以上的fd就绪了,select会通知上层,告诉调用方那些fd可以进行IO了。
可读:底层有数据,读事件就绪;可写,底层有空余,写事件就绪。对于fd来说,最开始因为读写缓冲区都是空的,那么就是默认情况下写事件就绪,读事件不就绪。
select是一种用来等待多个fd的就绪事件的通知机制。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds:表示select等待的所有fd中最大的fd+1,比如1、2、3、5、10,那么nfds就是11;
struct timeval *timeout:是一个结构体:
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};表示可以设置select的等待时间,设置为nullptr表示阻塞式等待多个fd,设置为0表示设置为非阻塞式的等待,设置为具体时间(tv_sec表示秒,tv_usec表示微秒,1s = 10^6um)表示等待的时间,在这个时间内阻塞等待,超过这个时间就非阻塞返回,此时的timeval就是剩余时间。
返回值:返回值大于0,表示等待到已经就绪的fd的个数;等于0表示在设置了等待时间时超时了也就是timeval时间内没有等待到fd,设置为非阻塞时表示数据没有准备就绪,小于0表示select报错,这个报错是系统调用报错。
fd_set表示一个位图结构,是内核提供给用户的数据结构,可以设置这个位图。
readfds表示读的fd,这是一个输入输出型参数,输入的位图的比特位标识fd编号,也就是是哪一个fd,这个位置为1表示用户现在要等待哪一个fd的读就绪,比如1000 0100就是等待值为2和7的fd读事件就绪;输出的时候比特位也表示fd编号,为1则说明这个fd的读事件就绪了。
writefds和exceptfds表示等待读和异常。
细节:位图是输入输出的,所以这个位图一定是在不停变换的,位图有多少个比特位就表明select一次性可以等待多少个fd。fd_set是系统提供的一个结构,它是固定的,也就是大小是固定的,这导致了一次性等待的fd的个数是有限的,可以用代码来看一下这个限制(不同系统可能不一样):是1024.


若是只关心读事件,那么就只设置readfds,读和异常也是;关心读写时间,那么两个参数都设置即可
写一个select tcp的echoserver
将之前tcp的代码搬过来,tcpsever变为selectserver,同样地申请套接字,然后启动。
但是这里有一个问题:accept可以直接获取连接吗?我们知道,这个链接的建立不是直接就有的,很多情况下都需要等待,因此直接accept就相当于是阻塞式IO了,所以这里就需要使用select来等待连接的就绪,这里将链接就绪作为读事件就绪了。
首先要定义一个fd_set,然后使用一系列接口进行初始化,下面是对fd_set操作的接口:
void FD_CLR(int fd, fd_set *set); // 把fd从指定集合中删除
int FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合中
void FD_SET(int fd, fd_set *set); // 将fd添加到集合中
void FD_ZERO(fd_set *set); // 清空集合


FD_SET这一步没有将fd设置进内核,只是在用户栈进行设置,真正设置进入内核的是select。
验证timeout:
设置为2s等待时间,一直没有链接到来所以n一直为0,打印time out


将timeout设置为0{0,0}即设置为非阻塞,那么会一直快速打印time out:

为了方便后续测试,将时间参数设置为nullptr;然后此时在另一个终端使用telnet连接这个服务器,会发现会打印事件就绪


为什么链接有了还是一直打印,因为此时没有调用accept获取链接,相当于读事件只是检测到了有数据可读,实际上还没读。
此时有链接(事件)到来,在switch中转而去处理事件,写一个HandlerEvent函数,在这个函数内部进行链接的获取,之后再进行测试,发现不会一直打印,这是因为内核的链接已经被拿到了用户层,底层检测不到了

这也是多路转接的一种模式,accept之前先调用select等待链接,有链接之后accept就是非阻塞的了,因为链接已经有了。
但是此时可以开始读了吗?不行,现在只知道fd就绪,但是不知道有没有数据库读,因此也要针对于这个fd也要进行select等待,可是这样可以直接设置吗?
当我们将一个集合设置进入select之后,看全部就绪可能部分就绪可能全部不就绪,返回的集合很有可能不包含开始我们传入集合的fd,下一次进行select的时候就会漏掉一些fd,此时有的读事件就被忽略了,为了解决这个问题,我们需要引入一个辅助数组,将fd提前记录下来;
首先添加一个成员变量fd_array,大小为size,size是一个静态常整形,大小为sizeof(fd_set)*8,然后又因为select的第一个参数不可能一直是listensockfd(最大fd一定是变化的,不可能一直保持不变),所以每次在select之前都要用辅助数组中的值对fd集合进行一个重置,并且找到最大的fd,记为maxfd;数组的初始化为-1
把监听套接字放大数组的0号位置;
现在要如何读呢,首先在新链接获取之后得到了新的fd,将这个fd添加到辅助数组中,这是HandlerEvent中的逻辑,当这个函数走完之后回到Start,此时进行新一轮的select,就会先将辅助数组中新的fd添加到集合中然后select,这样就保证新来的链接有没有读事件就绪能够被select多路转接了:
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class SelectServer
{
const static int size = sizeof(fd_set) * 8;
const static int defaultfd = -1;
public:
SelectServer(uint16_t port)
:_listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BuildTcpSocketMethod(port);
for (int i = 0; i < size; i++)
fd_array[i] = defaultfd;
fd_array[0] = _listensock->Fd();
}
void HandlerEvent()
{
InetAddr client;
int fd = _listensock->Accept(&client);
if (fd > 0)
{
LOG(LogLevel::INFO) << "get a new link success, sockfd:" << fd
<< " client is: " << client.StringAddr();
}
// 将新来的fd添加到辅助数组中
int pos = 0;
for (; pos < size; pos++)
{
if (fd_array[pos] != defaultfd)
break;
}
if (pos == size)
{
LOG(LogLevel::WARNING) << "server full";
close(fd);
}
else
{
fd_array[pos] = fd;
}
}
void Printfd()
{
for (int i = 0; i < size; i++)
{
if (fd_array[i] != defaultfd)
std::cout << fd_array[i] << " ";
}
std::cout << "/r/n";
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
fd_set rfds;
FD_ZERO(&rfds); // 先清空
// 每次select之前都要进行一个重置并且找到最大的fd
int maxfd = 0;
for (int i = 0; i < size; i++)
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds);
maxfd = std::max(maxfd, fd_array[i]);
}
// 打印看看结果
Printfd();
// FD_SET(_listensock->Fd(), &rfds); //再设置
//struct timeval timeout = {0, 0};
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case -1:
LOG(LogLevel::ERROR) << "select error";
break;
case 0:
LOG(LogLevel::INFO) << "time out";
break;
default:
// 有事件就绪了
LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
HandlerEvent();
break;
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~SelectServer()
{}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int fd_array[size];
};

总结一下:首先是链接的建立也是一个读事件就绪过程,需要使用select等待,当链接就绪之后可以accept获取链接,此时内核中没有链接,但是还是不能读,因为不知道真正的读事件是否就绪,同样需要select等待,但是此时要保证历史的需要等待的fd在下一次select的时候还能被我们得知,所以引入辅助数组,当读事件到来,将fd设置进入辅助数组,每次select之前都会将辅助数组中的fd添加到fd_set集合中,这样保证selecrt等待的是完整的需要被的等待的fd。
可以这样理解:监听fd监听链接到来的过程相当于等待读事件就绪的过程,链接到来就accept获取链接相当于从读写fd上面读数据的过程;此后获得链接fd,也就是可以读写的fd,然后在这个fd上面去进行真正的等待读事件就绪,有数据就从上面读即可。
rfds到来了,就accept拉去链接,但是这是针对于链接到来的处理,若是真正的读事件就绪了应该怎么处理?所以在调用HandlerEvent的时候要添加判断逻辑,只有当此时到来的rfds是链接fd的时候才会accept,否则则处理真正的读事件。处理真正的读事件的时候读取失败或者读取完成都要关闭文件描述符,并且将集合数组中的下标位置置为-1,这个读事件也是有bug的,你不能保证调用read的时候一次性将内容读完,这只是TCP套接字,不能保证,这个问题在后面解决(poll和epoll)
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class SelectServer
{
const static int size = sizeof(fd_set) * 8;
const static int defaultfd = -1;
public:
SelectServer(uint16_t port)
:_listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BuildTcpSocketMethod(port);
for (int i = 0; i < size; i++)
_fd_array[i] = defaultfd;
_fd_array[0] = _listensock->Fd();
}
// 监听fd上面链接到来处理
void Accepter()
{
InetAddr client;
int fd = _listensock->Accept(&client);
if (fd > 0)
{
LOG(LogLevel::INFO) << "get a new link success, sockfd:" << fd
<< " client is: " << client.StringAddr();
}
// 将新来的fd添加到辅助数组中
int pos = 0;
for (; pos < size; pos++) // 找到可以填入的位置
{
if (_fd_array[pos] == defaultfd)
break;
}
if (pos == size)
{
LOG(LogLevel::WARNING) << "server full";
close(fd);
}
else
{
_fd_array[pos] = fd;
}
}
// 读写fd数据到来处理
void Recver(fd_set& rfds, int pos)
{
char buffer[1024];
int n = read(_fd_array[pos], buffer, sizeof(buffer) - 1);
if (n == 0) // 读完,要关闭fd以及从辅助数组中清除
{
LOG(LogLevel::INFO) << "读数据结束..., n : " << n;
close(_fd_array[pos]);
_fd_array[pos] = defaultfd;
}
else if (n > 0) // 不需要关闭fd,这个fd同时也不能清除,可能还没有读或者还会读
{
buffer[n] = 0;
LOG(LogLevel::INFO) << ", client say@:" << buffer;
}
else // 读失败就关闭了
{
LOG(LogLevel::DEBUG) << "读数据失败, quit...";
close(_fd_array[pos]);
_fd_array[pos] = defaultfd;
}
}
void Dispatcher(fd_set& rfds)
{
// 判断是哪一种读事件:链接获取或者是读数据
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
continue;
if (FD_ISSET(_fd_array[i], &rfds))
{
if (_fd_array[i] == _listensock->Fd()) // 在监听fd上面等待链接就绪,就绪之后有一个fd,拉取这个fd就是读数据fd
{
Accepter();
}
else // 真正的读事件
{
Recver(rfds, i);
}
}
}
}
void Printfd()
{
for (int i = 0; i < size; i++)
{
if (_fd_array[i] != defaultfd)
std::cout << _fd_array[i] << " ";
}
std::cout << "/r/n";
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
fd_set rfds;
FD_ZERO(&rfds); // 先清空
// 每次select之前都要进行一个重置并且找到最大的fd
int maxfd = 0;
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
continue;
FD_SET(_fd_array[i], &rfds);
maxfd = std::max(maxfd, _fd_array[i]);
}
// 打印看看结果
Printfd();
// FD_SET(_listensock->Fd(), &rfds); //再设置
//struct timeval timeout = {0, 0};
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case -1:
LOG(LogLevel::ERROR) << "select error";
break;
case 0:
LOG(LogLevel::INFO) << "time out";
break;
default:
// 有事件就绪了
LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
Dispatcher(rfds);
break;
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~SelectServer()
{}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _fd_array[size];
};
select的缺点
- 使用不便:每次调用 select,都需要手动设置文件描述符(fd)集合,接口使用非常不方便。
- 拷贝开销大:每次调用 select,都需要把文件描述符集合从用户态拷贝到内核态,当文件描述符数量很多时,这个开销非常大。
- 内核遍历开销大:每次调用 select,内核都需要遍历所有传入的文件描述符,当文件描述符数量很多时,这个开销也很大。
- 支持数量有限:select 支持的文件描述符数量太小,存在上限限制。
1.2、poll
就是因为select优缺点所以才会出现新的接口,接下来介绍poll接口,它相较于select有了一定的优化。
poll接口介绍
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
第一个参数是一个数组的起始地址,第二个参数是数组中元素的个数,这个数组中存储的数据是一个结构体;第三个参数表示等待的时间,是一个整形值,单位是ms,设置为0表示非阻塞式等待,设置为-1表示阻塞,大于零表示等待时间。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
这个结构体中第一个是fd,表示等待的fd,第二个和第三个是一个短整型的位图,先看一张表,每一个大写的事件都是一个宏
用event&这张表中某一个事件表示要等待fd上面的哪种事件。revents&某一个事件如果是表示真那么说明这个事件就绪。当然可以让events|多个事件表示要等待多个事件就绪。fd为-1内核不处理这个。
RETURN VALUE
On success, poll() returns a nonnegative value which is the number of elements in the pollfds whose revents fields have been set to a nonzero value (indicating an event or an error). A return value of zero indicates that the system call timed out before any file descriptors
became read.On error, -1 is returned, and errno is set to indicate the cause of the error.
返回非零表示等待成功,此时的返回值表示有多少个fd就绪;返回0,表示超时;返回-1表示失败,此时要看errno看具体错误是什么
调用这个接口的时候fd和events都有效才会告诉内核此时要等待fd上面的events事件,返回的时候也一样。
poll相对于select的优点,它对内核的请求和响应分开了,每一次重新请求的时候不需要像select那样重置传入的fd_sets,这样减少了一个辅助数组。还有一点就是它的等待的fd是没有限制的,这个数组可以动态开辟,大小不固定。
接下来改写select代码为poll代码:
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <poll.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class PollServer
{
const static int size = 1024;
const static int defaultfd = -1;
public:
PollServer(uint16_t port)
:_listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BuildTcpSocketMethod(port);
for (int i = 0; i < size; i++)
{
_pollfd[i].fd = defaultfd;
_pollfd[i].events = 0;
_pollfd[i].revents = 0;
}
_pollfd[0].fd = _listensock->Fd(); // 用第一个位置表示监听fd上面监听
_pollfd[0].events = POLLIN;
}
// 监听fd上面链接到来处理
void Accepter()
{
InetAddr client;
int fd = _listensock->Accept(&client);
if (fd > 0)
{
LOG(LogLevel::INFO) << "get a new link success, sockfd:" << fd
<< " client is: " << client.StringAddr();
}
// 这里的fd是在监听fd上面监听到就绪的fd,拉取的链接,所以要将这个fd设置进结构体数组,从这个fd上面监听真正的读事件
// 找到一个空余位置,填入结构体数组信息
// 若是满了则直接关闭这个fd, 因为poll无法等待多的fd了,只有关闭
int pos = 0;
for (; pos < size; pos++)
{
if (_pollfd[pos].fd == defaultfd)
break;
}
if (pos == size) // 满了
{
LOG(LogLevel::INFO) << "pollfd full";
close(fd);
}
else
{
_pollfd[pos].fd = fd;
_pollfd[pos].events = POLLIN; // 开始等待真正的读事件
_pollfd[pos].revents = 0;
}
}
// 读写fd数据到来处理
void Recver(int pos)
{
char buffer[1024];
int n = read(_pollfd[pos].fd, buffer, sizeof(buffer) - 1);
if (n == 0) // 读完,要关闭fd以及将结构体数组中对应结构体重置
{
LOG(LogLevel::INFO) << "读数据结束..., n : " << n;
close(_pollfd[pos].fd);
_pollfd[pos].fd = defaultfd;
_pollfd[pos].events = 0;
_pollfd[pos].revents = 0;
}
else if (n > 0) // 不需要关闭fd,这个fd同时也不能清除,可能还没有读或者还会读
{
buffer[n] = 0;
LOG(LogLevel::INFO) << ", client say@:" << buffer;
}
else // 读失败就关闭了
{
LOG(LogLevel::DEBUG) << "读数据失败, quit...";
close(_pollfd[pos].fd);
_pollfd[pos].fd = defaultfd;
_pollfd[pos].events = 0;
_pollfd[pos].revents = 0;
}
}
void Dispatcher()
{
for (int i = 0; i < size; i++)
{
if (_pollfd[i].fd == defaultfd)
continue;
if (_pollfd[i].revents & POLLIN) // 只有fd和revents都有效才会进行数据读取
{
if (_pollfd[i].fd == _listensock->Fd())
{
Accepter();
}
else // 真正的读事件
{
Recver(i);
}
}
}
}
void Printfd()
{
for (int i = 0; i < size; i++)
{
if (_pollfd[i].fd != defaultfd)
std::cout << _pollfd[i].fd << " ";
}
std::cout << "/r/n";
}
void Start()
{
_isrunning = true;
int timeout = -1;
while (_isrunning)
{
Printfd();
int n = poll(_pollfd, size, timeout); // 成功之后内核会设置结构体中的revents
switch (n)
{
case -1:
LOG(LogLevel::ERROR) << "poll error";
break;
case 0:
LOG(LogLevel::INFO) << "time out";
break;
default:
LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
Dispatcher();
break;
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~PollServer()
{}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
struct pollfd _pollfd[size];
};

启动服务端,实例化出对象就会将结构体数组的第一个位置fd设置为监听的fd,并且events设置为pollfd,然后进入Start开始执行,poll会阻塞;
此时使用另一个终端telnet这个服务端,服务端的监听fd监听到链接到来,poll返回值大于0,进入事件派发器,此时会找到监听fd,然后检查其revents和POLLIN相与,为真说明内核检测到了链接来了;
之后就会走到Accepter中获取链接,这个链接fd就是真正读写的fd,此时将这个fd设置进结构体fd,并且events设置为POLLIN表示要等待这个fd上面读事件就绪。Accept此时不会阻塞,因为poll已经等了,此时直接拉去链接,走完之后重新回到Start;
此时发送数据,那么poll会检测到fd上面读事件就绪然后进入事件派发器,进入Recver,开始真正的读写数据,当然写失败或者写关闭就会关闭文件描述符,并且将此fd对应的结构体重置。
poll的缺点
- 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
- 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中。
- 同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
1.3、epoll
前面学习的poll有一个缺点,随着fd增多,效率会降低。epoll是为处理大批量句柄而改进的poll,它几乎具备了之前所说的一切优点,被认为是Linux2.6下性能最好的多路I/O就绪通知方法。
epoll接口
epoll_create()
#include <sys/epoll.h>
int epoll_create(int size);
RETURN VALUE
On success, these system calls return a file descriptor (a nonnegative integer). On error, -1 is returned, and errno is set to indicate the error.
这个接口的作用是在使用epoll之前需要调用的,它的作用是创建一个epoll模型。创建成功返回一个epfd,这个返回值作为接下来的接口的参数,很重要。
epoll_ctl()
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
RETURN VALUE
When successful, epoll_ctl() returns zero. When an error occurs, epoll_ctl() returns -1 and errno is set appropriately.
这个接口是用户用来设置等待事件的接口,第一个参数传入创建epoll模型的返回值,第三个参数是要对哪些fd进行操作,第二个参数是对fd进行的操作,最后一个参数是要关心对fd上面的哪些事件
这是op
EPOLL_CTL_ADD
Add an entry to the interest list of the epoll file descriptor, epfd. The entry includes the file descriptor, fd, a reference to the corresponding open file description (see epoll(7) and open(2)), and the settings specified in event.EPOLL_CTL_MOD
Change the settings associated with fd in the interest list to the new settings specified in event.EPOLL_CTL_DEL
Remove (deregister) the target file descriptor fd from the interest list. The event argument is ignored and can be NULL (but see BUGS below).
这是关心事件的结构体
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
第一个参数传入的是宏值,可以使用与运算关心多个事件,第二个参数是一个用户参数,内核不修改
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_event这个参数用户传给内核,内核会进行处理。

这些宏值,现在只需要关心前两个即可。
epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);RETURN VALUE
When successful, epoll_wait() returns the number of file descriptors ready for the requested I/O, or zero if no file descriptor became ready during the
requested timeout milliseconds. When an error occurs, epoll_wait() returns -1 and errno is set appropriately.
这个接口是内核告诉用户,你让我关心的fd上面哪些事件已经就绪
第二个参数是内核到用户的过程,第三个参数是这个第二个参数(可以看做一个数组)的长度,第三个参数是一个等待时间,单位是ms。
可以看出poll相比于select是将参数做了分离,而epoll是将接口都分离了。
epoll的原理--version1


创建epoll模型底层会有这三部分被创建:红黑树、就绪队列、回调机制
红黑树中几点内容可以看做一个结构体,里面有关心的fd以及具体事件。就绪队列的本质是内核告诉用户哪一个fd上面的事件已经就绪了。这个过程是这样的当底层网卡驱动检测到数据就绪,就触发硬件中断,然后进行触发回调机制,回调机制会将红黑树中的节点激活加入到就绪队列。
这个激活并且加入的过程并不是new出来一个对象加入,而是直接修改节点中的指针,内核中一个结构体对象可以属于多种数据结构。
epoll_create()调用之后会给你创建红黑树,就绪队列,以及组织一个回调机制(没有关心的文件描述符的时候这个回调机制还没有);epoll_ctl()调用就是对红黑树进行修改,红黑树以参数fd作为key值;
OS检测到fd就绪,就会激活节点,这是OS自动检测fd就绪的机制。
之后会调用epoll_wait(),会看就绪队列中有没有节点,这个过程是O(1)的,因为此时不再需要我们像select或者poll那样线性遍历,而是直接看就绪队列即可;不过从就绪队列中获取节点的过程是O(N)的。
有几个细节:
如何看待就绪队列?实际上就是一个基于事件就绪的生产消费者模型,这样也会使得epoll是线程安全的,以后调用epoll接口不会担心出现数据不一致的问题。
获取就绪事件的时候,缓冲区不足怎么办?这个不影响,默认会给你保存,下次再拿。
内核会严格从0下标开始依此拷贝保存就绪事件和fd,应用层处理的时候,处理的全部都是就绪的,不需要像select或者poll那样进行非法检测。
调用epoll_ctl,会向红黑树插入节点,并且向底层注册回调机制。
简单了解一下epoll内核
OS如何epfd这个文件描述符找到的红黑树以及就绪队列的呢(在eventpoll中,所以核心是找到这个eventpoll)?观察到epoll_ctl以及epoll_wait都传入的epfd这个参数。


核心流程: 1. 每个**fd**对应内核`struct file`结构体。 2. `eventpoll`对象会**挂载**到自身fd对应的`struct file`私有数据(`private_data`)。 3. 调用epoll相关系统调用时,通过传入fd查找到`struct file`,再取`private_data`,即可拿到**eventpoll**结构。
这就是为什么调用epoll_ctl的时候要传入epfd,这个通过这个参数找到struct_file中的private_data,紧接着找到eventpoll中的红黑树,然后将fd以及对应的关心事件插入(ADD宏,可以设置别的,那就不是插入了)红黑树(就绪队列一样)。
epfd一般是从3开始,但是先创建监听套接字那么就是从4开始。
如何理解回调机制?
内核 epoll 回调机制: 1. **挂载回调** 调用 `epoll_ctl(EPOLL_CTL_ADD)` 时,把待监听 fd 对应的**设备/文件驱动**,注册一个 **epoll 专属回调函数**,同时把 `eventpoll`、监听节点一并绑定。 2. **事件触发,执行回调** 当 fd 有读写等就绪事件,**设备驱动中断/状态变更**时,主动触发上面注册的回调。 3. **回调核心动作** 回调函数会把该 fd 对应的监听节点,**移入 epoll 就绪链表**,并唤醒阻塞在 `epoll_wait` 上的进程。 4. **epoll_wait 收尾** 进程被唤醒后,遍历就绪链表,把事件拷贝到用户态,返回就绪 fd。 一句话总结:**不是 epoll 轮询 fd,而是 fd 就绪后主动回调通知 epoll**。
epoll 用红黑树管理所有被监听的 fd(快速增删、查找),树中每个 fd 节点都会绑定回调。
当有事件就绪直接调用相应fd的回调机制加入就绪队列。
epollserver代码

先验证一下设置不同的timeout的效果:
首先将timeout设置为1000,表示等待1s
设置为0,表示非阻塞等待

设置为-1表示阻塞式等待

完善事件派发器代码
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/epoll.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class EpollServer
{
const static int size = 64;
public:
EpollServer(uint16_t port)
:_listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BuildTcpSocketMethod(port);
// 创建epoll模型
_epfd = epoll_create(1024);
if (_epfd < 0)
{
LOG(LogLevel::ERROR) << "epoll create fail";
exit(EPOLL_CREATE_ERR);
}
LOG(LogLevel::INFO) << "epoll create success" << " epfd: " << _epfd;
// 模型创建成功开始添加关心事件,最开始是监听fd
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = _listensock->Fd(); // 用户级数据,设置fd之后内核不会修改,epoll_wait的时候拿到这个fd
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &epev);
if (n == -1)
{
LOG(LogLevel::ERROR) << "epoll ctl add listenfd fail";
exit(EPOLL_CTL_ERR);
}
LOG(LogLevel::INFO) << "epoll ctl add listenfd success";
// 接下来去Start中wait
}
void Start()
{
_isrunning = true;
int timeout = -1;
while (_isrunning)
{
int n = epoll_wait(_epfd, _recv, size, timeout);
switch (n)
{
case -1:
LOG(LogLevel::ERROR) << "epoll wait error";
break;
case 0:
LOG(LogLevel::INFO) << "time out";
break;
default:
LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
Dispatcher(n);
break;
}
}
_isrunning = false;
}
void Dispatcher(int rnum)
{
for (int i = 0; i < rnum; i++) // 就绪队列严格按照就绪fd个数的下标升序编号
{
int fd = _recv[i].data.fd; // 用户级数据,设置之后不会改变,那么可以直接拿到ctl的时候关心的fd
uint32_t revent = _recv[i].events;
if (revent && EPOLLIN) // 读事件就绪
{
if (fd == _listensock->Fd())
{
Accepter();
}
else
{
Recver(fd);
}
}
}
}
// 监听fd上面链接到来处理
void Accepter()
{
InetAddr client;
int fd = _listensock->Accept(&client);
if (fd < 0)
{
LOG(LogLevel::ERROR) << "listen accept error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "get a new link success, sockfd:" << fd
<< " client is: " << client.StringAddr();
// 链接获取成功,将读写fd设置进入红黑树
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = fd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &epev);
if (n == -1)
{
LOG(LogLevel::ERROR) << "epoll ctl add sockfd fail";
}
else
{
LOG(LogLevel::INFO) << "epoll ctl add sockfd success";
}
}
// 读写fd数据到来处理
void Recver(int fd)
{
char buffer[1024];
int n = read (fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
LOG(LogLevel::INFO) << ", client say@:" << buffer;
}
// 读关闭或者读错误都需要将fd对应节点从内核红黑树删除,并且关闭fd
// 先调用epoll_ctl删,这个接口不能删除非法fd
else if (n == 0)
{
LOG(LogLevel::INFO) << "读数据结束..., n : " << n;
int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
if (m > 0)
LOG(LogLevel::INFO) << "epoll ctl remove success";
close(fd);
}
else // 读失败就关闭了
{
LOG(LogLevel::DEBUG) << "读数据失败, quit...";
int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
if (m > 0)
LOG(LogLevel::INFO) << "epoll ctl remove success";
close(fd);
}
}
void Stop()
{
_isrunning = false;
}
~EpollServer()
{}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _epfd;
struct epoll_event _recv[size];
};

Dispatcher中第一层for循环0到rnum-1,都是事件就绪的fd,因为wai返回值就是这样设置的。所以即便这里也是和select、poll一样遍历,但是这里的遍历没有多余没有就绪的fd,含义是不一样的。所有的fd都是要拿到的,遍历一边也是应该的。所以说epoll检测就绪时间复杂度是O(1)而从就绪队列中获取还是O(N)。
总体来说只是理解难了,但是代码实现比select和poll更简单,只需要每次将fd设置进入内核,就绪wait到就直接取出即可。
epoll的优点
接口使用方便:分为三个接口,并且等待和获取的接口分开;
数据拷贝轻量:只有在合适的时候调用epoll_cnt_add将文件描述符结构拷贝到内核中,这个操作并不频繁,不像select或者poll那样每次循环都需要拷贝;
事件回调机制:避免使用遍历,使用回调函数将文件描述符结构加入到内核中,这个时间复杂度是O(1)的;
没有数量限制:文件描述符没有数量限制。
水平触发LT、边缘触发ET
回顾之前写的select、poll或者是epoll代码,当链接到来的时候没有accpet,会一直打印链接到来,这是因为没有获取链接,这种模式称为水平触发模式。
也就是底层有报文,只要没有拿完,就会一直通知用户数据就绪了,告诉着用户去读数据。
那边缘触发呢?边缘触发是底层来了数据,只会通知用户一次,用户拿不拿完底层不关心,并且之后不会再通知,除非来了新的数据,才会再通知且只是通知一次。
可以得知边缘触发模式效率更高,因为有效通知更多,水平触发同一份数据没有读完会通知多次。
如何设置为ET模式?events设置一个EPOLLET宏即可。
ET模式必须搭配着非阻塞模式循环读取:因为只会通知一次,缓冲区的数据可能没有读完,这是不好的,因此需要循环读取。为什么是非阻塞呢?这是因为设置为阻塞的循环读取的最后一次读之后不能保证刚好读完,此时可能出现阻塞,比如3000的数据,设置的read读缓冲区是100,那么最后一次读完就是0数据,阻塞了此时进程会被挂起,当设置为非阻塞,最后一次会通知进程,此时不会阻塞住。
当然可以通过最后一次读取的数据大小也就是read的返回值来和设置的buffer比较,小于等于buffer则不读了,但是这种方法不能处理所有情况,所以设置为非阻塞是最好的。
但是LT也可以这样呀,设置为非阻塞循环读取,那ET的优势去哪了?LT模式可以说选择方式很多,在需要ET非阻塞循环读的情况下可以用LT非阻塞循环,但是并不是每一个程序员都会正确的写出这种场景的LT,可能会选错,因为有多种选择。但是若是使用ET,因为只有非阻塞循环这一种正确写法,这倒逼着程序员写出的一定是正确的符合需求的代码。这是一种约束,也能增加IO的正确性。
分析ET的优势:
第一个就是上面说的有效通知更多,效率更高;
第二个:循环读取保证了用户每一次读的数据一定将内核到用户缓冲区的数据读了更多可以说是全部读完,之后缓冲区的剩余空间更大,这样就会使得TCP层的应答报文的窗口字段更大,接受方接受此报文下一次就知道自己可以发送更多的数据。这样一来提高了TCP的传输效率。
ET的IO服务器(Reactor)
Common.hpp
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum ExitCode
{
OK = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR,
OPEN_ERR,
EPOLL_CREATE_ERR,
EPOLL_CTL_ERR
};
class NoCopy
{
public:
NoCopy(){}
~NoCopy(){}
NoCopy(const NoCopy &) = delete;
const NoCopy &operator = (const NoCopy&) = delete;
};
int defaultport = 8080;
#define CONV(addr) ((struct sockaddr*)&addr)
Connection.hpp
#pragma once
#include <iostream>
#include <string>
#include "InetAddr.hpp"
// 封装fd,保证给每一个fd一套缓冲
class TcpServer;
// 基类
class Connection
{
public:
Connection()
{
}
virtual void Recver() = 0;
virtual void Sender() = 0;
virtual void Excepter() = 0;
void SetEvent(const uint32_t &events)
{
_events = events;
}
uint32_t GetEvent()
{
return _events;
}
void SetFd(int sockfd)
{
_sockfd = sockfd;
}
int GetFd()
{
return _sockfd;
}
~Connection()
{
}
private:
int _sockfd;
std::string _inbuffer; // 充当缓冲区,vector<char>
std::string _outbuffer;
// 回指指针
TcpServer *_owner;
// client info
InetAddr _client_addr;
// 关心事件
uint32_t _events;
};
Epoller.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/epoll.h>
#include "Common.hpp"
#include "Log.hpp"
using namespace LogModule;
class Epoller
{
public:
Epoller():_epfd(-1)
{
_epfd = epoll_create(128);
if(_epfd < 0)
{
LOG(LogLevel::FATAL) << "epoll_create error!!!";
exit(EPOLL_CREATE_ERR);
}
LOG(LogLevel::INFO) << "create epoll success: " << _epfd;
}
void AddEvent(int sockfd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = sockfd; // 细节哦!
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
if(n < 0)
{
LOG(LogLevel::ERROR) << "epoll_ctl error";
return;
}
LOG(LogLevel::INFO) << "epoll_ctl success: " << sockfd;
}
void DelEvent()
{}
void ModEvent()
{}
void Wait()
{}
~Epoller()
{
if(_epfd >= 0)
{
close(_epfd);
}
}
private:
int _epfd;
};
// class Poll
// {
// };
// class Selector : public Poll
// {
// };
// class Poller: public Poll
// {
// };
// class Epoller: public Poll
// {
// };
InetAddr.hpp
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
InetAddr() {}
InetAddr(struct sockaddr_in &addr)
{
SetAddr(addr);
}
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
_addr.sin_port = htons(_port);
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
}
InetAddr(uint16_t port) : _port(port), _ip()
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = INADDR_ANY;
_addr.sin_port = htons(_port);
}
void SetAddr(struct sockaddr_in &addr)
{
_addr = addr;
// 网络转主机
_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
// _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
_ip = ipbuffer;
}
uint16_t Port() { return _port; }
std::string Ip() { return _ip; }
const struct sockaddr_in &NetAddr() { return _addr; }
const struct sockaddr *NetAddrPtr()
{
return CONV(_addr);
}
socklen_t NetAddrLen()
{
return sizeof(_addr);
}
bool operator==(const InetAddr &addr)
{
return addr._ip == _ip && addr._port == _port;
}
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
Listener.hpp
#pragma once
#include <iostream>
#include <memory>
#include "Epoller.hpp"
#include "Socket.hpp"
#include "Connection.hpp"
#include "Common.hpp"
using namespace SocketModule;
// Listener 专门进行获取新连接
class Listener : public Connection
{
public:
Listener(int port = defaultport)
:_port(port), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildTcpSocketMethod(_port);
SetEvent(EPOLLIN); //ET todo
SetFd(_listensock->Fd());
}
~Listener()
{}
void Recver()
{
//accept
}
void Sender(){}
void Excepter(){}
private:
int _port;
std::unique_ptr<Socket> _listensock;
};
Log.hpp
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 策略模式,C++多态特性
// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy()
{
}
private:
Mutex _mutex;
};
// 文件打印日志的策略 : 子类
const std::string defaultpath = "/var/log/";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
std::ofstream out(filename, std::ios::app); // 追加写入的 方式打开
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy()
{
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式
// 1. 形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2Str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900,
curr_tm.tm_mon+1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec
);
return timebuffer;
}
// 1. 形成日志 && 2. 根据不同的策略,完成刷新
class Logger
{
public:
Logger()
{
EnableConsoleLogStrategy();
}
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 表示的是未来的一条日志
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
// 日志的左边部分,合并起来
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
template <typename T>
LogMessage &operator<<(const T &info)
{
// a = b = c =d;
// 日志的右半部分,可变的
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo; // 合并之后,一条完整的信息
Logger &_logger;
};
// 这里故意写成返回临时对象
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
// 全局日志对象
Logger logger;
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
#endif
Main.cc
#include <iostream>
#include <string>
#include "Listener.hpp"
#include "TcpServer.hpp"
#include "Log.hpp"
#include "Common.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
LogModule::ConsoleLogStrategy();
uint16_t port = std::stoi(argv[1]);
std::shared_ptr<Connection> conn = std::make_shared<Listener>(port);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>();
tsvr->AddConnection(conn);
return 0;
}
Makefile
server:Main.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f server
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
(void)n;
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
(void)n;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
pthread_mutex_t *Get()
{
return &_mutex;
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex):_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex &_mutex;
};
}
Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 模版方法模式
// 基类socket, 大部分方法,都是纯虚方法
class Socket
{
public:
virtual ~Socket() {}
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
// virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual int Accept(InetAddr *client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &message) = 0;
virtual int Connect(const std::string &server_ip, uint16_t port) = 0;
virtual int Fd() = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
void BuildTcpClientSocketMethod()
{
SocketOrDie();
}
// void BuildUdpSocketMethod()
// {
// SocketOrDie();
// BindOrDie();
// }
};
const static int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultfd)
{
}
TcpSocket(int fd) : _sockfd(fd)
{
}
~TcpSocket() {}
void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
LOG(LogLevel::INFO) << "socket success: " << _sockfd;
}
void BindOrDie(uint16_t port) override
{
InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
//std::shared_ptr<Socket> Accept(InetAddr *client) override
int Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_sockfd, CONV(peer), &len);
if (fd < 0)
{
LOG(LogLevel::WARNING) << "accept warning ...";
return -1; // TODO
}
return fd;
// client->SetAddr(peer);
// return std::make_shared<TcpSocket>(fd);
}
// n == read的返回值
int Recv(std::string *out) override
{
// 流式读取,不关心读到的是什么
char buffer[4096*2];
ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
*out += buffer; // 故意
}
return n;
}
int Send(const std::string &message) override
{
return send(_sockfd, message.c_str(), message.size(), 0);
}
void Close() override //??
{
if (_sockfd >= 0)
::close(_sockfd);
}
int Connect(const std::string &server_ip, uint16_t port) override
{
InetAddr server(server_ip, port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}
int Fd()
{
return _sockfd;
}
private:
int _sockfd; // _sockfd , listensockfd, sockfd;
};
// class UdpSocket : public Socket
// {
// };
}
TcpServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <unordered_map>
#include "Epoller.hpp"
#include "Listener.hpp"
#include "Connection.hpp"
// 后面我们在更改名字
class TcpServer
{
public:
TcpServer():_epoller_ptr(std::make_unique<Epoller>())
{}
void Start()
{
while(true)
{
_epoller_ptr->Wait();
}
}
// 该接口要把所有的新连接添加到_connections,并且,写透到epoll内核中!!!!!
void AddConnection(std::shared_ptr<Connection> &conn)
{
// 1. conn对应的fd和他要关心的事件,写透到内核中!
uint32_t events = conn->GetEvent();
int sockfd = conn->GetFd();
_epoller_ptr->AddEvent(sockfd, events);
// 2. 将具体的connection添加到_connections
_connections[sockfd] = conn;
}
~TcpServer()
{}
private:
// 1. epoll模型
std::unique_ptr<Epoller> _epoller_ptr;
// 3. 管理所有的connection,本质是管理未来所有我获取到的fd
// fd : Connection
std::unordered_map<int, std::shared_ptr<Connection>> _connections;
};


1451

被折叠的 条评论
为什么被折叠?



