poll模型
poll采用一个描述符事件结构的方式对描述符所关心的事件进行监控;
1.所关心的描述符
2.描述符所关心的事件 POLLIN (可读) | POLLOUT(可写)
3.描述符实际监控时就绪的事件
- 定义事件结构数组,将用户所关心的描述符和事件添加到结构数组中
- 将事件结构拷贝到内核当中;进行轮询遍历方式的监控;若有描述符就绪,则返回
- 若有描述符就绪,则修改这个描述符事件结构中的实际就绪事件
- fds是一个polll函数监听的结构列表,每一个元素,包含了三部分元素:文件描述符,监听的事件集合,返回的事件集合
- nfds表示fds数组的长度
- timeout表示poll函数的超时时间,单位毫秒
poll优缺点分析
优点:
1.描述符没有具体上限
2.简化了操作流程(采用事件结构监控方式简化了select三种事件集合监控流程)
缺点:
1.poll采用轮询遍历方式判断就绪,性能随着描述符增多而下降
2.poll不会告诉用户具体的就绪描述符;需要用户进行轮询判断;性能随着增多而下降
3.代码复杂度较高
4.不能跨平台
epoll模型(Linux下能最高的io多路转接模型)
事件就绪:
可读事件就绪:接收缓冲区的数据大小大于低水位标记(1B),就会触发可读事件
可写事件就绪:发送缓冲区的空闲空间大小大于低水位标记(1B),就会触发可写事件
接口:
int epoll_create(int size) ; //创建epoll
功能:在内核中创建一个结构体:struct eventpoll{rbr--红黑树, rdlist - 双向链表}
size:能监控的描述符上限,linux2.6.8内核之后,被忽略--只要>0就可以
返回值:文件描述符--非负整数--epoll的操作句柄
int epoll_ctl(int epfd , int op , int fd, struct epoll_event* event);
功能:向内核中epfd所对应的epoll的结构进行添加/删除/修改/一个fd所关心的事件event,epoll也是采用事件结构的 形式对描述符进行监控
epfd: epoll_create返回的操作句柄
op: 用户要进行的操作
EPOLL_CTL_DEL 从内核的eventpoll中移除要监控的事件结构
EPOLL_CTL_MOD 修改内核中所监控的事件结构
fd: 用户所要监控的描述符
event: 描述符对应所要监控的事件
struct poll_event{
uint32_t events; 用户对描述所关心的事件 EPOLLIN|EPOLLOUT|EPOLLET|EPOLLLT
epoll_data_t data; 事件对应的数据(描述符就绪后就会返回事件结构,用户可以获得这个数据,
若这个数据就是所关心的描述符;用户就可以直接只用这个描述符进行操作了)
};
typedef union epoll_data{
void* ptr;
int fd;
}epoll_data_t;
int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout);
功能:epoll操作句柄
events:事件结构数组-用于保存就绪描述符对应的事件
maxevents: 用于确定一次最多获取的就绪事件个数(防止events溢出)
timeout : 超时等待时间设置--毫秒
返回值:<0出错 ==0超时等待 >0 就绪的事件个数
eventpoll{
rbr--红黑树--保存用户添加的事件结构节点
rdlist--双向链表
};
epoll触发方式
- 告诉内核要开始对描述进行监控
- 操作系统对描述符进行事件监控---采用的是事件触发方式,为每一个要监控的描述符都定义了一个事件,并且对这一个事件定义一个事件回调函数
- 这个事件回调函数要做的事情就是,将就绪的描述符所对应的epoll_event事件添加到双向链表当中
- epoll_wait并不是立即返回,而是没隔一会就会看一下,内核当中eventpoll中的双向链表是否为空;进而判断是否有描述符就绪
- 若链表不为空,表示有描述符就绪,则epoll_wait即将返回;再返回之前,就将就绪的描述符对应事件结构向用户态的结构数组拷贝一份
- epoll会将就绪的描述符以及对应的事件,拷贝一份到用户态,直接告诉用户有哪些描述符就绪,进而用户就可以直接操作就绪的描述符
水平触发(EPOLLLT):只要缓冲区的数据大小/空闲空间大于低水位标记,就会触发可读/可写就绪事件
边沿触发(EPOLLET):
可读事件:每次只有新数据到来时才会出发一次可读事件(不关注缓冲区数据,要求用户一次将缓冲区数据全部读取)
可写事件:每次只有缓冲区空闲空间从0变为大于低水位标记的时候才会触发可写事件
读写混合:在混合监控的时候,对于可写事件就会触发边沿触发(防止可写事件每次不写入数据,但是有空闲空间都会触 发事件,(但是有没有数据可写))
若是可读事件被设置为边沿触发,需要用户一次性将所有数据读取完毕;但是因为不知道数据有多少因此只能 从缓冲区中读取数据,当循环读取数据但是缓冲区中没有数据的时候,recv就会阻塞;
因此边沿触发的可读事件的描述符通常需要被设置为非阻塞
代码实现(epoll)
8 #include <iostream>
9 #include <vector>
10 #include <sys/epoll.h>
11 #include "tcpsocket.hpp"
12
13 class Epoll {
14 private:
15 int _epfd;
16 public:
17 bool Init() {
18 //创建epoll
19 //int epoll_create(int size);
20 _epfd = epoll_create(1);
21 if (_epfd < 0) {
22 perror("epoll create error");
23 return false;
24 }
25 return true;
26 }
27 bool Add(TcpSocket sock, uint32_t events = 0) {
28 sock.SetNonBlock();
29 int fd = sock.GetSockFd();
30 //epoll_ctl(int epfd,int op,int fd,epoll_event *event);
31 struct epoll_event ev;
32 ev.events = EPOLLIN | events;
33 ev.data.fd = fd;
34 int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
35 if (ret < 0) {
36 perror("epoll ctrl error");
37 return false;
38 }
39 return true;
40 }
41 bool Del(TcpSocket sock) {
42 int fd = sock.GetSockFd();
43 int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
44 if (ret < 0) {
45 perror("epoll ctrl error");
46 return false;
47 }
48 return true;
49 }
50 bool Wait(std::vector<TcpSocket> &list, int ms_timeout = 3000) {
51 //int epoll_wait(int epfd, struct epoll_event *events,
52 // int maxevents, int timeout);
53 struct epoll_event evs[10];
54 int nfds = epoll_wait(_epfd, evs, 10, ms_timeout);
55 if (nfds < 0) {
56 perror("epoll wait error");
57 return false;
58 }else if (nfds == 0) {
59 std::cout << "epoll wait timeout\n";
60 return false;
61 }
62 for (int i = 0; i < nfds; i++) {
63 int fd = evs[i].data.fd;
64 TcpSocket sock;
65 sock.SetSockFd(fd);
66 list.push_back(sock);
67 }
68 return true;
69 }
70 };
71
72 int main()
73 {
74 TcpSocket lst_sock;
75 CHECK_RET(lst_sock.Socket());
76 CHECK_RET(lst_sock.Bind("0.0.0.0", 9000));
77 CHECK_RET(lst_sock.Listen());
78
79 Epoll epoll;
80 CHECK_RET(epoll.Init());
81 CHECK_RET(epoll.Add(lst_sock, EPOLLET));
82 while(1) {
83 std::vector<TcpSocket> list;
84 bool ret = epoll.Wait(list);
85 if (ret == false) {
86 continue;
87 }
W> 88 for (int i = 0; i < list.size(); i++) {
89 if (list[i].GetSockFd() == lst_sock.GetSockFd()) {
90 TcpSocket cli_sock;
91 lst_sock.Accept(cli_sock);
92 epoll.Add(cli_sock, EPOLLET);
93 }else {
94 std::string buf;
95 list[i].Recv(buf);
96 std::cout << "client say: " << buf << std::endl;
97 }
98 }
99
100 }
101 lst_sock.Close();
102 return 0;
103 }
TcpSocket
25 #include <string>
26 #include <stdio.h>
27 #include <unistd.h>
28 #include <stdlib.h>
29 #include <errno.h>
30 #include <string.h>
31 #include <fcntl.h>
32 #include <netinet/in.h>
33 #include <arpa/inet.h>
34 #include <sys/socket.h>
35
36 #define CHECK_RET(q) if((q) == false){return -1;}
37
38 class TcpSocket
39 {
40 public:
41 TcpSocket() : _sockfd(-1){
42 }
43 void SetSockFd(int fd){
44 _sockfd = fd;
45 }
46 int GetSockFd() {
47 return _sockfd;
48 }
49 bool Socket() {
50 _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
51 if (_sockfd < 0) {
52 perror("socket error");
53 return false;
54 }
55 return true;
56 }
57 bool SetNonBlock() {
58 //int fcntl(int fd, int cmd, ... /* arg */ );
59 //设置/获取描述符状态属性信息
60 // cmd:
61 // F_GETFL 获取
62 // F_SETFL 设置
63 int flag = fcntl(_sockfd, F_GETFL, 0);
64 fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
65 return true;
66 }
67 bool Bind(const std::string &ip, const uint16_t port){
68 struct sockaddr_in addr;
69 addr.sin_family = AF_INET;
70 addr.sin_port = htons(port);
71 addr.sin_addr.s_addr = inet_addr(ip.c_str());
72
73 socklen_t len = sizeof(struct sockaddr_in);
74 int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
75 if (ret < 0) {
76 perror("bind error");
77 return false;
78 }
79 return true;
80 }
81 bool Listen(const int backlog = 10) {
82 //int listen(int sockfd, int backlog);
83 //backlog:最大并发连接数--内核中已完成连接队列的最大节点数
84 int ret = listen(_sockfd, backlog);
85 if (ret < 0) {
86 perror("listen error");
87 return false;
88 }
89 return true;
90 }
91 bool Connect(const std::string &ip, const uint16_t port) {
92 //int connect(int fd, struct sockaddr *addr,socklen_t len);
93 //addr: 要连接的服务端地址信息
94 struct sockaddr_in addr;
95 addr.sin_family = AF_INET;
96 addr.sin_port = htons(port);
97 addr.sin_addr.s_addr = inet_addr(ip.c_str());
98 socklen_t len = sizeof(struct sockaddr_in);
99
100 int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
101 if (ret < 0) {
102 perror("connect error");
103 return false;
104 }
105 return true;
106 }
107 bool Accept(TcpSocket &csock, struct sockaddr_in *addr = NULL){
108 //int accept(int sockfd, sockaddr *addr, socklen_t *len);
109 //addr: 客户端的地址信息
110 //len: 输入输出参数,既要指定接收长度,还要接收实际长度
111 //返回值:为新客户端新建的socket套接字描述符
112 //通过这个返回的描述符可以与指定的客户端进行通信
113 struct sockaddr_in _addr;
114 socklen_t len = sizeof(struct sockaddr_in);
115 int newfd = accept(_sockfd, (struct sockaddr*)&_addr, &len);
116 if (newfd < 0) {
117 perror("accept error");
118 return false;
119 }
120 if (addr != NULL) {
121 memcpy(addr, &_addr, len);
122 }
123 csock.SetSockFd(newfd);
124 //_sockfd--仅用于接收新客户端连接请求
125 //newfd----专门用于与客户端进行通信
126 return true;
127 }
128 bool Recv(std::string &buf) {
129 char tmp[4096] = {0};
130 //ssize_t recv(int sockfd, void *buf, size_t len, int flags)
131 //flags:0-默认阻塞接收 MSG_PEEK-获取数据但是不从缓冲区移除
132 //返回值:实际接收的数据长度 失败:-1 连接断开:0
133 while(1) {
134 int ret = recv(_sockfd, tmp, 5, 0);
135 if (ret < 0) {
136 //EAGAIN/EWOULDBLOCK 表示缓冲区没有数据
137 //EINTR 表示当前读取操作被信号打断
138 if (errno == EAGAIN || errno == EWOULDBLOCK
139 || errno == EINTR) {
140 return true;
141 }
142 perror("recv error");
143 return false;
144 }else if (ret == 0) {
145 printf("peer shutdown\n");
146 return false;
147 }
148 tmp[ret] = '\0';
149 buf += tmp;
150 }
151 return true;
152 }
153 bool Send(const std::string &buf) {
154 //ssize_t send(int sockfd, void *buf, size_t len, int flags)
155 int ret = send(_sockfd, buf.c_str(), buf.size(), 0);
156 if (ret < 0) {
157 perror("send error");
158 return false;
159 }
160 return true;
161 }
162 bool Close() {
163 close(_sockfd);
164 _sockfd = -1;
W>165 }
166 private:
167 int _sockfd;
168 };
epoll优缺点分析
优点:
- epoll没有监控的描述符上限
- 采用了事件结构的方式简化了select这种监控集合的监控流程
- epoll采用一个异步阻塞操作;发起调用,让操作系统进行描述符监控
- 操作系统使用事件回调的方式对描述符进行监控--避免了select的轮询遍历,因此性能不会随着描述符的增多而降低
- epoll发起调用之后进行等待--循环判断内核当中epoll就绪的事件链表是否为空来确定是否有事件就绪,则将对应的事件拷贝到用户态让用户操作
- 直接告诉了用户那些描述符就绪了,可以直接对描述符进行操作;没有空遍历;提高性能,并且简化了代码流程
- epoll描述符事件结构只需要向内核拷贝一次,(内核当中的红黑树当中);不需要每次拷贝
缺点:
不能跨平台,只能精确到毫秒
适用场景:有大量的客户端连接,但是同一时间只有少量活跃的场景