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

目录

一,往期文章

二,基本概念

IO多路复用

select 模型

poll 模型

epoll 模型

select,poll,epoll 三者对比

三,函数清单

1.select 方法

2.fd_set 结构体

3.poll 方法

4.struct pollfd 结构体

5.epoll_create 方法

6.epoll_ctl 方法

7.epoll_wait 方法

8.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多路复用机制,它通过内核的事件通知和就绪列表,能同时监视和处理大量的文件描述符。它具有高性能、可扩展性和边缘触发模式等特点,是开发高并发网络应用的重要工具

select,poll,epoll 三者对比

共同点

  • selectpoll, 和 epoll 都是在事件驱动的网络编程中用于多路复用 I/O 的机制。它们的主要目标是在一个线程内同时处理多个文件描述符的 I/O 操作,而不是为每个文件描述符创建一个单独的线程或进程。

优缺点分析

  • select
    • 优点
      • 广泛支持,可以在大部分的操作系统上使用。
      • 可以处理多种类型的文件描述符(包括普通文件、套接字文件等)。
    • 缺点
      • 效率较低,因为在调用 select 时需要遍历整个文件描述符集合,无论是否有真正的 I/O 事件发生。
      • 最大文件描述符数量有限,默认情况下通常为 1024 个。
      • select 函数在每次调用时都需要修改传入的文件描述符集合,导致效率低下。
  • poll
    • 优点
      • 不受最大文件描述符数量限制,可以动态地管理文件描述符的数量。
      • 效率较高,因为无需遍历整个文件描述符集合,只处理有事件发生的文件描述符。
    • 缺点
      • 与 select 类似,需要遍历整个文件描述符集合来查找是否有事件发生。
      • 对于大量的文件描述符,性能可能下降。
  • epoll
    • 优点
      • 高效的事件通知机制,能够快速响应大量的并发连接。
      • 没有最大文件描述符数量限制。
      • 使用回调机制,只处理活跃的文件描述符,避免无效的遍历。
      • 支持水平触发和边缘触发两种工作模式。
    • 缺点
      • 只在 Linux 系统上可用,不具有跨平台性。
      • API 相对复杂,有一定的学习成本。

总结

  • epoll 在性能和扩展性方面优于 select 和 poll。它在支持大规模并发连接和高效的事件通知方面表现出色。但如果只需要处理数百个并发连接,并且需要在多个平台上运行,select 或 poll 也可以是合理的选择。

三,函数清单

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.fd_set 结构体

typedef struct {
    unsigned long fds_bits[FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;

功能

  • fd_set 是一个用于存储文件描述符集合的数据结构。它通常是一个位图,每个位表示一个文件描述符的状态。在使用 select 函数时,通常需要使用 fd_set 结构体来设置、检查和处理文件描述符集合。

成员

  • fds_bits:用于存储文件描述符信息的数组。FD_SETSIZE 宏定义了数组的大小(默认为1024),表明文件描述符集合的最大容量。

fd_set 结构体用于存储文件描述符的集合,并通过相应的宏来进行相关操作,如添加、删除和检查文件描述符。

  • FD_ZERO(fd_set *set):清空 fd_set 结构体,将其初始化为空集。
  • FD_SET(int fd, fd_set *set):将指定的文件描述符 fd 添加到 fd_set 结构体中。
  • FD_CLR(int fd, fd_set *set):从 fd_set 结构体中移除指定的文件描述符 fd
  • FD_ISSET(int fd, fd_set *set):检查指定的文件描述符 fd 是否在 fd_set 结构体中。如果存在于集合中,则返回非零值,否则返回零。
  • FD_COPY(fd_set *src_set, fd_set *dest_set):将 src_set 结构体中的文件描述符复制到 dest_set 结构体中。
  • FD_SETSIZE:表示在一个 fd_set 结构体中可容纳的最大文件描述符数量。默认情况下,在大多数系统上都设置为 1024。可以使用这个宏来确定在使用 select 函数时 fd_set 结构体的大小。

注意事项

  • 需要注意的是,fd_set 结构体在使用 select 函数时会被修改,因此在每次调用 select 之前需要重新设置需要监听的文件描述符。

3.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。

4.struct pollfd 结构体

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

功能

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

成员

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

5.epoll_create 方法

#include <sys/epoll.h>

int epoll_create(int size);

功能

  • 创建一个epoll实例,并返回一个文件描述符用于操作该实例。(会在内核中分配并初始化一个用于存储事件的链表数据结构)

参数

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

返回值

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

6.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来指示错误。

7.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来指示错误。

8.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;
    }

    //设置服务端套接字为非阻塞模式
    // int flags = fcntl(sfd,F_GETFL,0);
    // fcntl(sfd,F_SETFL,flags | O_NONBLOCK);
    
    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);


    int epfd = epoll_create(1);     //在内核创建epoll对象,在内核中分配并初始化一个用于存储事件的链表数据结构
    printf("epoll fd: %d\n",epfd);
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = sfd;

    epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&event);   //往epoll实例中添加服务端文件描述符和对应的事件

    struct epoll_event events[EPOLL_LENGTH] = {0};

    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(struct sockaddr_in);
    while (1)   //mainloop 7*24小时工作
    {
        int nready = epoll_wait(epfd,events,EPOLL_LENGTH,-1);   //无限等待就绪事件的发生
        if(nready < 0)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);
                event.events = EPOLLIN;
                event.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);   //往epoll实例中添加客户端文件描述符和对应的事件
            }
            else if(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,&event);   //在epoll实例中移除断开的客户端文件描述符和对应的事件
                    close(connfd);
                }
            }
        }
    }
    
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/38395.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

plt绘图绘制主次刻度线

这里主要是介绍的坐标轴上的主次刻度的划分&#xff0c;这里需要单独引入ticker中的两个模块进行设置 MultipleLocator, FormatStrFormatter set_major_locator() 设置主刻度set_minor_locator() 设置次刻度set_major_formatter() 设置主刻度格式plt.NullLocator() 删除刻度显…

WPS/Office Excel 方向键无法切换表格

问题&#xff1a;WPS/Office Excel 方向键无法切换表格。 分析&#xff1a;键盘开启了Scroll Lock&#xff0c;导致Excel开启了滚动锁定。滚动锁定如图: 解决&#xff1a;再次按下Scroll Lock键解锁即可。&#xff08;Scroll Lock键在键盘右侧上方。&#xff09;

Java设计模式之行为型-解释器模式(UML类图+案例分析)

目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 五、总结 一、基础概念 解释器模式是指给定一个语言&#xff08;表达式&#xff09;&#xff0c;来表示它的文法&#xff0c;并定义一个解释器&#xff0c;使用该解释器来解释语言中的句子&#xff08;表达式&a…

小奇猫物语之产品经理篇(2)

小奇猫物语之产品经理篇&#xff08;2&#xff09; 喵喵提示&#xff1a;小奇的产品经理篇&#xff08;2&#xff09;来咯&#xff0c;预告一下&#xff0c;前面几篇主要是讲产品经理的思维模式以及怎样去从一个学生思维转变成一个能带领一个项目的产品经理思维&#xff0c;所…

CSDN发表文章的常用语法说明

CSDN常用语法说明 一、标题二、文本样式三、列表四、图片五、链接六、目录一级目录二级目录三级目录 七、表格八、注释九、自定义列表十、LaTeX 数学公式十一、插入甘特图十二、插入UML图十三、插入Mermaid流程图十五、插入Flowchart流程图十六、 插入类图十七、快捷键十八、脚…

macOS Sonoma 14beta 3 (23A5286i)第二个更新「附黑/白苹果镜像下载」

系统镜像下载&#xff1a; 系统介绍 黑果魏叔 7 月12 日消息&#xff0c;苹果今天发布 macOS Sonoma 14.0 Beta 3&#xff08;内部版本号&#xff1a;23A5286i&#xff09;第二个更新。 目前尚不清楚苹果为什么要发布 macOS Sonoma Beta 3 的第二个版本&#xff0c;但它可能…

WooCommerce适合企业电子商务吗?

目录 成功开展电子商务业务变得比以往任何时候都容易。市场上有几个现成的平台&#xff0c;完全有可能将一个初步的想法快速转变为在线贸易业务&#xff0c;并源源不断地收到订单。 什么是 WooCommerce&#xff1f; 为什么您应该考虑使用 WooCommerce 很灵活 重量轻且功…

家政服务小程序软件解决方案

家政服务小程序软件是近年来随着人们对家政服务需求的增长而逐渐兴起的一种数字化服务解决方案。通过小程序软件&#xff0c;用户可以轻松预约家政服务&#xff0c;包括保姆、月嫂、钟点工等&#xff0c;而且价格透明、服务规范&#xff0c;大大提高了用户对家政服务的满意度。…

C++ cin

cin 内容来自《C Primer》 cin使用>>运算符从输入流中抽取字符 int carrots;cin >> carrots;如下的例子&#xff0c;用户输入的字符串有空格 #include <iostream>int main() {using namespace std;const int ArSize 20;char name[ArSize]; //用户名char …

nodejs 下载地址 阿里云开源镜像站

nodejs 下载地址 阿里云开源镜像站 https://mirrors.aliyun.com/nodejs-release/ 我们下期见&#xff0c;拜拜&#xff01;

Vue3的watchEffect的妙用,与watch的区别

前言 在Vue3中&#xff0c;引入了Composition API&#xff0c;其中的watchEffect()函数是一个非常强大和灵活的工具&#xff0c;用于处理响应式数据的变化&#xff0c;使得项目更加弹性和灵活。它与watch有所不同&#xff0c;本文将介绍watchEffect()的定义、特点、与watch的区…

24 MFC文档串行化和单文档应用程序

文章目录 文档串行化全部代码 单文档应用程序搭建原理搭建框架Win32 过度到MFC 三部曲设置ID资源全部代码 单文档应用程序设置标题绘图 简单的管理系统部分代码 文档串行化 ui 设计 保存 void CfileDemoDlg::OnBnClickedBtnSave() {UpdateData();//CFile file(L"Demo.dat…

linux驱动开发:驱动开发框架,linux内核字符设备驱动开发过程

一、驱动框架 1.Linux内核模块和字符驱动的关系 模块是Linux进行组建管理的一种方式, 结构体:对设备的管理内核需要抽象出来一个结构体来描述设备所有的共性信息写驱动需要申请一个结构体并赋值(初始化),然后注册给内核让内核统一管理 驱动:由内核统一管理,所以驱动…

Flask_自定义flask的cmd命令

创建自定义命令 from flask import Flaskapp Flask(__name__)app.cli.command() def hello():"""命令说明写这里"""print("hello python")if __name__ __main__:app.run() 执行flask --help 可以在命令查看定义的命令 注意事项&a…

CDN技术(Content Delivery Network,内容分发网络)分布式网络架构(CND与P2P(Peer-to-Peer)区别)

文章目录 CDN是什么&#xff1f;CDN的优势CDN的应用1. 静态内容加速2. 动态内容加速3. 视频流媒体4. 软件分发5. 游戏加速6. 移动应用加速 CDN收费吗&#xff1f;CND与P2P区别什么是静态内容和动态内容&#xff1f; CDN是什么&#xff1f; CDN&#xff08;Content Delivery Ne…

设计模式大白话——工厂模式

文章目录 设计模式大白话——工厂模式1.1、简单工厂:1.2、工厂方法1.3、抽象工厂 设计模式大白话——工厂模式 1.1、简单工厂: 场景与思路 ​ 现在需要开一个 Pizza 店&#xff0c;Pizza 店可以生产各种口味的 Pizza ​ 既然要生产各种各样的 Pizza&#xff0c;那就会很容易想…

在新建环境下配置低版本opencv

我这边是要解决 python报错&#xff1a;AttributeError: ‘module’ object has no attribute xfeatures2d’的问题&#xff0c; xfeatures2d在新版本已经被取消&#xff0c;但是需要使用老版本的一个函数 确定opencv与python的版本对应关系 一般来说可以对照这个表 具体来说…

【Spring】Spring更简单的读取和存储对象---使用注解

目录 1.Spring的存储对象------存储Bean对象 1.前置工作&#xff0c;配置扫描路径 2.添加注解存储Bean对象 1.Controller&#xff08;控制器存储&#xff09; 2.service&#xff08;服务存储&#xff09; 3.Repository&#xff08;仓库存储&#xff09; 4.Component&…

Java面试突击

Java面向对象有哪些特征&#xff0c;如何应用 ​ 面向对象编程是利用类和对象编程的一种思想。万物可归类&#xff0c;类是对于世界事物的高度抽象 &#xff0c;不同的事物之间有不同的关系 &#xff0c;一个类自身与外界的封装关系&#xff0c;一个父类和子类的继承关系&…

手动下载composer项目放在vendor目录下并加载

比如添加easywechat。 说是手动&#xff0c;其实半手动。 到GitHub或gitee下载 1、下载后放在项目根目录下的vendor文件夹 2、在项目根目录的文件composer.json文件添加一段 "autoload": {"psr-4": {"EasyWeChat\\": "vendor/overtrue/wech…