帮助文档
专业提供香港服务器、香港云服务器、香港高防服务器租用、香港云主机、台湾服务器、美国服务器、美国云服务器vps租用、韩国高防服务器租用、新加坡服务器、日本服务器租用 一站式全球网络解决方案提供商!专业运营维护IDC数据中心,提供高质量的服务器托管,服务器机房租用,服务器机柜租用,IDC机房机柜租用等服务,稳定、安全、高性能的云端计算服务,实时满足您的多样性业务需求。 香港大带宽稳定可靠,高级工程师提供基于服务器硬件、操作系统、网络、应用环境、安全的免费技术支持。
服务器资讯 / 香港服务器租用 / 香港VPS租用 / 香港云服务器 / 美国服务器租用 / 台湾服务器租用 / 日本服务器租用 / 官方公告 / 帮助文档
高并发服务器--多路IO转接(多路IO复用)
发布时间:2024-03-09 18:26:49   分类:帮助文档
高并发服务器--多路IO转接(多路IO复用) 目录 1、select实现 1.1 基本原理: 1.2 API: 1.3 代码: 1.4 优缺点 2、poll实现 2.1 工作流程 2.1 API 2.2 代码 3、epoll实现 3.1 API 3.1.1 epoll_create 3.1.2 epoll_ctl  3.1.3 epoll_wait 3.2 代码 高并发服务器的三种方式: 阻塞等待--消耗资源(如多线程多进程实现)非阻塞忙轮询--消耗cpu多路IO转接(内核监听多个文件描述符的属性(读写缓冲区)变化,如果某个文件描述符的读缓冲区变化了,这个时候就是可以读了,将这个事件告知应用层)         多路IO转接三种方式:select(windows, 跨平台)、poll(少用)、epoll(linux)。         多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想时,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。 1、select实现         select能监听文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。         解决1024以下客户端时使用select是很合适的,但是如果客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。 1.1 基本原理: select核心实现原理是位图,select总共有三种位图,分别为读,写,异常位图。用户程序预先将socket文件描述符注册至读,写,异常位图,然后通过select系统调用轮询位图中的socket的读,写,异常事件。内核通过轮询方式获取读,写,异常位图中注册的socket文件事件,如果检测到有socket文件处于就绪状态,则会将socket对应的事件设置到输出位图,等所有位图中的socket都被轮询完,会统一将输出位图通过copy_to_user函数复制到输入位图,并且覆盖掉输入位图注册信息(也就是用户初始化的位图被内核修改)。select轮询完所有位图,如果未检测到任何socket文件处于就绪状态,根据超时时间确定是否返回或者阻塞进程。socket检测到读,写,异常事件后,会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询所有位图。select返回时会将剩余的超时时间通过copy_to_user覆盖原来的超时时间。 1.2 API: #include #include #include #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 功能:监听多个文件描述符的属性变化(读、写、异常) void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set); 参数: nfds : 最大文件描述符+1 readfds : 需要监听的读的文件描述符存放的集合 writefds : 需要监听的写的文件描述符存放的集合 NULL exceptfds : 需要监听的异常的文件描述符存放的集合 NULL timeout : 多长时间监听一次 固定的事件,限时等待 NULL永久监听 struct timeval{ long tv_sec; // seconds long tv_usec; // microseconds }; 返回值:返回的是变化的文件描述符的个数 注意:变化的文件描述符会存在监听的集合中,未变化的文件描述符会被删除 1.3 代码: #include #include #include #include #include #include"wrap.h" #include #define PORT 8800 int main() { // 创建监听套接字、绑定 int lfd = tcp4bind(PORT, NULL); // 监听 Listen(lfd, 128); int maxfd = lfd; fd_set oldset, rset; FD_ZERO(&oldset); FD_ZERO(&rset); FD_SET(lfd, &oldset); // 循环调用select,并处理发生变化的文件描述符 while(1) { rset = oldset; int n = select(maxfd+1, &rset, NULL, NULL, NULL); // n为发生变化文件描述符的数量 if(n < 0) { perror("select error:"); break; } else if(n == 0) // 超时 { continue; } else // 有文件描述符发生变化 { // lfd变化 if(FD_ISSET(lfd, &rset)) { struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); char ip[16] = ""; int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); printf("new client ip = %s; port = %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip ,16), ntohs(cliaddr.sin_port)); // 将cfd添加至oldset集合中,以下次监听 FD_SET(cfd, &oldset); // 更新maxfd if(cfd > maxfd) maxfd = cfd; // 只有lfd变化,continue if(--n == 0) continue; } } // cfd变化 for(int i = lfd + 1; i <= maxfd; i++) { // 如果i文件描述符在rset中 if(FD_ISSET(i, &rset)) { char buf[1500]= ""; int ret = Read(i, buf, sizeof(buf)); if(ret < 0) { perror("read error:"); close(i); FD_CLR(i, &oldset); continue; } else if(ret == 0) { printf("client close.\n"); close(i); FD_CLR(i, &oldset); } else { printf("%s\n", buf); write(i, buf, ret); } } } } return 0; } 1.4 优缺点         优点:跨平台         缺点:    文件描述符1024的限制                        只是返回变化的文件描述符的个数,具体哪个变化需要遍历                        每次都需要将需要监听的文件描述符集合由应用层拷贝到 内核                        效率低:                                 假设现在4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了?                                 假设现在4-1023个文件描述符需要监听,但是只有5,1002发来消息。 2、poll实现 2.1 工作流程 用户空间程序调用poll函数,并传入了一个pollfd结构数组,以及数组的大小和超时时间等参数。内核遍历该数组,检查每个文件描述符所对应的I/O事件是否发生如果有文件描述符的I/O事件发生,就在相应的pollfd结构中设置相应的标志位。poll函数返回给用户空间,并通知哪些文件描述符已经就绪。用户空间程序根据返回的结果进行相应的处理 2.1 API #include int poll(struct pollfd *fds, nfds_t nfds, int timeout); 功能:监听多个文件描述符的属性变化 参数: fds: 监听的数组的首元素地址 nfds: 数组的有效元素的最大下标+1 timeout: 超时事件 -1为永久监听 >=0为限时等待 数组元素: struct pollfd { int fd; // 需要监听的文件描述符 short events; // 需要监听文件描述符什么事件 POLLIN读事件、POLLOUT写事件 short revents; // 返回监听到的事件 同上 } 2.2 代码 #include #include #include #include #include #include #include #include #include #include"wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 #define OPEN_MAX 1024 int main() { int i, j, maxi, lfd, cfd, sockfd; int nready; ssize_t n; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct pollfd client[OPEN_MAX]; // 定义poll数组 struct sockaddr_in cliaddr, servaddr; lfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 设置端口复用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(lfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(lfd, 128); client[0].fd = lfd; // 要监听的第一个文件描述符,存入client[0] client[0].events = POLLIN; // lfd监听普通读事件 for(i = 1; i < OPEN_MAX; i++) client[i].fd = -1; // 用-1初始化client[]里剩下元素 0也是文件描述符,不能用来初始化 maxi = 0; while(1) { nready = poll(client, maxi+1, -1); // 阻塞监听是否有客户端链接请求 if(client[0].revents & POLLIN) { clilen = sizeof(cliaddr); cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &clilen); // 接收客户端请求 printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for(i = 1; i maxi) maxi = i; if(--nready <= 0) continue; } for(i = 1; i<=maxi; i++) { if((sockfd = client[i].fd) < 0) continue; // 找到第一个大于0的 if(client[i].revents & POLLIN) { if((n = Read(sockfd, buf, MAXLINE)) < 0) { // connection reset by client if(errno == ECONNRESET) // 收到RST标志 { printf("client[%d] aborted connection\n", i); close(sockfd); } } else if(n == 0) { printf("client[%d] closed connection\n", i); close(sockfd); client[i].fd = -1; } else { for(j = 0; j < n; j++) buf[j] = toupper(buf[j]); Write(sockfd, buf, n); } if(--nready <= 0) break; } } } return 0; } 3、epoll实现 3.1 API 3.1.1 epoll_create int epoll_creat(int size); 功能:创建一个epoll对象 参数:size取大于0即可 返回值:成功返回epoll对象epfd。 小于0表示创建失败 3.1.2 epoll_ctl int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 功能:epoll_ctl函数用于增加、删除、修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中。 参数: epfd:epoll文件描述符 op:操作码 EPOLL_CTL_ADD: 插入事件 EPOLL_CTL_DEL: 删除事件 EPOLL_CTL_MOD: 修改事件 fd:epoll事件绑定的套接字文件描述符 event:epoll事件结构体。 返回值: 成功:返回0。 失败:返回-1,并设置errno。 struct epoll_event { uint32_t events; // epoll事件 epoll_data_t data; }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data_t;         epoll_ctl函数增加epoll事件时,系统默认注册EPOLLERR和EPOLLHUP事件  3.1.3 epoll_wait epoll就绪事件处理示例:         1、注册epoll事件 struct epoll_event ev; ev.data.fd = sock_fd; ev.event = EPOLLIN; // 注册EPOLLIN事件 epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &ev);         2、就绪epoll事件 res = EPOLLIN | EPOLLRDNORM;         3、epoll_wait获取事件 events = (EPOLLIN | EPOLLERR | EPOLLHUP) & (EPOLLIN | EPOLLRDNORM) = EPOLLIN; 注意:只有注册的事件才能通过epoll_wait获取 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 功能:epoll_wait用于监听套接字事件。 参数: epfd: epoll文件描述符。 events: epoll事件数组。 maxevents:epoll事件数组长度 timeout:超时时间 小于0:一直等待。 等于0:立即返回。 大于0:等待超时时间返回,单位毫秒 返回值: 小于0:出错。 等于0:超时。 大于0:返回就绪事件个数。 3.2 代码 #include #include #include #include #include #include"wrap.h" int main() { // 创建套接字并绑定 int lfd = tcp4bind(8000, NULL); // 监听 Listen(lfd, 128); // 创建树 int epfd = epoll_create(1); // 将lfd上树 struct epoll_event ev, evs[1024]; ev.data.fd = lfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); // while监听套接字事件 while(1) { int nready = epoll_wait(epfd, evs, 1024, -1); // epoll_wait监听套接字事件, nready为就绪事件的数量,evs为epoll事件数组(用于接收) if(nready < 0) { perror(""); break; } else if(nready == 0) // 超时 continue; else // 有文件描述符变化 { for(int i = 0; i < nready; i++) { // 判断lfd变化,并且是读事件变化 if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN) { struct sockaddr_in cliaddr; char ip[16] = ""; socklen_t len = sizeof(cliaddr); int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len); // 提取新的连接 printf("new client ip = %s port = %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16), ntohs(cliaddr.sin_port)); // 将cfd上树 ev.data.fd = cfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); } else if(evs[i].events & EPOLLIN) // cfd变化,而且是读事件变化 { char buf[1024] = ""; int n = read(evs[i].data.fd, buf, sizeof(buf)); if(n < 0) // 出错,cfd下树 { perror(""); epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); } else if(n == 0) // 客户端关闭 { printf("client close\n"); close(evs[i].data.fd);//将cfd关闭 epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树 } else { printf("%s\n", buf); write(evs[i].data.fd, buf, n); } } } } } return 0; } 3.3 epoll两种工作方式 1、监听读缓冲区的变化         水平(LT)触发:只要读缓冲区有数据就会触发epoll_wait。         边沿(ET)触发:数据来一次,epoll_wait只触发一次。 2、监听写缓冲区的变化         水平触发:只要可以写,就会触发。         边沿触发:数据从有到无,就会触发。         LT模式只不过比ET模式多执行了一个步骤,就是当epoll_wait获取完就绪队列epoll事件后,LT模式会再次将epoll事件节点再次添加到就绪队列。         默认设置都是水平触发,水平触发如果数据一次性都不干净,就需要多次系统调用,浪费资源,一般都会设置为边沿触发,改为边沿触发只需要将监听事件或上一个宏: ev.events = EPOLLIN | EPLLET; // 监听读事件并设置为ET模式         设置完之后,则需要在读数据的时候一次性将数据读完,不然会出现读不完的情况。此时只需要将read那里加一个while循环。如果read读完了缓冲区的数据之后会阻塞,所以需要将其设置为非阻塞: int flags = fcntl(cfd, F_GETFL); // 获取cfd的标志位 flags |= O_NONBLOCK; fcntl(cfd, F_SETFL, flags); // 将cfd设置为非阻塞
香港云服务器租用推荐
服务器租用资讯
·广东云服务有限公司怎么样
·广东云服务器怎么样
·广东锐讯网络有限公司怎么样
·广东佛山的蜗牛怎么那么大
·广东单位电话主机号怎么填写
·管家婆 花生壳怎么用
·官网域名过期要怎么办
·官网邮箱一般怎么命名
·官网网站被篡改怎么办
服务器租用推荐
·美国服务器租用
·台湾服务器租用
·香港云服务器租用
·香港裸金属服务器
·香港高防服务器租用
·香港服务器租用特价