网络编程:reactor的原理与实现
1. epoll是什么
epoll是Linux内核为处理大批量文件描述符(FD)而做了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
2. epoll的特点
获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了
其实是有了epoll之后,Linux才开始火了起来
3. epoll的接口有哪些
- epoll_create(int)
- 相当于创建了一个丰巢的盒子
- epoll_ctl(epfd, OPERA, fd, ev)
- 相当于每个人要交管理费,管理人员的添加和删除
- 第一个是每个epoll,第二个是操作,第三个每个fd,第四个是每个事件
- epoll_wait(epfd, events, evlength, timeout)
- 相对于快递员去取快递,timeout是多久去一次,上午一次下午一次
- events就是袋子,evlength就是袋子有多大
- 第二个是返回的事件有多少
- 第三个是events的长度
- 第四个是时间
4. epoll代码示例
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/epoll.h>
#include<string.h>
#define BUFFER_LENGTH 128
#define EVENTS_LENGTH 128
char rbuffer[BUFFER_LENGTH] = {
0};
char wbuffer[BUFFER_LENGTH] = {
0};
int main(){
// fd创建的时候默认是阻塞的
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) return -1;
// listenfd绑定一个固定的地址
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
// 返回-1 失败
return -2;
}
listen(listenfd, 10); // 相当于是迎宾的人
// 传的参数只要大于0就可以了
// 这个函数的参数一开始是设置就绪的最大数量的, 但其实用链表来串就绪集合就可以了
int epfd = epoll_create(1);
struct epoll_event ev, events[EVENTS_LENGTH];
// 为什么一开始要添加事件, 不应该是内核有事件通知我吗
ev.events = EPOLLIN;
ev.data.fd = listenfd;
// 这个关注一个可读的事件,后面有事件了才会通知我们
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
// printf("fd : %d \n", epfd);
// 服务器 7 * 24 运行就是因为这个函数
// 所有的循环都是这样操作的
while(1){
// 如果是-1,直到有事件才返回,没事件就会阻塞
// 0代表立即返回, timeout时间是毫秒
int nready = epoll_wait(epfd, events, EVENTS_LENGTH, 0);
// nready 有数据的时候返回是大于0的数
// printf("-----nready -> %d\n", nready);
int i = 0;
for(i = 0; i < nready; i++){
int clientfd = events->data.fd;
// 用accept处理
if(listenfd == clientfd){
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 新增加的fd, 也加到可读的fd集合中
int connfd = accept(listenfd, (struct sockaddr*)&client, &len);
printf("accept: %d\n", connfd);
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}else if(events[i].events & EPOLLIN) {
//clientfd
// char rbuffer[BUFFER_LENGTH] = {0};
int n = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
if(n > 0){
rbuffer[n] = '\0';
printf("recv: %s, n: %d\n", rbuffer, n);
memcpy(wbuffer, rbuffer, BUFFER_LENGTH);
int j = 0;
for(j = 0; j < BUFFER_LENGTH; j++){
rbuffer[j] = 'A' + (j % 26);
}
// send
ev.events = EPOLLOUT;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
}
}else if(events[i].events & EPOLLOUT){
int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0);
printf("sent: %d\n", sent);
}
}
}
}
5. Reactor是什么
Reactor模式是处理并发I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的 I/O 事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上
一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中。
(如果多个fd公用缓冲区,就会出现沾包的现象。这就有了reactor,每个fd对应的事件都应该是自己的,做一个隔离)
6. Reactor的优点
- 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的
- 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
- 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源
- 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性
7. 水平触发和边沿触发
水平触发(level-triggered)
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,当文件描述符关联的内核写缓冲区不满时(只要有空间就能写入),会一直发出可写信号通知
- epoll默认是水平触发
- 小数据的时候倾向LT,一次性读完
- 水平触发是可以用阻塞IO的
边沿触发(edge-triggered)
当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知
当文件描述符关联的写内核缓冲区由满转化为不满的时候,则发出可写信号进行通知
- 一次触发,循环读,读到里面没数据可读
- 数据会存放在缓冲区
- 大数据的时候用ET,其实与LT没什么区别
- 边沿触发是循环去recv,不能陷入阻塞IO
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:
Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习