高性能网络编程 - 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。


相关文章
|
7月前
|
网络协议 Linux Python
Python网络编程基础(Socket编程)epoll在Linux下的使用
【4月更文挑战第12天】在上一节中,我们介绍了使用`select`模块来实现非阻塞IO的方法。然而,`select`模块在处理大量并发连接时可能会存在性能问题。在Linux系统中,`epoll`机制提供了更高效的IO多路复用方式,能够更好地处理大量并发连接。
|
1月前
|
消息中间件 编解码 网络协议
Netty从入门到精通:高性能网络编程的进阶之路
【11月更文挑战第17天】Netty是一个基于Java NIO(Non-blocking I/O)的高性能、异步事件驱动的网络应用框架。使用Netty,开发者可以快速、高效地开发可扩展的网络服务器和客户端程序。本文将带您从Netty的背景、业务场景、功能点、解决问题的关键、底层原理实现,到编写一个详细的Java示例,全面了解Netty,帮助您从入门到精通。
153 0
|
5月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
191 1
|
5月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
|
11天前
|
SQL 安全 网络安全
网络安全与信息安全:知识分享####
【10月更文挑战第21天】 随着数字化时代的快速发展,网络安全和信息安全已成为个人和企业不可忽视的关键问题。本文将探讨网络安全漏洞、加密技术以及安全意识的重要性,并提供一些实用的建议,帮助读者提高自身的网络安全防护能力。 ####
50 17
|
21天前
|
存储 SQL 安全
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
随着互联网的普及,网络安全问题日益突出。本文将介绍网络安全的重要性,分析常见的网络安全漏洞及其危害,探讨加密技术在保障网络安全中的作用,并强调提高安全意识的必要性。通过本文的学习,读者将了解网络安全的基本概念和应对策略,提升个人和组织的网络安全防护能力。
|
22天前
|
SQL 安全 网络安全
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
随着互联网的普及,网络安全问题日益突出。本文将从网络安全漏洞、加密技术和安全意识三个方面进行探讨,旨在提高读者对网络安全的认识和防范能力。通过分析常见的网络安全漏洞,介绍加密技术的基本原理和应用,以及强调安全意识的重要性,帮助读者更好地保护自己的网络信息安全。
45 10
|
24天前
|
SQL 安全 网络安全
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
在数字化时代,网络安全和信息安全已成为我们生活中不可或缺的一部分。本文将介绍网络安全漏洞、加密技术和安全意识等方面的内容,并提供一些实用的代码示例。通过阅读本文,您将了解到如何保护自己的网络安全,以及如何提高自己的信息安全意识。
50 10
|
24天前
|
存储 监控 安全
云计算与网络安全:云服务、网络安全、信息安全等技术领域的融合与挑战
本文将探讨云计算与网络安全之间的关系,以及它们在云服务、网络安全和信息安全等技术领域中的融合与挑战。我们将分析云计算的优势和风险,以及如何通过网络安全措施来保护数据和应用程序。我们还将讨论如何确保云服务的可用性和可靠性,以及如何处理网络攻击和数据泄露等问题。最后,我们将提供一些关于如何在云计算环境中实现网络安全的建议和最佳实践。
|
25天前
|
监控 安全 网络安全
网络安全与信息安全:漏洞、加密与意识的交织
在数字时代的浪潮中,网络安全与信息安全成为维护数据完整性、保密性和可用性的关键。本文深入探讨了网络安全中的漏洞概念、加密技术的应用以及提升安全意识的重要性。通过实际案例分析,揭示了网络攻击的常见模式和防御策略,强调了教育和技术并重的安全理念。旨在为读者提供一套全面的网络安全知识框架,从而在日益复杂的网络环境中保护个人和组织的资产安全。

热门文章

最新文章