Linux 网络编程 - epoll 模型原理和示例
分类: Linux高性能网络编程(32)
目录(?)[+]
1.简介
Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll 。
epoll是什么?按照man手册的说法是为处理大批量句柄而作了改进的poll。要使用epoll只需要以下的三个系统函数调用: epoll_create(2),epoll_ctl(2),epoll_wait(2)。
2.select模型的缺陷
(1) 在Linux内核中,select所用到的FD_SET是有限的
内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数:#define __FD_SETSIZE 1024。也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的;或者同时检测1025个句柄的可写状态也是不可能的。
(2) 内核中实现select是使用轮询方法
每次检测都会遍历所有FD_SET中的句柄,显然select函数的执行时间与FD_SET中句柄的个数有一个比例关系,即select要检测的句柄数越多就会越费时
3.Windows IOCP模型的缺陷
windows完成端口实现的AIO,实际上也只是使用内部用线程池实现的,最后的结果是IO有个线程池,你的应用程序也需要一个线程池。很多文档其实已经指出了这引发的线程context-switch所带来的代价。
4.EPOLL模型的优点
(1) 支持一个进程打开大数目的socket描述符(FD)
epoll没有select模型中的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于select 所支持的2048。下面是我的小PC机上的显示:
pt@ubuntu:~$ cat /proc/sys/fs/file-max
6815744
那么对于服务器而言,这个数目会更大。
(2) IO效率不随FD数目增加而线性下降
传统select/poll的另一个致命弱点就是当你拥有一个很大的socket集合,由于网络得延时,使得任一时间只有部分的socket是”活跃”的,而select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操作:这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。于是,只有”活跃”的socket才会主动去调用callback函数,其他idle状态的socket则不会。在这点上,epoll实现了一个”伪”AIO”,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的,比如一个高速LAN环境,epoll也不比select/poll低多少效率,但若过多使用的调用epoll_ctl,效率稍微有些下降。然而一旦使用idle connections模拟WAN环境,那么epoll的效率就远在select/poll之上了。
(3) 使用mmap加速内核与用户空间的消息传递
无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就显得很重要。在这点上,epoll是通过内核于用户空间mmap同一块内存实现。
5.EPOLL模型的工作模式
(1) LT模式
LT:level triggered,这是缺省的工作方式,同时支持block和no-block socket,在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
(2) ET模式
LT:edge-triggered,这是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
6.EPOLL模型的使用方法
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
(1) epoll_data、epoll_data_t、epoll_event
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t; struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}; 结
构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可能的取值为:
EPOLLIN: 表示对应的文件描述符可以读;
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读;
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 表示对应的文件描述符有事件发生;
联合体epoll_data用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。
(2)epoll_create
函数声明:intepoll_create(intsize)
函数说明:该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。
(3) epoll_ctl函数
函数声明:intepoll_ctl(int epfd,int op, int fd, struct epoll_event *event)
函数说明:该函数用于控制某个文件描述符上的事件,可以注册事件、修改事件、删除事件。
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功则返回0,不成功则返回-1。
(4) epoll_wait函数
函数声明:int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout)
函数说明:该函数用于轮询I/O事件的发生。
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;
返回发生事件数。
7 设计思路及模板
首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你的epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作都将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
然后在你的网络主循环里面,调用epoll_wait(int epfd, epoll_event events, int max_events,int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait函数操作成功之后,events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout参数指示 epoll_wait的超时条件,为0时表示马上返回;为-1时表示函数会一直等下去直到有事件返回;为任意正整数时表示等这么长的时间,如果一直没有事件,则会返回。一般情况下如果网络主循环是单线程的话,可以用-1来等待,这样可以保证一些效率,如果是和主循环在同一个线程的话,则可以用0来保证主循环的效率。epoll_wait返回之后,应该进入一个循环,以便遍历所有的事件。
对epoll 的操作就这么简单,总共不过4个API:epoll_create, epoll_ctl,epoll_wait和close。以下是man中的一个例子。
struct epoll_event ev, *events;
for(;;)
{
nfds = epoll_wait(kdpfd, events, maxevents, -1); //等待IO事件
for(n = 0; n < nfds; ++n)
{
//如果是主socket的事件,则表示有新连接进入,需要进行新连接的处理。
if(events[n].data.fd == listener)
{
client = accept(listener, (struct sockaddr *) &local, &addrlen);
if(client < 0)
{
perror("accept error");
continue;
}
// 将新连接置于非阻塞模式
setnonblocking(client);
ev.events = EPOLLIN | EPOLLET;
//注意这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
//如果有写操作的话,这个时候epoll是不会返回事件的,
//如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET。
// 并且将新连接也加入EPOLL的监听队列
ev.data.fd = client;
// 设置好event之后,将这个新的event通过epoll_ctl
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
{
//加入到epoll的监听队列里,这里用EPOLL_CTL_ADD
//来加一个新的 epoll事件。可以通过EPOLL_CTL_DEL来减少
//一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
fprintf(stderr, "epoll set insertion error: fd=%d"0, client);
return -1;
}
}
else
// 如果不是主socket的事件的话,则代表这是一个用户的socket的事件,
// 则用来处理这个用户的socket的事情是,比如说read(fd,xxx)之类,或者一些其他的处理。
do_use_fd(events[n].data.fd);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
8 EPOLL模型的简单实例
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000
void setnonblocking(int sock)
{
int opts;
opts = fcntl(sock, F_GETFL);
if(opts < 0)
{
perror("fcntl(sock, GETFL)");
exit(1);
}
opts = opts | O_NONBLOCK;
if(fcntl(sock, F_SETFL, opts) < 0)
{
perror("fcntl(sock,SETFL,opts)");
exit(1);
}
}
int main()
{
int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
//声明epoll_event结构体的变量, ev用于注册事件, events数组用于回传要处理的事件
struct epoll_event ev,events[20];
//生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256
epfd = epoll_create(256);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(listenfd); //把用于监听的socket设置为非阻塞方式
ev.data.fd = listenfd; //设置与要处理的事件相关的文件描述符
ev.events = EPOLLIN | EPOLLET; //设置要处理的事件类型
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //注册epoll事件
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
char *local_addr = "200.200.200.204";
inet_aton(local_addr, &(serveraddr.sin_addr));
serveraddr.sin_port = htons(SERV_PORT); //或者htons(SERV_PORT);
bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi = 0;
for( ; ; )
{
nfds = epoll_wait(epfd, events, 20, 500); //等待epoll事件的发生
for(i = 0; i < nfds; ++i) //处理所发生的所有事件
{
if(events[i].data.fd == listenfd) //监听事件
{
connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen);
if(connfd < 0)
{
perror("connfd<0");
exit(1);
}
setnonblocking(connfd); //把客户端的socket设置为非阻塞方式
char *str = inet_ntoa(clientaddr.sin_addr);
std::cout << "connect from " << str <<std::endl;
ev.data.fd=connfd; //设置用于读操作的文件描述符
ev.events=EPOLLIN | EPOLLET; //设置用于注测的读操作事件
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
//注册ev事件
}
else if(events[i].events&EPOLLIN) //读事件
{
if ( (sockfd = events[i].data.fd) < 0)
{
continue;
}
if ( (n = read(sockfd, line, MAXLINE)) < 0) // 这里和IOCP不同
{
if (errno == ECONNRESET)
{
close(sockfd);
events[i].data.fd = -1;
}
else
{
std::cout<<"readline error"<<std::endl;
}
}
else if (n == 0)
{
close(sockfd);
events[i].data.fd = -1;
}
ev.data.fd=sockfd; //设置用于写操作的文件描述符
ev.events=EPOLLOUT | EPOLLET; //设置用于注测的写操作事件
//修改sockfd上要处理的事件为EPOLLOUT
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
else if(events[i].events&EPOLLOUT)//写事件
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
ev.data.fd = sockfd; //设置用于读操作的文件描述符
ev.events = EPOLLIN | EPOLLET; //设置用于注册的读操作事件
//修改sockfd上要处理的事件为EPOLIN
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
9.epoll进阶思考
9.1. 问题来源
最近学习EPOLL模型,介绍中说将EPOLL与Windows IOCP模型进行比较,说其的优势在于解决了IOCP模型大量线程上下文切换的开销,于是可以看出,EPOLL模型不需要多线程,即单线程中可以处理EPOLL逻辑。如果引入多线程反而会引起一些问题。但是EPOLL模型的服务器端到底可以不可以用多线程技术,如果可以,改怎么取舍,这成了困扰我的问题。上网查了一下,有这样几种声音:
(1) “要么事件驱动(如epoll),要么多线程,要么多进程,把这几个综合起来使用,感觉更加麻烦。”;
(2) “单线程使用epoll,但是不能发挥多核;多线程不用epoll。”;
(3) “主通信线程使用epoll所有需要监控的FD,有事件交给多线程去处理”;
(4) “既然用了epoll, 那么线程就不应该看到fd, 而只看到的是一个一个的业务请求/响应; epoll将网络数据组装成业务数据后, 转交给业务线程进行处理。这就是常说的半同步半异步”。
我比较赞同上述(3)、(4)中的观点
EPOLLOUT只有在缓冲区已经满了,不可以发送了,过了一会儿缓冲区中有空间了,就会触发EPOLLOUT,而且只触发一次。如果你编写的程序的网络IO不大,一次写入的数据不多的时候,通常都是epoll_wait立刻就会触发 EPOLLOUT;如果你不调用 epoll,直接写 socket,那么情况就取决于这个socket的缓冲区是不是足够了。如果缓冲区足够,那么写就成功。如果缓冲区不足,那么取决你的socket是不是阻塞的,要么阻塞到写完成,要么出错返回。所以EPOLLOUT事件具有较大的随机性,ET模式一般只用于EPOLLIN, 很少用于EPOLLOUT。
9.2. 具体做法
(1) 主通信线程使用epoll所有需要监控的FD,负责监控listenfd和connfd,这里只监听EPOLLIN事件,不监听EPOLLOUT事件;
(2) 一旦从Client收到了数据以后,将其构造成一个消息,放入消息队列中;
(3) 若干工作线程竞争,从消息队列中取出消息并进行处理,然后把处理结果发送给客户端。发送客户端的操作由工作线程完成。直接进行write。write到EAGAIN或EWOULDBLOCK后,线程循环continue等待缓冲区队列
发送函数代码如下:
bool send_data(int connfd, char *pbuffer, unsigned int &len,int flag)
{
if ((connfd < 0) || (0 == pbuffer))
{
return false;
}
int result = 0;
int remain_size = (int) len;
int send_size = 0;
const char *p = pbuffer;
time_t start_time = time(NULL);
int time_out = 3;
do
{
if (time(NULL) > start + time_out)
{
return false;
}
send_size = send(connfd, p, remain_size, flag);
if (nSentSize < 0)
{
if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR))
{
continue;
}
else
{
len -= remain_size;
return false;
}
}
p += send_size;
remain_size -= send_size;
}while(remain_size > 0);
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
10 epoll 实现服务器和客户端例子
最后我们用C++实现一个简单的客户端回射,所用到的代码文件是
net.h server.cpp client.cpp
- 1
服务器端:epoll实现的,干两件事分别为:1.等待客户端的链接,2.接收来自客户端的数据并且回射;
客户端:select实现,干两件事为:1.等待键盘输入,2.发送数据到服务器端并且接收服务器端回射的数据;
/***********
net.h
***********/
#include <stdio.h>
#ifndef _NET_H
#define _NET_H
#include <iostream>
#include <vector>
#include <algorithm>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h> //epoll ways file
#include <sys/socket.h>
#include <fcntl.h> //block and noblock
#include <stdlib.h>
#include <error.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
using namespace std;
#define hand_error(msg) do{perror(msg); exit(EXIT_FAILURE);}while(0)
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
/***********
server.c
***********/
#include "net.h"
#define MAX_EVENTS 10000
int setblock(int sock)
{
int ret = fcntl(sock, F_SETFL, 0);
if (ret < 0 )
hand_error("setblock");
return 0;
}
int setnoblock(int sock) //设置非阻塞模式
{
int ret = fcntl(sock, F_SETFL, O_NONBLOCK );
if(ret < 0)
hand_error("setnoblock");
return 0;
}
int main()
{
signal(SIGPIPE,SIG_IGN);
int listenfd;
listenfd = socket( AF_INET, SOCK_STREAM,0 ); //create a socket stream
if( listenfd < 0 )
hand_error( "socket_create");
setnoblock(listenfd);
int on = 1;
if( setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))< 0)
hand_error("setsockopt");
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(18000); //here is host sequeue
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if( bind( listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0)
hand_error("bind");
int lisId = listen(listenfd, SOMAXCONN);
if( lisId < 0) //LISTEN
hand_error("listen");
struct sockaddr_in peer_addr; //用来 save client addr
socklen_t peerlen;
//下面是一些初始化,都是关于epoll的。
vector<int> clients;
int count = 0;
int cli_sock = 0;
int epfd = 0; //epoll 的文件描述符
int ret_events; //epoll_wait()的返回值
struct epoll_event ev_remov, ev, events[MAX_EVENTS]; //events 用来存放从内核读取的的事件
ev.events = EPOLLET | EPOLLIN; //边缘方式触发
ev.data.fd = listenfd;
epfd = epoll_create(MAX_EVENTS); //create epoll,返回值为epoll的文件描述符
//epfd = epoll_create(EPOLL_CLOEXEC); //新版写法
if(epfd < 0)
hand_error("epoll_create");
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //添加时间
if(ret < 0)
hand_error("epoll_ctl");
while(1)
{
ret_events = epoll_wait(epfd, events, MAX_EVENTS, -1); //类似于select函数,这里是等待事件的到来。
if(ret_events == -1)
{
cout<<"ret_events = "<<ret_events<<endl;
hand_error("epoll_wait");
}
if( ret_events == 0)
{
cout<<"ret_events = "<<ret_events<<endl;
continue;
}
// cout<<"ret_events = "<<ret_events<<endl;
for( int num = 0; num < ret_events; num ++)
{
cout<<"num = "<<num<<endl;
cout<<"events[num].data.fd = "<<events[num].data.fd<<endl;
if(events[num].data.fd == listenfd) //client connect
{
cout<<"listen sucess and listenfd = "<<listenfd<<endl;
cli_sock = accept(listenfd, (struct sockaddr*)&peer_addr, &peerlen);
if(cli_sock < 0)
hand_error("accept");
cout<<"count = "<<count++;
printf("ip=%s,port = %d\n", inet_ntoa(peer_addr.sin_addr),peer_addr.sin_port);
clients.push_back(cli_sock);
setnoblock(cli_sock); //设置为非阻塞模式
ev.data.fd = cli_sock;// 将新连接也加入EPOLL的监听队列
ev.events = EPOLLIN | EPOLLET ;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, cli_sock, &ev)< 0)
hand_error("epoll_ctl");
}
else if( events[num].events & EPOLLIN)
{
cli_sock = events[num].data.fd;
if(cli_sock < 0)
hand_error("cli_sock");
char recvbuf[1024];
memset(recvbuf, 0 , sizeof(recvbuf));
int num = read( cli_sock, recvbuf, sizeof(recvbuf));
if(num == -1)
hand_error("read have some problem:");
if( num == 0 ) //stand of client have exit
{
cout<<"client have exit"<<endl;
close(cli_sock);
ev_remov = events[num];
epoll_ctl(epfd, EPOLL_CTL_DEL, cli_sock, &ev_remov);
clients.erase(remove(clients.begin(), clients.end(), cli_sock),clients.end());
}
fputs(recvbuf,stdout);
write(cli_sock, recvbuf, strlen(recvbuf));
}
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
/***********
client.c
***********/
#include "net.h"
int main()
{
signal(SIGPIPE,SIG_IGN);
int sock;
sock = socket( AF_INET, SOCK_STREAM,0 ); //create a socket stream
if( sock< 0 )
hand_error( "socket_create");
struct sockaddr_in my_addr;
//memset my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(18000); //here is host sequeue
// my_addr.sin_addr.s_addr = htonl( INADDR_ANY );
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int conn = connect(sock, (struct sockaddr *)&my_addr, sizeof(my_addr)) ;
if(conn != 0)
hand_error("connect");
char recvbuf[1024] = {0};
char sendbuf[1024] = {0};
fd_set rset;
FD_ZERO(&rset);
int nready = 0;
int maxfd;
int stdinof = fileno(stdin);
if( stdinof > sock)
maxfd = stdinof;
else
maxfd = sock;
while(1)
{
//select返回后把原来待检测的但是仍没就绪的描述字清0了。所以每次调用select前都要重新设置一下待检测的描述字
FD_SET(sock, &rset);
FD_SET(stdinof, &rset);
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
cout<<"nready = "<<nready<<" "<<"maxfd = "<<maxfd<<endl;
if(nready == -1 )
break;
else if( nready == 0)
continue;
else
{
if( FD_ISSET(sock, &rset) ) //检测sock是否已经在集合rset里面。
{
int ret = read( sock, recvbuf, sizeof(recvbuf)); //读数据
if( ret == -1)
hand_error("read");
else if( ret == 0)
{
cout<<"sever have close"<<endl;
close(sock);
break;
}
else
{
fputs(recvbuf,stdout); //输出数据
memset(recvbuf, 0, strlen(recvbuf));
}
}
if( FD_ISSET(stdinof, &rset)) //检测stdin的文件描述符是否在集合里面
{
if(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
int num = write(sock, sendbuf, strlen(sendbuf)); //写数据
cout<<"sent num = "<<num<<endl;
memset(sendbuf, 0, sizeof(sendbuf));
}
}
}
}
return 0;
}
1.简介
Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll 。
epoll是什么?按照man手册的说法是为处理大批量句柄而作了改进的poll。要使用epoll只需要以下的三个系统函数调用: epoll_create(2),epoll_ctl(2),epoll_wait(2)。
2.select模型的缺陷
(1) 在Linux内核中,select所用到的FD_SET是有限的
内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数:#define __FD_SETSIZE 1024。也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的;或者同时检测1025个句柄的可写状态也是不可能的。
(2) 内核中实现select是使用轮询方法
每次检测都会遍历所有FD_SET中的句柄,显然select函数的执行时间与FD_SET中句柄的个数有一个比例关系,即select要检测的句柄数越多就会越费时
3.Windows IOCP模型的缺陷
windows完成端口实现的AIO,实际上也只是使用内部用线程池实现的,最后的结果是IO有个线程池,你的应用程序也需要一个线程池。很多文档其实已经指出了这引发的线程context-switch所带来的代价。
4.EPOLL模型的优点
(1) 支持一个进程打开大数目的socket描述符(FD)
epoll没有select模型中的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于select 所支持的2048。下面是我的小PC机上的显示:
pt@ubuntu:~$ cat /proc/sys/fs/file-max
6815744
那么对于服务器而言,这个数目会更大。
(2) IO效率不随FD数目增加而线性下降
传统select/poll的另一个致命弱点就是当你拥有一个很大的socket集合,由于网络得延时,使得任一时间只有部分的socket是”活跃”的,而select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操作:这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。于是,只有”活跃”的socket才会主动去调用callback函数,其他idle状态的socket则不会。在这点上,epoll实现了一个”伪”AIO”,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的,比如一个高速LAN环境,epoll也不比select/poll低多少效率,但若过多使用的调用epoll_ctl,效率稍微有些下降。然而一旦使用idle connections模拟WAN环境,那么epoll的效率就远在select/poll之上了。
(3) 使用mmap加速内核与用户空间的消息传递
无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就显得很重要。在这点上,epoll是通过内核于用户空间mmap同一块内存实现。
5.EPOLL模型的工作模式
(1) LT模式
LT:level triggered,这是缺省的工作方式,同时支持block和no-block socket,在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
(2) ET模式
LT:edge-triggered,这是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
6.EPOLL模型的使用方法
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
(1) epoll_data、epoll_data_t、epoll_event
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t; struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可能的取值为:
EPOLLIN: 表示对应的文件描述符可以读;
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读;
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 表示对应的文件描述符有事件发生;
联合体epoll_data用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。
(2)epoll_create
函数声明:intepoll_create(intsize)
函数说明:该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。
(3) epoll_ctl函数
函数声明:intepoll_ctl(int epfd,int op, int fd, struct epoll_event *event)
函数说明:该函数用于控制某个文件描述符上的事件,可以注册事件、修改事件、删除事件。
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功则返回0,不成功则返回-1。
(4) epoll_wait函数
函数声明:int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout)
函数说明:该函数用于轮询I/O事件的发生。
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;
返回发生事件数。
7 设计思路及模板
首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你的epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作都将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
然后在你的网络主循环里面,调用epoll_wait(int epfd, epoll_event events, int max_events,int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait函数操作成功之后,events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout参数指示 epoll_wait的超时条件,为0时表示马上返回;为-1时表示函数会一直等下去直到有事件返回;为任意正整数时表示等这么长的时间,如果一直没有事件,则会返回。一般情况下如果网络主循环是单线程的话,可以用-1来等待,这样可以保证一些效率,如果是和主循环在同一个线程的话,则可以用0来保证主循环的效率。epoll_wait返回之后,应该进入一个循环,以便遍历所有的事件。
对epoll 的操作就这么简单,总共不过4个API:epoll_create, epoll_ctl,epoll_wait和close。以下是man中的一个例子。
struct epoll_event ev, *events;
for(;;)
{
nfds = epoll_wait(kdpfd, events, maxevents, -1); //等待IO事件
for(n = 0; n < nfds; ++n)
{
//如果是主socket的事件,则表示有新连接进入,需要进行新连接的处理。
if(events[n].data.fd == listener)
{
client = accept(listener, (struct sockaddr *) &local, &addrlen);
if(client < 0)
{
perror("accept error");
continue;
}
// 将新连接置于非阻塞模式
setnonblocking(client);
ev.events = EPOLLIN | EPOLLET;
//注意这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
//如果有写操作的话,这个时候epoll是不会返回事件的,
//如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET。
// 并且将新连接也加入EPOLL的监听队列
ev.data.fd = client;
// 设置好event之后,将这个新的event通过epoll_ctl
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
{
//加入到epoll的监听队列里,这里用EPOLL_CTL_ADD
//来加一个新的 epoll事件。可以通过EPOLL_CTL_DEL来减少
//一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
fprintf(stderr, "epoll set insertion error: fd=%d"0, client);
return -1;
}
}
else
// 如果不是主socket的事件的话,则代表这是一个用户的socket的事件,
// 则用来处理这个用户的socket的事情是,比如说read(fd,xxx)之类,或者一些其他的处理。
do_use_fd(events[n].data.fd);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
8 EPOLL模型的简单实例
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000
void setnonblocking(int sock)
{
int opts;
opts = fcntl(sock, F_GETFL);
if(opts < 0)
{
perror("fcntl(sock, GETFL)");
exit(1);
}
opts = opts | O_NONBLOCK;
if(fcntl(sock, F_SETFL, opts) < 0)
{
perror("fcntl(sock,SETFL,opts)");
exit(1);
}
}
int main()
{
int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
//声明epoll_event结构体的变量, ev用于注册事件, events数组用于回传要处理的事件
struct epoll_event ev,events[20];
//生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256
epfd = epoll_create(256);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(listenfd); //把用于监听的socket设置为非阻塞方式
ev.data.fd = listenfd; //设置与要处理的事件相关的文件描述符
ev.events = EPOLLIN | EPOLLET; //设置要处理的事件类型
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //注册epoll事件
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
char *local_addr = "200.200.200.204";
inet_aton(local_addr, &(serveraddr.sin_addr));
serveraddr.sin_port = htons(SERV_PORT); //或者htons(SERV_PORT);
bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi = 0;
for( ; ; )
{
nfds = epoll_wait(epfd, events, 20, 500); //等待epoll事件的发生
for(i = 0; i < nfds; ++i) //处理所发生的所有事件
{
if(events[i].data.fd == listenfd) //监听事件
{
connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen);
if(connfd < 0)
{
perror("connfd<0");
exit(1);
}
setnonblocking(connfd); //把客户端的socket设置为非阻塞方式
char *str = inet_ntoa(clientaddr.sin_addr);
std::cout << "connect from " << str <<std::endl;
ev.data.fd=connfd; //设置用于读操作的文件描述符
ev.events=EPOLLIN | EPOLLET; //设置用于注测的读操作事件
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
//注册ev事件
}
else if(events[i].events&EPOLLIN) //读事件
{
if ( (sockfd = events[i].data.fd) < 0)
{
continue;
}
if ( (n = read(sockfd, line, MAXLINE)) < 0) // 这里和IOCP不同
{
if (errno == ECONNRESET)
{
close(sockfd);
events[i].data.fd = -1;
}
else
{
std::cout<<"readline error"<<std::endl;
}
}
else if (n == 0)
{
close(sockfd);
events[i].data.fd = -1;
}
ev.data.fd=sockfd; //设置用于写操作的文件描述符
ev.events=EPOLLOUT | EPOLLET; //设置用于注测的写操作事件
//修改sockfd上要处理的事件为EPOLLOUT
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
else if(events[i].events&EPOLLOUT)//写事件
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
ev.data.fd = sockfd; //设置用于读操作的文件描述符
ev.events = EPOLLIN | EPOLLET; //设置用于注册的读操作事件
//修改sockfd上要处理的事件为EPOLIN
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
9.epoll进阶思考
9.1. 问题来源
最近学习EPOLL模型,介绍中说将EPOLL与Windows IOCP模型进行比较,说其的优势在于解决了IOCP模型大量线程上下文切换的开销,于是可以看出,EPOLL模型不需要多线程,即单线程中可以处理EPOLL逻辑。如果引入多线程反而会引起一些问题。但是EPOLL模型的服务器端到底可以不可以用多线程技术,如果可以,改怎么取舍,这成了困扰我的问题。上网查了一下,有这样几种声音:
(1) “要么事件驱动(如epoll),要么多线程,要么多进程,把这几个综合起来使用,感觉更加麻烦。”;
(2) “单线程使用epoll,但是不能发挥多核;多线程不用epoll。”;
(3) “主通信线程使用epoll所有需要监控的FD,有事件交给多线程去处理”;
(4) “既然用了epoll, 那么线程就不应该看到fd, 而只看到的是一个一个的业务请求/响应; epoll将网络数据组装成业务数据后, 转交给业务线程进行处理。这就是常说的半同步半异步”。
我比较赞同上述(3)、(4)中的观点
EPOLLOUT只有在缓冲区已经满了,不可以发送了,过了一会儿缓冲区中有空间了,就会触发EPOLLOUT,而且只触发一次。如果你编写的程序的网络IO不大,一次写入的数据不多的时候,通常都是epoll_wait立刻就会触发 EPOLLOUT;如果你不调用 epoll,直接写 socket,那么情况就取决于这个socket的缓冲区是不是足够了。如果缓冲区足够,那么写就成功。如果缓冲区不足,那么取决你的socket是不是阻塞的,要么阻塞到写完成,要么出错返回。所以EPOLLOUT事件具有较大的随机性,ET模式一般只用于EPOLLIN, 很少用于EPOLLOUT。
9.2. 具体做法
(1) 主通信线程使用epoll所有需要监控的FD,负责监控listenfd和connfd,这里只监听EPOLLIN事件,不监听EPOLLOUT事件;
(2) 一旦从Client收到了数据以后,将其构造成一个消息,放入消息队列中;
(3) 若干工作线程竞争,从消息队列中取出消息并进行处理,然后把处理结果发送给客户端。发送客户端的操作由工作线程完成。直接进行write。write到EAGAIN或EWOULDBLOCK后,线程循环continue等待缓冲区队列
发送函数代码如下:
bool send_data(int connfd, char *pbuffer, unsigned int &len,int flag)
{
if ((connfd < 0) || (0 == pbuffer))
{
return false;
}
int result = 0;
int remain_size = (int) len;
int send_size = 0;
const char *p = pbuffer;
time_t start_time = time(NULL);
int time_out = 3;
do
{
if (time(NULL) > start + time_out)
{
return false;
}
send_size = send(connfd, p, remain_size, flag);
if (nSentSize < 0)
{
if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR))
{
continue;
}
else
{
len -= remain_size;
return false;
}
}
p += send_size;
remain_size -= send_size;
}while(remain_size > 0);
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
10 epoll 实现服务器和客户端例子
最后我们用C++实现一个简单的客户端回射,所用到的代码文件是
net.h server.cpp client.cpp
- 1
服务器端:epoll实现的,干两件事分别为:1.等待客户端的链接,2.接收来自客户端的数据并且回射;
客户端:select实现,干两件事为:1.等待键盘输入,2.发送数据到服务器端并且接收服务器端回射的数据;
/***********
net.h
***********/
#include <stdio.h>
#ifndef _NET_H
#define _NET_H
#include <iostream>
#include <vector>
#include <algorithm>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h> //epoll ways file
#include <sys/socket.h>
#include <fcntl.h> //block and noblock
#include <stdlib.h>
#include <error.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
using namespace std;
#define hand_error(msg) do{perror(msg); exit(EXIT_FAILURE);}while(0)
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
/***********
server.c
***********/
#include "net.h"
#define MAX_EVENTS 10000
int setblock(int sock)
{
int ret = fcntl(sock, F_SETFL, 0);
if (ret < 0 )
hand_error("setblock");
return 0;
}
int setnoblock(int sock) //设置非阻塞模式
{
int ret = fcntl(sock, F_SETFL, O_NONBLOCK );
if(ret < 0)
hand_error("setnoblock");
return 0;
}
int main()
{
signal(SIGPIPE,SIG_IGN);
int listenfd;
listenfd = socket( AF_INET, SOCK_STREAM,0 ); //create a socket stream
if( listenfd < 0 )
hand_error( "socket_create");
setnoblock(listenfd);
int on = 1;
if( setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))< 0)
hand_error("setsockopt");
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(18000); //here is host sequeue
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if( bind( listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0)
hand_error("bind");
int lisId = listen(listenfd, SOMAXCONN);
if( lisId < 0) //LISTEN
hand_error("listen");
struct sockaddr_in peer_addr; //用来 save client addr
socklen_t peerlen;
//下面是一些初始化,都是关于epoll的。
vector<int> clients;
int count = 0;
int cli_sock = 0;
int epfd = 0; //epoll 的文件描述符
int ret_events; //epoll_wait()的返回值
struct epoll_event ev_remov, ev, events[MAX_EVENTS]; //events 用来存放从内核读取的的事件
ev.events = EPOLLET | EPOLLIN; //边缘方式触发
ev.data.fd = listenfd;
epfd = epoll_create(MAX_EVENTS); //create epoll,返回值为epoll的文件描述符
//epfd = epoll_create(EPOLL_CLOEXEC); //新版写法
if(epfd < 0)
hand_error("epoll_create");
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //添加时间
if(ret < 0)
hand_error("epoll_ctl");
while(1)
{
ret_events = epoll_wait(epfd, events, MAX_EVENTS, -1); //类似于select函数,这里是等待事件的到来。
if(ret_events == -1)
{
cout<<"ret_events = "<<ret_events<<endl;
hand_error("epoll_wait");
}
if( ret_events == 0)
{
cout<<"ret_events = "<<ret_events<<endl;
continue;
}
// cout<<"ret_events = "<<ret_events<<endl;
for( int num = 0; num < ret_events; num ++)
{
cout<<"num = "<<num<<endl;
cout<<"events[num].data.fd = "<<events[num].data.fd<<endl;
if(events[num].data.fd == listenfd) //client connect
{
cout<<"listen sucess and listenfd = "<<listenfd<<endl;
cli_sock = accept(listenfd, (struct sockaddr*)&peer_addr, &peerlen);
if(cli_sock < 0)
hand_error("accept");
cout<<"count = "<<count++;
printf("ip=%s,port = %d\n", inet_ntoa(peer_addr.sin_addr),peer_addr.sin_port);
clients.push_back(cli_sock);
setnoblock(cli_sock); //设置为非阻塞模式
ev.data.fd = cli_sock;// 将新连接也加入EPOLL的监听队列
ev.events = EPOLLIN | EPOLLET ;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, cli_sock, &ev)< 0)
hand_error("epoll_ctl");
}
else if( events[num].events & EPOLLIN)
{
cli_sock = events[num].data.fd;
if(cli_sock < 0)
hand_error("cli_sock");
char recvbuf[1024];
memset(recvbuf, 0 , sizeof(recvbuf));
int num = read( cli_sock, recvbuf, sizeof(recvbuf));
if(num == -1)
hand_error("read have some problem:");
if( num == 0 ) //stand of client have exit
{
cout<<"client have exit"<<endl;
close(cli_sock);
ev_remov = events[num];
epoll_ctl(epfd, EPOLL_CTL_DEL, cli_sock, &ev_remov);
clients.erase(remove(clients.begin(), clients.end(), cli_sock),clients.end());
}
fputs(recvbuf,stdout);
write(cli_sock, recvbuf, strlen(recvbuf));
}
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
/***********
client.c
***********/
#include "net.h"
int main()
{
signal(SIGPIPE,SIG_IGN);
int sock;
sock = socket( AF_INET, SOCK_STREAM,0 ); //create a socket stream
if( sock< 0 )
hand_error( "socket_create");
struct sockaddr_in my_addr;
//memset my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(18000); //here is host sequeue
// my_addr.sin_addr.s_addr = htonl( INADDR_ANY );
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int conn = connect(sock, (struct sockaddr *)&my_addr, sizeof(my_addr)) ;
if(conn != 0)
hand_error("connect");
char recvbuf[1024] = {0};
char sendbuf[1024] = {0};
fd_set rset;
FD_ZERO(&rset);
int nready = 0;
int maxfd;
int stdinof = fileno(stdin);
if( stdinof > sock)
maxfd = stdinof;
else
maxfd = sock;
while(1)
{
//select返回后把原来待检测的但是仍没就绪的描述字清0了。所以每次调用select前都要重新设置一下待检测的描述字
FD_SET(sock, &rset);
FD_SET(stdinof, &rset);
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
cout<<"nready = "<<nready<<" "<<"maxfd = "<<maxfd<<endl;
if(nready == -1 )
break;
else if( nready == 0)
continue;
else
{
if( FD_ISSET(sock, &rset) ) //检测sock是否已经在集合rset里面。
{
int ret = read( sock, recvbuf, sizeof(recvbuf)); //读数据
if( ret == -1)
hand_error("read");
else if( ret == 0)
{
cout<<"sever have close"<<endl;
close(sock);
break;
}
else
{
fputs(recvbuf,stdout); //输出数据
memset(recvbuf, 0, strlen(recvbuf));
}
}
if( FD_ISSET(stdinof, &rset)) //检测stdin的文件描述符是否在集合里面
{
if(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
int num = write(sock, sendbuf, strlen(sendbuf)); //写数据
cout<<"sent num = "<<num<<endl;
memset(sendbuf, 0, sizeof(sendbuf));
}
}
}
}
return 0;
}
上一篇: 外显子原理详解
推荐阅读
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
Linux 网络编程:Epoll 示例
-
Linux 网络编程 - epoll 模型原理和示例
-
c++ 网络编程,采用适用于 Linux 的 epoll 技术和适用于 Windows 的 IOCP 模型
-
Linux C语言|网络编程】gethostbyaddr函数和C语言示例
-
Linux设备驱动开发详解——学习笔记-设备驱动来联系。在没有操作系统的情况下,工程师可以根据硬件设备的特点自行定义接口。而在有操作系统的情况下,驱动的架构则由相应的操作系统来定义。驱动存在的意义就是给上层应用提供便利。 驱动针对的对象是存储器和外设。Linux将存储器和外设分为 3 个基础大类:字符设备、块设备、网络设备。 字符设备和块设备都被 Linux 映射到文件系统的文件和目录中,通过文件系统的接口(open、read、write、close等)来访问。其中,块设备可以通过类似 dd 命令对应的原始块设备来访问,也可以通过建立文件系统,以文件路径来访问。 学习 Linux 设备驱动,要求非常好的硬件基础、非常好的软件基础、一定的 Linux 内核基础和非常好的多任务并发控制和同步的基础。学习 Linux 设备驱动要将学习的函数、数据结构等放到整体架构中去理解,才能理清驱动中各组成部分之间的关系。 驱动设计的硬件基础 驱动工程师需要掌握 处理器、存储器、接口和总线、可编程门电路、原理图、硬件时序、芯片手册、仪器使用 等方面的内容。 处理器