文章目录
- 基本介绍
- Reator的优点
- epoll多路复用接口
- epoll水平触发以及边缘触发
- 一个用epoll实现的小web服务器
基本介绍
Reactor模式称为反应堆模式或应答者模式,是基于事件驱动
的设计模式,拥有一个或多个并发输入源,有一个服务处理器和多个请求处理器,服务处理器会同步的将输入的请求事件以多路复用的方式分发给相应的请求处理器
。
空闲的进程或者线程会时刻监视着就绪事件队列,看有没有事件要处理,如果有事件来了,那么进程线程就会进行处理(并会注册新的事件) epoll就是采用的Reactor设计模式,与select和poll进行对比,很大的区别体现在内核部分,epoll的reator设计模式内核采用的是红黑树
,方便更快的插入与检索事件(会将发生的事件单独保存到一个数组中去,而select和poll就只能循环遍历整个事件数组看哪个事件发生了)。
Reator的优点
- 响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的;
- 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
- 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
- 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;
epoll多路复用接口
1.创建EPOLL 句柄
int epoll_create(int size); //可处理的句柄最大数量(但其实这个设置在很多操作系统中都无效了)
返回值为epoll句柄即epfd
2.向EPOLL对象中添加、修改或者删除感兴趣的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op取值:EPOLL_CTL_ADD 添加新的事件到epoll中
EPOLL_CTL_MOD 修改EPOLL中的事件
EPOLL_CTL_DEL 删除epoll中的事件
fd:要监视哪个fd
epoll_event:
struct epoll_event{
__uint32_t events;
/*
events取值:
EPOLLIN 表示有数据可以读出(接受连接、关闭连接)
EPOLLOUT 表示连接可以写入数据发送(向服务器发起连接,连接成功事件)
EPOLLERR 表示对应的连接发生错误
EPOLLHUP 表示对应的连接被挂起
*/
epoll_data_t data;//上下文,其他东西
}
typedef union epoll_data{
void *ptr;//上下文的数据
int fd; //文件句柄
uint32_t u32;
uint64_t u64;
}epoll_data_t
3.收集在epoll监控的事件中已经发生的事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epfd: epoll的描述符。
- events:则是分配好的 epoll_event结构体数组,
epoll将会把发生的事件复制到 events数组中
(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。 - maxevents: 本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。
- timeout: 表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果
timeout为0,立刻返回,不会等待。-1表示无限期阻塞
epoll水平触发以及边缘触发
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
设置方式: 默认即水平触发
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!
这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
设置方式: stat->_ev.events = EPOLLIN | EPOLLET
一个用epoll实现的小web服务器
注释全部都在代码中了,我就不单独说明了(如果还有一些困难的话,可以看我之前写的关于select和poll的文章参考参考),如果哪位朋友有兴趣可以进行编译玩一下😀编译命令:
gcc epoll_web_server.c -o epoll_web_server.exe
运行命令
./epoll_web_server.exe [ip] [port]//绑定ip和端口号,然后我们就可以用浏览器访问了
epoll_web_server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<fcntl.h>
#include<unistd.h>
typedef struct _ConnectStat ConnectStat;
typedef void(*response_handler) (ConnectStat * stat);
struct _ConnectStat {
int fd;
char name[64];
char age[64];
struct epoll_event _ev;//保存我们现在的这个文件的event
int status;//0 -未登录 1 - 已登陆
response_handler handler;//不同页面的处理函数,函数指针(未登录调用处理、已登录调用处理)
};
//http协议相关代码
ConnectStat * stat_init(int fd);
void connect_handle(int new_fd);
void do_http_respone(ConnectStat * stat);
void do_http_request(ConnectStat * stat);
void welcome_response_handler(ConnectStat * stat);
void commit_respone_handler(ConnectStat * stat);
const char *main_header = "HTTP/1.0 200 OK\r\nServer: Martin Server\r\nContent-Type: text/html\r\nConnection: Close\r\n";
static int epfd = 0;
void usage(const char* argv)
{
printf("%s:[ip][port]\n", argv);
}
void set_nonblock(int fd)
{
int fl = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int startup(char* _ip, int _port) //创建一个套接字,绑定,检测服务器
{
//sock
//1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror("sock");
exit(2);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//2.填充本地 sockaddr_in 结构体(设置本地的IP地址和端口)
struct sockaddr_in local;
local.sin_port = htons(_port);
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip);
//3.bind()绑定
if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
//4.listen()监听 检测服务器
if (listen(sock, 5) < 0)
{
perror("listen");
exit(4);
}
//sleep(1000);
return sock; //这样的套接字返回
}
int main(int argc, char *argv[])
{
if (argc != 3) //检测参数个数是否正确
{
usage(argv[0]);
exit(1);
}
//创建一个绑定了本地ip和端口号的套接字描述符
int listen_sock = startup(argv[1], atoi(argv[2]));
//1.创建epoll
epfd = epoll_create(256); //可处理的最大句柄数256个(其实在很多操作系统中这个限制都无效了)
if (epfd < 0)
{
perror("epoll_create");
exit(5);
}
/*
struct epoll_event{
__uint32_t events;
epoll_data_t data;//上下文,其他东西
}
typedef union epoll_data{
void *ptr;//上下文的数据
int fd;//文件句柄
uint32_t u32;
uint64_t u64;
}epoll_data_t
*/
struct epoll_event _ev; //epoll结构填充 ,创建事件
ConnectStat * stat = stat_init(listen_sock);
_ev.events = EPOLLIN; //初始关心事件为读,请求客户端进行连接
_ev.data.ptr = stat; //将ev中的指针指向stat,以此很多信息
//2.托管
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &_ev); //将listen sock添加到epfd中,关心读事件
//64可表示为同时接收的事件最大数目
struct epoll_event revs[64];
int timeout = -1;
int num = 0;
int done = 0;
while (!done)
{
//epoll_wait()相当于在检测事件
switch ((num = epoll_wait(epfd, revs, 64, timeout))) //返回需要处理的事件数目,64表示返回事件接收的最大数量
{
case 0: //返回0 ,表示监听超时
printf("timeout\n");
break;
case -1: //出错
perror("epoll_wait");
break;
default: //大于零 即就是返回了需要处理事件的数目
{
struct sockaddr_in peer; //保存客户端的地址结构
socklen_t len = sizeof(peer);
int i;
for (i = 0; i < num; i++)//num为已经就绪的事件
{
//就绪的事件也会保存到revs中
ConnectStat * stat = (ConnectStat *)revs[i].data.ptr;//拿到客户端连接状态,拿到stat
//准确获取哪个事件的描述符
int rsock = stat->fd;
//如果是请求连接
if (rsock == listen_sock && (revs[i].events) && EPOLLIN)
{
//保存客户端的fd
int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len);
//得到一个新ip
if (new_fd > 0)
{
printf("get a new client:%s:%d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
//sleep(1000);
connect_handle(new_fd);//将新的fd放到epoll里面去监听
}
}
else // 接下来对num - 1 个事件处理
{
if (revs[i].events & EPOLLIN)
{
do_http_request((ConnectStat *)revs[i].data.ptr);
}
else if (revs[i].events & EPOLLOUT)
{
do_http_respone((ConnectStat *)revs[i].data.ptr);
}
else
{
}
}
}
}
break;
}//end switch
}//end while
return 0;
}
//初始化connectstat,在这没有设置哪个handler进行处理
ConnectStat * stat_init(int fd) {
ConnectStat * temp = NULL;
temp = (ConnectStat *)malloc(sizeof(ConnectStat));
if (!temp) {
fprintf(stderr, "malloc failed. reason: %m\n");
return NULL;
}
memset(temp, '\0', sizeof(ConnectStat));
temp->fd = fd;
temp->status = 0;
//temp->handler = welcome_response_handler;
}
//初始化连接,然后等待浏览器发送请求
void connect_handle(int new_fd) {
ConnectStat *stat = stat_init(new_fd);
set_nonblock(new_fd);
stat->_ev.events = EPOLLIN;//套接字可读
stat->_ev.data.ptr = stat;
epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &stat->_ev); //二次托管,将event再传进去
}
void do_http_respone(ConnectStat * stat) {
stat->handler(stat);
}
void do_http_request(ConnectStat * stat) {
//读取和解析http 请求
char buf[4096];
char * pos = NULL;
//while header \r\n\r\ndata
ssize_t _s = read(stat->fd, buf, sizeof(buf) - 1);
if (_s > 0)
{
buf[_s] = '\0';
printf("receive from client:%s\n", buf);
pos = buf;
//Demo 仅仅演示效果,不做详细的协议解析
if (!strncasecmp(pos, "GET", 3)) {
stat->handler = welcome_response_handler;
}
else if (!strncasecmp(pos, "Post", 4)) {
//获取 uri
printf("---Post----\n");
pos += strlen("Post");
while (*pos == ' ' || *pos == '/') ++pos;
if (!strncasecmp(pos, "commit", 6)) {//获取名字和年龄
int len = 0;
printf("post commit --------\n");
pos = strstr(buf, "\r\n\r\n");
char *end = NULL;
if (end = strstr(pos, "name=")) {
pos = end + strlen("name=");
end = pos;
while (('a' <= *end && *end <= 'z') || ('A' <= *end && *end <= 'Z') || ('0' <= *end && *end <= '9')) end++;
len = end - pos;
if (len > 0) {
memcpy(stat->name, pos, end - pos);
stat->name[len] = '\0';
}
}
if (end = strstr(pos, "age=")) {
pos = end + strlen("age=");
end = pos;
while ('0' <= *end && *end <= '9') end++;
len = end - pos;
if (len > 0) {
memcpy(stat->age, pos, end - pos);
stat->age[len] = '\0';
}
}
stat->handler = commit_respone_handler;
}
else {
stat->handler = welcome_response_handler;
}
}
else {
stat->handler = welcome_response_handler;
}
//生成处理结果 html ,write
stat->_ev.events = EPOLLOUT;
//stat->_ev.data.ptr = stat;
epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev); //二次托管
}
else if (_s == 0) //client:close
{
printf("client: %d close\n", stat->fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, stat->fd, NULL);
close(stat->fd);
free(stat);
}
else
{
perror("read");
}
}
void welcome_response_handler(ConnectStat * stat) {
const char * welcome_content = "\
<html lang=\"zh-CN\">\n\
<head>\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
<title>This is a test</title>\n\
</head>\n\
<body>\n\
<div align=center height=\"500px\" >\n\
<br/><br/><br/>\n\
<h2>大家好,欢迎来到我的网页!</h2><br/><br/>\n\
<form action=\"commit\" method=\"post\">\n\
尊姓大名: <input type=\"text\" name=\"name\" />\n\
<br/>芳龄几何: <input type=\"password\" name=\"age\" />\n\
<br/><br/><br/><input type=\"submit\" value=\"提交\" />\n\
<input type=\"reset\" value=\"重置\" />\n\
</form>\n\
</div>\n\
</body>\n\
</html>";
char sendbuffer[4096];
char content_len[64];
strcpy(sendbuffer, main_header);
snprintf(content_len, 64, "Content-Length: %d\r\n\r\n", (int)strlen(welcome_content));
strcat(sendbuffer, content_len);
strcat(sendbuffer, welcome_content);
printf("send reply to client \n%s", sendbuffer);
write(stat->fd, sendbuffer, strlen(sendbuffer));
stat->_ev.events = EPOLLIN;
//stat->_ev.data.ptr = stat;
epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);
}
void commit_respone_handler(ConnectStat * stat) {
const char * commit_content = "\
<html lang=\"zh-CN\">\n\
<head>\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
<title>This is a test</title>\n\
</head>\n\
<body>\n\
<div align=center height=\"500px\" >\n\
<br/><br/><br/>\n\
<h2>欢迎学霸同学 %s ,你的芳龄是 %s!</h2><br/><br/>\n\
</div>\n\
</body>\n\
</html>\n";
char sendbuffer[4096];
char content[4096];
char content_len[64];
int len = 0;
len = snprintf(content, 4096, commit_content, stat->name, stat->age);
strcpy(sendbuffer, main_header);
snprintf(content_len, 64, "Content-Length: %d\r\n\r\n", len);
strcat(sendbuffer, content_len);
strcat(sendbuffer, content);
printf("send reply to client \n%s", sendbuffer);
write(stat->fd, sendbuffer, strlen(sendbuffer));
stat->_ev.events = EPOLLIN;
//stat->_ev.data.ptr = stat;
epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);
}
感谢您看完了这一篇文章希望这篇文章对您有帮助,如果本篇文章有任何错误或者您有相关建议,也请告诉我,谢谢(●’◡’●)