文章目录
IO 多路复用作用于数据准备阶段。
1、select
1.1、数据结构
fdset 是一个 fd 的位图,采用数组的形式来存储。每个 bit 位代表一个 fd,0表示该 fd 的事件未就绪,1表示该 fd 事件的已就绪。
// fd_set 里面文件描述符的数量,可以使用 ulimit -n 进行查看,默认1024
#define FD_SETSIZE __FD_SETSIZE
// fd_set 的成员是一个长整型的结构体
typedef long int __fd_mask;
// fd_set 记录要监听的 fd 集合,及其对应状态
typedef struct {
// fds_bits是long类型数组,用来实现位图,长度: 1024/32=32
// 共1024个bit位,每个bit位代表一个fd,0表示未就绪,1表示就绪
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;
// fd_set 集合的相关操作
void FD_ZERO(fd_set *fdset); // 将所有fd清零
void FD_SET(int fd, fd_set *fdset); // 增加一个fd
void FD_CLR(int fd, fd_set *fdset); // 删除一个fd
int FD_ISSET(int fd, fd_set *fdset); // 判断一个fd是否有设置
1.2、select 函数
#include <sys/select.h>
#include <sys/time.h>
/* 返回值:返回就绪(可读、可写、异常)文件描述符的总数,若没有返回 0 */
int select(int nfds, // 要监听 fd_set 的最大 fd+1(监听总数),fd 从0开始计数
fd_set *readset, // 内核读操作的 fd 集合
fd_set *writeset, // 内核写操作的 fd 集合
fd_set *exceptionset, // 内核异常操作的 fd 集合
struct timeval * timeout // 超时时间,NULL-永不超时,0-不阻塞等待。
);
1.3、selcet 使用
- 创建集合(集合的元素:文件描述符),把要等待的fd放入集合(监听)
- 创建
fd_set
集合 - 用
FD_ZERO
初始化集合(将位图所有位置0) - 用
FD_SET
把要监听文件描述符FD加入集合(将位图所对应的位置为1)
- 创建
- 用 select 系统调用,进程阻塞,当集合中任意一个FD准备就绪,解除阻塞
- 用
FD_INSSET
检查 fd 是否就绪,就绪就解除阻塞
1.4、select 问题
select 的问题
- 监听的 fd 的数量有限制,默认是1024
- 每次 select 都需要把监听的 fd 从用户态拷贝到内核态,返回后从内核态拷贝数据到用户态
- 轮询遍历所有的 fd 来判断就绪状态,效率低
1.5、例:即时聊天
// ./姓名 pipe1 pipe2
// 双方都需要读键盘、读管道
//女神进程
#include <func.h>
int main(int argc, char *argv[]) {
ARGS_CHECK(argc,3);
int fdr = open(argv[1], O_RDONLY);
int fdw = open(argv[2], O_WRONLY);
puts("阿珍:");
char buf[4096] = {0};
fd_set rdset;
while(1){
FD_ZERO(&rdset);
FD_SET(fdr,&rdset);
FD_SET(STDIN_FILENO, &rdset);
//超时机制的实现
struct timeval timeout;
timeout.tv_sec = 60;
int nret = select(fdr+1,&rdset,NULL,NULL,&timeout);
if(nret == 0){
puts("timeout");
break;
}
//读端:读对方的数据
if(FD_ISSET(fdr,&rdset)){
puts("备胎1024");
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
//对方写端关闭,防止读端阻塞
if(ret == 0){
puts("byebye");
break;
}
puts(buf);
}
//写端:读入键盘数据,发送给对方
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf,0,sizeof(buf));
int ret = read(STDIN_FILENO,buf,sizeof(buf));
//键盘关闭,ctrl+d
if(ret == 0){
puts("I quit");
write(fdw,"你是好人",13);
break;
}
struct stat statbuf;
stat(argv[2],&statbuf);
char * curtime = ctime(&statbuf.st_mtime);
strcat(curtime, " ");
strcat(curtime, buf);
write(fdw, curtime, strlen(curtime));
}
}
return 0;
}
//舔狗进程
#include <func.h>
int main(int argc, char *argv[]) {
ARGS_CHECK(argc,3);
int fdw = open(argv[1], O_WRONLY);
int fdr = open(argv[2], O_RDONLY);
puts("阿强:");
char buf[4096] = {0};
fd_set rdset;
while(1){
FD_ZERO(&rdset);
FD_SET(fdr,&rdset);
FD_SET(STDIN_FILENO, &rdset);
struct timeval timeout;
timeout.tv_sec = 60;
int nret = select(fdr+1,&rdset,NULL,NULL,&timeout);
if(nret == 0){
puts("timeout");
break;
}
if(FD_ISSET(fdr,&rdset)){
puts("女神");
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
if(ret == 0){
puts("byebye");
break;
}
puts(buf);
}
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf,0,sizeof(buf));
int ret = read(STDIN_FILENO,buf,sizeof(buf));
if(ret == 0){
puts("I quit");
write(fdw,"多喝热水",13);
break;
}
struct stat statbuf;
stat(argv[1],&statbuf);
char * curtime = ctime(&statbuf.st_mtime);
strcat(curtime, " ");
strcat(curtime, buf);
write(fdw, curtime, strlen(curtime));
}
}
return 0;
}
2、poll
2.1、数据结构
// pollfd 的事件类型
#define POLLIN
#define POLLOUT
#define POLLERR
#define POLLNVAL
// pollfd 结构
struct pollfd {
int fd; // 要监听的fd
short int events; // 要监听的事件类型
short int revents; // 实际发生的事件类型
}
2.2、poll 函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.3、poll 使用
- 创建 pollfd 数组,向其中添加关注的 fd 信息,数组大小自定义
- 调用 poll 函数,将 pollfd 数组拷贝到内核空间,转链表存储,无上限
- 内核遍历 fd,判断是否就绪
- 数据就绪或超时后,拷贝 pollfd 数组到用户空间,返回就绪 fd 数量 n
- 用户进程判断 n 是否大于0,大于0则遍历 pollfd 数组,找到就绪的 fd
4.2、poll 问题
poll 利用链表解决了 select 中监听 fd 上限的问题,但仍需要遍历所有的 fd
3、epoll
3.1、数据结构
struct eventpoll {
// ...
struct rb_root rbr; // 红黑树,记录要监听的fd
struct list_head rdllist; // 双向链表,记录已就绪的fd epoll_wait
// ...
};
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
// ...
};
struct epoll_event {
__uint32_t events; // epoll事件,每1位对应1个epol事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr; // 指定与 fd 相关的用户数据
int fd; // 指定事件从属的文件描述符,常用
uint32_t u32;
uint64_t u64;
} epoll_data_t; //联合体,fd 与 ptr 只能使用其中1个
3.2、epoll 函数
epoll_create
创建 epoll 对象
int epoll_create1(int size)
/*
返回值:成功返回对应的 epfd ( eventpoll 结构体) ,失败返回 -1。
参数 size:现无意义,必须大于0,一般填1,告知内核事件表有多大。
*/
epoll_ctl
将一个 fd 添加到 epoll 的红黑树中,并设置 ep_poll_callback,callback 触发时,把对应 fd 加入到rdlist 就绪列表中。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
/*
返回值:成功返回0,失败返回-1。
参数:
- 参数1 epfd:epoll对象
- 参数2 op:指定操作类型。
EPOLL_CTL_ADD 增加
EPOLL_CTL_MOD 修改
EPOLL_CTL_DEL 删除
- 参数3 fd:要监听的 fd
- 参数4 event: 要监听的事件类型,红黑树键值对kv:fd-event
event.events:
EPOLLIN 可读
EPOLLOUT 可写
EPOLLET 边缘触发,默认⽔平触发LT
*/
epoll_wait
检测 rdlist 列表是否为空,不为空则返回就绪的 fd 的数量
// 收集 epoll 监控的事件中已经发生的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待 timeout 毫秒后返回。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
/*
返回值:成功返回实际就绪的 fd 数量,失败返回-1
参数:
- 参数1 epfd:epoll 对象。
- 参数2 events:用户态创建的evnet数组,内核拷贝就绪的 fd 到该数组中。
events[i].events:
EPOLLIN 触发读事件
EPOLLOUT 触发写事件
EPOLLERR 连接发生错误
EPOLLRDHUP 连接读端关闭
EPOLLHUP 连接双端关闭
- 参数3 maxevents:可以返回的最⼤事件数目,一般设置为event数组的⻓度
- 参数4 timeout:超时时间 ms。-1(一直等待),0(不等待),>0(等待时间)。断开与服务器没有交互的客户端
*/
3.3、epoll 原理
注:调用 epoll_create
会创建一个 epoll 对象。调用 epoll_ctl
添加到 epoll 中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用回调函数(ep_poll_callback
),将触发的事件拷贝到 rdlist
双向链表中。调用epoll_wait
将会把 rdlist
中就绪事件拷贝到用户态中;
调用 epoll_create 函数:返回一个epfd,同时内核会创建 eventpoll 结构体,该结构体的成员由红黑树和双向链表组成。红黑树:保存需要监听的描述符。双向链表:保存就绪的文件描述符。
**调用 epoll_ctl 函数:**对红黑树进行增添,修改、删除。
添加 fd 到红黑树上,从用户空间拷贝到内核空间。一次注册 fd,永久生效。内核会给每一个节点注册一个回调函数,当该 fd 就绪时,会主动调用自己的回调函数,将其加入到双向链表当中。
调用 epoll_wait 函数:内核监控双向链表,如果双向链表里面有数据,从内核空间拷贝数据到用户空间;如果没有数据,就阻塞。
3.4、epoll 事件
3.4.1、连接建立
接收连接
accept
(epoll_event)ev.events |= EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
主动连接
connect = -1 & errno = EINPROGRESS;
(epoll_event)ev.events |= EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
3.4.2、连接断开
客户端断开
1、客户端发送 FIN 包,客户端写端关闭
2、服务端收到 FIN 包
read = 0
ev.events & EPOLLRDHUB
3、支持半关闭,关闭服务端读端
4、不支持半关闭,close(fd)
服务端断开
1、shutdown(SHUT_WR),发送 FIN 包给客户端
2、write = -1 && errno = EPIPE,写端关闭,但是可能收到数据,读端可能未关闭
3.4.3、消息到达
- read = 0,收到客户端关闭的 FIN 包
- read > 0,正常,处理相应的业务逻辑
- read = -1
errno = EWOULDBLOCK
,非阻塞 IO,读缓冲为空errno = EINTER
,系统调用被中断打断,重试errno = ETIMEOUT
,tcp 探活超时
3.4.4、消息发送完毕
- write > 0,正常,处理相应的业务逻辑
- write = -1
errno = EWOULDBLOCK
,非阻塞 IO,写缓冲不够,注册写事件,等待下次发送errno = EINTER
,系统调用被中断打断,重试errno = EPIPE
,写端关闭
4、IO 多路复用总结
select总结
- select 的底层实现:位图
- 监控的 fd 数量有限,默认是1024
- 每次调用 select 时,会发生用户空间到内核空间的数据拷贝,select返回时,又要从内核空间拷贝数据到用户空间
- select 每次轮询检查就绪的描述符,即需要遍历所有的文件描述符
- select 适用于并发量低,活动连接多的情况
epoll总结
- epoll 的底层实现:红黑树 + 双向链表
- 监控的 fd 数量没有限制,仅与内存大小有关
cat /proc/sys/fs/file-max
- epoll 把需要监听的描述符加入红黑树,一次加入永久生效。内核会给每一个加入到红黑树上的文件描述符注册一个回调函数,当内核检测到就绪的文件描述符时,触发回调函数,将其加入到双向链表中。
- epoll 每次只检查就绪链表,不需要遍历所有文件描述符,效率高
- epoll_wait 适用于并发量高,活动连接少的情况
5、事件通知机制
5.1、LT 和 ET
当 fd 有数据可读时,调用 epoll_wait(select、poll)可以得到通知。事件通知的模式有两种:
- 水平触发 LT:当内核读缓冲区非空,写缓冲区不满,则一直触发,直至数据处理完成。
- 边缘触发 ET:当 IO 状态发生改变,触发一次。每次触发需要一次性把事件处理完。
LT 和 ET 的特性,决定了对于数据的读操作不同
- LT + 一次性读
- ET + 循环读
// lt + 一次性读,小数据
ret = read(fd, buf, sizeof(buf));
// et + 循环读,大数据
while(true) {
ret = read(fd, buf, sizeof(buf);
// 此时,说明读缓冲区已经空了
if (ret == EAGAIN || ret == EWOULDBLOCK) break;
}
5.2、ET 的优势
- ET 模式避免了 LT 模式可能出现的惊群现象(如:一个 listenfd 被多个 epoll 监听,一个调用accept 接受连接,其他 accept 阻塞)
- ET 模式减少了 EPOLL 事件被重复触发的次数,效率高。
5.3、ET 的使用
ET 的使用:ET+ 非阻塞 IO + 循环读
循环读:若数据不能一次性处理完,只能等到下次数据到达网卡后才触发读事件。
非阻塞 IO:fcntl 函数可以将 fd 设置为非阻塞。
//修改(获取)文件描述符属性
int fcntl(int fd, int cmd, ... /* arg */ );
/*
返回值:失败返回-1
参数
- 参数1:需要修改的文件描述符,
- 参数2:修改(获取)文件描述符的操作
*/
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, O_NONBLOCK);
例如:将 recv 函数设置为非阻塞的两种方式
-
recv 函数的属性设置为MSG_DONWAIT
ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT);
-
fcntl 函数将文件描述符设置为非阻塞性的。
void setNoblock(int fd) { //1、获取原有套接字状态的信息 int status = fcntl(fd, F_GETFL); //2、将非阻塞的标志与原有的标志信息做或操作 status |= O_NONBLOCK; //3、将标志位信息写回到socket中 fcntl(fd, F_SETFL, status); }
6、实例
6.1、TCP聊天
//server.c
#include <func.h>
int main(int argc,char*argv[]) {
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本地的IP地址和端口号
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//设置地址可重用
int ret = 0;
int reuse = 1;
ret = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int));
//绑定本机的IP和端口号到sfd上
ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
//监听
ret = listen(sfd, 10);
ERROR_CHECK(ret, -1, "listen");
//创建epoll
int epfd = epoll_create(1);
ERROR_CHECK(epfd, -1, "epoll_create");
struct epoll_event events, evs[3];
memset(&events, 0, sizeof(events));
//监听键盘
events.events = EPOLLIN;
events.data.fd = STDIN_FILENO;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &events);
ERROR_CHECK(ret, -1, "epoll_ctl1");
//监听客户端
events.events = EPOLLIN;
events.data.fd = sfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &events);
ERROR_CHECK(ret, -1, "epoll_ctl2");
char buffer[512] = { 0 };
int readyNum = 0;
int newFd = 0;
while(1) {
readyNum = epoll_wait(epfd, evs, 3, -1);
ERROR_CHECK(readyNum, -1, "epoll_wait");
for(int i = 0; i < readyNum; ++i) {
//客户端请求建立连接
if (evs[i].data.fd == sfd) {
//建立连接
newFd = accept(sfd, NULL, NULL);
ERROR_CHECK(newFd, -1, "accept");
puts("client connect\n");
//监听newFd,即客服
events.events = EPOLLIN;
events.data.fd = newFd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newFd, &events);
ERROR_CHECK(ret, -1, "epoll_ctl3");
}
//向客户端发送数据
if (evs[i].data.fd == STDIN_FILENO) {
memset(buffer, 0, sizeof(buffer));
ret = read(STDIN_FILENO, buffer, sizeof(buffer));
ERROR_CHECK(ret, -1, "read");
ret = send(newFd, buffer, strlen(buffer)-1, 0);
ERROR_CHECK(ret, -1, "send");
}
//从客户端读取数据
if (evs[i].data.fd == newFd) {
memset(buffer, 0, sizeof(buffer));
ret = recv(newFd, buffer, sizeof(buffer), 0);
ERROR_CHECK(ret, -1, "recv");
//当客户端发送完数据后,关闭连接
if (0 == ret) {
//关闭newFd后,newFd成为整型数值
//再次调用recv会报参数错误,返回-1
close(newFd);
continue;
}
//newFd关闭后,直接跳出循环,防止其他fd饥饿
if (-1 == ret) {
break;
}
printf("客户端: %s\n", buffer);
}
}
}
close(sfd);
close(newFd);
return 0;
}
//client.c
#include <func.h>
int main(int argc,char*argv[]) {
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本地的IP地址和端口号
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//连接到服务器
int ret = connect(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "newFd");
//创建epoll
int epfd = epoll_create(1);
ERROR_CHECK(epfd, -1, "epoll_create");
struct epoll_event events, evs[2];
memset(&events, 0, sizeof(events));
//监听键盘
events.events = EPOLLIN;
events.data.fd = STDIN_FILENO;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &events);
ERROR_CHECK(ret, -1, "epoll_ctl1");
//监听服务器端
events.events = EPOLLIN;
events.data.fd = sfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &events);
ERROR_CHECK(ret, -1, "epoll_ctl2");
char buffer[512] = { 0 };
int readyNum = 0;
while(1) {
readyNum = epoll_wait(epfd,evs,2,-1);
ERROR_CHECK(readyNum, -1, "epoll_wait");
for(int i = 0; i < readyNum; ++i) {
//向服务器端发送数据
if(evs[i].data.fd == STDIN_FILENO) {
memset(buffer, 0, sizeof(buffer));
read(STDIN_FILENO, buffer, sizeof(buffer));
send(sfd, buffer, strlen(buffer)-1, 0);
ERROR_CHECK(ret, -1, "send");
}
//接收服务器端发来的数据
if(evs[i].data.fd == sfd) {
memset(buffer, 0, sizeof(buffer));
ret = recv(sfd, buffer, sizeof(buffer), 0);
ERROR_CHECK(ret, -1, "recv");
if(ret == 0) {
continue;
}
printf("服务器: %s\n", buffer);
}
}
}
close(sfd);
return 0;
}
6.2、UDP聊天
服务器端:socket - bind - while(1) {- recvfrom - sendto -} - close
//server.c
#include <func.h>
int main(int argc,char*argv[]) {
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存服务器的IP地址和端口号
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//绑定本机的IP和端口号到sfd上
int ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
//创建epoll
int epfd = epoll_create(1);
ERROR_CHECK(epfd, -1, "epoll_create");
struct epoll_event events, evs[3];
memset(&events, 0, sizeof(events));
//监听键盘
events.events = EPOLLIN;
events.data.fd = STDIN_FILENO;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &events);
ERROR_CHECK(ret, -1, "epoll_ctl1");
//监听客户端
events.events = EPOLLIN;
events.data.fd = sfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &events);
ERROR_CHECK(ret, -1, "epoll_ctl2");
char buffer[512] = { 0 };
int readyNum = 0;
//创建接收客户端IP和端口的结构体,才能知道客户的位置
struct sockaddr_in cliAddr;
socklen_t len = sizeof(cliAddr);
memset(&cliAddr, 0, sizeof(len));
while(1) {
readyNum = epoll_wait(epfd, evs, 2, -1);
ERROR_CHECK(readyNum, -1, "epoll_wait");
for(int i = 0; i < readyNum; ++i) {
//收到来自客户端的数据
if (evs[i].data.fd == sfd) {
puts("client connect\n");
memset(buffer, 0, sizeof(buffer));
ret = recvfrom(sfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&serAddr, &len);
ERROR_CHECK(ret, -1, "recvfrom");
printf("客户端:%s\n", buffer);
}
//发送服务器端的数据
if (evs[i].data.fd == STDIN_FILENO) {
memset(buffer, 0, sizeof(buffer));
ret = read(STDIN_FILENO, buffer, sizeof(buffer));
ERROR_CHECK(ret, -1, "read");
ret = sendto(sfd, buffer, strlen(buffer)-1, 0, (struct sockaddr*)&serAddr, len);
ERROR_CHECK(ret, -1, "sendto");
}
}
}
close(sfd);
return 0;
}
客户端:socket - sendto - recvfrom - close
//client.c
#include <func.h>
int main(int argc,char*argv[]) {
int ret = 0;
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存服务器的IP地址和端口号
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
socklen_t len = sizeof(serAddr);
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//创建epoll
int epfd = epoll_create(1);
ERROR_CHECK(epfd, -1, "epoll_create");
struct epoll_event events, evs[2];
memset(&events, 0, sizeof(events));
//监听键盘
events.events = EPOLLIN;
events.data.fd = STDIN_FILENO;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &events);
ERROR_CHECK(ret, -1, "epoll_ctl1");
//监听服务器端
events.events = EPOLLIN;
events.data.fd = sfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &events);
ERROR_CHECK(ret, -1, "epoll_ctl2");
char buffer[512] = { 0 };
int readyNum = 0;
while(1) {
readyNum = epoll_wait(epfd, evs, 2, -1);
ERROR_CHECK(readyNum, -1, "epoll_wait");
for(int i = 0; i < readyNum; ++i) {
//先执行,将键盘输入的数据发送给服务器
if(evs[i].data.fd == STDIN_FILENO) {
memset(buffer, 0, sizeof(buffer));
read(STDIN_FILENO, buffer, sizeof(buffer));
sendto(sfd, buffer, strlen(buffer)-1, 0, (struct sockaddr*)&serAddr, len);
ERROR_CHECK(ret, -1, "sendto");
}
//收到来自服务器的数据
if(evs[i].data.fd == sfd) {
memset(buffer, 0, sizeof(buffer));
ret = recvfrom(sfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&serAddr, &len);
ERROR_CHECK(ret, -1, "recvfrom");
printf("服务器: %s\n", buffer);
}
}
}
close(sfd);
return 0;
}