epoll
初识
按照
man
手册的说法
:
是为处理大批量句柄而作了改进的
poll
.
它是在
2.5.44
内核中被引进的
(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为
Linux2.6
下性能最好的多路
I/O
就绪通知方法
.
epoll的相关系统调用
epoll 有3个相关的系统调用
epoll_create
int epoll_create(int size);
创建一个epoll的句柄.
自从linux2.6.8之后,size参数是被忽略的.
用完之后
,
必须调用
close()
关闭
.
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll
的事件注册函数
它不同于
select()
是在监听事件时告诉内核要监听什么类型的事件
,
而是在这里先注册要监听的事件类型
.
第一个参数是
epoll_create()
的返回值
(epoll
的句柄
).
第二个参数表示动作,用三个宏来表示
.
第三个参数是需要监听的
fd.
第四个参数是告诉内核需要监听什么事
第二个参数的取值
:
EPOLL_CTL_ADD
:注册新的
fd
到
epfd
中;
EPOLL_CTL_MOD
:修改已经注册的
fd
的监听事件;
EPOLL_CTL_DEL
:从
epfd
中删除一个
fd;
struct epoll_event
结构如下
:
![](https://i-blog.csdnimg.cn/blog_migrate/383600a92ab8ec947b584d073d705fd5.png)
events可以是以下几个宏的集合:
EPOLLIN :
表示对应的文件描述符可以读
(
包括对端
SOCKET
正常关闭
);
EPOLLOUT :
表示对应的文件描述符可以写
;
EPOLLPRI :
表示对应的文件描述符有紧急的数据可读
(
这里应该表示有带外数据到来
);
EPOLLERR :
表示对应的文件描述符发生错误
;
EPOLLHUP :
表示对应的文件描述符被挂断
;
EPOLLET :
将
EPOLL
设为边缘触发
(Edge Triggered)
模式
,
这是相对于水平触发
(Level Triggered)
来说的
.
EPOLLONESHOT
:只监听一次事件
,
当监听完这次事件之后
,
如果还需要继续监听这个
socket
的话
,
需要 再次把这个socket
加入到
EPOLL
队列里
.
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在
epoll
监控的事件中已经发送的事件
参数
events
是分配好的
epoll_event
结构体数组
.
epoll
将会把发生的事件赋值到
events
数组中
(events
不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存
). maxevents告之内核这个
events
有多大,这个
maxevents
的值不能大于创建
epoll_create()
时的
size.
参数
timeout
是超时时间
(
毫秒,
0
会立即返回,
-1
是永久阻塞
).
如果函数调用成功,返回对应
I/O
上已准备好的文件描述符数目,如返回
0
表示已超时
,
返回小于
0
表示函数失败
epoll
工作原理
![](https://i-blog.csdnimg.cn/blog_migrate/e8f3a3395478b12b236f39543544498f.png)
当某一进程调用
epoll_create
方法时,
Linux
内核会创建一个
eventpoll
结构体,这个结构体中有两个成 员与epoll
的使用方式密切相关
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
结构体
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,
等待文件描述符就绪
epoll
的优点
(
和
select
的缺点对应
)
接口使用方便
:
虽然拆分成了三个函数
,
但是反而使用起来更方便高效
.
不需要每次循环都设置关注的文 件描述符,
也做到了输入输出参数分离开
数据拷贝轻量
:
只在合适的时候调用
EPOLL_CTL_ADD
将文件描述符结构拷贝到内核中
,
这个操作并不频 繁(
而
select/poll
都是每次循环都要进行拷贝
)
事件回调机制:
避免使用遍历
,
而是使用回调函数的方式
,
将就绪的文件描述符结构加入到就绪队列中
, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪
.
这个操作时间复杂度
O(1).
即使文件描述 符数目很多,
效率也不会受到影响
.
没有数量限制
:
文件描述符数目无上限
注意
!!
网上有些博客说
, epoll
中使用了内存映射机制
内存映射机制
:
内核直接将就绪队列通过
mmap
的方式映射到用户态
.
避免了拷贝内存这样的额外性能开销
这种说法是不准确的
.
我们定义的
struct epoll_event
是我们在用户空间中分配好的内存
.
势必还是需要将内核的数据拷贝到这个用户空间的内存中的.
epoll
工作方式
你妈喊你吃饭的例子
你正在吃鸡
,
眼看进入了决赛圈
,
你妈饭做好了
,
喊你吃饭的时候有两种方式
:
1.
如果你妈喊你一次
,
你没动
,
那么你妈会继续喊你第二次
,
第三次
...(
亲妈
,
水平触发
)
2.
如果你妈喊你一次
,
你没动
,
你妈就不管你了
(
后妈
,
边缘触发
)
epoll
有
2
种工作方式
-
水平触发
(LT)
和边缘触发
(ET)
假如有这样一个例子
水平触发
Level Triggered
工作模式
epoll
默认状态下就是
LT
工作模式
.
当
epoll
检测到
socket
上事件就绪的时候
,
可以不立刻进行处理
.
或者只处理一部分
.
如上面的例子
,
由于只读了
1K
数据
,
缓冲区中还剩
1K
数据
,
在第二次调用
epoll_wait
时
,
epoll_wait
仍然会立刻返回并通知
socket
读事件就绪
.
直到缓冲区上所有的数据都被处理完
,
epoll_wait
才不会立刻返回
.
支持阻塞读写和非阻塞读写
边缘触发
Edge Triggered
工作模式
如果我们在第
1
步将
socket
添加到
epoll
描述符的时候使用了
EPOLLET
标志
, epoll
进入
ET
工作模式
.
当
epoll
检测到
socket
上事件就绪时
,
必须立刻处理
.
如上面的例子
,
虽然只读了
1K
的数据
,
缓冲区还剩
1K
的数据
,
在第二次调用
epoll_wait
的时候
,
epoll_wait
不会再返回了
.
也就是说
, ET
模式下
,
文件描述符上的事件就绪后
,
只有一次处理机会
.
ET
的性能比
LT
性能更高
(
epoll_wait
返回的次数少了很多
). Nginx
默认采用
ET
模式使用
epoll.
只支持非阻塞的读写
select
和
poll
其实也是工作在
LT
模式下
. epoll
既可以支持
LT,
也可以支持
ET.
LT
是
epoll
的默认行为
.
使用
ET
能够减少
epoll
触发的次数
.
但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.
相当于一个文件描述符就绪之后
,
不会反复被提示就绪
,
看起来就比
LT
更高效一些
.
但是在
LT
情况下如果也能做到 每次就绪的文件描述符都立刻处理,
不让这个就绪被重复提示的话
,
其实性能也是一样的
.
另一方面, ET
的代码复杂程度更高了
使用
ET
模式的
epoll,
需要将文件描述设置为非阻塞
.
这个不是接口上的要求
,
而是
"
工程实践
"
上的要求
.
假设这样的场景
:
服务器接受到一个
10k
的请求
,
会向客户端返回一个应答数据
.
如果客户端收不到应答
,
不会发送第二个10k
请求
![](https://i-blog.csdnimg.cn/blog_migrate/3d12844372a6e6564ea4d0c8bbd3ca0c.png)
如果服务端写的代码是阻塞式的
read,
并且一次只
read 1k
数据的话
(read
不能保证一次就把所有的数据都读出来
,参考 man
手册的说明
,
可能被信号打断
),
剩下的
9k
数据就会待在缓冲区中
![](https://i-blog.csdnimg.cn/blog_migrate/7efd8594eaf96c1196165c68bd3f1ccf.png)
此时由于
epoll
是
ET
模式
,
并不会认为文件描述符读就绪
.
epoll_wait
就不会再次返回
.
剩下的
9k
数据会一直在缓冲区中.
直到下一次客户端再给服务器写数据
.
epoll_wait
才能返回
但是问题来了
服务器只读到
1k
个数据
,
要
10k
读完才会给客户端返回响应数据
.
客户端要读到服务器的响应
,
才会发送下一个请求
客户端发送了下一个请求
,
epoll_wait
才会返回
,
才能去读缓冲区中剩余的数据
所以
,
为了解决上述问题
(
阻塞
read
不一定能一下把完整的请求读完
),
于是就可以使用非阻塞轮训的方式来读缓冲区
,保证一定能把完整的请求都读出来.
而如果是
LT
没这个问题
.
只要缓冲区中的数据没读完
,
就能够让
epoll_wait
返回文件描述符读就绪
.
epoll的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
epoll
中的惊群问题
epoll
示例
: epoll
服务器
(LT
模式
)
tcp_socket.hpp
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
#define CHECK_RET(exp) \
if (!(exp)) \
{ \
return false; \
} \
class TcpSocket
{
public:
TcpSocket() : fd_(-1) {}
TcpSocket(int fd) : fd_(fd) {}
bool Socket()
{
fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (fd_ < 0)
{
perror("socket");
return false;
}
printf("open fd = %d\n", fd_);
return true;
}
bool Close() const
{
close(fd_);
printf("close fd = %d\n", fd_);
return true;
}
bool Bind(const std::string &ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
int ret = bind(fd_, (sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return false;
}
return true;
}
bool Listen(int num) const
{
int ret = listen(fd_, num);
if (ret < 0)
{
perror("listen");
return false;
}
return true;
}
bool Accept(TcpSocket *peer, std::string *ip = NULL, uint16_t *port = NULL) const
{
sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
int new_sock = accept(fd_, (sockaddr *)&peer_addr, &len);
if (new_sock < 0)
{
perror("accept");
return false;
}
printf("accept fd = %d\n", new_sock);
peer->fd_ = new_sock;
if (ip != NULL)
{
*ip = inet_ntoa(peer_addr.sin_addr);
}
if (port != NULL)
{
*port = ntohs(peer_addr.sin_port);
}
return true;
}
bool Recv(std::string *buf) const
{
buf->clear();
char tmp[1024 * 10] = {0};
// [注意!] 这里的读并不算很严谨, 因为一次 recv 并不能保证把所有的数据都全部读完
// 参考 man 手册 MSG_WAITALL 节.
ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
if (read_size < 0)
{
perror("recv");
return false;
}
if (read_size == 0)
{
return false;
}
buf->assign(tmp, read_size);
return true;
}
bool Send(const std::string &buf) const
{
ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
if (write_size < 0)
{
perror("send");
return false;
}
return true;
}
bool Connect(const std::string &ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = connect(fd_, (sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("connect");
return false;
}
return true;
}
int GetFd() const
{
return fd_;
}
private:
int fd_;
}
;
tcp_epoll_sever.hpp
#pragma once
#include <vector>
#include <functional>
#include <sys/epoll.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string &, std::string *resp)> Handler;
class Epoll
{
public:
Epoll()
{
_epoll_fd = epoll_create(10);
}
~Epoll()
{
close(_epoll_fd);
}
bool Add(const TcpSocket &sock) const
{
int fd = sock.GetFd();
printf("[epoll add ] fd = %d\n", fd);
epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;
int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, fd, &ev);
if (ret < 0)
{
perror("epoll_ctl Add");
return false;
}
return true;
}
bool Del(const TcpSocket &sock) const
{
int fd = sock.GetFd();
printf("[epoll del] fd = %d\n", fd);
int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_DEL, fd, NULL);
if (ret < 0)
{
perror("epoll_ctl del");
return false;
}
return true;
}
bool Wait(std::vector<TcpSocket> *output) const
{
output->clear();
epoll_event events[1000];
int nfds = epoll_wait(_epoll_fd, events, sizeof(events) / sizeof(events[0]), -1);
if (nfds < 0)
{
perror("epool_wait");
return false;
}
// [注意!] 此处必须是循环到 nfds, 不能多循环
for (size_t i = 0; i < nfds; ++i)
{
TcpSocket sock(events[i].data.fd);
output->push_back(sock);
}
return true;
}
private:
int _epoll_fd;
};
class TcpEpollServer
{
public:
TcpEpollServer(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
{
}
TcpEpollServer(uint16_t port) :port_(port)
{
}
bool Start(Handler handler)
{
// 1. 创建 socket
TcpSocket listen_sock;
CHECK_RET(listen_sock.Socket());
// 2. 绑定
CHECK_RET(listen_sock.Bind(ip_, port_));
// 3. 监听
CHECK_RET(listen_sock.Listen(5));
// 4. 创建 Epoll 对象, 并将 listen_sock 加入进去
Epoll epoll;
epoll.Add(listen_sock);
// 5. 进入事件循环
for (;;)
{
// 6. 进行 epoll_wait
std::vector<TcpSocket> output;
if (!epoll.Wait(&output))
{
continue;
}
// 7. 根据就绪的文件描述符的种类决定如何处理
for (size_t i = 0; i < output.size(); ++i)
{
if (output[i].GetFd() == listen_sock.GetFd())
{
// 如果是 listen_sock, 就调用 accept
TcpSocket new_sock;
listen_sock.Accept(&new_sock);
epoll.Add(new_sock);
}
else
{
// 如果是 new_sock, 就进行一次读写
std::string req, resp;
bool ret = output[i].Recv(&req);
if (!ret)
{
// [注意!!] 需要把不用的 socket 关闭
// 先后顺序别搞反. 不过在 epoll 删除的时候其实就已经关闭 socket 了
epoll.Del(output[i]);
output[i].Close();
continue;
}
handler(req, &resp);
output[i].Send(resp);
} // end for
} // end for (;;)
}
return true;
}
private:
std::string ip_;
uint16_t port_;
};
server.cc
#include <unordered_map>
#include "tcp_epoll_server.hpp"
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{
auto it = g_dict.find(req);
if (it == g_dict.end())
{
*resp = "未找到";
return;
}
*resp = it->second;
return;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage ./dict_server [port]\n");
return 1;
}
// 1. 初始化词典
g_dict.insert(std::make_pair("hello", "你好"));
g_dict.insert(std::make_pair("world", "世界"));
g_dict.insert(std::make_pair("bit", "贼NB"));
// 2. 启动服务器
TcpEpollServer server(atoi(argv[1]));
server.Start(Translate);
return 0;
}
epoll
示例
: epoll
服务器
(ET
模式
)
基于
LT
版本稍加修改即可
1.
修改
tcp_socket.hpp,
新增非阻塞读和非阻塞写接口
2.
对于
accept
返回的
new_sock
加上
EPOLLET
这样的选项
注意
:
此代码暂时未考虑
listen_sock ET
的情况
.
如果将
listen_sock
设为
ET,
则需要非阻塞轮询的方式
accept.
否则 会导致同一时刻大量的客户端同时连接的时候,
只能
accept
一次的问题
tcp_socket.hpp
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
#define CHECK_RET(exp) \
if (!(exp)) \
{ \
return false; \
}
class TcpSocket
{
public:
TcpSocket() : fd_(-1) {}
TcpSocket(int fd) : fd_(fd) {}
// 以下代码添加在 TcpSocket 类中
// 非阻塞 IO 接口
bool SetNoBlock()
{
int fl = fcntl(fd_, F_GETFL);
if (fl < 0)
{
perror("fcntl F_GETFL");
return false;
}
int ret = fcntl(fd_, F_SETFL, fl | O_NONBLOCK);
if (ret < 0)
{
perror("fcntl F_SETFL");
return false;
}
return true;
}
bool RecvNoBlock(std::string *buf) const
{
// 对于非阻塞 IO 读数据, 如果 TCP 接受缓冲区为空, 就会返回错误
// 错误码为 EAGAIN 或者 EWOULDBLOCK, 这种情况也是意料之中, 需要重试
// 如果当前读到的数据长度小于尝试读的缓冲区的长度, 就退出循环
// 这种写法其实不算特别严谨(没有考虑粘包问题)
buf->clear();
char tmp[1024 * 10] = {0};
for (;;)
{
ssize_t read_size = recv(fd_, tmp, sizeof(tmp) - 1, 0);
if (read_size < 0)
{
if (errno == EWOULDBLOCK || errno == EAGAIN)
{
continue;
}
perror("recv");
return false;
}
if (read_size == 0)
{
// 对端关闭, 返回 false
return false;
}
tmp[read_size] = '\0';
*buf += tmp;
if (read_size < (ssize_t)sizeof(tmp) - 1)
{
printf("数据太少\n");
break;
}
}
return true;
}
bool SendNoBlock(const std::string &buf) const
{
// 对于非阻塞 IO 的写入, 如果 TCP 的发送缓冲区已经满了, 就会出现出错的情况
// 此时的错误号是 EAGAIN 或者 EWOULDBLOCK. 这种情况下不应放弃治疗
// 而要进行重试
ssize_t cur_pos = 0; // 记录当前写到的位置
ssize_t left_size = buf.size();
for (;;)
{
ssize_t write_size = send(fd_, buf.data() + cur_pos, left_size, 0);
if (write_size < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 重试写入
continue;
}
return false;
}
cur_pos += write_size;
left_size -= write_size;
// 这个条件说明写完需要的数据了
if (left_size <= 0)
{
break;
}
}
return true;
}
bool Socket()
{
fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (fd_ < 0)
{
perror("socket");
return false;
}
printf("open fd = %d\n", fd_);
return true;
}
bool Close() const
{
close(fd_);
printf("close fd = %d\n", fd_);
return true;
}
bool Bind(const std::string &ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
int ret = bind(fd_, (sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return false;
}
return true;
}
bool Listen(int num) const
{
int ret = listen(fd_, num);
if (ret < 0)
{
perror("listen");
return false;
}
return true;
}
bool Accept(TcpSocket *peer, std::string *ip = NULL, uint16_t *port = NULL) const
{
sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
int new_sock = accept(fd_, (sockaddr *)&peer_addr, &len);
if (new_sock < 0)
{
perror("accept");
return false;
}
printf("accept fd = %d\n", new_sock);
peer->fd_ = new_sock;
if (ip != NULL)
{
*ip = inet_ntoa(peer_addr.sin_addr);
}
if (port != NULL)
{
*port = ntohs(peer_addr.sin_port);
}
return true;
}
bool Recv(std::string *buf) const
{
buf->clear();
char tmp[1024 * 10] = {0};
// [注意!] 这里的读并不算很严谨, 因为一次 recv 并不能保证把所有的数据都全部读完
// 参考 man 手册 MSG_WAITALL 节.
ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
if (read_size < 0)
{
perror("recv");
return false;
}
if (read_size == 0)
{
return false;
}
buf->assign(tmp, read_size);
return true;
}
bool Send(const std::string &buf) const
{
ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
if (write_size < 0)
{
perror("send");
return false;
}
return true;
}
bool Connect(const std::string &ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = connect(fd_, (sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("connect");
return false;
}
return true;
}
int GetFd() const
{
return fd_;
}
private:
int fd_;
};
tcp_epoll_server.hpp
#pragma once
#include <vector>
#include <functional>
#include <sys/epoll.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string &, std::string *resp)> Handler;
class Epoll
{
public:
Epoll()
{
_epoll_fd = epoll_create(10);
}
~Epoll()
{
close(_epoll_fd);
}
bool Add(const TcpSocket &sock, bool epoll_et = false) const
{
int fd = sock.GetFd();
printf("[Epoll Add] fd = %d\n", fd);
epoll_event ev;
ev.data.fd = fd;
if (epoll_et)
{
ev.events = EPOLLIN | EPOLLET;
}
else
{
ev.events = EPOLLIN;
}
int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, fd, &ev);
if (ret < 0)
{
perror("epoll_ctl ADD");
return false;
}
return true;
}
bool Del(const TcpSocket &sock) const
{
int fd = sock.GetFd();
printf("[Epoll Del] fd = %d\n", fd);
int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_DEL, fd, NULL);
if (ret < 0)
{
perror("epoll_ctl DEL");
return false;
}
return true;
}
bool Wait(std::vector<TcpSocket> *output) const
{
output->clear();
epoll_event events[1000];
int nfds = epoll_wait(_epoll_fd, events, sizeof(events) / sizeof(events[0]), -1);
if (nfds < 0)
{
perror("epool_wait");
return false;
}
// [注意!] 此处必须是循环到 nfds, 不能多循环
for (size_t i = 0; i < nfds; ++i)
{
TcpSocket sock(events[i].data.fd);
output->push_back(sock);
}
return true;
}
private:
int _epoll_fd;
};
class TcpEpollServer
{
public:
TcpEpollServer(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
{
}
TcpEpollServer(uint16_t port) : port_(port)
{
}
bool Start(Handler handler)
{
// 1. 创建 socket
TcpSocket listen_sock;
CHECK_RET(listen_sock.Socket());
// 2. 绑定
CHECK_RET(listen_sock.Bind(ip_, port_));
// 3. 监听
CHECK_RET(listen_sock.Listen(5));
// 4. 创建 Epoll 对象, 并将 listen_sock 加入进去
Epoll epoll;
epoll.Add(listen_sock);
// 5. 进入事件循环
for (;;)
{
// 6. 进行 epoll_wait
std::vector<TcpSocket> output;
if (!epoll.Wait(&output))
{
continue;
}
// 7. 根据就绪的文件描述符的种类决定如何处理
for (size_t i = 0; i < output.size(); ++i)
{
if (output[i].GetFd() == listen_sock.GetFd())
{
// 如果是 listen_sock, 就调用 accept
TcpSocket new_sock;
listen_sock.Accept(&new_sock);
epoll.Add(new_sock,true);
}
else
{
// 如果是 new_sock, 就进行一次读写
std::string req, resp;
bool ret = output[i].RecvNoBlock(&req);
if (!ret)
{
// [注意!!] 需要把不用的 socket 关闭
// 先后顺序别搞反. 不过在 epoll 删除的时候其实就已经关闭 socket 了
epoll.Del(output[i]);
output[i].Close();
continue;
}
handler(req, &resp);
output[i].SendNoBlock(resp);
printf("[client %d] req: %s, resp: %s\n", output[i].GetFd(),
req.c_str(), resp.c_str());
} // end for
} // end for (;;)
}
return true;
}
private:
std::string ip_;
uint16_t port_;
};
server.cc
#include <unordered_map>
#include "tcp_epoll_server.hpp"
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{
auto it = g_dict.find(req);
if (it == g_dict.end())
{
*resp = "未找到";
return;
}
*resp = it->second;
return;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage ./dict_server [port]\n");
return 1;
}
// 1. 初始化词典
g_dict.insert(std::make_pair("hello", "你好"));
g_dict.insert(std::make_pair("world", "世界"));
g_dict.insert(std::make_pair("bit", "贼NB"));
// 2. 启动服务器
TcpEpollServer server(atoi(argv[1]));
server.Start(Translate);
return 0;
}