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_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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值