ZLMediaKit 源码分析——[3] ZLToolKit 中EventPoller之网络事件处理

系列文章目录

第一篇 基于SRS 的 WebRTC 环境搭建

第二篇 基于SRS 实现RTSP接入与WebRTC播放

第三篇 centos下基于ZLMediaKit 的WebRTC 环境搭建

第四篇 WebRTC学习一:获取音频和视频设备

第五篇 WebRTC学习二:WebRTC音视频数据采集

第六篇 WebRTC学习三:WebRTC音视频约束

第七篇 WebRTC学习四:WebRTC常规视觉滤镜

第八篇 WebRTC学习五:从视频中提取图片

第九篇 WebRTC学习六:MediaStream 常用API介绍

第十篇 WebRTC学习七:WebRTC 中 STUN 协议详解

ZLMediaKit源码分析------[1] 开篇:基础库 ZLToolKit 之 onceToken 源码分析

ZLMediaKit源码分析------[2] 从 ZLToolKit 代码看 CPU 亲和性设计

ZLMediaKit源码分析------[3] ZLToolKit 中EventPoller之网络事件处理

文章目录

  • 系列文章目录
  • 前言
  • 一、EventPoller网络事件处理机制
  • 二、EventPoller类图分析
    • [2.1 类图](#2.1 类图)
    • [2.2 相关类功能介绍](#2.2 相关类功能介绍)
  • [三、 事件循环驱动 - runLoop 函数](#三、 事件循环驱动 - runLoop 函数)
  • 四、事件监听的添加,修改和删除
    • [4.1 添加事件监听AddEvent](#4.1 添加事件监听AddEvent)
    • [4.2 删除事件监听 delEvent](#4.2 删除事件监听 delEvent)
    • [4.3 修改监听事件类型 modifyEvent](#4.3 修改监听事件类型 modifyEvent)
  • 五、数据结构
    • [5.1 _event_map](#5.1 _event_map)
    • [5.2 _event_cache_expired的之作用](#5.2 _event_cache_expired的之作用)
      • [5.2.1 在 delEvent 函数中的作用](#5.2.1 在 delEvent 函数中的作用)
      • [5.2.2 在 runLoop 函数中的作用](#5.2.2 在 runLoop 函数中的作用)
      • [5.2.3 总结](#5.2.3 总结)
  • 总结

前言

当我在ZLToolKit里看到事件轮询管理类EventPoller的runLoop函数时,脑海中瞬间浮现出多年前live555里的设计------那里同样有一个doEventLoop()函数,在该函数里执行着网络事件的select操作、异步任务以及延时队列。出于好奇,我仔细研读了EventPoller类的代码。发现如今其IO事件驱动模型采用的是多实例epoll模型,不过网络事件、异步任务和延时队列这些概念依旧存在,这样的设计让人倍感熟悉。那么,今天我就来详细剖析一下ZLM里网络事件处理模型的具体运作机制。


一、EventPoller网络事件处理机制

zlmediakit基于‌事件驱动+非阻塞I/O‌架构,事件处理机制如下:

‌核心机制‌:使用epoll(Linux)或kqueue(BSD)实现I/O多路复用,监控所有socket的可读/可写事件。

‌执行流程‌:

‌事件收集‌:调用epoll_wait等待socket事件(如新连接、数据到达)。

事件触发:调用epoll_ctl添加/修改/删除网络事件。

事件处理:调用回调函数执行任务。

其核心类为 EventPoller,关键函数是 runLoop ,网络事件处理中关键的数据结构为_event_map,_event_cache_expired,以及事件监听,删除,修改的接口构成。

二、EventPoller类图分析

2.1 类图

因为press on上面显示类太多了会看不清晰,这里省略了一些和本节介绍无关的类,保留主要类图如下:

2.2 相关类功能介绍

AnyStorage 类:这是一个通用的数据存储类,它可以存储任意类型的数据。在 zlmediakit 中,它提供了一种灵活的方式来存储不同类型的值,增强了代码的通用性和可扩展性,方便在不同模块之间传递和管理数据。

ThreadLoadCounter 类:主要用于统计线程的负载情况。它会对线程在运行过程中的各种操作进行计数和时间统计,帮助开发者了解线程的繁忙程度,为系统的性能优化和资源分配提供数据支持。

TaskExecutorInterface 类:作为一个接口类,它定义了任务执行器的基本行为和功能规范。不同的任务执行器可以实现该接口,遵循统一的操作接口,从而实现多态性,方便代码的扩展和维护。其中最重要的就是它提供的4个接口,是异步任务执行的基础。

TaskExecutor 类:是具体的任务执行器类,实现了 TaskExecutorInterface 接口所定义的功能。它负责接收并执行各种任务,合理地分配系统资源,将任务分配到合适的线程或执行环境中进行处理。

EventPoller 类:是整个事件处理的核心类,负责事件的轮询和管理。它能够监听网络套接字的各种事件,通过事件循环不断检查事件状态,并在事件发生时触发相应的处理逻辑。

三、 事件循环驱动 - runLoop 函数

EventPoller::runLoop 函数是 EventPoller 类的核心函数,它实现了事件循环的逻辑,不断地监听网络套接字的事件并进行处理。

c 复制代码
void EventPoller::runLoop(bool blocked, bool ref_self) {
    if (blocked) {
        if (ref_self) {
            s_current_poller = shared_from_this();
        }
        _sem_run_started.post();
        _exit_flag = false;
        uint64_t minDelay;
#if defined(HAS_EPOLL)
        struct epoll_event events[EPOLL_SIZE];
        while (!_exit_flag) {
            minDelay = getMinDelay();
            startSleep();//用于统计当前线程负载情况
            int ret = epoll_wait(_event_fd, events, EPOLL_SIZE, minDelay ? minDelay : -1);
            sleepWakeUp();//用于统计当前线程负载情况
            if (ret <= 0) {
                //超时或被打断
                continue;
            }

            _event_cache_expired.clear();

            for (int i = 0; i < ret; ++i) {
                struct epoll_event &ev = events[i];
                int fd = ev.data.fd;
                if (_event_cache_expired.count(fd)) {
                    //event cache refresh
                    continue;
                }

                auto it = _event_map.find(fd);
                if (it == _event_map.end()) {
                    epoll_ctl(_event_fd, EPOLL_CTL_DEL, fd, nullptr);
                    continue;
                }
                auto cb = it->second;
                try {
                    (*cb)(toPoller(ev.events)); // 将epoll事件类型转换回自定事件类型
                } catch (std::exception &ex) {
                    ErrorL << "Exception occurred when do event task: " << ex.what();
                }
            }
        }
#elif defined(HAS_KQUEUE)
         // ... 其他系统的处理代码
#else
         // ... 其他系统的处理代码
#endif //HAS_EPOLL
    } else {
        _loop_thread = new thread(&EventPoller::runLoop, this, true, ref_self);
        _sem_run_started.wait();
    }
}

先屏蔽掉其它系统的网络处理,可以看到该函数处理主要流程如下:

1、初始化和循环条件判断:首先进行一些初始化操作,如设置当前的 EventPoller 实例,将退出标志 _exit_flag 置为 false,表示开始事件循环。

2、获取最小延迟时间:调用 getMinDelay() 函数获取最小延迟时间,用于 epoll_wait 的超时设置。

3、调用 epoll_wait 监听事件:使用 epoll_wait 函数监听网络事件,当有事件发生时,该函数会返回发生事件的数量。

4、事件处理:遍历发生事件的数组,对于每个事件,检查其对应的文件描述符是否在 _event_map 中。如果存在,则调用相应的回调函数进行处理;如果不存在,则从 epoll 中删除该文件描述符。

四、事件监听的添加,修改和删除

4.1 添加事件监听AddEvent

c 复制代码
int EventPoller::addEvent(int fd, int event, PollEventCB cb) {
    // 时间检查
    TimeTicker();

    // 回调函数检查
    if (!cb) {
        WarnL << "PollEventCB is empty";
        return -1;
    }
    // 当前线程检查,
    // 如果是当前线程:根据不同的操作系统平台选择不同的事件通知机制来添加事件监听。
    if (isCurrentThread()) {
#if defined(HAS_EPOLL)
        struct epoll_event ev = {0};
        ev.events = toEpoll(event) ;
        ev.data.fd = fd;
        int ret = epoll_ctl(_event_fd, EPOLL_CTL_ADD, fd, &ev);
        if (ret != -1) {
            _event_map.emplace(fd, std::make_shared<PollEventCB>(std::move(cb)));
        }
        return ret;
#elif defined(HAS_KQUEUE)
        struct kevent kev[2];
        int index = 0;
        if (event & Event_Read) {
            EV_SET(&kev[index++], fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, nullptr);
        }
        if (event & Event_Write) {
            EV_SET(&kev[index++], fd, EVFILT_WRITE, EV_ADD | EV_CLEAR, 0, 0, nullptr);
        }
        int ret = kevent(_event_fd, kev, index, nullptr, 0, nullptr);
        if (ret != -1) {
            _event_map.emplace(fd, std::make_shared<PollEventCB>(std::move(cb)));
        }
        return ret;
#else
#ifndef _WIN32
        // win32平台,socket套接字不等于文件描述符,所以可能不适用这个限制
        if (fd >= FD_SETSIZE) {
            WarnL << "select() can not watch fd bigger than " << FD_SETSIZE;
            return -1;
        }
#endif
        auto record = std::make_shared<Poll_Record>();
        record->fd = fd;
        record->event = event;
        record->call_back = std::move(cb);
        _event_map.emplace(fd, record);
        return 0;
#endif
    }
    // 如果不是当前线程,发起异步操作:
    // 使用 async 函数异步调用 addEvent 函数,将事件添加操作放到合适的线程中执行,并立即返回 0。
    async([this, fd, event, cb]() mutable {
        addEvent(fd, event, std::move(cb));
    });
    return 0;
}

EventPoller::addEvent 函数的主要功能是向事件轮询器中添加一个文件描述符(fd)的事件监听,并关联一个回调函数(cb)。该函数会根据不同的操作系统平台,采用不同的事件通知机制(如 epoll、kqueue 或 select)来添加事件监听,同时会处理回调函数为空以及非当前线程调用的情况。

4.2 删除事件监听 delEvent

c 复制代码
int EventPoller::delEvent(int fd, PollCompleteCB cb) {
    // 时间检查
    TimeTicker();

    // 回调函数检查
    if (!cb) {
        cb = [](bool success) {};
    }

    // 当前线程检查
    // 如果是当前线程:根据不同的操作系统平台选择不同的事件通知机制来删除事件监听。
    if (isCurrentThread()) {
#if defined(HAS_EPOLL)
        int ret = -1;
        if (_event_map.erase(fd)) {
            _event_cache_expired.emplace(fd);
            ret = epoll_ctl(_event_fd, EPOLL_CTL_DEL, fd, nullptr);
        }
        cb(ret != -1);
        return ret;
#elif defined(HAS_KQUEUE)
        int ret = -1;
        if (_event_map.erase(fd)) {
            _event_cache_expired.emplace(fd);
            struct kevent kev[2];
            int index = 0;
            EV_SET(&kev[index++], fd, EVFILT_READ, EV_DELETE, 0, 0, nullptr);
            EV_SET(&kev[index++], fd, EVFILT_WRITE, EV_DELETE, 0, 0, nullptr);
            ret = kevent(_event_fd, kev, index, nullptr, 0, nullptr);
        }
        cb(ret != -1);
        return ret;
#else
        int ret = -1;
        if (_event_map.erase(fd)) {
            _event_cache_expired.emplace(fd);
            ret = 0;
        }
        cb(ret != -1);
        return ret;
#endif //HAS_EPOLL
    }

    // 跨线程操作
    // 使用 async 函数异步调用 delEvent 函数,将事件删除操作放到合适的线程中执行,并立即返回 0
    async([this, fd, cb]() mutable {
        delEvent(fd, std::move(cb));
    });
    return 0;
}

EventPoller::delEvent 函数的主要功能是从事件轮询器中删除指定文件描述符(fd)的事件监听,并在操作完成后调用一个完成回调函数(cb)。该函数会根据不同的操作系统平台,采用不同的事件通知机制(如 epoll、kqueue)来删除事件监听,同时会处理回调函数为空以及非当前线程调用的情况。

4.3 修改监听事件类型 modifyEvent

c 复制代码
int EventPoller::modifyEvent(int fd, int event, PollCompleteCB cb) {

    // 时间检查
    TimeTicker();

    // 回调函数检查与默认设置
    if (!cb) {
        cb = [](bool success) {};
    }

    // 当前线程检查
    // 如果是当前线程:根据不同的操作系统平台选择不同的事件通知机制来修改事件监听。
    if (isCurrentThread()) {
#if defined(HAS_EPOLL)
        struct epoll_event ev = { 0 };
        ev.events = toEpoll(event);
        ev.data.fd = fd;
        auto ret = epoll_ctl(_event_fd, EPOLL_CTL_MOD, fd, &ev);
        cb(ret != -1);
        return ret;
#elif defined(HAS_KQUEUE)
        struct kevent kev[2];
        int index = 0;
        EV_SET(&kev[index++], fd, EVFILT_READ, event & Event_Read ? EV_ADD | EV_CLEAR : EV_DELETE, 0, 0, nullptr);
        EV_SET(&kev[index++], fd, EVFILT_WRITE, event & Event_Write ? EV_ADD | EV_CLEAR : EV_DELETE, 0, 0, nullptr);
        int ret = kevent(_event_fd, kev, index, nullptr, 0, nullptr);
        cb(ret != -1);
        return ret;
#else
        auto it = _event_map.find(fd);
        if (it != _event_map.end()) {
            it->second->event = event;
        }
        cb(it != _event_map.end());
        return it != _event_map.end() ? 0 : -1;
#endif // HAS_EPOLL
    }

    // 如果不是当前线程, 发起异步操作:
    // 使用 async 函数异步调用 modifyEvent 函数,将事件修改操作放到合适的线程中执行。
    async([this, fd, event, cb]() mutable {
        modifyEvent(fd, event, std::move(cb));
    });
    return 0;
}

EventPoller::modifyEvent 函数的主要功能是修改指定文件描述符(fd)对应的事件监听设置,并在操作完成后调用一个完成回调函数(cb)。该函数会根据不同的操作系统平台,采用不同的事件通知机制(如 epoll、kqueue)来修改事件监听,同时会处理回调函数为空以及非当前线程调用的情况。

五、数据结构

5.1 _event_map

在 EventPoller 类中,有一个重要的数据结构 std::unordered_map<int, std::shared_ptr > _event_map,它用于存储网络事件的相关信息。

键(Key):为文件描述符(int 类型),每个文件描述符对应一个网络套接字。

值(Value):为 std::shared_ptr 类型,它是一个智能指针,指向一个事件处理回调函数。当该文件描述符对应的网络事件发生时,会调用该回调函数进行处理。

通过这个数据结构,EventPoller 能够快速地查找和处理不同文件描述符的网络事件,提高事件处理的效率。

5.2 _event_cache_expired的之作用

在EventPoller 类中,_event_cache_expired完整声明为std::unordered_set _event_cache_expired;

5.2.1 在 delEvent 函数中的作用

c 复制代码
int EventPoller::delEvent(int fd, PollCompleteCB cb) {
    // ...
    if (_event_map.erase(fd)) {
        _event_cache_expired.emplace(fd);
        // ...
    }
    // ...
}

标记待删除的文件描述符:当调用 delEvent 函数删除一个文件描述符对应的事件时,如果该文件描述符存在于 _event_map 中(即 _event_map.erase(fd) 返回 true),会将这个文件描述符添加到 _event_cache_expired 集合里。这样做是为了在后续的事件处理循环中标记这个文件描述符已经被删除,以便后续可以跳过对它的处理。

避免误处理已删除的事件:在多线程环境下,删除事件和处理事件可能会并发执行。将待删除的文件描述符添加到 _event_cache_expired 中,可以确保即使在删除操作还未完全完成时,事件处理循环也能识别并跳过这些文件描述符,避免对已删除的事件进行误处理。

5.2.2 在 runLoop 函数中的作用

c 复制代码
void EventPoller::runLoop(bool blocked, bool ref_self) {
    // ...
    _event_cache_expired.clear();
    for (int i = 0; i < ret; ++i) {
        struct epoll_event &ev = events[i];
        int fd = ev.data.fd;
        if (_event_cache_expired.count(fd)) {
            continue;
        }
        // ...
    }
    // ...
}

清空过期事件缓存:在每次 epoll_wait 调用返回且有事件发生时,首先会调用 _event_cache_expired.clear() 清空集合。这是因为每次新的事件轮询周期开始时,需要重新判断哪些文件描述符是需要跳过的,避免上一轮的过期标记影响当前轮次的事件处理。

过滤已标记的文件描述符:在遍历 epoll_wait 返回的事件数组时,对于每个事件对应的文件描述符 fd,会检查它是否存在于 _event_cache_expired 集合中。如果存在,说明这个文件描述符已经在 delEvent 函数中被标记为待删除或需要跳过,此时会直接跳过对该事件的处理,继续处理下一个事件,从而提高事件处理的效率和准确性。

5.2.3 总结

_event_cache_expired 集合在整个事件处理机制中起到了一个中间标记的作用,它在 delEvent 函数中标记需要删除或跳过的文件描述符,在 runLoop 函数中过滤这些标记过的文件描述符,避免重复处理,确保事件处理的正确性和高效性,尤其在多线程环境下可以有效避免并发操作带来的问题。


总结

本文深入分析了开源 zlmediakit 中针对网络套接字的事件监听和事件循环驱动机制。通过对 EventPoller 类和其基类的详细剖析,我们了解了其核心函数 runLoop 的工作原理,以及相关函数和数据结构的作用。在 Linux 系统下,epoll 机制的使用使得网络事件的处理更加高效和灵活。同时,AddEvent()、ModifyEvent() 和 delEvent() 函数提供了对网络事件监听的添加、修改和删除操作,方便开发者根据业务需求进行动态调整。_event_map 数据结构则为网络事件的管理提供了有效的支持。_event_cache_expired 作为 std::unordered_set 类型变量,在事件处理流程中起关键中间标记作用,在 delEvent 函数里标记待删除文件描述符以避免多线程下误处理已删事件,在 runLoop 函数中先清空集合再过滤标记过的描述符来提升事件处理效率与准确性。

通过对这些机制的理解,开发者可以更好地利用 zlmediakit 进行网络编程,实现高效、稳定的网络通信。后续我们还将对异步事件和延时任务进行专门的分析,敬请期待。

相关推荐
yunteng52121 分钟前
音视频(一)ZLMediaKit搭建部署
音视频·zlmediakit·安装搭建
yuzhangfeng2 小时前
【云计算物理网络】从传统网络到SDN:云计算的网络演进之路
网络·云计算
TDengine (老段)2 小时前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
zhu12893035563 小时前
网络安全的现状与防护措施
网络·安全·web安全
zhu12893035564 小时前
网络安全与防护策略
网络·安全·web安全
沫夕残雪5 小时前
HTTP,请求响应报头,以及抓包工具的讨论
网络·vscode·网络协议·http
π2705 小时前
爬虫:网络请求(通信)步骤,http和https协议
网络·爬虫
zhu12893035566 小时前
网络安全基础与防护策略
网络·安全·web安全
古希腊掌握嵌入式的神8 小时前
[物联网iot]对比WIFI、MQTT、TCP、UDP通信协议
网络·物联网·网络协议·tcp/ip·udp
爱写代码的小朋友8 小时前
华三交换机配置常用命令
运维·服务器·网络