代码开源:
介绍:
webserver_HTTP
使用了线程池,通过epoll实现的Proactor版本的web服务器。参考了游双老师的《Linux高性能服务器编程》以及牛客网的《Linux高并发服务器开发》课程。在自己复现的基础上进行模块的整合并添加一些小更改。所有代码拥有完备的注释。访问的资源在 同级目录"resources"文件夹中
在epoll_class中,我们简单介绍了使用epoll和socket的应用场景,但是关于epoll的事件等特性,还没有详细介绍,还有本项目封装的关于epoll事件的对应函数
目录
5.1 epoll 事件
5.1.1 epoll简介
在之前的部分中我们介绍了epoll的使用、epoll的结构、epoll进行TCP网络通信的基本架构,但是最重要的部分:epoll事件(events)还未介绍。
在使用epoll_ctl向内核注册事件,然后通过epoll_wait来获取监听到的事件列表,这样反复循环,就是监听部分的主要逻辑了。
5.1.2 常用的事件类型
事件是通过位掩码的方式表示的,因此表示多个事件的时候是使用| 按位或运算符连接。
本项目主要用到以下事件
EPOLLIN 文件描述符是否可读
EPOLLOUT 文件描述符是否可写
EPOLLRDHUP 对端是否关闭
EPOLLONESHOT 文件描述符上的注册事件只触发一次
即使可以使用 ET 模式,一个 socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个 问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该 socket 上又有新数据可读( EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于 是就出现了两个线程同时操作一个 socket 的局面。一个 socket连接在任一时刻都只被一个线程处理,可 以使用 epoll 的 EPOLLONESHOT 事件实现。对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异 常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事 件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的。但反过来思 考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕, 该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket 。
5.1.3 对事件进行操作
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数
- epfd:epoll对应的文件描述符
- op:options 有以下三种
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
- fd:要操作的fd
- event:要注册的事件,多个事件用 | 连接
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;
联合体,一般是使用其中的fd
};
- 返回值
- 成功: 0
- 失败: -1
这里摘录一段很清晰的示例代码
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}
来自 epoll使用详解:epoll_create、epoll_ctl、epoll_wait、close - 雾穹 - 博客园
5.2 本项目中对事件的操作
5.2.1 设置文件描述符非阻塞
文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引
在产生文件描述符的时候返回的数字是指在文件描述符表中注册后对应的索引。
我们可以使用fcntl来对文件描述符(fd,file descriptor)进行操作,比如将其设置为非阻塞。
API如下
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd: 表示对文件描述符进行如何操作
- F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
int ret = fcntl(fd, F_DUPFD);
- F_GETFL : 获取指定的文件描述符文件状态flag
获取的flag和我们通过open函数传递的flag是一个东西。
- F_SETFL : 设置文件描述符文件状态flag
必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
可选性:O_APPEND, O_NONBLOCK
O_APPEND 表示追加数据
O_NONBLOK 设置成非阻塞
阻塞和非阻塞:描述的是函数调用的行为。
因此设置非阻塞的思路:获取fd原有的状态flag,然后对其添加O_NONBLOCK
// 设置文件描述符非阻塞
void setnonblocking(int fd)
{
int old_flag = fcntl(fd, F_GETFL);
int new_flag = old_flag | O_NONBLOCK;
fcntl(fd, F_SETFL, new_flag);
}
5.2.2 添加、删除、重置操作的封装
实际上就是调用epoll_ctl,但是在封装的函数中可以统一指定添加、删除、修改的时候添加的事件,达到统一的目的。
比如因为注册了oneshot事件,因此修改时一定要添加oneshot使其可以再次被使用,否则下一次可读时EPOLLIN事件不会被触发。
// 添加需要监听的文件描述符到epoll
// 添加一个非阻塞的文件描述符,根据需要添加ONE_SHOT
// 添加EPOLLIN的同时,也添加了EPOLLRDHUP来判断对方是否中断
void addfd(int epollfd, int fd, bool one_shot)
{
epoll_event event;
event.data.fd = fd;
// EPOLLRDHUP判断是否挂起
event.events = EPOLLIN | EPOLLRDHUP;
if (one_shot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
// 设置文件描述符非阻塞
setnonblocking(fd);
}
// 从epoll中删除文件描述符,并close文件描述符
// 同时完成epoll内核中的删除和关闭文件描述符两个行为
void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
// 修改文件描述符,重置socket上EPOLLONESHOT事件,确保下次可读时被触发
// 设定文件描述符的事件为设定事件+ oneshot + 检测中断
// 因此每次监听完后要再次调用modfd重置描述符
void modfd(int epollfd, int fd, int ev)
{
epoll_event event;
event.data.fd = fd;
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}