理解io复用——阻塞与非阻塞

589 阅读9分钟

理解io复用——阻塞与非阻塞

image-20210504235902100

流:

  • 可以进行I/O操作的内核对象
  • 包括文件、管道、套接字等等
  • 流的入口:文件描述符(fd)
image-20210504235957411

流是有方向的,read和write

如果当前还没有数据,去读的话就会阻塞在这.

image-20210505000203288

比如你要洗袜子,需要肥皂,家里没有肥皂了。有个快递小哥送肥皂给你,在送到前是没有任何事可以做的,也就是阻塞状态.

image-20210505000242331

我们也可以尝试非阻塞的方式,一直轮询.

阻塞vs非阻塞:

image-20210505000344233

阻塞等待:不占用CPU的时间片,等到这个进程或者线程准备好了这个资源不再阻塞了,进程或线程调度就可以让他接着执行非阻塞轮询:占用CPU,会一直去轮询这个资源准备好了没,准备好了就接着执行下面的程序阻塞等待:不占用CPU的时间片,等到这个进程或者线程准备好了这个资源不再阻塞了,进程或线程调度就可以让他接着执行 \\非阻塞轮询:占用CPU,会一直去轮询这个资源准备好了没,准备好了就接着执行下面的程序

大部分情况下,阻塞等待都是更好的选择,因为不会消耗CPU资源,而且资源准备好了他自己就会继续执行的。

阻塞等待的缺点:

image-20210505000949441

如果在等一个资源的话,阻塞等待一般是最好的选择。但是如果在同时要等很多个, 阻塞等待的办法就只能顺序着一个一个等待,这样效率其实是比较低的 这种情况下,非阻塞轮询,即每次都轮询所有的资源,如果有准备好了的,直接去处理

image-20210505001247872

一般场景都会选择阻塞等待,但由于它不能很好的处理多个并发的请求的问题,同一个阻塞,同一时刻只能处理一个流的阻塞监听,此时,我们会选择多路IO复用:

  1. 能够阻塞等待,不浪费CPU资源
  2. 能够同一时刻监听多个IO请求的状态

IO复用解决的问题

如何解决大量IO读写的请求并且不浪费CPU如何解决大量IO读写的请求并且不浪费CPU?

方法一:阻塞+多线程/多进程

image-20210505102054427

方法一确实可以解决大量IO读写的请求,就让每个线程去各自处理一个IO请求嘛,而且也不浪费CPU资源,因为每个线程都是阻塞等待的,但是缺点也很明显:

  • 多线程或多线程会消耗资源,而且线程切换也需要较高成本

方法二:非阻塞+忙轮询

image-20210505102310399

方法二可以解决大量IO读写的请求,但是很耗费CPU资源

image-20210505102353541

// 非阻塞忙轮询的实现
while true{
    for i in 流[]{	//一次性遍历所有的流
        if i has 数据{
            读 或者 其他处理
        }
    }
}

方法三:select

image-20210505102553094

我们的select会代替我们去管理所有的IO请求,如果当前有IO资源到了,select会通知我们当前有IO资源到了,但是仅仅是通知有无资源到了噢,不会通知哪个到了,哪些到了。

select资源有限,最多只能监听1024个IO状态

简而言之:调用select会阻塞在那,直到有资源好了才返回

image-20210505102826933

while true{
    select(流[]);	//阻塞
    // 如果有资源到了,就从阻塞返回,然后轮询所有的流
	for i in 流[]{
        if i has 数据{
            读 或者 其他处理
		}
    }
}

方法四:epoll

image-20210505103124888

epoll是比select更强大的工具,它不仅告诉我们有快递到了,还会告诉我们是哪些快递到了,所以我们就不用遍历所有流去看到底是哪些流准备好了。 而且,epoll能处理的IO请求比select大得多

image-20210505103304950

while true{
    可处理的流[] = epoll_wait(epoll_fd);	//阻塞
    
    for i in 可处理的流[]{
        读 或者 其他处理
    }
}
image-20210505103417455 $$ 什么是epoll?\\ 1.是一种IO多路复用计数 2.只关心活跃的链接,无序遍历全部 3.能够处理大量的链接请求 $$

image-20210505103708122

epoll的API及内部机制

1.创建epoll(epoll_create)

image-20210505105304626

int epoll_create(int size);	//size:内核监听的数目
// 返回一个epoll句柄

image-20210505105550466

epoll_create(size)所做的就是在内核中创建一颗红黑树的根节点,返回给用户

2.控制epoll(epoll_ctl)

image-20210505105756276

/**
* @1.param epfd	刚才创建好的那个根节点
* @2.param op		op是一些可选参数
* 	EPOLL_CTL_ADD表示注册新的fd到epfd中
* 	EPOLL_CTL_MOD表示修改已经注册的fd的监听事件             
*   EPOLL_CTL_DEL表示epfd删除一个fd
* @3.param fd		fd是需要监听的文件描述符
* @4.param event	告诉内核需要监听的事件
*
* @returns		成功返回0,失败返回-1,errno查看错误信息
*/
int epoll_ctl(int epfd,int op,int fd,strucct epoll_event *event);

struct epoll_event:

image-20210505110411033

struct epoll_event{
    __uint32_t events;	//epoll事件
    epoll_data_t data;	//用户传递的数据
}
typedef union epoll_data{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}

实例:

image-20210505111026586

struct epoll_event new_event;
new_event.events = EPOLLIN | EPOLLOUT;	//可读可写事件
new_evnet.data.fd = 5;	//event绑定文件描述符5

epoll_ctl(epfd,EPOLL_CLT_ADD,5,&new_event);//将文件描述符5作为可读可写事件添加到根节点中
epoll_ctl添加一个读写文件描述符5的事件到我们的epoll中,这样就会在Kernel的那个树中插入一个new_event,之后在epoll_wait的时候就可以从Kernel中遍历这颗树,找到所有触发这些事件的,并返回给用户.用epoll\_ctl添加一个读写文件描述符5的事件到我们的epoll中, \\这样就会在Kernel的那个树中插入一个new\_event, \\之后在epoll\_wait的时候就可以从Kernel中遍历这颗树,找到所有触发这些事件的,并返回给用户.

3.等待epoll(epoll_wait)

image-20210505111423589

/**
*
* @param epfd			用epoll_create所创建的epoll句柄
* @param event			从内核得到的事件集合(不是传入数据,而是用于得到触发的事件)
* @param maxevents		告知内核这个events有多大(不得>创建时的size)
* @param timeout		超时时间
*			-1:永久阻塞
*			 0:立即返回,非阻塞	
*			>0:指定微秒
*
* @returns		   成功:返回就绪的文件描述符个数,时间到时返回0
* 				 .失败:-1,errno查看错误
*/

int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);

实例:

image-20210505112536123

struct epoll_event my_event[1000];int event_cnt = epoll_wait(epfd,my_event,1000,-1);

如果epoll_wait返回,会把所有的事件放在my_event中,同时,event_cnt中返回了事件的个数,所以我们就可以利用my_event数组和告知的数量event_cnt,去遍历my_event的[0,event_cnt-1],去处理这些.

epoll编程架构

image-20210505113640435

// 创建epollint epfd = epfd_create(1000);// 将listen_fd添加进epoll中epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&listen_event);while (1){    // 阻塞等待epoll中的fd触发    int active_cnt = epoll_wait(epfd,events,1000,-1);        // 遍历所有触发的事件    for (int i = 0;i < active_cnt;i++)    {        if (events[i].data.fd == listen.fd){//监听的fd触发了,所以就说明有新的客户端发来了请求,我们需要和他建立连接            accept	//和这个客户端进行三次握手,创建这个连接            并将accept的fd加进epoll中	//把这个连接的fd加进epoll        }        else if (events[i].events & EPOLLIN) //如果是一个读事件        {            对此fd 进行读操作        }        else if (events[i].events & EPOLLOUT)//如果是一个写事件        {			对此fd 进行写操作        }	}    }

下面是一段博客中看到的epoll常用框架:epoll使用详解(精髓) - Boblim - 博客园 (cnblogs.com)

 1 for( ; ; ) 2     { 3         nfds = epoll_wait(epfd,events,20,500); 4         for(i=0;i<nfds;++i) 5         { 6             if(events[i].data.fd==listenfd) //有新的连接 7             { 8                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接 9                 ev.data.fd=connfd;10                 ev.events=EPOLLIN|EPOLLET;11                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中12             }13             else if( events[i].events&EPOLLIN ) //接收到数据,读socket14             {15                 n = read(sockfd, line, MAXLINE)) < 0    //读16                 ev.data.ptr = md;     //md为自定义类型,添加数据17                 ev.events=EPOLLOUT|EPOLLET;18                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓19             }20             else if(events[i].events&EPOLLOUT) //有数据待发送,写socket21             {22                 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据23                 sockfd = md->fd;24                 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据25                 ev.data.fd=sockfd;26                 ev.events=EPOLLIN|EPOLLET;27                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据28             }29             else30             {31                 //其他的处理32             }33         }34     }

下面是另一个博客中的epoll使用的示例代码epoll TCP服务器与客户端简明例子 - 简书 (jianshu.com)

socket_server.cpp:

#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>

using namespace std;

const int MAX_EPOLL_EVENTS = 1000;
const int MAX_MSG_LEN = 1024;

void setFdNonblock(int fd)
{
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

void err_exit(const char *s){
    printf("error: %s\n",s);
    exit(0);
}

int create_socket(const char *ip, const int port_number)//创建一个监听端口
{
    struct sockaddr_in server_addr = {0};
    /* 设置ipv4模式 */
    server_addr.sin_family = AF_INET;           /* ipv4 */
    /* 设置端口号 */
    server_addr.sin_port = htons(port_number);
    /* 设置主机地址 */
    if(inet_pton(server_addr.sin_family, ip, &server_addr.sin_addr) == -1){
        err_exit("inet_pton");
    }
    /* 建立socket */
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        err_exit("socket");
    }
    /* 设置复用模式 */
    int reuse = 1;
    if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
    {
        err_exit("setsockopt");
    }
    /* 绑定端口 */
    if(bind(sockfd, (sockaddr *)&server_addr, sizeof(server_addr)) == -1){
        err_exit("bind");
    }
    /* 设置被动打开 */
    if(listen(sockfd, 5) == -1){
        err_exit("listen");
    }
    return sockfd;
}

int main(int argc, const char *argv[])
{
    /* 帮助 */
    if(argc < 3){
        printf("usage:%s ip port\n", argv[0]);
        exit(0);
    }
    /* 获取服务器参数 */
    const char * ip = argv[1];
    const int port = atoi(argv[2]);
    /* 创建套接字 */
    int sockfd = create_socket(ip, port);
    printf("success create sockfd %d\n", sockfd);
    setFdNonblock(sockfd);
    /* 创建epoll */
    int epollfd = epoll_create1(0);
    if(epollfd == -1) err_exit("epoll_create1");
    /* 添加sockfd到epollfd兴趣列表 */
    struct epoll_event ev;
    ev.data.fd = sockfd;
    ev.events = EPOLLIN ;
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1){
        err_exit("epoll_ctl1");
    }
    /* 创建一个列表用于存放wait所返回的events */
    struct epoll_event events[MAX_EPOLL_EVENTS] = {0};
    /* 开始等待所有在epoll上挂上去的事件 */

    while(1){
        /* 等待事件 */
        printf("begin wait\n");
        int number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
        printf("end wait\n");
        sleep(1);
        if(number > 0){
            /* 遍历所有事件 */
            for (int i = 0; i < number; i++)
            {
                int eventfd = events[i].data.fd;
                /* 如果触发事件的fd是sockfd,则说明有人连接上来了,我们需要accept他 */
                if(eventfd == sockfd){
                    printf("accept new client...\n");
                    struct sockaddr_in client_addr;
                    socklen_t client_addr_len = sizeof(client_addr);
                    int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
                    setFdNonblock(connfd);
                    /* accept之后,需要将文件描述符加入到监听列表中 */
                    struct epoll_event ev;
                    ev.data.fd = connfd;
                    ev.events = EPOLLIN;
                    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1){
                        err_exit("epoll_ctl2");
                    }
                    printf("accept new client end.\n");
                }
                /* 如果触发的fd不是sockfd,那就是新加的connfd */
                else{
                    /* 读出内容,直到遇到回车。然后显示该内容。 */
                    printf("read start...\n");
                    while(1){
                        char buff = -1;
                        int ret = read(eventfd, &buff, 1);
                        if(ret > 0){
                            printf("%c", buff);
                        }
                        if(buff == '\n'){
                            break;
                        }
                        else if (ret == 0){
                            printf("client close.\n");
                            close(eventfd);
                            epoll_ctl(epollfd, EPOLL_CTL_DEL, eventfd, NULL);
                            break;
                        }
                        else if (ret < 0){
                            printf("read error.\n");
                            break;
                        }
                    }
                    printf("read end.\n");
                }
            }
        }
    }
}

可以看到,server所做的事情如下:

1.准备阶段:

  • int sockfd = create_socket(ip, port);	//创建监听连接的文件描述符sockfd
    
  • int epollfd = epoll_create1(0);			//创建epoll
    
  • // 将监听文件描述符的IN事件添加到epoll中
    struct epoll_event ev;
    ev.data.fd = sockfd;
    ev.events = EPOLLIN ;
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1){
        err_exit("epoll_ctl1");
    }
    
  •     /* 创建一个数组用于存放wait所返回的events */
        struct epoll_event events[MAX_EPOLL_EVENTS] = {0};
    
  • 2.主体循环

        while(1){
            /* 等待事件 */
            printf("begin wait\n");
            int number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
            printf("end wait\n");
            sleep(1);
            if(number > 0){
                /* 遍历所有事件 */
                for (int i = 0; i < number; i++)
                {
                    int eventfd = events[i].data.fd;
                    /* 如果触发事件的fd是sockfd,则说明有人连接上来了,我们需要accept他 */
                    if(eventfd == sockfd){
                        printf("accept new client...\n");
                        struct sockaddr_in client_addr;
                        socklen_t client_addr_len = sizeof(client_addr);
                        int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
                        setFdNonblock(connfd);
                        /* accept之后,需要将文件描述符加入到监听列表中 */
                        struct epoll_event ev;
                        ev.data.fd = connfd;
                        ev.events = EPOLLIN;
                        if(epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1){
                            err_exit("epoll_ctl2");
                        }
                        printf("accept new client end.\n");
                    }
                    /* 如果触发的fd不是sockfd,那就是新加的connfd */
                    else{
                        /* 读出内容,直到遇到回车。然后显示该内容。 */
                        printf("read start...\n");
                        while(1){
                            char buff = -1;
                            int ret = read(eventfd, &buff, 1);
                            if(ret > 0){
                                printf("%c", buff);
                            }
                            if(buff == '\n'){
                                break;
                            }
                            else if (ret == 0){
                                printf("client close.\n");
                                close(eventfd);
                                epoll_ctl(epollfd, EPOLL_CTL_DEL, eventfd, NULL);
                                break;
                            }
                            else if (ret < 0){
                                printf("read error.\n");
                                break;
                            }
                        }
                        printf("read end.\n");
                    }
                }
            }
    

socket_client.cpp:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string>
#include <iostream>

using namespace std;

void err_exit(const char *s){
    printf("error: %s\n",s);
    exit(0);
}

int create_socket(const char *ip, const int port_number)
{
    struct sockaddr_in server_addr = {0};
    /* 设置ipv4模式 */
    server_addr.sin_family = AF_INET;           /* ipv4 */
    /* 设置端口号 */
    server_addr.sin_port = htons(port_number);
    /* 设置主机地址 */
    if(inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1){
        err_exit("inet_pton");
    }

    /* 建立socket */
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        err_exit("socket");
    }

    if(connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1){
        err_exit("connect");
    }

    return sockfd;
}

int main(int argc, const char *argv[]){
    if(argc < 3){
        printf("usage:%s ip port\n", argv[0]);
        exit(0);
    }
    /* 获取服务器参数 */
    const char * ip = argv[1];
    const int port = atoi(argv[2]);
    //创建套接字
    int sock = create_socket(ip, port);
    //向服务器(特定的IP和端口)发起请求
    
    while(1){
        string buff;
        getline(cin, buff);
        if(buff == "exit") break;
        write(sock, buff.c_str(), buff.size());
        char end = '\n';
        write(sock, &end, 1);
    }
    close(sock);
    return 0;
}
g++ -Wall socket_server.cpp -o server && g++ -Wall socket_client.cpp -o client
./server localhost 1234
./client localhost 1234

在client端输入文字并回车,会出现在服务器端。按下Ctrl C关闭客户端或者输入exit关闭客户端。

服务器首先创建被动打开socket文件描述符,然后将该文件描述符加入到epoll兴趣列表。接下来进入循环。每当兴趣列表wait结束时,说明对应文件描述符可以进行操作。当有客户端连上被动打开socket文件描述符时,说明有客户端连上,被动打开文件描述符可以被accept。accept后所创建的新的文件描述符是与客户端通信的文件描述符,该文件描述符继续加入兴趣列表。当客户端发送数据时,该文件描述符也会产生可读信号,会导致wait结束,此时进入处理模式,读取并显示客户端所发送的数据。

epoll水平触发与边缘触发

image-20210505132217127

水平触发

image-20210505132236308

水平触发:

第一次epoll_wait的时候,A、B事件都触发了,那么我们拿到A、B事件后,对他进行处理。假如说A事件处理时,我们只读了一半,还没处理完,还有一半是留在了event_A中。那么如果是水平触发,下一次还会把event_A返回,直到真正处理完了event_A或者删掉event_A

所以这就保证了处理这个事件的完整性和安全性。

缺点:如果用户没有处理,每次都会返回这个event_A,这个涉及syscall系统调用,是很耗费性能的.

边缘触发

image-20210505132728014

边缘触发:

第一次epoll_wait的时候,A、B事件都触发了,那么我们拿到A、B事件。边缘触发是不管你有没有真的去处理,哪怕你一个字节也没去读,他也会认为你是处理完了的,那么下次就不会触发这个同一个A、B事件了。

优点:性能好

缺点:可能没有完整的处理这个事件

他们的区别就类似于TCP和UDP,TCP发了这个数据后,还有重传机制,UDP是发了就不管了

image-20210505133132017

epoll代码实例

下面是刘丹冰大佬所给的一个例子

(1)服务端

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <sys/epoll.h>

#define SERVER_PORT (7778)
#define EPOLL_MAX_NUM (2048)
#define BUFFER_MAX_LEN (4096)

char buffer[BUFFER_MAX_LEN];

void str_toupper(char *str)	//整个字符串变成大写
{
    int i;
    for (i = 0; i < strlen(str); i ++) {
        str[i] = toupper(str[i]);
    }
}

int main(int argc, char **argv)
{
    int listen_fd = 0;
    int client_fd = 0;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_len;

    int epfd = 0;
    struct epoll_event event, *my_events;

    // socket
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);	//创建一个socket

    // bind
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);
    bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));	//将listen_fd与端口绑定

    // listen
    listen(listen_fd, 10);	//将这个socket描述符设置为一个监听描述符

    // epoll create
    epfd = epoll_create(EPOLL_MAX_NUM);
    if (epfd < 0) {
        perror("epoll create");
        goto END;
    }

    // listen_fd -> epoll
    event.events = EPOLLIN;		//读事件
    event.data.fd = listen_fd;	//event的文件绑定到listen_fd
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event) < 0) {	//listen_fd的读事件加入到epoll
        perror("epoll ctl add listen_fd ");
        goto END;
    }

    my_events = malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM);	//创建一个event[]数组


    while (1) {
        // epoll wait
        int active_fds_cnt = epoll_wait(epfd, my_events, EPOLL_MAX_NUM, -1);	
        int i = 0;
        for (i = 0; i < active_fds_cnt; i++) {
            // if fd == listen_fd
            if (my_events[i].data.fd == listen_fd) {
                //accept
                client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                if (client_fd < 0) {
                    perror("accept");
                    continue;
                }

                char ip[20];
                printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port));

                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);
            }
            else if (my_events[i].events & EPOLLIN) {
                printf("EPOLLIN\n");
                client_fd = my_events[i].data.fd;

                // do read

                buffer[0] = '\0';
                int n = read(client_fd, buffer, 5);
                if (n < 0) {
                    perror("read");
                    continue;
                }
                else if (n == 0) {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event);
                    close(client_fd);
                }
                else {
                    printf("[read]: %s\n", buffer);
                    buffer[n] = '\0';
#if 1
                    str_toupper(buffer);
                    write(client_fd, buffer, strlen(buffer));
                    printf("[write]: %s\n", buffer);
                    memset(buffer, 0, BUFFER_MAX_LEN);
#endif

                    /*
                       event.events = EPOLLOUT;
                       event.data.fd = client_fd;
                       epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
                       */
                }
            }
            else if (my_events[i].events & EPOLLOUT) {
                printf("EPOLLOUT\n");
                /*
                   client_fd = my_events[i].data.fd;
                   str_toupper(buffer);
                   write(client_fd, buffer, strlen(buffer));
                   printf("[write]: %s\n", buffer);
                   memset(buffer, 0, BUFFER_MAX_LEN);

                   event.events = EPOLLIN;
                   event.data.fd = client_fd;
                   epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
                   */
            }
        }
    }

END:
    close(epfd);
    close(listen_fd);
    return 0;
}

(2)客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>

#define MAX_LINE (1024)
#define SERVER_PORT (7780)

void setnoblocking(int fd)
{
    int opts = 0;
    opts = fcntl(fd, F_GETFL);
    opts = opts | O_NONBLOCK;
    fcntl(fd, F_SETFL);
}

int main(int argc, char **argv)
{
    int sockfd;
    char recvline[MAX_LINE + 1] = {0};

    struct sockaddr_in server_addr;

    if (argc != 2) {
        fprintf(stderr, "usage ./client <SERVER_IP>\n");
        exit(0);
    }


    // 创建socket
    if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "socket error");
        exit(0);
    }


    // server addr 赋值
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);

    if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
        fprintf(stderr, "inet_pton error for %s", argv[1]);
        exit(0);
    }


    // 链接服务端
    if (connect(sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        fprintf(stderr, "connect error\n");
        exit(0);
    }

    setnoblocking(sockfd);

    char input[100];
    int n = 0;
    int count = 0;



    // 不断的从标准输入字符串
    while (fgets(input, 100, stdin) != NULL)
    {
        printf("[send] %s\n", input);
        n = 0;
        // 把输入的字符串发送 到 服务器中去
        n = send(sockfd, input, strlen(input), 0);
        if (n < 0) {
            perror("send");
        }

        n = 0;
        count = 0;


        // 读取 服务器返回的数据
        while (1)
        {
            n = read(sockfd, recvline + count, MAX_LINE);
            if (n == MAX_LINE)
            {
                count += n;
                continue;
            }
            else if (n < 0){
                perror("recv");
                break;
            }
            else {
                count += n;
                recvline[count] = '\0';
                printf("[recv] %s\n", recvline);
                break;
            }
        }
    }

    return 0;
}

结果:

image-20210508214011225