高性能网络编程 - select、 poll 、epoll 、libevent

简介: 高性能网络编程 - select、 poll 、epoll 、libevent


概述

  1. Select(选择):
  • Select 是一种传统的 I/O 多路复用机制,用于在类 Unix 操作系统(如 Linux)中同时管理多个文件描述符(如网络套接字或文件)。
  • 它允许程序监视多个 I/O 源以检测可读性或可写性,并在数据可读或可写时触发事件。
  • Select 相对简单,但在处理大量文件描述符时性能和可扩展性有限。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
int main()
{
    // 1.创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    // 2. 绑定 ip, port
    struct sockaddr_in addr;
    addr.sin_port = htons(10000);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }
    // 3. 监听
    ret = listen(lfd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }
    // 4. 等待连接 -> 循环
    // 检测 -> 读缓冲区, 委托内核去处理
    // 数据初始化, 创建自定义的文件描述符集
    fd_set rdset, tmp; 
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;
    while(1)
    {
        // 委托内核检测
        tmp = rdset;
        ret = select(maxfd+1, &tmp, NULL, NULL, NULL);
        if(ret == -1)
        {
            perror("select");
            exit(0);
        }
        // 检测的度缓冲区有变化
        // 有新连接
        if(FD_ISSET(lfd, &tmp))
        {
            // 接收连接请求
            struct sockaddr_in sockcli;
            int len = sizeof(sockcli);
            // 这个accept是不会阻塞的
            int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
            // 委托内核检测connfd的读缓冲区
            FD_SET(connfd, &rdset);
            maxfd = connfd > maxfd ? connfd : maxfd;
        }
        // 通信, 有客户端发送数据过来
        for(int i=lfd+1; i<=maxfd; ++i)
        {
            // 如果在集合中, 说明读缓冲区有数据
            if(FD_ISSET(i, &tmp))
            {
                char buf[128];
                int ret = read(i, buf, sizeof(buf));
                if(ret == -1)
                {
                    perror("read");
                    exit(0);
                }
                else if(ret == 0)
                {
                    printf("对方已经关闭了连接...\n");
                    FD_CLR(i, &rdset);
                    close(i);
                }
                else
                {
                    printf("客户端say: %s\n", buf);
                    write(i, buf, strlen(buf)+1);
                }
            }
        }
    }
    close(lfd);
    return 0;
}
  1. Poll(轮询):
  • Poll 是另一种在类 Unix 系统中可用的 I/O 多路复用机制。在性能和可扩展性方面优于 select。
  • 与 select 类似,poll 允许程序监视多个文件描述符,但它可以更高效地处理大量文件描述符。
  • Poll 仍然广泛使用,但像 epoll 这样的更现代替代方案因性能更好而备受青睐。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <poll.h>
int main()
{
    // 1.创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    // 2. 绑定 ip, port
    struct sockaddr_in addr;
    addr.sin_port = htons(10000);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }
    // 3. 监听
    ret = listen(lfd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }
    // 4. 等待连接 -> 循环
    // 检测 -> 读缓冲区, 委托内核去处理
    // 数据初始化, 创建自定义的文件描述符集
    struct pollfd fds[1024];
    // 初始化
    for(int i=0; i<1024; ++i)
    {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int maxfd = 0;
    while(1)
    {
        // 委托内核检测
        ret = poll(fds, maxfd+1, -1);
        if(ret == -1)
        {
            perror("select");
            exit(0);
        }
        // 检测的度缓冲区有变化
        // 有新连接
        if(fds[0].revents & POLLIN)
        {
            // 接收连接请求
            struct sockaddr_in sockcli;
            int len = sizeof(sockcli);
            // 这个accept是不会阻塞的
            int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
            // 委托内核检测connfd的读缓冲区
            int i;
            for(i=0; i<1024; ++i)
            {
                if(fds[i].fd == -1)
                {
                    fds[i].fd = connfd;
                    break;
                }
            }
            maxfd = i > maxfd ? i : maxfd;
        }
        // 通信, 有客户端发送数据过来
        for(int i=1; i<=maxfd; ++i)
        {
            // 如果在集合中, 说明读缓冲区有数据
            if(fds[i].revents & POLLIN)
            {
                char buf[128];
                int ret = read(fds[i].fd, buf, sizeof(buf));
                if(ret == -1)
                {
                    perror("read");
                    exit(0);
                }
                else if(ret == 0)
                {
                    printf("对方已经关闭了连接...\n");
                    close(fds[i].fd);
                    fds[i].fd = -1;
                }
                else
                {
                    printf("客户端say: %s\n", buf);
                    write(fds[i].fd, buf, strlen(buf)+1);
                }
            }
        }
    }
    close(lfd);
    return 0;
}
  1. Epoll(事件轮询):
  • Epoll(事件轮询)是一种较新且高效的 I/O 事件通知机制,主要用于 Linux
  • 与 select 和 poll 不同,epoll 专为高性能 I/O 事件处理而设计。它可以有效地管理大量文件描述符而不会显著降低性能。
  • Epoll 特别适用于构建可扩展和高性能的网络服务器和应用程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
int main()
{
    // 1.创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    // 2. 绑定 ip, port
    struct sockaddr_in addr;
    addr.sin_port = htons(10000);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }
    // 3. 监听
    ret = listen(lfd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }
    // 创建epoll树
    int epfd = epoll_create(1000);
    if(epfd == -1)
    {
        perror("epoll_create");
        exit(0);
    }
    // 将监听lfd添加到树上
    struct epoll_event ev;
    // 检测事件的初始化
    ev.events = EPOLLIN ;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    struct epoll_event events[1024];
    // 开始检测
    while(1)
    {
        int nums = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1);
        printf("numbers = %d\n", nums);
        // 遍历状态变化的文件描述符集合
        for(int i=0; i<nums; ++i)
        {
            int curfd = events[i].data.fd;
            // 有新连接
            if(curfd == lfd)
            {
                struct sockaddr_in clisock;
                int len = sizeof(clisock);
                int connfd = accept(lfd, (struct sockaddr*)&clisock, &len);
                if(connfd == -1)
                {
                    perror("accept");
                    exit(0);
                }
                // 将通信的fd挂到树上
                //ev.events = EPOLLIN | EPOLLOUT;
                ev.events = EPOLLIN;
                ev.data.fd  = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            // 通信
            else
            {
                // 读事件触发, 写事件触发
                if(events[i].events & EPOLLOUT) 
                {
                    continue;
                }
                char buf[128];
                int count = read(curfd, buf, sizeof(buf));
                if(count == 0)
                {
                    printf("client disconnect ...\n");
                    close(curfd);
                    // 从树上删除该节点
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                }
                else if(count == -1)
                {
                    perror("read");
                    exit(0);
                }
                else
                {
                    // 正常情况
                    printf("client say: %s\n", buf);
                    write(curfd, buf, strlen(buf)+1);
                }
            }
        }
    }
    close(lfd);
    return 0;
}
  1. Libevent(事件库):
  • Libevent 是一个提供了简单和一致的事件通知机制 API 的 C 库,包括 select、poll、epoll 等多种机制。
  • 它允许开发人员编写可移植且高效的网络和事件驱动软件。
  • Libevent 抽象了不同平台和事件通知机制之间的差异,使开发人员能够更容易地编写能够在不同系统上运行而无需担心底层细节的代码。

sever

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <event2/event.h>
int main()
{
    // 1. 创建事件处理框架
    struct event_base* base = event_base_new();
    // 打印支持的IO转接函数
    const char** method = event_get_supported_methods();
    for(int i=0; method[i] != NULL; ++i)
    {
        printf("%s\n", method[i]);
    }
    printf("current method: %s\n", event_base_get_method(base));
    // 创建子进程
    pid_t pid = fork();
    if(pid == 0)
    {
        // 子进程中event_base也会被复制,在使用这个base时候要重新初始化
        event_reinit(base); 
    }
    // 2. 释放资源
    event_base_free(base);
    return 0;
}

client

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
// read缓冲区的回调
void read_cb(struct bufferevent* bev, void* arg)
{
    printf("arg value: %s\n", (char*)arg);
    // 读缓冲区的数据
    char buf[128];
    int len = bufferevent_read(bev, buf, sizeof(buf));
    printf("read data: len = %d, str = %s\n", len, buf);
    // 回复数据
    bufferevent_write(bev, buf, len);
    printf("数据发送完毕...\n");
}
// 写缓冲区的回调
// 调用的时机: 写缓冲区中的数据被发送出去之后, 该函数被调用
void write_cb(struct bufferevent* bev, void* arg)
{
    printf("arg value: %s\n", (char*)arg);
    printf("数据已经发送完毕...xxxxxxxxxxxx\n");
}
// 事件回调
void events_cb(struct bufferevent* bev, short event, void* arg)
{
    if(event & BEV_EVENT_ERROR)
    {
        printf("some error happened ...\n");
    }
    else if(event & BEV_EVENT_EOF)
    {
        printf("server disconnect ...\n");
    }
    // 终止连接
    bufferevent_free(bev);
}
void send_msg(evutil_socket_t fd, short ev, void * arg)
{
    // 将写入到终端的数据读出
    char buf[128];
    int len = read(fd, buf, sizeof(buf));
    // 发送给服务器
    struct bufferevent* bev = (struct bufferevent*)arg;
    bufferevent_write(bev, buf, len);
}
int main()
{
    struct event_base * base = event_base_new();
    // 1. 创建通信的套接字
    struct bufferevent* bufev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
    // 2. 连接服务器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9898);    // 服务器监听的端口
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
    // 这个函数调用成功, == 服务器已经成功连接
    bufferevent_socket_connect(bufev, (struct sockaddr*)&addr, sizeof(addr));
    // 3. 通信
    // 给bufferevent的缓冲区设置回调
    bufferevent_setcb(bufev, read_cb, write_cb, events_cb, (void*)"hello, world");
    bufferevent_enable(bufev, EV_READ);
    // 创建一个普通的输入事件
    struct event* myev = event_new(base, STDIN_FILENO, EV_READ|EV_PERSIST, send_msg, bufev);
    event_add(myev, NULL);
    event_base_dispatch(base);
    event_free(myev);
    event_base_free(base);
    return 0;
}

总之,这些是用于编程的工具和库,用于高效地处理多个 I/O 操作,特别是在网络通信的背景下。Select 和 poll 是较旧、性能较低的选项,而 epoll 是一种高性能的替代方案。Libevent 是一个库,简化了使用这些机制的工作,同时提供了跨不同平台的可移植性。


优缺点

以下是每种方案的优点和缺点:

Select

优点:

  • 简单易用,易于理解和实现。
  • 在小规模连接数的情况下,性能通常足够。
  • 跨平台兼容性较好。

缺点:

  • 性能不够高,随着连接数的增加,性能会下降。
  • 需要维护大量文件描述符集合,开销较大。
  • 对于大规模并发连接,存在效率问题。

Poll

优点:

  • 性能相对于Select有所提升,可以处理更多文件描述符。
  • 在某些场景下,仍然是一个可行的选择。

缺点:

  • 仍然存在性能问题,特别是在大规模并发连接的情况下。
  • 对于每个事件的轮询会导致不必要的开销。

Epoll

优点:

  • 高性能:Epoll 针对大规模并发连接进行了优化,性能较高。
  • 有效地管理大量文件描述符,不会随连接数增加而降低性能。
  • 支持边缘触发模式,只在事件发生时通知应用程序,减少了不必要的处理开销。
  • 仅在Linux系统上可用。

缺点:

  • 不具备跨平台兼容性,只能在Linux上使用。
  • 相对于Select和Poll,编写代码可能稍微复杂一些。

LibEvent

优点:

  • 提供统一的事件通知 API,能够适应不同操作系统和事件通知机制。
  • 简化了跨平台开发,使代码更具可移植性。
  • 在性能方面,可以利用底层高性能机制,如Epoll,以提高性能。

缺点:

  • 与直接使用底层机制相比,可能引入轻微的性能开销。
  • 需要学习Libevent的API和概念。

总的来说,选择哪种方案取决于你的应用需求。如果需要处理大规模并发连接,特别是在Linux上,Epoll通常是最佳选择。对于跨平台开发,Libevent可以提供便利。如果只需处理少量连接,Select和Poll也可以工作,但性能可能不如Epoll。


相关文章
|
3月前
|
C++
基于Reactor模型的高性能网络库之地址篇
这段代码定义了一个 InetAddress 类,是 C++ 网络编程中用于封装 IPv4 地址和端口的常见做法。该类的主要作用是方便地表示和操作一个网络地址(IP + 端口)
187 58
|
3月前
|
网络协议 算法 Java
基于Reactor模型的高性能网络库之Tcpserver组件-上层调度器
TcpServer 是一个用于管理 TCP 连接的类,包含成员变量如事件循环(EventLoop)、连接池(ConnectionMap)和回调函数等。其主要功能包括监听新连接、设置线程池、启动服务器及处理连接事件。通过 Acceptor 接收新连接,并使用轮询算法将连接分配给子事件循环(subloop)进行读写操作。调用链从 start() 开始,经由线程池启动和 Acceptor 监听,最终由 TcpConnection 管理具体连接的事件处理。
71 2
|
3月前
基于Reactor模型的高性能网络库之Tcpconnection组件
TcpConnection 由 subLoop 管理 connfd,负责处理具体连接。它封装了连接套接字,通过 Channel 监听可读、可写、关闭、错误等
92 1
|
3月前
|
负载均衡 算法 安全
基于Reactor模式的高性能网络库之线程池组件设计篇
EventLoopThreadPool 是 Reactor 模式中实现“一个主线程 + 多个工作线程”的关键组件,用于高效管理多个 EventLoop 并在多核 CPU 上分担高并发 I/O 压力。通过封装 Thread 类和 EventLoopThread,实现线程创建、管理和事件循环的调度,形成线程池结构。每个 EventLoopThread 管理一个子线程与对应的 EventLoop(subloop),主线程(base loop)通过负载均衡算法将任务派发至各 subloop,从而提升系统性能与并发处理能力。
159 3
|
3月前
基于Reactor模式的高性能网络库github地址
https://github.com/zyi30/reactor-net.git
57 0
|
3月前
基于Reactor模型的高性能网络库之Poller(EpollPoller)组件
封装底层 I/O 多路复用机制(如 epoll)的抽象类 Poller,提供统一接口支持多种实现。Poller 是一个抽象基类,定义了 Channel 管理、事件收集等核心功能,并与 EventLoop 绑定。其子类 EPollPoller 实现了基于 epoll 的具体操作,包括事件等待、Channel 更新和删除等。通过工厂方法可创建默认的 Poller 实例,实现多态调用。
212 60
|
3月前
基于Reactor模型的高性能网络库之Channel组件篇
Channel 是事件通道,它绑定某个文件描述符 fd,注册感兴趣的事件(如读/写),并在事件发生时分发给对应的回调函数。
171 60
|
3月前
|
安全 调度
基于Reactor模型的高性能网络库之核心调度器:EventLoop组件
它负责:监听事件(如 I/O 可读写、定时器)、分发事件、执行回调、管理事件源 Channel 等。
201 57
|
3月前
基于Reactor模型的高性能网络库之时间篇
是一个用于表示时间戳(精确到微秒)**的简单封装类
144 57
|
22天前
|
监控 前端开发 安全
Netty 高性能网络编程框架技术详解与实践指南
本文档全面介绍 Netty 高性能网络编程框架的核心概念、架构设计和实践应用。作为 Java 领域最优秀的 NIO 框架之一,Netty 提供了异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。本文将深入探讨其 Reactor 模型、ChannelPipeline、编解码器、内存管理等核心机制,帮助开发者构建高性能的网络应用系统。
153 0