Linux网络编程(epoll函数的使用)

简介: Linux网络编程(epoll函数的使用)

前言

本篇文章我们讲解epoll函数的使用方法,epoll相比于poll来说性能方面有所提升和改进。

一、epoll概念特点讲解

epoll 是 Linux 上一种高性能的多路复用机制,用于监视大量文件描述符并在它们就绪时通知应用程序。它是在 select 和 poll 的基础上进一步优化和改进而来的。

epoll 的主要特点包括:

1.没有文件描述符数量限制:与 select 和 poll 不同,epoll 采用了基于事件的就绪通知机制,没有预定义的文件描述符数量限制,可以支持更大规模的并发连接。

2.高效的事件通知:epoll 使用了内核和用户空间共享的事件数据结构,将文件描述符的事件注册到内核空间,当事件就绪时,内核直接将就绪的事件通知给用户空间,避免了每次调用都需要遍历整个文件描述符数组的性能开销。

3.分离的就绪事件集合:epoll 将就绪的事件从内核空间复制到用户空间,形成一个分离的就绪事件集合,用户可以直接遍历这个集合来处理就绪的事件,而不需要遍历整个文件描述符数组。

4.支持边缘触发和水平触发:epoll 提供了两种模式来处理事件,一种是边缘触发模式(EPOLLET),只在状态发生变化时通知应用程序,另一种是水平触发模式(默认),在事件就绪期间一直通知应用程序。

5.更低的内存拷贝开销:epoll 使用内存映射技术,避免了每次调用都需要将事件数据从内核复制到用户空间的开销,从而减少了系统调用的次数和内存拷贝的开销。

6.支持较高精度的超时控制:与 poll 不同,epoll 的超时参数以毫秒和纳秒为单位,提供了较高精度的超时控制。

总体来说,epoll 在性能上相比于 select 和 poll 有较大的优势,特别适用于高并发场景下的网络编程。它的高效事件就绪通知、支持大规模并发连接、较低的内存拷贝开销以及较高的超时精度,使得它成为开发高性能服务器和网络应用的首选机制。

二、epoll实现机理

epoll是使用红黑树(Red-Black Tree)实现的。epoll是Linux操作系统提供的一种高效的事件通知机制,用于处理大量的并发连接。它能够监视多个文件描述符的状态变化,当文件描述符就绪时,通过回调函数进行相应的处理。

在Linux内核中,epoll使用红黑树作为其主要的数据结构,用于维护注册的文件描述符集合。红黑树是一种自平衡的二叉搜索树,具有较快的插入、删除和搜索操作的时间复杂度。通过使用红黑树,epoll能够高效地检索和管理大量的文件描述符。

当文件描述符发生事件时,epoll通过红黑树的查找操作快速定位到相应的结点,并触发注册的回调函数进行事件处理。使用红黑树的原因是它能够保持良好的平衡性,保证搜索、插入和删除操作的最坏情况时间复杂度为O(log n),从而保证了epoll的高性能和可伸缩性。

总结来说,epoll是利用红黑树作为其底层数据结构实现的,这使得它在处理大量并发连接时能够提供高效的事件通知机制。

三、epoll相关函数讲解

1.epoll_create函数

函数原型:

int epoll_create(int size);

epoll_create 函数创建一个 epoll 实例,并返回一个文件描述符,用于标识该 epoll 实例。参数 size 是一个提示,表示 epoll 实例可以监视的文件描述符的数量上限。但在大多数情况下,该参数会被忽略,可以传递任意的值。

返回的文件描述符可以用于之后对 epoll 实例进行操作,比如注册、修改和删除文件描述符的事件。

需要注意的是,epoll_create 函数在成功时返回一个非负整数,表示 epoll 实例的文件描述符,如果出现错误,返回值为 -1,并设置 errno 错误码来指示具体的错误类型。

2.epoll_ctl函数

函数原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 函数通过指定的 epfd 参数来操作特定的 epoll 实例,op 参数表示操作类型,可以是以下三种之一:

EPOLL_CTL_ADD:将指定的文件描述符 fd 添加到 epoll 实例中,并注册相应的事件。这样,当该文件描述符上的事件就绪时,就会通知应用程序。

EPOLL_CTL_MOD:修改已经注册在 epoll 实例中的文件描述符 fd 对应的事件。可以修改事件的类型、关注的事件、关联的用户数据等。

EPOLL_CTL_DEL:删除已经注册在 epoll 实例中的文件描述符 fd。

fd 参数是目标文件描述符,用于指定需要进行操作的文件描述符。

event 参数是一个指向 struct epoll_event 结构体的指针,用于指定事件相关的配置。该结构体包含两个成员:

uint32_t events:表示注册的事件类型,可以是 EPOLLIN(可读事件)、EPOLLOUT(可写事件)、EPOLLRDHUP(对端关闭连接)、EPOLLPRI(有紧急数据可读)、EPOLLERR(错误事件)等。可以使用位掩码进行组合。

epoll_data_t data:用于存储用户数据信息,可以是文件描述符本身的值,也可以是用户自定义的数据结构指针。

函数的返回值为 0 表示操作成功,-1 表示出现错误,具体的错误信息可以通过检查 errno 变量获得。

3.epoll_wait函数

函数原型:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epfd 参数是 epoll 实例的描述符,通过它指定要进行事件等待的 epoll 实例。

events 参数是一个用于存放事件信息的数组,每个数组元素都是 struct epoll_event 类型的结构体,用于存储就绪的文件描述符以及对应的事件信息。

maxevents 参数表示 events 数组的大小,即最多可以等待多少个事件。

timeout 参数是超时时间,单位为毫秒。它决定了 epoll_wait 函数的阻塞行为:

如果 timeout 设置为 -1,表示无限期阻塞,直到有事件发生为止。

如果 timeout 设置为 0,表示非阻塞,立即返回当前就绪的事件,如果没有事件就绪,则返回 0。

如果 timeout 设置为一个正整数,表示阻塞等待指定的时间后返回,如果在超时时间内没有等到事件就绪,则返回 0。

epoll_wait 函数在成功时返回就绪事件的文件描述符数量,如果出现错误则返回 -1,并设置 errno 错误码来指示具体的错误类型。

四、epoll实现并发服务器

当使用epoll实现并发服务器时,通常的步骤包括以下几个主要环节:

1.创建socket:使用socket函数创建一个监听套接字,用于接受客户端的连接请求。

2.绑定socket:使用bind函数将监听套接字绑定到一个特定的IP地址和端口。

3.监听连接:使用listen函数开始监听连接请求,指定服务器可接受的最大连接数。

4.创建epoll实例:使用epoll_create函数创建一个epoll实例,返回一个文件描述符。

5.将监听套接字添加到epoll实例:使用epoll_ctl函数将监听套接字添加到epoll实例中,并注册对读事件的关注。

6.进入事件循环:循环调用epoll_wait函数来等待事件的发生,该函数会阻塞当前线程直至有事件发生。一旦有事件发生,它将返回一个就绪事件的列表。

7.处理就绪事件:遍历就绪事件列表,对每个事件进行处理。根据事件类型,可以进行接受连接、读取数据、发送数据或关闭连接等操作。

8.根据需要添加或删除文件描述符:在处理完一个事件后,可以根据需要使用epoll_ctl函数动态地添加或删除文件描述符,以便继续监听其他事件。

9.重复步骤6-8:继续循环执行步骤6-8,处理新的就绪事件,直到服务器主动关闭或出现错误条件为止。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#define MAX_EVENTS  1024 //最多可以等待多少个事件
int main()
{
    int server = 0;
    struct sockaddr_in saddr = {0};
    int client = 0;
    struct sockaddr_in caddr = {0};
    socklen_t asize = 0;
    int len = 0;
    char buf[32] = {0};
    int maxfd;
    int ret = 0;
    int i = 0;
    server = socket(PF_INET, SOCK_STREAM, 0);
    if( server == -1 )
    {
        printf("server socket error\n");
        return -1;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(8888);
    if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 )
    {
        printf("server bind error\n");
        return -1;
    }
    if( listen(server, 128) == -1 )
    {
        printf("server listen error\n");
        return -1;
    }
    printf("server start success\n");
    struct epoll_event event, events[MAX_EVENTS];
    /*创建epoll*/
    int epollInstance = epoll_create1(0);
    if (epollInstance == -1) 
    {
        printf("Failed to create epoll instance\n");
    }
    /*将服务器添加进入event中*/
    event.events = EPOLLIN;
    event.data.fd = server;
    if (epoll_ctl(epollInstance, EPOLL_CTL_ADD, server, &event) == -1) 
    {
        printf("Failed to add server socket to epoll instance");
    }        
    while( 1 )
    {        
        int numEventsReady = epoll_wait(epollInstance, events, MAX_EVENTS, -1);
        if (numEventsReady == -1) 
        {
            printf("Failed to wait for events");
            return -1;
        }
        for(i = 0; i < numEventsReady; i++)
        {
            if(events[i].data.fd == server)
            {
                /*有客户端连接上来了*/
                asize = sizeof(caddr);  
                client = accept(server, (struct sockaddr*)&caddr, &asize);
                printf("client is connect\n");
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client;
                if (epoll_ctl(epollInstance, EPOLL_CTL_ADD, client, &event) == -1) 
                {
                    printf("Failed to add client socket to epoll instance");
                    return -1;
                }                
            }
            else
            {
                /*处理客户端的请求*/
                len = read(events[i].data.fd, buf, 1024);
                if(len == 0)
                {
                    printf("client is disconnect\n");
                    close(events[i].data.fd);
                }
                else
                {
                    /*对接收到的数据进行处理*/
                    printf("read buf : %s\n", buf);
                    write(events[i].data.fd, buf, len);
                }
            }
        }        
    }
    close(server);
    return 0;
}

总结

本篇文章就讲解到这里,下篇文章继续讲解Linux网络编程的知识。


相关文章
|
13天前
|
安全 Linux 网络安全
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
48 0
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
|
2月前
|
运维 Linux 开发者
Linux系统中使用Python的ping3库进行网络连通性测试
以上步骤展示了如何利用 Python 的 `ping3` 库来检测网络连通性,并且提供了基本错误处理方法以确保程序能够优雅地处理各种意外情形。通过简洁明快、易读易懂、实操性强等特点使得该方法非常适合开发者或系统管理员快速集成至自动化工具链之内进行日常运维任务之需求满足。
127 18
|
2月前
|
网络协议 关系型数据库 Linux
【App Service Linux】在Linux App Service中安装 tcpdump 并抓取网络包
在App Service for Linux环境中,无法像Windows一样直接使用网络排查工具抓包。本文介绍了如何通过TCPDUMP在Linux环境下抓取网络包,包括SSH进入容器、安装tcpdump、执行抓包命令及下载分析文件的完整操作步骤。
121 5
|
3月前
|
Web App开发 网络协议 Linux
【Linux】网络基础
TCP/IP五层模型是网络通信的基础框架,将复杂的数据传输过程分为物理层、数据链路层、网络层、传输层和应用层,每层各司其职,协同完成远程通信。该模型确保了不同设备和网络之间的互联互通,是现代互联网运行的核心机制。
189 5
|
3月前
|
网络协议 Linux 开发者
深入Linux中UDP网络通信机制编程探索
以上步骤概述了Linux中UDP网络通信的编程机制。在实现时,因关注细节和上下文环境可能有所调整,但大致流程是一致的。这些知识片段旨在帮助开发者快速上手Linux下的UDP编程,并提供可靠的信息作为编程的基础。在编程实践中,应结合实际业务需求,设计合适的数据传输协议,确保数据的正确性和实时性。
71 0
|
5月前
|
安全 网络协议 Linux
Linux网络应用层协议展示:HTTP与HTTPS
此外,必须注意,从HTTP迁移到HTTPS是一项重要且必要的任务,因为这不仅关乎用户信息的安全,也有利于你的网站评级和粉丝的信心。在网络世界中,信息的安全就是一切,选择HTTPS,让您的网站更加安全,使您的用户满意,也使您感到满意。
148 18
|
5月前
|
Linux 数据安全/隐私保护
使用Linux命令行接入无线网络Wi-Fi的示例。
现在,你已经使用命令行成功地连接到 Wi-Fi 网络了。这两个示例涵盖了用 `nmcli` 和 `wpa_supplicant` 连接无线网络的常见场景,让你能够不依赖图形化界面来完成这个任务。在日常使用中熟练掌握这些基本操作能增强你对 Linux 系统的理解,帮助你更有效地处理各种问题。
242 12
|
5月前
|
安全 Ubuntu Linux
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
159 0
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
|
7月前
|
Ubuntu Linux
Linux系统管理:服务器时间与网络时间同步技巧。
以上就是在Linux服务器上设置时间同步的方式。然而,要正确运用这些知识,需要理解其背后的工作原理:服务器根据网络中的其他机器的时间进行校对,逐步地精确自己的系统时间,就像一只犹豫不决的啮齿动物,通过观察其他啮齿动物的行为,逐渐确定自己的行为逻辑,既简单,又有趣。最后希望这个过程既能给你带来乐趣,也能提高你作为系统管理员的专业素养。
1094 20
|
7月前
|
JSON 运维 Ubuntu
Linux下如何使用Curl进行网络请求
希望这篇文章能帮助您在Linux下更好地使用Curl进行网络请求。如有疑问,请随时提问!
345 10