目录
[1,同步通信 vs 异步通信](#1,同步通信 vs 异步通信)
[2,阻塞 vs 非阻塞](#2,阻塞 vs 非阻塞)
2,实现函数SetNoBlock:将一个文件描述符设置为非阻塞
[6,示例: 使用select实现echo服务器](#6,示例: 使用select实现echo服务器)
[4,示例: 使用poll监控标准输入](#4,示例: 使用poll监控标准输入)
[7,epoll示例: epoll服务器(LT模式)](#7,epoll示例: epoll服务器(LT模式))
[8,epoll示例: epoll服务器(ET模式)](#8,epoll示例: epoll服务器(ET模式))
一、五种IO模型
1,阻塞IO
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

2,非阻塞IO
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用.

3,信号驱动IO
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

4,IO多路转接
IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态.

5,异步IO
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

6,IO模型小结
网络通信的本质就是:IO
在任何IO过程中, 都包含两个步骤. 第一是等待 , 第二是拷贝.
IO为什么低效?
在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间.
以读取为例:
1.当我们read/recv的时候,如果底层缓冲区没有数据,read/recv会怎么办? 阻塞 >等待
2.当我们read/recv的时候,如果底层缓冲区有数据,read/recv会怎么办? 拷贝
如何提高IO效率?
在单位时间中,让等待的比重变得越低,效率越高。
让IO更高效, 最核心的办法就是让等待的时间尽量少.
二、高级IO重要概念
1,同步通信 vs 异步通信
同步和异步关注的是消息通信机制.
所谓同步,就是在发出一个调用 时,在没有得到结果之前,该调用 就不返回. 但是一旦调用返回,就得 到返回值了; 换句话说,就是由调用者 主动等待这个调用的结果;
异步则是相反,调用 在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用 发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用;
另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相关的概念.
进程/线程同步也是进程/线程之间直接的制约关系;
是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候;
2,阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回. 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
3,其他高级IO
非阻塞IO, I/O多路转接(也叫I/O多路复用),纪录锁,系统V流机制,readv和writev函数以及存储映射 IO( mmap),这些统称为高级IO.
三、非阻塞IO
1,fcntl函数
fcntl的函数原型如下:
头文件:
#include <unistd.h>
#include <fcntl.h>
函数:
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
根据传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
复制一个现有的描述符 (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).
我们设置一个文件描述符, 默认都是阻塞IO.
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
2,实现函数SetNoBlock:将一个文件描述符设置为非阻塞
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.
bool SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
if(fl < 0)
return false;
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
return true;
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
3,示例:使用fcntl实现轮询方式读取标准输入
[user@iZwz9eoohx59fs5a6ampomZ SetNoBlock]$ cat myfile.cc
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
bool SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
if(fl < 0)
return false;
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
return true;
}
int main()
{
// 把0号文件描述符(标准输入流)设置为非阻塞
SetNoBlock(0); // 只要设置一次,后续都是非阻塞了
char buffer[1024];
while(true)
{
sleep(1);
errno = 0;
// 非阻塞的时候,我们以出错的形式返回, 告知上层没有就绪
// a,我们如何甄别是真的出错了
// b,还是仅仅是数据没有就绪呢?
// 数据就绪了的话,我们就正常读取就行
// 出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s-1] = 0;
std::cout << "echo# " << buffer << " read success " << "errno: " << errno << " " << strerror(errno) << std::endl;
}
else
{
// 如果失败的errno值是11,就代表其实没错,只不过是底层数据没就绪
// std::cout << "read failure " << "errno: " << errno << " " << strerror(errno) << std::endl;
if(errno == EWOULDBLOCK || errno == EAGAIN)
{
std::cout << "当前0号fd数据没有就绪, 再试一次吧" << std::endl;
continue;
}
else if(errno == EINTR)
{
std::cout << "当前IO可能被信号中断, 再试一次吧" << std::endl;
continue;
}
else
{
// 差错处理
}
}
}
return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ SetNoBlock]$ ./myfile
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
asdfsa当前0号fd数据没有就绪, 再试一次吧
fWD
echo# asdfsafWD read success errno: 0 Success
ASDFASFE当前0号fd数据没有就绪, 再试一次吧
echo# ASDFASFE read success errno: 0 Success
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
AESFASEFAS
echo# AESFASEFAS read success errno: 0 Success
当前0号fd数据没有就绪, 再试一次吧
23154RDF当前0号fd数据没有就绪, 再试一次吧
1234T
echo# 23154RDF1234T read success errno: 0 Success
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
^C
四、I/O多路转接之select
1,select函数
系统提供select函数来实现多路复用输入/输出模型.
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
select的函数原型如下:
头文件:
#include <sys/select.h>
函数:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
参数nfds是需要监视的最大的文件描述符值+1;
rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;
参数timeout为结构timeval,用来设置select()的等待时间;
函数返回值:
执行成功则返回文件描述词状态已改变的个数;
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测;
错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭;
EINTR 此调用被信号所中断;
EINVAL 参数n 为负值;
ENOMEM 核心内存不足;
参数fd_set的结构
其实这个结构就是一个整数数组, 更严格的说, 是一个"位图".
使用位图中对应的位来表示要监视的文件描述符.
系统提供了一组操作fd_set的接口, 来比较方便的操作位图.
参数结构:
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
系统接口:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
参数timeval的结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。
参数结构:
struct timeval
{
__time_t tv_sec; //秒
__suseconds_t tv_usec; //微秒
};
参数timeout取值:
NULL:则表示select()没有timeout ,select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回;
常见的程序写法
fd_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){......}
2,select的执行过程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.
*(1)执行fd_set set;FD_ZERO(&set); 则set用位表示是0000,0000。
*(2)若fd=5,执行FD_SET(fd,&set); 后set变为0001,0000。(第5位置为1)
*(3)若再加入fd=2,fd=1,则set变为0001,0011。(第5/2/1位置为1)
*(4)执行 select(6,&set,0,0,0) 阻塞等待。
*(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。(第2/1位置为1)注意:没有事件发生的fd=5被会清空。
3,socket的就绪条件
读就绪
socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
监听的socket上有新的连接请求;
socket上有未处理的错误;
写就绪
socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
socket的写操作被关闭(close或者shutdown).对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE 信号;
socket使用非阻塞connect连接成功或失败之后;
socket上有未读取的错误;
异常就绪
socket上收到带外数据;
关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段,就与此相关);
4,select的特点
可监控的文件描述符个数取决于sizeof(fd_set)的值.
如果服务器上sizeof(fd_set)=512,每bit表示一个文件 描述符,则服务器上支持的最大文件描述符是512*8=4096.
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd:
一是,用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
二是,select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得 fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
备注:fd_set的大小可以调整,可能涉及到重新编译内核.
5,select的缺点
每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大.
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大.
select支持的文件描述符数量太小.
6,示例: 使用select实现echo服务器
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ ls
Log.hpp Main.cc Makefile SelectServer.hpp Server Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ cat Main.cc
#include "SelectServer.hpp"
int main()
{
// fd_set是一个固定大小的位图, 直接决定了select能同时关心的个数是有上限的
// std::cout << sizeof(fd_set) * 8 << std::endl; // max值为1024
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Start();
return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ cat SelectServer.hpp
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__
#include <iostream>
#include <string>
#include <vector>
#include <sys/select.h>
#include <sys/time.h>
#include "Log.hpp"
#include "Sock.hpp"
#define BITS 8
#define NUM (sizeof(fd_set)*BITS) //总共为1024
#define FD_NONE -1
// SelectServer 我们只完成读取,写入和异常不做处理 -- 在EpollServer会写完整
class SelectServer
{
public:
SelectServer(const uint16_t &port = 8080) : _port(port)
{
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
logMessage(DEBUG,"%s","create base socket success");
// 把fd数组初始化,没有用到的位置设置为-1
for(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;
// 规定监听到的套接字放在数组首部 _fd_array[0] = _listensock;
_fd_array[0] = _listensock;
}
~SelectServer()
{
if (_listensock >= 0) close(_listensock);
}
void Start()
{
while (true)
{
// struct timeval timeout = {0, 0};
// 如何看待listensock? 获取新连接,我们把它依旧看做成为IO,input事件,如果没有连接到来呢?阻塞
// int sock = Sock::Accept(listensock, ...); //不能直接调用accept了
// 将listensock添加到读文件描述符集中
// FD_SET(_listensock, &rfds);
// int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);
// 1. nfds: 随着我们获取的sock越来越多,随着我们添加到select的sock越来越多,注定了nfds每一次都可能要变化,我们需要对它动态计算
// 2. rfds/writefds/exceptfds:都是输入输出型参数,输入输出不一定相同,所以注定了我们每一次都要对rfds进行重新添加
// 3. timeout: 也是输入输出型参数,每一次都要进行重置,前提是你需要使用它
// 4. 1和2点 => 注定了我们必须自己将合法的文件描述符需要单独全部保存起来,用来支持:1. 更新最大fd 2.更新位图结构
DebugPrint();
// 每次select后,fd_set都只保留状态变化的文件描述符
// 所以需要设置_fd_array,保存所有需要监听的sock
fd_set rfds; // 定义fd位图
FD_ZERO(&rfds); // 清空位图
int maxfd = _listensock; //标记fd最大值
// 1. 遍历数组,更新最大值,并添加所有需要关心的fd到fd_set位图中
for(int i = 0; i < NUM; i++)
{
if(_fd_array[i] == FD_NONE) continue;
FD_SET(_fd_array[i], &rfds);
if(maxfd < _fd_array[i]) maxfd = _fd_array[i];
}
// 2. 调用select进行事件的检测
// rfds未来,一定会有两类sock,监听listensock,普通sock
// 我们select中,就绪的fd会越来越多!
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
// 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
logMessage(DEBUG, "%s", "time out...");
break;
case -1:
// 当有错误发生时则返回-1,错误原因存于errno
logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
break;
default:
// 为什么会一直打印连接到来呢?连接已经建立完成,就绪了,但是你没有取走,select要一直通知你!
logMessage(DEBUG, "get a new link event...");
// 3. 遍历数组,找到就绪事件,完成对应动作
HandlerEvent(rfds);
break;
}
}
}
private:
void HandlerEvent(const fd_set &rfds) // fd_set 是一个集合,里面可能会存在多个sock
{
for(int i = 0; i < NUM; i++)
{
// 1. 去掉不合法的fd
if(_fd_array[i] == FD_NONE) continue;
// 2. 合法的就一定就绪了?不一定
if(FD_ISSET(_fd_array[i], &rfds)) // 用FD_ISSET测试描述词组rfds中相关fd的位是否为真
{
// 新获取到的sock放到_fd_array数组里
if(_fd_array[i] == _listensock) Accepter();
// 监听的fd,读事件就绪了,就可以读取了
else Recver(i);
}
}
}
void Accepter()
{
std::string clientip;
uint16_t clientport = 0;
// listensock上面的读事件就绪了,表示可以读取了
// 1. 获取新连接
int sock = Sock::Accept(_listensock, &clientip, &clientport); // 这里在进行accept会不会阻塞?不会!
if(sock < 0)
{
logMessage(WARNING, "accept error");
return;
}
logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
// 这里可以调用read / recv吗? 不能!
// 为什么不能?我们不清楚该sock上面数据什么时候到来,recv、read就有可能先被阻塞,IO = 等+数据拷贝
// 谁可能最清楚呢?select!
// 得到新连接的时候,此时我们应该考虑的是,将新的sock托管给select,让select帮我们进行检测sock上是否有新的数据
// 有了数据select,读事件就绪,select就会通知我,我们在进行读取,此时我们就不会被阻塞了
// 要将sock 添加给 select,其实我们只要将fd放入到数组中即可!
// 2.把获取的sock放入fd数组
int pos = 1;
for(; pos < NUM; pos++)
{
if(_fd_array[pos] == FD_NONE) break;
}
if(pos == NUM)
{
logMessage(WARNING, "%s:%d", "select server already full, close: %d", sock);
close(sock);
}else
{
_fd_array[pos] = sock;
}
}
void Recver(int pos)
{
// 读事件就绪:INPUT事件到来、recv,read
logMessage(DEBUG, "message in, get IO event: %d", _fd_array[pos]);
// 暂时先不做封装, 此时select已经帮我们进行了事件检测,fd上的数据一定是就绪的,即 本次 不会被阻塞
// 这样读取有bug吗?有的,你怎么保证以读到了一个完整包文呢?
char buffer[1024];
int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);
if(n > 0)
{
// 读取信息
buffer[n] = 0;
logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);
}
else if(n == 0)
{
// 对端关闭
logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[pos]);
// 1. 我们也要关闭不需要的fd
close(_fd_array[pos]);
// 2. 不要让select帮我关心当前的fd了
_fd_array[pos] = FD_NONE;
}
else
{
//读取出错
logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));
// 1. 我们也要关闭不需要的fd
close(_fd_array[pos]);
// 2. 不要让select帮我关心当前的fd了
_fd_array[pos] = FD_NONE;
}
}
void DebugPrint()
{
std::cout << "_fd_array[]: ";
for(int i = 0; i < NUM; i++)
{
if(_fd_array[i] == FD_NONE) continue;
std::cout << _fd_array[i] << " ";
}
std::cout << std::endl;
}
private:
uint16_t _port;
int _listensock;
int _fd_array[NUM];
// int _fd_write[NUM];
// std::vector<int> arr;
};
#endif[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ ./Server
[DEBUG] [1761992446] create base socket success
_fd_array[]: 3
[DEBUG] [1761992460] get a new link event...
[DEBUG] [1761992460] get a new line success : [127.0.0.1:59404] : 4
_fd_array[]: 3 4
[DEBUG] [1761992487] get a new link event...
[DEBUG] [1761992487] message in, get IO event: 4
[DEBUG] [1761992487] client[4]# hello select!!!
_fd_array[]: 3 4
[DEBUG] [1761992497] get a new link event...
[DEBUG] [1761992497] message in, get IO event: 4
[DEBUG] [1761992497] client[4]# nihao6666666
_fd_array[]: 3 4
[DEBUG] [1761992505] get a new link event...
[DEBUG] [1761992505] message in, get IO event: 4
[DEBUG] [1761992505] client[4] quit, me too...
_fd_array[]: 3
^C
// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet>
hello select!!!
nihao6666666
^]
telnet> quit
Connection closed.
五、I/O多路转接之poll
1,poll函数
poll函数的原型如下:
头文件:
#include <poll.h>
函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
fds是一个poll函数监听的结构列表;
每一个元素中, 包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合;
nfds表示fds数组的长度;
timeout表示poll函数的超时时间, 单位是毫秒(ms);
返回结果:
返回值小于0, 表示出错;
返回值等于0, 表示poll函数等待超时;
返回值大于0, 表示poll由于监听的文件描述符就绪而返回;
参数struct pollfd的结构:
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
参数events和revents的取值:
|------------|--------------------------------------|----------|----------|
| 事件 | 描述 | 是否可以作为输入 | 是否可以作为输出 |
| POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
| POLLRDNORM | 普通数据可读 | 是 | 是 |
| POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
| POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
| POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
| POLLWRNORM | 普通数据可写 | 是 | 是 |
| POLLWRBAND | 优先级带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作。它由GNU 引人。 | 是 | 是 |
| POLLERR | 错误 | 否 | 是 |
| POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到 POLLHUP 事件。 | 否 | 是 |
| POLLNVAL | 文件描述符没有打开 | 否 | 是 |
2,poll函数的优点
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.
pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式. 接口使用比 select 更方便.
poll并没有最大数量限制(但是数量过大后性能也是会下降).
3,poll函数的缺点
poll中监听的文件描述符数目会逐渐增多.
和select函数一样, poll返回后,需要轮询pollfd来获取就绪的描述符.
每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
4,示例: 使用poll监控标准输入
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ ls
Log.hpp Main.cc Makefile PollServer.hpp Server Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ cat PollServer.hpp
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__
#include <iostream>
#include <string>
#include <vector>
#include <poll.h>
#include <sys/time.h>
#include "Log.hpp"
#include "Sock.hpp"
#define FD_NONE -1
class PollServer
{
public:
static const int nfds = 100;
public:
PollServer(const uint16_t &port = 8080)
: _port(port), _nfds(nfds)
{
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
logMessage(DEBUG,"%s","create base socket success");
_fds = new struct pollfd[_nfds];
for(int i = 0; i < _nfds; i++)
{
_fds[i].fd = FD_NONE;
_fds[i].events = _fds[i].revents = 0;
}
_fds[0].fd = _listensock;
_fds[0].events = POLLIN;
_timeout = 1000;
}
~PollServer()
{
if (_listensock >= 0) close(_listensock);
if (_fds) delete [] _fds;
}
void Start()
{
while (true)
{
int n = poll(_fds, _nfds, _timeout);
switch (n)
{
case 0:
logMessage(DEBUG, "%s", "time out...");
break;
case -1:
logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
break;
default:
HandlerEvent();
break;
}
}
}
private:
void HandlerEvent()
{
for(int i = 0; i < _nfds; i++)
{
if(_fds[i].fd == FD_NONE) continue;
if(_fds[i].revents & POLLIN)
{
if(_fds[i].fd == _listensock) Accepter();
else Recver(i);
}
}
}
void Accepter()
{
std::string clientip;
uint16_t clientport = 0;
int sock = Sock::Accept(_listensock, &clientip, &clientport); // 这里在进行accept会不会阻塞?不会!
if(sock < 0)
{
logMessage(WARNING, "accept error");
return;
}
logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
int pos = 1;
for(; pos < _nfds; pos++)
{
if(_fds[pos].fd == FD_NONE) break;
}
if(pos == _nfds)
{
logMessage(WARNING, "%s:%d", "select server already full, close: %d", sock);
close(sock);
}else
{
_fds[pos].fd = sock;
_fds[pos].events = POLLIN;
}
}
void Recver(int pos)
{
logMessage(DEBUG, "message in, get IO event: %d", _fds[pos]);
char buffer[1024];
int n = recv(_fds[pos].fd, buffer, sizeof(buffer)-1, 0);
if(n > 0)
{
buffer[n] = 0;
logMessage(DEBUG, "client[%d]# %s", _fds[pos].fd, buffer);
}
else if(n == 0)
{
logMessage(DEBUG, "client[%d] quit, me too...", _fds[pos]);
close(_fds[pos].fd);
_fds[pos].fd = FD_NONE;
_fds[pos].events = 0;
}
else
{
logMessage(WARNING, "%d sock recv error, %d : %s", _fds[pos], errno, strerror(errno));
close(_fds[pos].fd);
_fds[pos].fd = FD_NONE;
_fds[pos].events = 0;
}
}
void DebugPrint()
{
std::cout << "_fd_array[]: ";
for(int i = 0; i < _nfds; i++)
{
if(_fds[i].fd == FD_NONE) continue;
std::cout << _fds[i].fd << " ";
}
std::cout << std::endl;
}
private:
uint16_t _port;
int _listensock;
struct pollfd *_fds;
int _nfds;
int _timeout;
};
#endif
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ ./Server
[DEBUG] [1762059604] create base socket success
[DEBUG] [1762059605] time out...
[DEBUG] [1762059606] time out...
[DEBUG] [1762059606] get a new line success : [127.0.0.1:43702] : 4
[DEBUG] [1762059607] time out...
[DEBUG] [1762059608] time out...
[DEBUG] [1762059609] time out...
[DEBUG] [1762059610] time out...
[DEBUG] [1762059611] message in, get IO event: 4
[DEBUG] [1762059611] client[4]# hahahaha
[DEBUG] [1762059612] time out...
[DEBUG] [1762059612] message in, get IO event: 4
[DEBUG] [1762059612] client[4]# 66666
[DEBUG] [1762059613] time out...
[DEBUG] [1762059614] message in, get IO event: 4
[DEBUG] [1762059614] client[4]# 77777
[DEBUG] [1762059615] message in, get IO event: 4
[DEBUG] [1762059615] client[4]# dfvsbtsdh
[DEBUG] [1762059616] time out...
[DEBUG] [1762059616] message in, get IO event: 4
[DEBUG] [1762059616] client[4]# 325r3r43tq
[DEBUG] [1762059617] time out...
[DEBUG] [1762059618] time out...
[DEBUG] [1762059619] time out...
[DEBUG] [1762059620] time out...
[DEBUG] [1762059620] message in, get IO event: 4
[DEBUG] [1762059620] client[4] quit, me too...
[DEBUG] [1762059621] time out...
^C
// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet>
hahahaha
66666
77777
dfvsbtsdh
325r3r43tq
^]
telnet> quit
Connection closed.
六、I/O多路转接之epoll
1,epoll函数
按照man手册的说法: epoll 是为处理大批量句柄而作了改进的poll。
它是在2.5.44内核中被引进的,几乎具备了之前所说的一切优点。
它公认为Linux2.6以下,性能最好的多路I/O就绪通知方法。
epoll 有3个相关的系统调用:
(1)epoll_create:
功能:
创建一个epoll的句柄.
函数:
int epoll_create(int size);
说明:
自从linux2.6.8之后, size参数是被忽略的.
用完之后, 必须调用close()关闭.
(2)epoll_ctl:
功能:
epoll的事件注册函数.
它不同于select()是在监听事件时,告诉内核要监听什么类型的事件.
epoll在这里要先注册要监听的事件类型.
函数:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事
第二个参数op取值:
EPOLL_CTL_ADD : 注册新的fd到epfd中;
EPOLL_CTL_MOD : 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL : 从epfd中删除一个fd;
第四个参数struct epoll_event结构:
struct epoll event
{
uint32 t events; /* Epoll events */
epoll data t data; /* User data variable */
} EPOLL PACKED;
typedef union epoll data
{
void *ptr;
int fd;
uint32 t u32;
uint64 t u64;
} epoll_data_t;
第四个参数events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); . EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的;
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里;
(3)epoll_wait:
功能:
收集在epoll监控的事件中已经发送的事件.
函数:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数说明:
参数events是分配好的epoll_event结构体数组.
epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.
2,epoll的工作原理
epoll使用到了红黑树和双向链表:

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员,与epoll的使用方式密切相关.
eventpoll结构体
struct eventpoll
{
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着,将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
......
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插 入时间效率是lgn,其中n为树的高度).
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时,会调用这个回调方法.
这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中. 在epoll中,对于每一个事件,都会建立一个epitem结构体.
epitem结构体:
struct epitem
{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
};
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem 元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度 是O(1).
总结一下, 使用epoll的三部曲:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪;
3,epoll的优点
接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而select/poll都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上限.
4,epoll的工作方式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
举个例子:你妈喊你吃饭
你正在吃鸡 , 眼看进入了决赛圈 , 你妈饭做好了 , 喊你吃饭的时候有两种方式 :
-
如果你妈喊你一次 , 你没动 , 那么你妈会继续喊你第二次 , 第三次 ...(亲妈 , 水平触发)
-
如果你妈喊你一次 , 你没动 , 你妈就不管你了(后妈 , 边缘触发)
再举个例子:
我们已经把一个tcp socket添加到epoll描述符
这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回 . 说明它已经准备好读取操作
然后调用read, 只读取了1KB的数据
继续调用epoll_wait......
水平触发 Level Triggered 工作模式
epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分;
如上面的例子,由于只读了1K数据,缓冲区中还剩1K数据;
在第二次调用 epoll_wait 时,epoll_wait仍然会立刻返回并通知socket读事件就绪;
直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回;
支持阳塞读写和非阻塞读写;
边缘触发 Edge Triggered 工作模式
如果我们在第1步将socket添加到epoll描述符的时候,使用了EPOLLET标志,epoll进入ET工作模式.
当epol检测到socket上事件就绪时,必须立刻处理;
如上面的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用 epoll_wait 的时候epoll_wait 不会再返回了;
也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会;
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多):Nginx默认采用ET模式使用epoll;
只支持非阻塞的读写;
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET。
5,理解LT和ET模式
LT是epoll 的默认行为. 使用ET 能够减少epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比LT 更高效一些. 但是在LT 情况下如果也能做到 每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高了.
使用ET 模式的epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是"工程实践" 上的要求.
假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求.

如果服务端写的代码是阻塞式的read, 并且一次只read 1k 数据的话(read不能保证一次就把所有的数据都读出来, 参考man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中.

此时由于 epol 是ET模式,并不会认为文件描述符读就绪;epo11_wait 就不会再次返回;剩下的 9k数据会一直在缓冲区中;直到下一次客户端再给服务器写数据;epoll _wait 才能返回
但是问题来了:
服务器只读到1k个数据, 要10k读完才会给客户端返回响应数据.
客户端要读到服务器的响应, 才会发送下一个请求
客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据.

所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来.
而如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.
6,epoll的使用场景
epoll的高性能, 是有一定的特定场景的.
如果场景选择的不适宜, epoll的性能可能适得其反.
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根 据需求和场景特点来决定使用哪种IO模型.
7,epoll示例: epoll服务器(LT模式)
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ ll
total 64
-rw-rw-r-- 1 user user 835 Nov 4 15:36 Epoll.hpp
-rw-rw-r-- 1 user user 4923 Nov 4 15:36 EpollServer.hpp
-rw-rw-r-- 1 user user 2582 Nov 2 16:28 Log.hpp
-rw-rw-r-- 1 user user 338 Nov 4 15:36 Main.cc
-rw-rw-r-- 1 user user 88 Nov 2 13:30 Makefile
-rwxrwxr-x 1 user user 34088 Nov 4 15:36 Server
-rw-rw-r-- 1 user user 3573 Nov 2 16:28 Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ cat Main.cc
#include "EpollServer.hpp"
#include <memory>
using namespace std;
using namespace ns_epoll;
// 业务处理回调函数
void change(std::string request)
{
std::cout << "change : " << request << std::endl;
}
int main()
{
unique_ptr<EpollServer> epoll_server(new EpollServer(change));
epoll_server->start();
return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ cat Epoll.hpp
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
class Epoll
{
public:
static const int gsize = 256;
public:
static int CreateEpoll()
{
int epfd = epoll_create(gsize);
if(epfd > 0) return epfd;
exit(5);
}
static bool CtlEpoll(int epfd, int oper, int sock, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
int n = epoll_ctl(epfd, oper, sock, &ev);
return n == 0;
}
static bool WaitEpoll(int epfd, struct epoll_event revs[], int num, int timeout)
{
// 细节1: 如果底层就绪的sock非常多,revs装不下怎么办? 不影响,下次接着拿
// 细节2: 关于epoll_wait的返回值问题
return epoll_wait(epfd, revs, num, timeout);
}
};
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ cat EpollServer.hpp
#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__
#include <iostream>
#include <string>
#include <error.h>
#include <functional>
#include <cassert>
#include "Log.hpp"
#include "Sock.hpp"
#include "Epoll.hpp"
namespace ns_epoll
{
static const int default_port = 8080;
static const int gnum = 64;
// LT模式 -- 只处理读取
class EpollServer
{
public:
using func_t = std::function<void(std::string)>;
public:
EpollServer(func_t HandlerRequest, const int &port = default_port)
:_port(port), _revs_num(gnum), _HandlerRequest(HandlerRequest)
{
// 0.申请对应空间
_revs = new struct epoll_event[_revs_num];
// 1.创建listensock
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
// 2.创建epoll模型
_epfd = Epoll::CreateEpoll();
logMessage(DEBUG, "create epoll mode success, _listensock:%d, _epfd:%d", _listensock, _epfd);
// 3.将listensock添加到epoll中
if(!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD , _listensock, EPOLLIN)) exit(6);
logMessage(DEBUG, "add listensock to epoll success, _listensock:%d, _epfd:%d", _listensock, _epfd);
}
~EpollServer()
{
if(_listensock >= 0) close(_listensock);
if(_epfd >= 0) close(_epfd);
if(_revs) delete[] _revs;
}
void start()
{
int timeout = -1;
while(true)
{
loopOnce(timeout);
}
}
void loopOnce(int timeout) // 循环一次
{
int n = Epoll::WaitEpoll(_epfd, _revs, _revs_num, timeout);
switch (n)
{
case 0:
logMessage(DEBUG, "timeout ......");
break;
case -1:
logMessage(WARNING, "epoll wait error: %s", strerror(errno));
break;
default:
// 等待成功
logMessage(DEBUG, "get a event");
HandlerEvents(n);
break;
}
}
void HandlerEvents(int n)
{
assert(n > 0);
for(int i = 0; i < n; i++)
{
uint32_t revents = _revs[i].events;
int sock = _revs[i].data.fd;
// 读事件就绪
if(revents & EPOLLIN)
{
if(sock == _listensock) Accepter(_listensock); // 1. listensock 就绪
else Recver(sock); // 2. 一般sock就绪 - read
}
}
}
void Accepter(int listensock)
{
std::string clientip;
uint16_t clientport;
int sock = Sock::Accept(listensock, &clientip, &clientport);
if(sock < 0)
{
logMessage(WARNING, "accept error!");
return;
}
// 能不能直接读取?不能,因为你并不清楚,底层是否有数据!
// 将新的sock,添加给epoll
if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN)) return;
logMessage(DEBUG, "add new sock : %d to epoll success", sock);
}
void Recver(int sock)
{
// 1. 读取数据
char buffer[10240];
ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);
if(n > 0)
{
// 假设这里就是读到了一个完整的报文 // 如何保证??
buffer[n] = 0;
_HandlerRequest(buffer); // 2. 处理数据
}
else if(n == 0)
{
// 1. 先在epoll中去掉对sock的关心
bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
assert(res);
(void)res;
// 2. 在close文件
close(sock);
logMessage(NORMAL, "client %d quit, me too...", sock);
}
else
{
// 1. 先在epoll中去掉对sock的关心
bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
assert(res);
(void)res;
// 2. 在close文件
close(sock);
logMessage(NORMAL, "client recv %d error, close error sock", sock);
}
}
private:
uint16_t _port;
int _listensock;
int _epfd; // epoll_create()的返回值(epoll的句柄)
struct epoll_event *_revs; // 就绪元素
int _revs_num; // 就绪元素容量大小
func_t _HandlerRequest; // 回调处理函数
};
} // namespace name
#endif
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ ./Server
[DEBUG] [1762241823] create epoll mode success, _listensock:3, _epfd:4
[DEBUG] [1762241823] add listensock to epoll success, _listensock:3, _epfd:4
[DEBUG] [1762241836] get a event
[DEBUG] [1762241836] add new sock : 5 to epoll success
[DEBUG] [1762241844] get a event
change : hahahahaha
[DEBUG] [1762241848] get a event
change : 6666666666
[DEBUG] [1762241859] get a event
change : qaqaqaqaqa
[DEBUG] [1762241863] get a event
[NORMAL] [1762241863] client 5 quit, me too...
^C
// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet>
hahahahaha
6666666666
qaqaqaqaqa
^]
telnet> quit
Connection closed.
8,epoll示例: epoll服务器(ET模式)
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ ll
total 168
-rw-rw-r-- 1 user user 1068 Nov 6 11:44 Epoll.hpp
-rw-rw-r-- 1 user user 10263 Nov 6 11:57 EpollServer.hpp
-rw-rw-r-- 1 user user 2582 Nov 2 16:28 Log.hpp
-rw-rw-r-- 1 user user 1635 Nov 6 11:57 Main.cc
-rw-rw-r-- 1 user user 88 Nov 2 13:30 Makefile
-rw-rw-r-- 1 user user 3043 Nov 6 11:35 Protocol.hpp
-rwxrwxr-x 1 user user 131712 Nov 6 11:58 Server
-rw-rw-r-- 1 user user 3992 Nov 5 15:05 Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Main.cc
#include "EpollServer.hpp"
#include <memory>
static Response calculator(const Request &req)
{
Response resp(0, 0);
switch (req.op_)
{
case '+':
resp.result_ = req.x_ + req.y_;
break;
case '-':
resp.result_ = req.x_ - req.y_;
break;
case '*':
resp.result_ = req.x_ * req.y_;
break;
case '/':
if (0 == req.y_)
resp.code_ = 1;
else
resp.result_ = req.x_ / req.y_;
break;
case '%':
if (0 == req.y_)
resp.code_ = 2;
else
resp.result_ = req.x_ % req.y_;
break;
default:
resp.code_ = 3;
break;
}
return resp;
}
void NetCal(Connection *conn, std::string &request)
{
logMessage(DEBUG, "NetCal been called, get request: %s", request.c_str());
// 1. 反序列化
Request req;
if (!req.Deserialized(request))
return;
// 2. 业务处理
Response resp = calculator(req);
// 3. 序列化,构建应答
std::string sendstr = resp.Serialize();
sendstr = Encode(sendstr);
// 4. 交给服务器conn
conn->_outbuffer += sendstr;
// 5. 想办法,让底层的TcpServer,让它开始发送
// a. 需要有完整的发送逻辑
// b. 我们触发发送的动作,一旦我们开启EPOLLOUT,epoll会自动立马触发一次发送事件就绪,如果后续保持发送的开启,epoll会一直发送
conn->_esvr->EnableReadWrite(conn, true, true);
}
int main()
{
std::unique_ptr<EpollServer> epoll_server(new EpollServer());
epoll_server->start(NetCal);
return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Protocol.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <vector>
// 1. 报文和报文之间,我们采用特殊字符来进行解决粘报问题
// 2. 获取一个一个独立完整的报文,序列和反序列化 -- 自定义
// 100+19X100+19X100+19
// 支持解决粘报问题,处理独立报文
#define SEP "X"
#define SEP_LEN strlen(SEP)
// 自己手写序列反序列化
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
// 我们要把传入进来的缓冲区进行切分
// 1. buffer被切走的,也同时要从buffer中移除
// 2. 可能会存在多个报文,多个报文依次放入out
// buffer: 输入输出型参数
// out: 输出型参数
void SpliteMessage(std::string &buffer, std::vector<std::string> *out)
{
// 100+
// 100+19X1
// 100+19X100+19
while (true)
{
auto pos = buffer.find(SEP);
if (std::string::npos == pos)
break;
std::string message = buffer.substr(0, pos);
buffer.erase(0, pos + SEP_LEN);
out->push_back(message);
// std::cout << "debug: " << message << " : " << buffer << std::endl;
// sleep(1);
}
}
// TODO
std::string Encode(std::string &s)
{
return s + SEP;
}
class Request
{
public:
std::string Serialize()
{
std::string str;
str = std::to_string(x_);
str += SPACE;
str += op_; // TODO
str += SPACE;
str += std::to_string(y_);
return str;
}
bool Deserialized(const std::string &str) // 1 + 1
{
std::size_t left = str.find(SPACE);
if (left == std::string::npos)
return false;
std::size_t right = str.rfind(SPACE);
if (right == std::string::npos)
return false;
x_ = atoi(str.substr(0, left).c_str());
y_ = atoi(str.substr(right + SPACE_LEN).c_str());
if (left + SPACE_LEN > str.size())
return false;
else
op_ = str[left + SPACE_LEN];
return true;
}
public:
Request() {}
Request(int x, int y, char op) : x_(x), y_(y), op_(op)
{
}
~Request() {}
public:
int x_; // 是什么?
int y_; // 是什么?
char op_; // '+' '-' '*' '/' '%'
};
class Response
{
public:
// "code_ result_"
std::string Serialize()
{
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
}
// "111 100"
bool Deserialized(const std::string &s)
{
std::size_t pos = s.find(SPACE);
if (pos == std::string::npos)
return false;
code_ = atoi(s.substr(0, pos).c_str());
result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
return true;
}
public:
Response() {}
Response(int result, int code) : result_(result), code_(code)
{
}
~Response() {}
public:
// 约定!
// result_? code_? code_ 0? 1?2?3?
int result_; // 计算结果
int code_; // 计算结果的状态码
};
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Epoll.hpp
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
class Epoll
{
public:
static const int gsize = 256;
public:
Epoll() {}
~Epoll() {}
bool CtrlEpoll(int sock, uint32_t events)
{
events |= EPOLLET;
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
int n = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
return n == 0;
}
bool AddSockToEpoll(int sock, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
return n == 0;
}
int WaitEpoll(struct epoll_event revs[], int num, int timeout)
{
return epoll_wait(_epfd, revs, num, timeout);
}
void CreateEpoll()
{
_epfd = epoll_create(gsize);
if (_epfd < 0)
exit(5);
}
bool DelFromEpoll(int sock)
{
int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
return n == 0;
}
private:
int _epfd;
};
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat EpollServer.hpp
#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__
#include <iostream>
#include <string>
#include <error.h>
#include <functional>
#include <cassert>
#include <vector>
#include <unordered_map>
#include "Log.hpp"
#include "Sock.hpp"
#include "Epoll.hpp"
#include "Protocol.hpp"
class EpollServer;
class Connection;
using func_t = std::function<void(Connection *)>;
using callback_t = std::function<void(Connection *, std::string &request)>;
// ET模式 -- 常规sock必须要有自己的 接受缓冲区 && 发送缓冲区
class Connection
{
public:
Connection(int sock = -1) : _sock(sock), _esvr(nullptr)
{
}
~Connection() {}
void SetHandler(func_t recv_cb, func_t send_cb, func_t except_cb)
{
_recv_cb = recv_cb;
_send_cb = send_cb;
_except_cb = except_cb;
}
public:
int _sock; // 负责进行IO的fd
// 三个回调方法,表征的就是对_sock进行特点读写对应的方法
func_t _recv_cb; // 读取 -- 回调函数
func_t _send_cb; // 写入 -- 回调函数
func_t _except_cb; // 异常 -- 回调函数
// 接受缓冲区 && 发送缓冲区
std::string _inbuffer; // 接收缓冲区 -- 暂时没有办法处理二进制流,文本是可以的
std::string _outbuffer; // 发送缓冲区
// 设置对EpollServer的回值指针
EpollServer *_esvr;
};
// ET模式
class EpollServer
{
const static int gport = 8080;
const static int gnum = 128;
const static int gtimeout = 5;
public:
EpollServer(int port = gport) : _port(port), _revs_num(gnum)
{
// 0.构建一个获取就绪事件的缓冲区
_revs = new struct epoll_event[_revs_num];
// 1.创建listensock
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
// 2.创建epoll模型
_epoll.CreateEpoll();
// 3.添加_listensock到服务器中
AddConnection(_listensock, std::bind(&EpollServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
}
~EpollServer()
{
if (_listensock >= 0)
close(_listensock);
if (_revs)
delete[] _revs;
}
// 专门针对任意sock添加到EpollServer
void AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb)
{
// 1.使用ET模式,需要先把sock设置为非阻塞
Sock::SetNonBlock(sock);
// 2.构建conn对象,封装sock
// 除了_listensock, 未来会获取大量socket,每一个socket都要封装成Connection
// 所以EpollServer本身,就要管理所有的Connection
Connection *conn = new Connection(sock);
conn->SetHandler(recv_cb, send_cb, except_cb);
conn->_esvr = this;
// 3.将sock[]添加到epoll中
// 任何多路转接的服务器,一般默认只打开对读取的关心
_epoll.AddSockToEpoll(sock, EPOLLIN | EPOLLET);
// 4.将Connection*添加到map映射表
_Connections.insert(std::make_pair(sock, conn));
}
// 接收连接请求,并把sock添加到Epoll模型
void Accepter(Connection *conn)
{
// logMessage(DEBUG, "Accepter is called");
// 这里一定是_listensock已经就绪了,此次读取会阻塞吗?不会
// 底层不只一个连接就绪,需要用while多次读取
while (true)
{
std::string clientip;
uint16_t clientport;
int accept_errno = 0;
// sock一定是常规的IO sock
int sock = Sock::Accept(conn->_sock, &clientip, &clientport, &accept_errno);
if (sock < 0)
{
if (accept_errno == EAGAIN || accept_errno == EWOULDBLOCK)
break;
else if (accept_errno == EINTR)
continue; // 概率非常低
else
{
// accept失败
logMessage(WARNING, "accept error, %d : %s", accept_errno, strerror(accept_errno));
break;
}
}
// 将sock托管给epollserver
if (sock >= 0)
{
// accept成功
AddConnection(sock, std::bind(&EpollServer::Recver, this, std::placeholders::_1),
std::bind(&EpollServer::Sender, this, std::placeholders::_1),
std::bind(&EpollServer::Excepter, this, std::placeholders::_1));
logMessage(DEBUG, "accept client %s:%d success, add to epoll && EpollServer success", clientip.c_str(), clientport);
}
}
}
// 读取处理 (v1 -> v2还能优化)
void Recver(Connection *conn)
{
const int num = 1024;
bool err = false;
// logMessage(DEBUG, "Recver event exists, Recver() been called");
// v1: 直接面向字节流,先进行常规读取
while (true)
{
char buffer[num];
ssize_t n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);
if (n < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; // 正常的
else if (errno == EINTR)
continue;
else
{
logMessage(ERROR, "recv error, %d : %s", errno, strerror(errno));
conn->_except_cb(conn);
err = true;
break;
}
}
else if (n == 0)
{
logMessage(DEBUG, "client[%d] quit, server close [%d]", conn->_sock, conn->_sock);
conn->_except_cb(conn);
err = true;
break;
}
else
{
// 读取成功
buffer[n] = 0;
conn->_inbuffer += buffer;
}
}
// end while
logMessage(DEBUG, "conn->_inbuffer[sock: %d]: %s", conn->_sock, conn->_inbuffer.c_str());
// err = false 报文完整
if (!err)
{
std::vector<std::string> messages;
SpliteMessage(conn->_inbuffer, &messages);
// 我能保证走到这里,就是一个完整报文
for (auto &msg : messages)
_cb(conn, msg); // 可以在这里将message封装成为task,然后push到任务队列,任务处理交给后端线程池
}
}
// 发送处理 - 最开始的时候,我们的conn 是没有被触发的!
void Sender(Connection *conn)
{
while (true)
{
ssize_t 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;
else
{
logMessage(ERROR, "send error, %d : %s", errno, strerror(errno));
conn->_except_cb(conn);
break;
}
}
}
// 发完了吗?不确定,但是我们保证,如果没有出错,一定是要么发完,要么发送条件不满足,下次发送
if (conn->_outbuffer.empty())
EnableReadWrite(conn, true, false);
else
EnableReadWrite(conn, true, true);
}
// 使事件能够读和写
void EnableReadWrite(Connection *conn, bool readable, bool writeable)
{
uint32_t events = ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
bool res = _epoll.CtrlEpoll(conn->_sock, events);
assert(res); // 更改成if
}
// 异常处理
void Excepter(Connection *conn)
{
if (!IsConnectionExists(conn->_sock))
return;
// 1. 从epoll中移除
bool res = _epoll.DelFromEpoll(conn->_sock);
assert(res); // 要判断
// 2. 从我们的unorder_map中移除
_Connections.erase(conn->_sock);
// 3. close(sock);
close(conn->_sock);
// 4. delete conn;
delete conn;
logMessage(DEBUG, "Excepter 回收完毕,所有的异常情况");
}
// 根据就绪的事件,进行特点事件派发
void start(callback_t cb)
{
_cb = cb;
while (true)
{
LoopOnce();
}
}
// 循环一次
void LoopOnce()
{
int n = _epoll.WaitEpoll(_revs, _revs_num, gtimeout);
for (int i = 0; i < n; i++)
{
uint32_t revents = _revs[i].events;
int sock = _revs[i].data.fd;
// 读事件就绪
if (revents & EPOLLIN)
{
if (IsConnectionExists(sock) && _Connections[sock]->_recv_cb != nullptr)
_Connections[sock]->_recv_cb(_Connections[sock]);
}
// 写事件就绪
if (revents && EPOLLOUT)
{
if (IsConnectionExists(sock) && _Connections[sock]->_send_cb != nullptr)
_Connections[sock]->_recv_cb(_Connections[sock]);
}
}
}
// 判断连接是否存在
bool IsConnectionExists(int sock)
{
auto iter = _Connections.find(sock);
if (iter == _Connections.end())
return false;
else
return true;
}
private:
uint16_t _port; // 监听套接字
int _listensock; // 端口号
struct epoll_event *_revs; // 就绪元素
int _revs_num; // 就绪元素容量大小
Epoll _epoll; // epll模型
std::unordered_map<int, Connection *> _Connections; // 哈希映射 Sock : Connections
callback_t _cb; // 上层业务处理的回调函数
};
#endif
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// #include "Log.hpp"
// 套接字类
class Sock
{
public:
Sock() {}
~Sock() {}
// 创建socket文件描述符 (TCP/UDP, 客户端 + 服务器)
static int Socket()
{
// 1.创建套接字
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
// logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
// logMessage(NORMAL, "create socket success , _listensock:%d", listensock);
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return listensock;
}
// 绑定端口号 (TCP/UDP, 服务器)
static int Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
// 2.设置缓冲区,把ip和port转换成网络序列
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
// 3.绑定进程
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
}
// 监听套接字 (TCP, 服务器)
static void Listen(int sock)
{
// 4.建立链接
// tcp是面向连接的,正式通信的时候,需要先建立链接
if (listen(sock, gbacklog) < 0)
{
// logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
// logMessage(NORMAL, "listen socket success");
}
// 三种参数类型
// const std::string &: 输入型参数
// std::string *: 输出型参数
// std::string &: 输入输出型参数
// 接收请求 (TCP, 服务器)
static int Accept(int listensock, std::string *ip, uint16_t *port, int *accept_errno)
{
// 5.获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
*accept_errno = 0;
// ServiceSock(提供服务的李四王五) vs listensock(拉客的张三)
int ServiceSock = accept(listensock, (struct sockaddr *)&src, &len);
if (ServiceSock < 0) // 获取连接失败
{
// logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
*accept_errno = errno;
return -1;
}
// 6.获取连接成功,把ip和port转换成主机序列
if (port)
*port = ntohs(src.sin_port);
if (ip)
*ip = inet_ntoa(src.sin_addr);
return ServiceSock;
}
// 建立连接 (TCP, 客户端)
static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
return true;
else
return false;
}
// 把socket设置为非阻塞 (Epoll - ET模式)
static bool SetNonBlock(int sock)
{
int fl = fcntl(sock, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
if(fl < 0) return false;
fcntl(sock, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
return true;
}
private:
// gbacklog连接队列总容量 = accept已连接数 + SYN待连接数
const static int gbacklog = 10;
};
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
// 日志级别映射表
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
// 日志文件设置
#define LOGFILE "./log.log"
// 可变参数日志
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
// 条件编译
#ifndef DEBUG_SHOW
// 如果编译的时候没有携带DEBUG_SHOW选项,就不会打印DEBUG级别的日志
if(level== DEBUG) return;
#endif
// 传统写法: 手动提取参数列表
// va_list ap;
// va_start(ap, format);
// int x = va_arg(ap, int);
// va_end(ap);
// 现代写法: 调用可变参数打印函数
// 让用户传入参数的打印函数
// #include <stdio.h>
// int printf(const char *format, ...);
// int fprintf(FILE *stream, const char *format, ...);
// int sprintf(char *str, const char *format, ...);
// int snprintf(char *str,size_t size,const char *format, ...);
// 处理可变参数的打印函数
// #include <stdarg.h>
// int vprintf(const char *format, va_list ap); // 格式化显示到 -> 显示器
// int vfprintf(FILE *stream, const char *format, va_list ap); // 格式化显示到 -> 文件
// int vsprintf(char *str, const char *format,va_list ap); // 格式化显示到 -> 字符串
// int vsnprintf(char *str,size_t size, const char *format,va_list ap); // 格式化显示到 -> 指定长度字符串
// 日志固定消息部分 -- 等级/时间
char stdBuffer[1024];
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
// 可变参数部分
char logBuffer[1024];
va_list ap;
va_start(ap, format);
// vprintf(format, ap); // 格式化显示到 -> 显示器
vsnprintf(logBuffer, sizeof(logBuffer), format, ap); // 格式化显示到 -> 指定长度字符串
va_end(ap);
// 打印日志消息 = 固定部分 + 可变部分
printf("%s%s\n", stdBuffer, logBuffer);
// 日志消息 -- 还可以打印到文件里
// FILE *fp = fopen(LOGFILE, "a");
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ ./Server
[DEBUG] [1762402213] accept client 127.0.0.1:50944 success, add to epoll && EpollServer success
[DEBUG] [1762402219] conn->_inbuffer[sock: 5]: 1+2X3-4X5*6X
[DEBUG] [1762402219] NetCal been called, get request: 1+2
[DEBUG] [1762402219] NetCal been called, get request: 3-4
[DEBUG] [1762402219] NetCal been called, get request: 5*6
[DEBUG] [1762402219] conn->_inbuffer[sock: 5]:
[DEBUG] [1762402226] conn->_inbuffer[sock: 5]:
7/8
[DEBUG] [1762402226] conn->_inbuffer[sock: 5]:
7/8
[DEBUG] [1762402230] conn->_inbuffer[sock: 5]:
7/8
X9%9X
[DEBUG] [1762402230] NetCal been called, get request:
7/8
[DEBUG] [1762402230] NetCal been called, get request: 9%9
[DEBUG] [1762402230] conn->_inbuffer[sock: 5]:
[DEBUG] [1762402245] conn->_inbuffer[sock: 5]:
1#10
[DEBUG] [1762402245] conn->_inbuffer[sock: 5]:
1#10
[DEBUG] [1762402251] conn->_inbuffer[sock: 5]:
1#10
X
[DEBUG] [1762402251] NetCal been called, get request:
1#10
[DEBUG] [1762402251] conn->_inbuffer[sock: 5]:
[DEBUG] [1762402258] client[5] quit, server close [5]
[DEBUG] [1762402258] Excepter 回收完毕,所有的异常情况
[DEBUG] [1762402258] conn->_inbuffer[sock: -1685493832]:
// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet>
1+2X3-4X5*6X
7/8
X9%9X
10#10
X
^]
telnet> quit
Connection closed.
点个赞吧!666