原文做者:aircrafthtml
原文连接:https://www.cnblogs.com/DOMLX/p/9622548.html前端
锲子:关于并发服务器中的I/O复用实现方式,前面在网络编程系列四仍是五来着????咱们讲过select的方式,但select的性能比较低,当链接数量超过几百个的时候就很慢了,并不适合以Web服务器端开发为主流的现代开发环境。所以就有了Linux下的epoll,BSD的kqueue,Solaris的/dev/poll和Windows的IOCP等复用技术。本章就来说讲Linux下的epoll技术和Windows下的IOCP模型。python
一:IOCP和Epoll之间的异同。
异:
1:IOCP是WINDOWS系统下使用。Epoll是Linux系统下使用。
2:IOCP是IO操做完毕以后,经过Get函数得到一个完成的事件通知。
Epoll是当你但愿进行一个IO操做时,向Epoll查询是否可读或者可写,若处于可读或可写状态后,Epoll会经过epoll_wait进行通知。
3:IOCP封装了异步的消息事件的通知机制,同时封装了部分IO操做。但Epoll仅仅封装了一个异步事件的通知机制,并不负责IO读写操做。Epoll保持了事件通知和IO操做间的独立性,更加简单灵活。
4:基于上面的描述,咱们能够知道Epoll不负责IO操做,因此它只告诉你当前可读可写了,而且将协议读写缓冲填充,由用户去读写控制,此时咱们能够作出额外的许多操做。IOCP则直接将IO通道里的读写操做都作完了才通知用户,当IO通道里发生了堵塞等情况咱们是没法控制的。c++
同:
1:它们都是异步的事件驱动的网络模型。
2:它们均可以向底层进行指针数据传递,当返回事件时,除可通知事件类型外,还能够通知事件相关数据。编程
二:Epoll理解与应用。后端
一、epoll是什么?数组
epoll是当前在Linux下开发大规模并发网络程序的热门人选,epoll 在Linux2.6内核中正式引入,和select类似,都是I/O多路复用(IO multiplexing)技术。服务器
Linux下设计并发网络程序,经常使用的模型有:网络
Apache模型(Process Per Connection,简称PPC)数据结构
TPC(Thread PerConnection)模型
select模型和poll模型。
epoll模型
二、epoll与select对比优化:
基于select的I/O复用技术速度慢的缘由:
1,调用select函数后常见的针对全部文件描述符的循环语句。它每次事件发生须要遍历全部文件描述符,找出发生变化的文件描述符。(之前写的示例没加循环)
2,每次调用select函数时都须要向该函数传递监视对象信息。即每次调用select函数时向操做系统传递监视对象信息,至于为何要传?是由于咱们监视的套接字变化的函数,而套接字是操做系统管理的。(这个才是最耗效率的)
注释:基于这样的缘由并非说select就没用了,在这样的状况下就适合选用select:1,服务端接入者少 2,程序应具备兼容性。
epoll是怎么优化select问题的:
1,每次发生事件它不须要循环遍历全部文件描述符,它把发生变化的文件描述符单独集中到了一块儿。
2,仅向操做系统传递1次监视对象信息,监视范围或内容发生变化时只通知发生变化的事项。
实现epoll时必要的函数和结构体
函数:
epoll_create:建立保存epoll文件描述符的空间,该函数也会返回文件描述符,因此终止时,也要调用close函数。(建立内存空间)epoll_ctl:向空间注册,添加或修改文件描述符。(注册监听事件)
epoll_wait:与select函数相似,等待文件描述符发生变化。(监听事件回调)
结构体:
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}typedef union epoll_data
{
void *ptr;
int fd;
__uinit32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll的几个函数的介绍:
一、epoll_create函数:
/** * @brief 该函数生成一个epoll专用的文件描述符。它实际上是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。 * * @param size size就是你在这个epoll fd上能关注的最大socket fd数 * * @return 生成的文件描述符 */ int epoll_create(int size);
二、epoll_ctl函数:
/** * @brief 该函数用于控制某个epoll文件描述符上的事件,能够注册事件,修改事件,删除事件。 * * @param epfd 由 epoll_create 生成的epoll专用的文件描述符 * @param op 要进行的操做例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除 * @param fd 关联的文件描述符 * @param event 指向epoll_event的指针 * * @return 0 succ * -1 fail */ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
其中用到的数据结构结构以下:
op值:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
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 */
};
经常使用的事件类型:
EPOLLIN :表示对应的文件描述符能够读;
EPOLLOUT:表示对应的文件描述符能够写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 表示对应的文件描述符有事件发生;
例:
<code class="language-cpp">struct epoll_event ev; //设置与要处理的事件相关的文件描述符 ev.data.fd=listenfd; //设置要处理的事件类型 ev.events=EPOLLIN|EPOLLET; //注册epoll事件 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); </code>
三、epoll_wait函数:
/** * @brief 该函数用于轮询I/O事件的发生 * * @param epfd 由epoll_create 生成的epoll专用的文件描述符 * @param events 用于回传代处理事件的数组 * @param maxevents 每次能处理的事件数 * @param timeout 等待I/O事件发生的超时值;-1至关于阻塞,0至关于非阻塞。通常用-1便可 * * @return >=0 返回发生事件数 * -1 错误 */ int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);
用改良的epoll实现回声服务端代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define BUF_SIZE 100 #define EPOLL_SIZE 50 void error_handling(char *buf); int main(int argc, const char * argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; socklen_t adr_sz; int str_len, i; char buf[BUF_SIZE]; //相似select的fd_set变量查看监视对象的状态变化,epoll_event结构体将发生变化的文件描述符单独集中到一块儿 struct epoll_event *ep_events; struct epoll_event event; int epfd, event_cnt; if(argc != 2) { printf("Usage: %s <port> \n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); if(serv_sock == -1) error_handling("socket() error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); if(listen(serv_sock, 5) == -1) error_handling("listen() error"); //建立文件描述符的保存空间称为“epoll例程” epfd = epoll_create(EPOLL_SIZE); ep_events = malloc(sizeof(struct epoll_event) *EPOLL_SIZE); //添加读取事件的监视(注册事件) event.events = EPOLLIN; //读取数据事件 event.data.fd = serv_sock; epoll_ctl(epdf, EPOLL_CTL_ADD, serv_sock, &event); while (1) { //响应事件,返回发生事件的文件描述符数 event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //传-1时,一直等待直到事件发生 if(event_cnt == -1) { puts("epoll_wait() error"); break; } //服务端套接字和客服端套接字 for (i = 0; i < event_cnt; i++) { if(ep_events[i].data.fd == serv_sock)//服务端与客服端创建链接 { adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); event.events = EPOLLIN; event.data.fd = clnt_sock; epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); printf("connected client: %d \n", clnt_sock); } else //链接以后传递数据 { str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); if(str_len == 0) { //删除事件 epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); close(ep_events[i].data.fd); printf("closed client: %d \n", ep_events[i].data.fd); } else { write(ep_events[i].data.fd, buf, str_len); } } } } close(serv_sock); close(epfd); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
epoll客户端代码:
#define _GNU_SOURCE #include "sysutil.h" #include "buffer.h" #include <sys/epoll.h> int main(int argc, char const *argv[]) { //建立client套接字 int sockfd = tcp_client(0); //调用非阻塞connect函数 int ret = nonblocking_connect(sockfd, "localhost", 9981, 5000); if(ret == -1) { perror("Connect Timeout ."); exit(EXIT_FAILURE); } //将三个fd设置为Non-Blocking activate_nonblock(sockfd); activate_nonblock(STDIN_FILENO); activate_nonblock(STDOUT_FILENO); buffer_t recvbuf; //sockfd -> Buffer -> stdout buffer_t sendbuf; //stdin -> Buffer -> sockfd //初始化缓冲区 buffer_init(&recvbuf); buffer_init(&sendbuf); //建立epoll int epollfd = epoll_create1(0); if(epollfd == -1) ERR_EXIT("create epoll"); struct epoll_event events[1024]; uint32_t sockfd_event = 0; uint32_t stdin_event = 0; uint32_t stdout_event = 0; epoll_add_fd(epollfd, sockfd, sockfd_event); epoll_add_fd(epollfd, STDIN_FILENO, stdin_event); epoll_add_fd(epollfd, STDOUT_FILENO, stdout_event); while(1) { //从新装填epoll事件 sockfd_event = 0; stdin_event = 0; stdout_event = 0; //epoll没法每次都从新装填,因此给每一个fd添加一个空事件 if(buffer_is_readable(&sendbuf)) { sockfd_event |= kWriteEvent; } if(buffer_is_writeable(&sendbuf)) { stdin_event |= kReadEvent; } if(buffer_is_readable(&recvbuf)) { stdout_event |= kWriteEvent; } if(buffer_is_writeable(&recvbuf)) { sockfd_event |= kReadEvent; } epoll_mod_fd(epollfd, sockfd, sockfd_event); epoll_mod_fd(epollfd, STDIN_FILENO, stdin_event); epoll_mod_fd(epollfd, STDOUT_FILENO, stdout_event); //监听fd数组 int nready = epoll_wait(epollfd, events, 1024, 5000); if(nready == -1) ERR_EXIT("epoll wait"); else if(nready == 0) { printf("epoll timeout.\n"); continue; } else { int i; for(i = 0; i < nready; ++i) { int peerfd = events[i].data.fd; int revents = events[i].events; if(peerfd == sockfd && revents & kReadREvent) { //从sockfd接收数据到recvbuf if(buffer_read(&recvbuf, peerfd) == 0) { fprintf(stderr, "server close.\n"); exit(EXIT_SUCCESS); } } if(peerfd == sockfd && revents & kWriteREvent) { buffer_write(&sendbuf, peerfd); //将sendbuf中的数据写入sockfd } if(peerfd == STDIN_FILENO && revents & kReadREvent) { //从stdin接收数据写入sendbuf if(buffer_read(&sendbuf, peerfd) == 0) { fprintf(stderr, "exit.\n"); exit(EXIT_SUCCESS); } } if(peerfd == STDOUT_FILENO && revents & kWriteREvent) { buffer_write(&recvbuf, peerfd); //将recvbuf中的数据输出至stdout } } } } }
什么是条件触发和边缘触发?它们是指事件响应的方式,epoll默认是条件触发的方式。条件触发是指:只要输入缓冲中有数据就会一直通知该事件,循环响应epoll_wait。而边缘触发是指:输入缓冲收到数据时仅注册1次该事件,即便输入缓冲中还留有数据,也不会再进行注册,只响应一次。
边缘触发相对条件触发的优势:能够分离接收数据和处理数据的时间点,从实现模型的角度看,边缘触发更有可能带来高性能。
将上面epoll实例改成边缘触发:
1,首先改写 event.events = EPOLLIN | EPOLLET; (EPOLLIN:读取数据事件 EPOLLET:边缘触发方式)
2,边缘触发只响应一次接收数据事件,因此要一次性所有读取输入缓冲中的数据,那么就须要判断何时数据读取完了?Linux声明了一个全局的变量:int errno; (error.h中),它能记录发生错误时提供额外的信息。这里就能够用它来判断是否读取完数据:
str_len = read(...); if(str_len < 0) { if(errno == EAGAIN) //读取输入缓冲中的所有数据的标志 break; }
3,边缘触发方式下,以阻塞方式工做的read&write有可能会引发服务端的长时间停顿。因此边缘触发必定要采用非阻塞的套接字数据传输形式。那么怎么将套接字的read,write数据传输形式修改成非阻塞模式呢?
//fd套接字文件描述符,将此套接字数据传输模式修改成非阻塞 void setnonblockingmode(int fd) { int flag = fcntl(fd, F_GETFL,0); //获得套接字原来属性 fcntl(fd, F_SETFL, flag | O_NONBLOCK);//在原有属性基础上设置添加非阻塞模式 }
三.IOCP理解与应用。
扯远点。首先传统服务器的网络IO流程以下:
接到一个客户端链接->建立一个线程负责这个链接的IO操做->持续对新线程进行数据处理->所有数据处理完毕->终止线程。
可是这样的设计代价是:
此时咱们能够考虑使用线程池解决其中3和4的问题。这种传统的服务器网络结构称之为会话模型。
后来咱们为防止大量线程的维护,建立了I/O模型,它被但愿要求能够:
1:容许一个线程在不一样时刻给多个客户端进行服务。
2:容许一个客户端在不一样时间被多个线程服务。
这样作的话,咱们的线程则会大幅度减小,这就要求如下两点:
1:客户端状态的分离,以前会话模式咱们能够经过线程状态得知客户端状态,但如今客户端状态要经过其余方式获取。
2:I/O请求的分离。一个线程再也不服务于一个客户端会话,则要求客户端对这个线程提交I/O处理请求。
那么就产生了这样一个模式,分为三部分:
上面的作法,则将网络链接 和I/O工做线程分离为三个部分,相互通信仅依靠 I/O请求。此时可知有如下一些建议:
它是一种WIN32的网络I/O模型,既包括了网络链接部分,也负责了部分的I/O操做功能,用于方便咱们控制有并发性的网络I/O操做。它有以下特色:
使用IOCP的基本步骤很简单:
最后说一句啦。本网络编程入门系列博客是连载学习的,有兴趣的能够看我博客其余篇。。。。c++ 网络编程课设入门超详细教程 ---目录
参考博客:http://blog.csdn.net/penzo/article/details/5986574
参考博客:https://blog.csdn.net/educast/article/details/15500349
参考博客:https://blog.csdn.net/u010223072/article/details/49276415
参考书籍:《TCP/IP网络编程--尹圣雨》
如有兴趣交流分享技术,可关注本人公众号,里面会不按期的分享各类编程教程,和共享源码,诸如研究分享关于c/c++,python,前端,后端,opencv,halcon,opengl,机器学习深度学习之类有关于基础编程,图像处理和机器视觉开发的知识