IO 多路复用

简介: IO 多路复用

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;
 }
相关文章
|
2月前
|
存储 监控 Linux
【Linux IO多路复用 】 Linux下select函数全解析:驾驭I-O复用的高效之道
【Linux IO多路复用 】 Linux下select函数全解析:驾驭I-O复用的高效之道
63 0
|
4月前
|
存储 Linux 调度
io复用之epoll核心源码剖析
epoll底层实现中有两个关键的数据结构,一个是eventpoll另一个是epitem,其中eventpoll中有两个成员变量分别是rbr和rdlist,前者指向一颗红黑树的根,后者指向双向链表的头。而epitem则是红黑树节点和双向链表节点的综合体,也就是说epitem即可作为树的节点,又可以作为链表的节点,并且epitem中包含着用户注册的事件。当用户调用epoll_create()时,会创建eventpoll对象(包含一个红黑树和一个双链表);
72 0
io复用之epoll核心源码剖析
|
4月前
|
存储 网络协议
TCP服务器 IO多路复用的实现:select、poll、epoll
TCP服务器 IO多路复用的实现:select、poll、epoll
36 0
|
4月前
|
网络协议 Linux C++
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
62 0
|
11天前
|
存储 监控 网络协议
IO多路复用
IO多路复用
|
2月前
|
NoSQL Java Linux
【Linux IO多路复用 】 Linux 网络编程 认知负荷与Epoll:高性能I-O多路复用的实现与优化
【Linux IO多路复用 】 Linux 网络编程 认知负荷与Epoll:高性能I-O多路复用的实现与优化
67 0
|
2月前
|
JavaScript Unix Linux
IO多路复用:提高网络应用性能的利器
IO多路复用:提高网络应用性能的利器
|
4月前
|
网络协议 架构师 Linux
一文说透IO多路复用select/poll/epoll
一文说透IO多路复用select/poll/epoll
172 0
|
4月前
|
网络协议 Linux
2.1.1网络io与io多路复用select/poll/epoll
2.1.1网络io与io多路复用select/poll/epoll
|
4月前
|
监控 Linux
IO多路复用,epoll和select的区别
IO多路复用,epoll和select的区别
20 0