IO 多路复用作用于数据准备阶段。
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_原理图
注:调用 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; }