【高并发网络通信架构】引入IO多路复用(select,poll,epoll)实现高并发tcp服务端

目录

一,往期文章

二,基本概念

IO多路复用

select 模型

poll 模型

epoll 模型

三,函数清单

1.select 方法

2.poll 方法

3.struct pollfd 结构体

4.epoll_create 方法

5.epoll_ctl 方法

6.epoll_wait 方法

7.struct epoll_event 结构体

四,代码实现

select 操作流程

select 完整代码

poll 操作流程

poll 完整代码

epoll 操作流程

epoll 完整代码


一,往期文章

【高并发网络通信架构】1.Linux下实现单客户连接的tcp服务端

【高并发网络通信架构】2.引入多线程实现多客户端连接的tcp服务端

二,基本概念

IO多路复用

介绍

  • I/O多路复用(IO Multiplexing)是一种并发编程技术,用于同时监视多个I/O事件并选择就绪的事件进行处理。它可以通过一个线程或进程同时处理多个I/O操作,而不需要为每个I/O操作创建一个独立的线程或进程。I/O多路复用可以提高系统的并发性能,减少资源的消耗。
  • 在传统的编程模型中,每个I/O操作通常都需要一个独立的线程或进程来处理。这种方式在面对大量的并发连接时,会导致系统资源的浪费和性能下降。而I/O多路复用通过使用一种事件驱动的方式,可以同时监视多个I/O事件,只有当有事件就绪时才进行相应的处理,从而提高了系统的并发性能。
  • 常见的I/O多路复用函数包括"select"、"poll"、"epoll"等。这些函数可以同时监视多个文件描述符的状态,并确定哪些文件描述符已经准备好可进行相应的读取、写入或异常处理。通过使用这些函数,可以避免为每个I/O操作创建一个独立的线程或进程,从而减少了系统资源的消耗。
  • I/O多路复用的工作原理是通过一个事件循环来监视多个I/O事件。当有一个或多个事件就绪时,事件循环会通知程序进行相应的处理。这种方式可以大大提高系统的并发性能,减少了线程或进程的切换开销。

总结

  • I/O多路复用是一种并发编程技术,用于同时监视多个I/O事件并选择就绪的事件进行处理。它可以提高系统的并发性能,减少资源的消耗。通过使用I/O多路复用函数,可以避免为每个I/O操作创建一个独立的线程或进程,从而提高系统的效率。

select 模型

前言

  • io多路复用select模型是一种用于实现高效的事件驱动I/O的技术。它的基本思想是在一个线程中同时监视多个文件描述符(包括套接字、管道等),并在有事件到达时进行处理。
  • 在传统的阻塞式I/O中,一个线程只能处理一个请求,并且需要等待I/O操作完成后才能处理下一个请求。而在使用select模型时,它可以同时监视多个文件描述符的状态,当有一个或多个文件描述符准备好进行读写时,就可以通过select函数得知,并进行相应的处理操作。
  • 通过使用select模型,可以在一个线程中同时处理多个文件描述符的I/O操作,从而提高程序的并发性和响应性能。它适用于需要同时处理多个客户端请求的情况,比如服务器程序中的多连接处理、多用户聊天程序等。在实际应用中,select模型已被广泛使用,并且也衍生出其他更高效的模型,如poll、epoll、kqueue等。

定义

  • "select函数"是一种用于在一组文件描述符(文件、套接字、管道等)中选择就绪的文件描述符的函数。它在不同的操作系统中有不同的实现。

功能

  • "select函数"的主要功能是监视文件描述符的状态并确定哪些文件描述符已经准备好可进行相应的读取、写入或异常处理。

工作原理

  • 当调用"select函数"时,在指定的文件描述符集合上进行监视。当有一个或多个文件描述符准备好进行读取、写入或出现异常时,"select函数"将返回,告诉程序哪些文件描述符已经就绪。程序可以利用这些就绪的文件描述符进行相应的I/O操作。

优点

  • 多路复用:"select函数"可以同时监视多个文件描述符的状态,这些文件描述符可以是与网络连接相关的套接字、标准输入/输出等。通过使用"select函数",可以避免单独为每个文件描述符创建线程或进程来处理事件,从而实现更高效的并发编程。
  • 非阻塞:使用"select函数"可以将文件描述符设置为非阻塞模式,因此在等待事件就绪的同时,可以执行其他任务,充分利用CPU资源。
  • 兼容性:"select函数"是一种跨平台的解决方案,在大多数操作系统上都得到支持。

缺点

  • 所有文件描述符集合的线性遍历:在调用"select函数"时,需要将待监视的文件描述符集合传递给函数。然而,在文件描述符数量较大时,需要线性遍历集合以找到就绪的文件描述符,这可能导致性能下降。
  • 限制文件描述符数量:不同操作系统对最大支持的文件描述符数量有限制,如果需要监视的文件描述符数量超出此限制,"select函数"可能无法满足需求。此外,当文件描述符数量增加时,调用"select函数"的开销也会增加。

总结

  • "select函数"是一种常用的函数,用于实现多路复用I/O操作,监视多个文件描述符的状态,并确定哪些文件描述符已准备好进行相应的I/O操作。
  • 它在网络编程中特别有用,可以实现高效的事件驱动编程。

poll 模型

前言

  • "poll函数"提供了一种替代"select函数"的方法,用于监视一组文件描述符的状态。

定义

  • "poll函数"是一个系统调用,用于监视一组文件描述符的状态并确定哪些文件描述符已经准备好可进行相应的读取、写入或异常处理。

功能

  • "poll函数"的主要功能是监视文件描述符的状态,类似于"select函数"。它可以同时监视多个文件描述符,以确定它们是否处于可读、可写或异常状态。

工作原理

  • 当调用"poll函数"时,它会监视指定的文件描述符并等待事件的发生。如果有一个或多个文件描述符准备好进行读取、写入或出现异常,"poll函数"将返回,并标记就绪的文件描述符的状态。程序可以根据文件描述符的状态进行相应的I/O操作。

优点

  • "poll函数"解决了"select函数"中线性遍历文件描述符集合的性能问题,它使用轮询算法,避免了遍历集合的开销。
  • "poll函数"支持监视大量文件描述符,没有文件描述符数量的限制。
  • "poll函数"比"select函数"更加可读,并且在处理异常情况时更加灵活。

缺点

  • "poll函数"的可移植性可能略低于"select函数",因为不同的操作系统和编程语言对"poll函数"的实现可能有所不同。
  • 当处理大量文件描述符时,"poll函数"的性能可能会受到影响。

总结

  • "poll函数"是一种常用的函数,用于实现多路复用I/O操作,它可以同时监视多个文件描述符的状态并确定哪些文件描述符已准备好进行相应的I/O操作。
  • 相对于"select函数","poll函数"提供了更好的性能和可读性。但仍需要考虑平台兼容性和大量文件描述符的性能问题。

epoll 模型

前言

  • epoll(事件轮询)是Linux操作系统提供的一种高性能I/O多路复用机制,在处理大规模的并发连接时具有优越的效率和可扩展性。它通过使用内核的事件通知机制来同时监视和处理多个文件描述符的事件,大大减少了轮询的开销,提高了系统的并发性能。
  • 与传统的select和poll函数相比,epoll在处理大规模并发连接时具有更高的效率。

基本流程

  1. 调用epoll_create函数创建一个epoll实例,并获得一个文件描述符(epfd)。
  2. 使用epoll_ctl函数向epoll实例中加入需要监视的文件描述符和对应的事件类型,包括读、写、错误等。
  3. 使用epoll_wait函数阻塞等待事件的发生,当有文件描述符就绪时,epoll_wait将返回就绪的文件描述符集合。
  4. 遍历就绪的文件描述符集合,进行相应的I/O操作。

工作原理

  1. 调用epoll_create函数创建一个epoll实例,获得一个epoll文件描述符(epfd)。
  2. 使用epoll_ctl函数向epoll实例注册需要监视的文件描述符和相应的事件类型,并将其加入到内核维护的事件表中。
  3. 调用epoll_wait函数阻塞等待事件的发生,此时线程会进入休眠状态,不再需要轮询文件描述符。
  4. 当被监视的文件描述符上的事件发生时,内核会将事件加入到就绪列表中,同时唤醒处于阻塞状态的线程。
  5. epoll_wait函数返回时,通过遍历就绪列表,获取到就绪的文件描述符集合。
  6. 应用程序根据文件描述符的就绪事件类型进行相应的I/O操作,如读取、写入数据等。

相关函数

  • epoll_create(int size):创建一个epoll实例。
  • epoll_ctl(int epfd, int op, int fd, struct epoll_event* event):向epoll实例注册/修改/删除一个文件描述符及其对应的事件。
  • epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout):等待事件的发生,并获取就绪的文件描述符。

事件类型(events参数)

  • EPOLLIN:文件描述符上有数据可读。
  • EPOLLOUT:文件描述符上可以写入数据。
  • EPOLLERR:文件描述符发生错误。
  • EPOLLHUP:文件描述符挂起。
  • EPOLLET:边缘触发模式。
  • EPOLLONESHOT:一次性事件,事件发生后会被自动从epoll实例中删除。

优点

  1. 高性能:epoll使用红黑树(内核使用的数据结构),就绪列表和事件通知机制,避免了遍历整个文件描述符集合的开销,在大规模并发连接时性能比传统的select和poll函数更好。
  2. 可扩展性:epoll的设计支持超大规模的并发连接,能够更好地适应高并发的网络环境。
  3. 边缘触发模式:epoll提供了边缘触发(ET)模式,只有在文件描述符状态发生变化时才触发事件通知,相比于水平触发(LT)模式可以减少事件通知次数,提高效率。
  4. 持久化的事件注册:注册事件时,可以选择EPOLLONESHOT标志,使得内核在将事件通知给应用程序后,自动将该事件从epoll实例中删除,避免重复通知。
  5. 支持多种数据结构:epoll支持红黑树和链表两种数据结构,用于快速查找文件描述符和就绪列表的操作,提高了处理效率。
  6. 大规模并发:epoll可以高效地管理大量的文件描述符,支持高并发的网络编程。

总结

  • epoll是Linux操作系统提供的一种高性能I/O多路复用机制,它通过内核的事件通知和就绪列表,能同时监视和处理大量的文件描述符。它具有高性能、可扩展性和边缘触发模式等特点,是开发高并发网络应用的重要工具

三,函数清单

1.select 方法

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

功能

  • 监视文件描述符的状态并确定哪些文件描述符已经准备好可进行相应的读取、写入或异常处理。

参数

  • nfds:监视文件描述符集合的最大值加1。
  • readfds:要监视可读事件的文件描述符集合。
  • writefds:要监视可写事件的文件描述符集合。
  • exceptfds:要监视异常事件的文件描述符集合。
  • timeout:超时时间,可以设置为NULL(无限等待),或者指定一个时间段。

返回值

  • 修改给定的文件描述符集合,并返回就绪的文件描述符的数量,若超时则返回0,若出错则返回-1。

2.poll 方法

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能

  • 监视文件描述符的状态并确定哪些文件描述符已经准备好可进行相应的读取、写入或异常处理。

参数

  • fds:指向一个结构体数组的指针,每个结构体中包含待监视的文件描述符和监视事件类型。
  • nfds:结构体数组的大小,即待监视的文件描述符的最大值加1。
  • timeout:指定一个超时值,以毫秒为单位,控制 poll 的阻塞行为,可以设置为以下几种情况之一:
    • timeout 大于0,表示阻塞 timeout 毫秒后超时返回。
    • timeout 等于0,表示立即返回,不进行阻塞。
    • timeout 小于0,表示无限阻塞,直到有事件发生。

返回值

  • 修改给定的结构体数组,并返回就绪的文件描述符数量,若超时则返回0,若出错则返回-1。

3.struct pollfd 结构体

struct pollfd {
    int fd;         // 关注的文件描述符
    short events;   // 等待的事件类型
    short revents;  // 实际发生的事件类型
};

功能

  • poll函数通过 struct pollfd 结构体来描述文件描述符和关注的事件类型。

成员

  • fd:表示文件描述符。
  • events:表示关注的事件类型,可以是以下几种事件之一或它们的结合:
    • POLLIN:可读事件。
    • POLLOUT:可写事件。
    • POLLERR:错误事件。
    • POLLHUP:挂起事件。
  • revents:在 poll 返回时,revents 字段表示实际发生的事件类型,由操作系统填充。

4.epoll_create 方法

#include <sys/epoll.h>

int epoll_create(int size);

功能

  • 创建一个epoll实例,并返回一个文件描述符用于操作该实例。

参数

  • size:表示实例能够监视的文件描述符的最大数量(在Linux 2.6.8之后被忽略)。

返回值

  • 如果成功,这些系统调用返回一个文件描述符(一个非负整数)。如果出现错误,则返回-1,并设置errno来指示错误。

5.epoll_ctl 方法

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能

  • 向epoll实例添加/修改/删除一个文件描述符及其对应的事件。

参数

  • epfd:为epoll实例的文件描述符。
  • op:指定操作类型,可以是EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(删除)。
  • fd:待操作的文件描述符。
  • event:指向一个epoll_event结构体,描述了待添加/修改/删除的事件类型。

返回值

  • 当成功时,epoll_ctl()返回零。当发生错误时,epoll_ctl()返回-1,并设置errno来指示错误。

6.epoll_wait 方法

#include <sys/epoll.h>

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

功能

  • 等待事件的发生并获取就绪的文件描述符。

参数

  • epfd:epoll实例的文件描述符。
  • events:存储就绪事件的数组。
  • maxevents:表示events数组的最大大小。
  • timeout:指定超时时间(以毫秒为单位),可以设置为-1(无限等待)或0(立即返回)。

返回值

  • 若成功则返回就绪的文件描述符的数量,若超时则返回0,若出错则返回-1,并设置errno来指示错误。

7.struct epoll_event 结构体

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;    /* Epoll events */
    epoll_data_t data;      /* User data variable */
};

功能

  • 描述一个文件描述符及其对应的事件类型。

成员

  • events:表示关注的事件类型,包括EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLERR(错误)、EPOLLHUP(挂起)等。
  • data:可以是一个32或64位的整数,或者一个指向void的指针,用于存储与文件描述符关联的用户数据。

四,代码实现

select 操作流程

  1. 创建并初始化fd_set集合:fd_set是一个用于存储文件描述符的数据结构。通过定义一个fd_set变量,并使用FD_ZERO和FD_SET宏来初始化。
  2. 设置超时时间:通过设置timeval结构体来指定select函数的超时时间。如果不需要设置超时时间,可以将该参数设置为NULL。
  3. 调用select函数:使用select函数来等待文件描述符的就绪事件。
  4. 检查select函数的返回值:根据select函数的返回值,确定是否有文件描述符就绪。
  5. 遍历就绪文件描述符集合:通过遍历fd_set集合来判断哪些文件描述符已经就绪。使用FD_ISSET宏来判断文件描述符是否在集合中。

注意事项

  • 在使用select函数之前,需要手动设置文件描述符的阻塞或非阻塞状态。
  • 每次调用select函数之后,需要重新初始化fd_set集合,并将需要监听的文件描述符重新设置进去。
  • select函数每次只能监听一部分文件描述符,因此在大规模并发连接时,性能可能会受到限制。
  • select函数支持的文件描述符数量有限,通常取决于操作系统的限制。

总结

  • 通过创建并初始化fd_set集合(准备需要监听的文件描述符集合),设置超时时间,并使用select函数进行监听文件描述符的就绪事件。然后根据select函数的返回值和遍历就绪文件描述符集合来执行相应的I/O操作。

select 完整代码

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

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/select.h>


#define BUFFER_LENGTH   1024

int init_server(int port){
    //获取服务端fd,通常为3,前面0,1,2用于表示标准输入,输出,错误值
    int sfd = socket(AF_INET,SOCK_STREAM,0);

    if(-1 == sfd){
        printf("socket error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    struct sockaddr_in servAddr;
    memset(&servAddr,0,sizeof(struct sockaddr_in));
    servAddr.sin_family = AF_INET;  //ipv4
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   //0.0.0.0
    servAddr.sin_port = htons(port);
    
    //绑定IP和端口号
    if(-1 == bind(sfd,(struct sockaddr*)&servAddr,sizeof(struct sockaddr_in)))
    {
        printf("bind error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    //监听该套接字上的连接
    if(-1 == listen(sfd,10))
    {
        printf("listen error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    return sfd;
}

int main(int argc,char *argv[]){

    if(argc < 2)return -1;

    int port = atoi(argv[1]);       //字符串转整型
    int sfd = init_server(port);
    printf("server fd: %d\n",sfd);
    if(sfd == -1)return -1;

    fd_set rfds,rset;    //fd位数组
    FD_ZERO(&rfds);
    FD_SET(sfd,&rfds);

    int maxfd = sfd;
    int cfd = 0;
    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(struct sockaddr_in);
    
    while (1)   //表示每周都去东莞      主线程
    {
        rset = rfds;
        int nready = select(maxfd+1,&rset,NULL,NULL,NULL);    //系统调用

        if(FD_ISSET(sfd,&rset)){
            cfd = accept(sfd,(struct sockaddr*)&clientAddr,&len);   //阻塞等待客户端连接
            printf("client fd: %d\n",cfd);
            FD_SET(cfd,&rfds);
            if(cfd > maxfd)maxfd = cfd;     //将需要监听的文件描述符重新设置进去

            if(--nready == 0)continue;      //超时时间到,没有就绪的文件描述符
            
        }

        int i = 0;
        //服务端sfd=3
        for(i = sfd+1;i<=maxfd;i++){
            if(FD_ISSET(i,&rset)){
                //有bug,下一个客户端发送数据后,上一个客户端发送数据不能接收
                char data[BUFFER_LENGTH] = {0};
                int recvLen = recv(cfd,data,BUFFER_LENGTH,0);
                if(recvLen == 0){
                    printf("cfd: %d disconnect\n",cfd);
                    close(cfd);
                    break;  //客户端断开连接
                }
                printf("recv cfd: %d data: %s\n",cfd,data);

                send(cfd,data,recvLen,0);
            }
        }
    }
    
    return 0;
}

poll 操作流程

  1. 准备需要监听的文件描述符集合。
  2. 设置超时时间(可选)。
  3. 调用 poll 函数等待就绪事件。
  4. 检查 poll 函数的返回值。

注意事项

  • 在使用 poll 函数之前,需要设置文件描述符的阻塞或非阻塞状态。
  • 每次调用 poll 函数之前,需要重新设置需要监听的文件描述符集合。
  • poll 函数一次可以监听多个文件描述符,相比于 select 函数能更好地适应高并发环境。
  • poll 函数将就绪的文件描述符信息填充到 revents 字段中,可以通过按位与操作来判断文件描述符的就绪事件类型。

总结

  • 通过准备需要监听的文件描述符集合,调用 poll 函数等待就绪事件,然后根据返回值和遍历就绪文件描述符集合来执行相应的 I/O 操作。

poll 完整代码

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

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <poll.h>


#define BUFFER_LENGTH   1024
#define POLL_LENGTH   1024

int init_server(int port){
    //获取服务端fd,通常为3,前面0,1,2用于指定输入,输出,错误值
    int sfd = socket(AF_INET,SOCK_STREAM,0);

    if(-1 == sfd){
        printf("socket error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    struct sockaddr_in servAddr;
    memset(&servAddr,0,sizeof(struct sockaddr_in));
    servAddr.sin_family = AF_INET;  //ipv4
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   //0.0.0.0
    servAddr.sin_port = htons(port);
    
    //绑定IP和端口号
    if(-1 == bind(sfd,(struct sockaddr*)&servAddr,sizeof(struct sockaddr_in)))
    {
        printf("bind error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    //监听该套接字上的连接
    if(-1 == listen(sfd,10))
    {
        printf("listen error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    return sfd;
}

int main(int argc,char *argv[]){

    if(argc < 2)return -1;

    int port = atoi(argv[1]);
    int sfd = init_server(port);
    printf("server fd: %d\n",sfd);
    if(sfd == -1)return -1;

    struct pollfd fds[POLL_LENGTH] = {0};   // 定义一个 pollfd 数组,大小根据需要监听的文件描述符个数来设置
    fds[sfd].fd = sfd;  // 设置第一个需要监听的文件描述符
    fds[sfd].events = POLLIN;   // 设置需要监听的事件类型
    fds[sfd].revents = 0; // 清空返回的就绪事件

    int maxfd = sfd;
    int cfd;
    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(struct sockaddr_in);
    while (1)
    {
        int nready = poll(fds,maxfd+1,-1);
        if(fds[sfd].revents & POLLIN){
            cfd = accept(sfd,(struct sockaddr*)&clientAddr,&len);   //阻塞等待客户端连接
            printf("client fd: %d\n",cfd);

            fds[cfd].fd = cfd;
            fds[cfd].events = POLLIN;

            if(cfd > maxfd)maxfd = cfd;

            if(--nready == 0)continue;
        }

        int i = 0;
        for(i=0;i<=maxfd;i++){
            if(fds[i].revents & POLLIN){
                char data[BUFFER_LENGTH] = {0};
                int recvLen = recv(cfd,data,BUFFER_LENGTH,0);
                if(recvLen == 0){
                    //有点问题
                    fds[cfd].fd = -1;
                    fds[cfd].events = 0;
                    printf("client fd: %d disconnect\n",cfd);
                    close(cfd);
                    break;  //客户端断开连接
                }

                printf("recv cfd: %d data: %s\n",cfd,data);

                send(cfd,data,recvLen,0);
            }
        }
    }
    
    return 0;
}

epoll 操作流程

  1. 创建 epoll 实例。
  2. 准备需要监听的文件描述符集合并添加到 epoll 实例中。
  3. 设置超时时间(可选)。
  4. 调用 epoll_wait 函数等待就绪事件。
  5. 检查 epoll_wait 函数的返回值。
  6. 移除文件描述符或关闭 epoll 实例(可选)。

注意事项

  • 在使用 epoll 函数之前,需要设置文件描述符的阻塞或非阻塞状态。
  • 每次调用 epoll_wait 函数之前,需要重新设置需要监听的文件描述符集合,或是通过 epoll_ctl 函数动态改变监听的文件描述符集合。
  • epoll 提供了更高效的事件通知机制,可以同时监听大量的文件描述符。
  • epoll_wait 函数将就绪的文件描述符信息填充到 events 数组中,可以通过遍历数组来处理就绪事件。

总结

  • 通过调用 epoll_create 创建 epoll 实例,准备需要监听的文件描述符集合并添加到 epoll 实例中,调用 epoll_wait 函数等待就绪事件,然后根据返回值和遍历就绪事件集合来执行相应的 I/O 操作。最后,根据需要,可以移除文件描述符或关闭 epoll 实例。

epoll 完整代码

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

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/epoll.h>


#define BUFFER_LENGTH   1024
#define EPOLL_LENGTH    1024

int init_server(int port){
    //获取服务端fd,通常为3,前面0,1,2用于指定输入,输出,错误值
    int sfd = socket(AF_INET,SOCK_STREAM,0);

    if(-1 == sfd){
        printf("socket error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    struct sockaddr_in servAddr;
    memset(&servAddr,0,sizeof(struct sockaddr_in));
    servAddr.sin_family = AF_INET;  //ipv4
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   //0.0.0.0
    servAddr.sin_port = htons(port);
    
    //绑定IP和端口号
    if(-1 == bind(sfd,(struct sockaddr*)&servAddr,sizeof(struct sockaddr_in)))
    {
        printf("bind error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    //监听该套接字上的连接
    if(-1 == listen(sfd,10))
    {
        printf("listen error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    return sfd;
}

int main(int argc,char *argv[]){

    if(argc < 2)return -1;

    int port = atoi(argv[1]);
    int sfd = init_server(port);
    printf("server fd: %d\n",sfd);
    if(sfd == -1)return -1;

    int epfd = epoll_create(1);
    printf("epoll fd: %d\n",epfd);
    if (epfd == -1) {
        // 创建 epoll 实例失败
        return -1;
    }

    struct epoll_event ev;  // 定义存储就绪事件的数组
    ev.events = EPOLLIN;    // 设置需要监听的事件类型,例如 EPOLLIN 表示可读事件,EPOLLOUT 表示可写事件。
    ev.data.fd = sfd;   // 设置第一个需要监听的文件描述符

    if (epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&ev) == -1) {
        // 添加第一个文件描述符到 epoll 实例失败
        return -1;
    }

    struct epoll_event events[EPOLL_LENGTH] = {0};
    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(struct sockaddr_in);
    while (1)   //mainloop 7*24小时工作
    {
        // 第一个参数是 epoll 实例的文件描述符。
        // 第二个参数是一个用于存储就绪事件的结构体数组。
        // 第三个参数是 events 数组的大小,表示最多能够处理的就绪事件数量。
        // 第四个参数是超时时间,可以设置为-1表示无限等待。
        int nready = epoll_wait(epfd,events,EPOLL_LENGTH,-1);
        if(nready < 0){
            //epoll_wait 调用出错
            continue;
        }

        int i = 0;
        for (i = 0; i < nready; i++)
        {
            int connfd = events[i].data.fd;

            if(sfd == connfd)
            {  //accept
                int cfd = accept(sfd,(struct sockaddr*)&clientAddr,&len);
                if(cfd < 0)continue;

                printf("client fd: %d\n",cfd);
                ev.events = EPOLLIN;
                ev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
            }
            else if(events[i].events & EPOLLIN)
            {
                //通过按位与操作判断就绪事件的类型,例如 events[i].events & EPOLLIN 判断是否是可读事件。
                char buffer[BUFFER_LENGTH] = {0};
                int recvLen = recv(connfd,buffer,BUFFER_LENGTH,0);
                if(recvLen > 0){
                    printf("recv client fd %d data: %s\n",connfd,buffer);

                    send(connfd,buffer,recvLen,0);
                }else if(recvLen == 0)
                {
                    printf("client fd %d close\n",connfd);

                    epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,&ev);   // 移除文件描述符
                    close(connfd);
                }
            }
        }
    }
    
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_43729127/article/details/131593011