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

2023-11-09 10:57:36 浏览数 (4)

概述

  1. Select(选择):
    • Select 是一种传统的 I/O 多路复用机制,用于在类 Unix 操作系统(如 Linux)中同时管理多个文件描述符(如网络套接字或文件)。
    • 它允许程序监视多个 I/O 源以检测可读性或可写性,并在数据可读或可写时触发事件。
    • Select 相对简单,但在处理大量文件描述符时性能和可扩展性有限。
代码语言:javascript复制
#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: %sn", buf);
                    write(i, buf, strlen(buf) 1);
                }
            }
        }
    }
    
    close(lfd);

    return 0;
}
  1. Poll(轮询):
    • Poll 是另一种在类 Unix 系统中可用的 I/O 多路复用机制。在性能和可扩展性方面优于 select。
    • 与 select 类似,poll 允许程序监视多个文件描述符,但它可以更高效地处理大量文件描述符。
    • Poll 仍然广泛使用,但像 epoll 这样的更现代替代方案因性能更好而备受青睐。
代码语言:javascript复制
#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: %sn", 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 特别适用于构建可扩展和高性能的网络服务器和应用程序。
代码语言:javascript复制
#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 = %dn", 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: %sn", buf);
                    write(curfd, buf, strlen(buf) 1);
                }
            }
        }
    }
    
    close(lfd);

    return 0;
}
  1. Libevent(事件库):
    • Libevent 是一个提供了简单和一致的事件通知机制 API 的 C 库,包括 select、poll、epoll 等多种机制。
    • 它允许开发人员编写可移植且高效的网络和事件驱动软件。
    • Libevent 抽象了不同平台和事件通知机制之间的差异,使开发人员能够更容易地编写能够在不同系统上运行而无需担心底层细节的代码。

sever

代码语言:javascript复制
#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("%sn", method[i]);
    }
    printf("current method: %sn", 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

代码语言:javascript复制
#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: %sn", (char*)arg);
    // 读缓冲区的数据
    char buf[128];
    int len = bufferevent_read(bev, buf, sizeof(buf));
    printf("read data: len = %d, str = %sn", len, buf);

    // 回复数据
    bufferevent_write(bev, buf, len);
    printf("数据发送完毕...n");
}

// 写缓冲区的回调
// 调用的时机: 写缓冲区中的数据被发送出去之后, 该函数被调用
void write_cb(struct bufferevent* bev, void* arg)
{

    printf("arg value: %sn", (char*)arg);
    printf("数据已经发送完毕...xxxxxxxxxxxxn");
}

// 事件回调
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。

0 人点赞