多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
IO 多路转接方式比较:
常见的 IO 多路转接方式有:select、poll、epoll,他们的区别为:
- select 可以跨平台,Linux、Mac、Windows 都支持,而 poll 和 epoll 只能在 Linux 上使用
- epoll 底层为红黑树,select 和 poll 底层为线性表,epoll 的效率较高
- select 连接的设备上限为 1024,poll 和 epoll 没上限,取决于当前操作系统的配置
IO 多路转接本质:
- 在服务器端有两类文件描述符,分别对应一个读缓冲区和写缓冲区
- 用于监听的文件描述符对应的读缓冲区主要用来存储客户端的连接请求,当调用 accept 时会检测这个读缓冲区是否有连接请求
- 用于通信的文件描述符对应的读缓冲区用于存储客户端发送来的数据,服务器调用 read 方法能够将数据读取出来,写缓冲区用于服务器通过 write 写入的数据
- 当只有一个线程时,accpet、read、write 只要有一个阻塞,就不能继续运行了
- IO 多路转接实际上就是将本该由用户进行的文件描述符读/写缓冲区的检测交给了内核,内核可以同时检测若干个文件描述符以及它们的读/写缓冲区,检测读缓冲区是否有数据/检测写缓冲区是否有剩余的空间,当条件满足时,内核会告知用户相关信息(可操作的文件描述符),此时 accpet、read、write 就不会阻塞了,若内核通知多个文件描述符,在用户空间处理时是按顺序处理的
select
-
select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
-
解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
// 返回值就绪描述符的数目
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
示例图如下:
这个图的意思是,应用层先把要监听的文件描述符做标记1,之后再将其拷贝一份,将拷贝的这一份文件描述符在拷贝到内核中,让内核监听这些做标记的文件描述符,如果被监听的文件没有变化,那么内核中的文件描述的标记就会被抹消,然后在将改变的文件描述符集合复制到应用层,让其对改变的文件描述符进行读取,
例如应用层准备监听4567这四个文件描述符,复制到内核去监听,内核发现只有5号发生了改变,所以告知应用层去5号文件描述符读取数据,如果是lfd即4发生了变化,就说明有新的连接产生了
server
流程图示:
#include <stdio.h>
#include <sys/select.h> // select多路复用API
#include <sys/types.h> // 基本系统数据类型
#include <unistd.h> // POSIX API(read/write/close等)
#include "wrap.h" // 自定义错误处理函数封装
#include <sys/time.h>#define PORT 8888 // 服务器监听端口int main(int argc, char *argv[])
{// 创建TCP套接字并绑定端口int lfd = tcp4bind(PORT, NULL);// 设置监听队列长度为128Listen(lfd, 128);int maxfd = lfd; // 初始化最大文件描述符(当前只有监听套接字)fd_set oldset, rset; // 定义两个fd_set:// oldset:永久记录所有需监控的fd// rset:每次select调用传入的临时集合FD_ZERO(&oldset); // 清空文件描述符集合FD_ZERO(&rset);FD_SET(lfd, &oldset); // 将监听套接字加入监控集合while (1){rset = oldset; // 复制永久集合到临时集合(select会修改传入的集合)// 核心:阻塞监听所有文件描述符的可读事件int n = select(maxfd + 1, &rset, NULL, NULL, NULL);// 错误处理if (n < 0){perror("select error");break;}else if (n == 0){ // 无事件发生(超时)continue;}// 处理监听套接字事件(新连接到达)查看lfd监听描述符是否在就绪的rset集合中,在表示有新连接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));// 将新连接加入永久监控集合FD_SET(cfd, &oldset);// 更新最大文件描述符if (cfd > maxfd)maxfd = cfd;// 若已无其他事件,跳过后续处理if (--n == 0)continue;}// 处理已连接套接字的数据事件//在 Unix/Linux 中,文件描述符按从小到大的顺序分配。lfd 是服务器启动时最早创建的套接字,其值通常为3(0-2 被标准输入/输出/错误占用),后续 cfd 依次递增(4, 5, ...)//因此 lfd + 1 自然指向第一个客户端连接套接字。for (int i = lfd + 1; i <= maxfd; i++){if (FD_ISSET(i, &rset)){ // 检查当前fd是否就绪char buf[1500] = "";int ret = Read(i, buf, sizeof(buf)); // 读取数据// 错误处理if (ret < 0){perror("read error");close(i);FD_CLR(i, &oldset); // 从监控集合移除}// 客户端关闭连接else if (ret == 0){struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(i, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("client%d close\n",remote_port);close(i);FD_CLR(i, &oldset);}// 正常数据处理else{struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(i, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("客户端%d:%s\n", remote_port,buf);Write(i, buf, ret); // 回显数据}}}}return 0;
}
几个问题?
1.select(maxfd + 1, &rset, NULL, NULL, NULL);为什么要maxfd + 1?
fd_set rset;
int maxfd = 5; // 当前最大FD为5
FD_ZERO(&rset);
FD_SET(3, &rset); // 监控FD=3
FD_SET(5, &rset); // 监控FD=5// 内核会检查0~5的FD(共6个),但仅FD=3和5实际被监控
select(5 + 1, &rset, NULL, NULL, NULL);
若误传 maxfd=5
(未+1),内核可能漏检FD=5,导致数据就绪却未被触发
2.int n = select(maxfd + 1, &rset, NULL, NULL, NULL);这里是怎样遍历文件描述符集合的?是从0开始遍历rset里的文件描述符吗?
- 在
select
函数中,内核遍历文件描述符集合(rset
)的方式是通过线性扫描位图,从文件描述符 0 开始,依次检查每个比特位是否被置位(即是否为1),直到达到maxfd + 1
指定的范围。从0到maxfd
,无论文件描述符是否打开或活跃。这种设计简单但效率低,是select
被epoll
取代的主要原因之一
3.select(maxfd + 1, &rset, NULL, NULL, NULL);里的rset作用是什么
- 在
select
函数中,rset
是一个fd_set
类型的位图集合,其核心作用是标识需要监控的可读文件描述符(FD)集合,并在函数返回时标记哪些FD已就绪可读。
4.for (int i = lfd + 1; i <= maxfd; i++) 为什么从 lfd + 1开始遍历?
-
lfd
通常是较小的值:
在 Unix/Linux 中,文件描述符按从小到大的顺序分配。lfd
是服务器启动时最早创建的套接字,其值通常为3(0-2 被标准输入/输出/错误占用),后续cfd
依次递增(4, 5, ...)。
因此lfd + 1
自然指向第一个客户端连接套接字。
client
客户端使用:
nc 127.0.0.1 8888
模拟客户端链接服务器
结果显示如下:
但这样有个问题,无论连接服务器的客户端是否活跃,遍历时都会遍历这些连接的客户端,所以这就会引发一个问题(大量并发,少了活跃):
假设现在 4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了,遍历时还是要从4-1023进行遍历,实际只需要遍历4、1001-1023即可。
假设现在 4-1023个文件描述符需要监听,但是只有 5,1002 发来消息,遍历时还是要从4-1023进行遍历,实际只需要遍历5、1002即可。
select进阶优化版
之前的代码,如果最大fd是1023,每次确定有事件发生的fd时,就要扫描3-1023的所有文件描述符,这看起来很蠢。于是定义一个数组,把要监听的活跃的文件描述符存下来,每次扫描这个数组就行了。看起来科学得多。
server
//进阶版select,通过数组防止遍历1024个描述符
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>#include "wrap.h"#define SERV_PORT 8888int main(int argc, char *argv[])
{int i, j, n, maxi;int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */int maxfd, listenfd, connfd, sockfd;char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */listenfd = Socket(AF_INET, SOCK_STREAM, 0);//端口复用int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family= AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port= htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1; /* 用-1初始化client[] */FD_ZERO(&allset);FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */while (1) { rset = allset; /* 每次循环时都从新设置select监控信号集 */nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfdif (nready < 0)perr_exit("select error");if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */clie_addr_len = sizeof(clie_addr);connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) { /* 找client[]中没有使用的位置 */client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */break;}if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */if (connfd > maxfd)maxfd = connfd; /* select第一个参数需要 */if (i > maxi)maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */if (--nready == 0)continue;} for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */if ((sockfd = client[i]) < 0)continue;//数组内的文件描述符如果被释放有可能变成-1if (FD_ISSET(sockfd, &rset)) {if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */Close(sockfd);FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */client[i] = -1;} else if (n > 0) {for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Write(sockfd, buf, n);Write(STDOUT_FILENO, buf, n);}if (--nready == 0)break; /* 跳出for, 但还在while中 */}}}Close(listenfd);return 0;
}
若
lfd = 3
,maxfd = 100
,但仅 5 个活跃连接,基础版仍需循环 97 次(4~100),而进阶版仅需循环 5 次(数组中的活跃 FD)。
局限性
虽然使用 select 这种 IO 多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:
待检测集合(第 2、3、4 个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
内核对于 select 传递进来的待检测集合的检测方式是线性的
检测效率与集合内待检测的文件描述符有关:如果集合内待检测的文件描述符很多,检测效率会比较低;如果集合内待检测的文件描述符相对较少,检测效率会比较高
使用 select 能够检测的最大文件描述符个数有上限,默认是 1024,这是在内核中被写死了的
poll
poll 的机制与 select 类似,与 select 在本质上没有多大差别,使用方法也类似,下面的是对于二者的对比:
- 内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
- poll 和 select 检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
- select 检测的文件描述符个数上限是 1024,poll 没有最大文件描述符数量的限制
- select 可以跨平台使用,poll 只能在 Linux 平台使用
- select 通过 fd_set(位图集合)来记录文件描述符,poll 使用一个整型数来记录
poll 函数
#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 -> 传出 */ // 不需要进行初始化
};struct pollfd myfd[100]; // 可能需要检测若干个文件描述符,要存储在数组中
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数含义:
fds: 这是一个 struct pollfd 类型的数组, 里边存储了待检测的文件描述符的信息,这个数组中有三个成员:fd:委托内核检测的文件描述符
events:委托内核检测的 fd 事件(输入、输出、错误),每一个事件有多个取值
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果(不需要初始化,根据 events 委托内核检测的时间传出结果)
nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数 1 数组的元素总个数)
timeout: 指定 poll 函数的阻塞时长
-1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
大于 0:阻塞指定的毫秒(ms)数之后,解除阻塞
返回值:成功返回集合中已就绪的文件描述符的总个数,失败返回-1
server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h" // 自定义的包裹函数头文件(如Socket、Bind等)#define MAXLINE 80 // 缓冲区大小
#define SERV_PORT 6666 // 服务器端口
#define OPEN_MAX 1024 // 最大文件描述符数量int main(int argc, char *argv[])
{int i, j, maxi, listenfd, connfd, sockfd;int nready; // poll返回的就绪文件描述符数量ssize_t n; // 读取的字节数char buf[MAXLINE], str[INET_ADDRSTRLEN]; // 缓冲区和IP地址字符串socklen_t clilen; // 客户端地址长度struct pollfd client[OPEN_MAX]; // poll监控的文件描述符数组struct sockaddr_in cliaddr, servaddr; // 客户端和服务器地址结构/* 1. 创建监听套接字 */listenfd = Socket(AF_INET, SOCK_STREAM, 0); // IPv4 TCP套接字/* 2. 绑定服务器地址 */bzero(&servaddr, sizeof(servaddr)); // 清空结构体servaddr.sin_family = AF_INET; // IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有本地IPservaddr.sin_port = htons(SERV_PORT); // 设置端口Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));/* 3. 开始监听 */Listen(listenfd, 20); // 监听队列最大长度为20/* 4. 初始化poll监控数组 */client[0].fd = listenfd; // 第一个元素是监听套接字client[0].events = POLLRDNORM; // 监听普通读事件(新连接)for (i = 1; i < OPEN_MAX; i++)client[i].fd = -1; // 其余元素初始化为-1(表示空闲)maxi = 0; // 当前client数组中有效元素的最大下标/* 5. 主循环:处理poll事件 */for (;;){// 阻塞等待事件发生,监控maxi+1个描述符(从0到maxi),无限等待nready = poll(client, maxi + 1, -1);/* 5.1 处理监听套接字(新连接) */if (client[0].revents & POLLRDNORM) // 监听套接字可读(有新连接){clilen = sizeof(cliaddr);connfd = Accept(listenfd, (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)); // 打印客户端IP和端口/* 将新连接加入client数组 */for (i = 1; i < OPEN_MAX; i++){if (client[i].fd < 0){ // 找到第一个空闲位置client[i].fd = connfd; // 存储新连接的描述符break;}}if (i == OPEN_MAX) // 超过最大连接数限制perr_exit("too many clients");client[i].events = POLLRDNORM; // 对新连接监控读事件if (i > maxi)maxi = i; // 更新最大有效下标if (--nready <= 0) // 如果没有更多就绪事件,继续pollcontinue;}/* 5.2 处理已连接套接字的数据 */for (i = 1; i <= maxi; i++) // 遍历所有可能的连接{if ((sockfd = client[i].fd) < 0) // 跳过无效描述符continue;/* 检查读事件或错误事件 */if (client[i].revents & (POLLRDNORM | POLLERR)){n = Read(sockfd, buf, MAXLINE); // 读取数据if (n < 0){// 读取错误if (errno == ECONNRESET) // 客户端发送RST重置连接{printf("client[%d] aborted connection\n", i);Close(sockfd);client[i].fd = -1; // 重置为未使用}else{perr_exit("read error"); // 其他错误直接退出}}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]);Writen(sockfd, buf, n); // 回写给客户端}if (--nready <= 0) // 没有更多就绪事件,跳出循环break;}}}return 0;
}
client
客户端使用:
nc 127.0.0.1 6666
模拟客户端链接服务器
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
可以使用cat命令查看一个进程可以打开的socket描述符上限。
cat /proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。
sudo vi /etc/security/limits.conf
在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。
* soft nofile 65536
* hard nofile 100000
基础API
1.创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
#include <sys/epoll.h>
int epoll_create(int size) size:监听数目
2.控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd: 为epoll_creat的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
event: 告诉内核需要监听的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3.等待所监控文件描述符上有事件的产生,类似于select()调用。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
events: 用来存内核得到事件的集合,
maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout: 是超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
server
处理流程:
1.创建 epoll 实例对象 epoll_create
2.将用于监听的套接字添加到 epoll 实例中 epoll_ctl
3.检测添加到 epoll 实例中的文件描述符是否已就绪,并将这些已就绪的文件描述符进行处理 epoll_wait
- 如果是监听的文件描述符,和新客户端建立连接,将得到的文件描述符添加到 epoll 实例中
- 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从 epoll 实例中删除
#include <stdio.h>
#include <fcntl.h>
#include "wrap.h"
#include <sys/epoll.h>
int main(int argc, char *argv[])
{//创建套接字 绑定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);//监听printf("epoll wait _________________\n");if(nready <0){perror("");break;}else if( nready == 0){continue;}else//有文件描述符变化{ int i = 0;for( 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为非阻塞int flags = fcntl(cfd,F_GETFL);//获取的cfd的标志位flags |= O_NONBLOCK;fcntl(cfd,F_SETFL,flags);//将cfd上树ev.data.fd =cfd;ev.events =EPOLLIN | EPOLLET;//设置为边沿触发epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}else if( evs[i].events & EPOLLIN)//cfd变化 ,而且是读事件变化{while(1){char buf[4]="";//如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待,如果//是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAINint n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听if(errno == EAGAIN){break;}//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}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]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}}}}}return 0;
}
client
客户端使用:
nc 127.0.0.1 8000
模拟客户端链接服务器