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

这篇具有很好参考价值的文章主要介绍了【高并发网络通信架构】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事件。当有一个或多个事件就绪时,事件循环会通知程序进行相应的处理。这种方式可以大大提高系统的并发性能,减少了线程或进程的切换开销。

总结文章来源地址https://www.toymoban.com/news/detail-627390.html

  • 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宏来初始化。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  2. 设置超时时间:通过设置timeval结构体来指定select函数的超时时间。如果不需要设置超时时间,可以将该参数设置为NULL。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  3. 调用select函数:使用select函数来等待文件描述符的就绪事件。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  4. 检查select函数的返回值:根据select函数的返回值,确定是否有文件描述符就绪。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  5. 遍历就绪文件描述符集合:通过遍历fd_set集合来判断哪些文件描述符已经就绪。使用FD_ISSET宏来判断文件描述符是否在集合中。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络

注意事项

  • 在使用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. 准备需要监听的文件描述符集合。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  2. 设置超时时间(可选)。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  3. 调用 poll 函数等待就绪事件。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  4. 检查 poll 函数的返回值。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络

注意事项

  • 在使用 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 实例。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  2. 准备需要监听的文件描述符集合并添加到 epoll 实例中。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  3. 设置超时时间(可选)。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  4. 调用 epoll_wait 函数等待就绪事件。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  5. 检查 epoll_wait 函数的返回值。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络
  6. 移除文件描述符或关闭 epoll 实例(可选)。【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端,网络IO管理,架构,tcp/ip,网络

注意事项

  • 在使用 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;
}

到了这里,关于【高并发网络通信架构】3.引入IO多路复用(select,poll,epoll)实现高并发tcp服务端的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • linux并发服务器 —— IO多路复用(八)

    半关闭只能实现数据单方向的传输;当TCP 接中A向 B 发送 FIN 请求关闭,另一端 B 回应ACK 之后 (A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态 (半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据 close不会影响到其他进程,shutdown会

    2024年02月09日
    浏览(51)
  • 【高并发服务器 02】——线程池与IO多路复用

    线程池的好处 :所有的池都是为了事先把资源准备好,在后续用的时候可以更加方便的拿到这个资源—— 不用去申请、释放资源 什么时候用线程池 ? IO事务并发较高 :人在杭州,但是数据库在北京,想要查询数据库,需要通过互联网建立TCP三次握手,频繁地创建和销毁线

    2024年03月23日
    浏览(50)
  • Linux TCP/UDP socket 通信和IO多路复用

    主机字节序 16 位值 == 网络字节序 16 位值 主机字节序 32 位值 == 网络字节序 32 位值 主机字节序的字符串IP地址  == 网络字节序的整形IP地址 将监听的套接字和本地IP和端口进行关联 给监听的套接字设置监听,开始检测客户端链接 等待并接受客户端的连接,阻塞函数,没有客

    2024年02月05日
    浏览(61)
  • 网络模型与 IO 多路复用

      socket也称作“套接字”,用于描述IP地址和端口,是一个通信链路的描述符。应用程序通常通过“套接字”向对端发出请求或者应答网络请求。   socket是连接运行在网络上的两个程序之间的通信端点。通信的两端都有socket,它是一个通道,数据在两个socket之间进行传输

    2024年02月01日
    浏览(52)
  • 【网络编程】IO多路复用

            IO多路复用是一种高效的I/O处理方式,它 允许单个进程能够同时监视多个文件描述符 (sockets、文件等),并在其中任何一个文件描述符准备好进行I/O操作时进行处理。它的核心在于 使用少量的线程或进程来管理多个I/O操作,以提高系统的性能和响应速度 。 1.

    2024年02月09日
    浏览(48)
  • 使用Linux系统IO多路复用中eopll创建基于TCP通信协议的多人聊天室

    一.1.搭建好TCP的通信模型 2.创建红黑树根节点 3.将套接字事件添加到红黑树中,使其被监听 4.当套接字事件发生,表示有客户端连接,将连接事件加入到红黑树节点当中 5.每当连接事件发生时,表示客户端发送信息到服务器 6.每当有事件准备就绪时,将对应的红黑树节点信息

    2024年02月13日
    浏览(45)
  • 网络编程 IO多路复用 [select版] (TCP网络聊天室)

    //head.h                 头文件 //TcpGrpSer.c        服务器端 //TcpGrpUsr.c        客户端 select函数  功能:阻塞函数,让内核去监测集合中的文件描述符是否准备就绪,若准备就绪则解除阻塞。 原型: head.h TcpGrpSer.c TcpGrpUsr.c    

    2024年02月14日
    浏览(52)
  • 网络编程 IO多路复用 [epoll版] (TCP网络聊天室)

    //head.h            头文件 //TcpGrpSer.c     服务器端 //TcpGrpUsr.c     客户端 通过IO多路复用实现服务器在单进程单线程下可以与多个客户端交互  API epoll函数  head.h TcpGrpSer.c TcpGrpUsr.c  

    2024年02月11日
    浏览(57)
  • BIO、NIO、IO多路复用模型详细介绍&Java NIO 网络编程

    上文介绍了网络编程的基础知识,并基于 Java 编写了 BIO 的网络编程。我们知道 BIO 模型是存在巨大问题的,比如 C10K 问题,其本质就是因其阻塞原因,导致如果想要承受更多的请求就必须有足够多的线程,但是足够多的线程会带来内存占用问题、CPU上下文切换带来的性能问题

    2024年02月14日
    浏览(48)
  • 08 - 网络通信优化之IO模型:如何解决高并发下IO瓶颈?

    提到 Java I/O,相信你一定不陌生。你可能使用 I/O 操作读写文件,也可能使用它实现 Socket 的信息传输…这些都是我们在系统中最常遇到的和 I/O 有关的操作。 我们都知道,I/O 的速度要比内存速度慢,尤其是在现在这个大数据时代背景下,I/O 的性能问题更是尤为突出,I/O 读写

    2024年02月12日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包