目录
类关系及基本功能
类关系
类继承关系
类从属关系
Base 和 Event 类
Procedure 类
Server 类
Session 类
Client 类
DNSClient
UDPServer——基于 libevent 的协程实现
libco 协程接口
创建和销毁协程
进入协程
暂停协程
恢复协程
协程调度实现
暂停和恢复协程
销毁协程
会话模式(Session Mode)
UDPSession 实现
设计目标
recv()
reply()
quit
应用示例
SubRoutine
UDPServer
TCPServer
后记
首页 后端开发 C#.Net教程 基于汇编的 C/C++ 协程(用于服务器)的实现

基于汇编的 C/C++ 协程(用于服务器)的实现

Aug 02, 2018 pm 04:09 PM
c++ 协程 异步io 服务器开发

本篇文章,是 对C/C++ 协程的实现。我们需要实现这两个目标:

  1. 有同步式服务器编程的顺序思路,便于功能设计和代码调试——我使用了 libco 中的协程部分

  2. 有异步 I/O 的性能——我使用了 libevent 中的 event I/O     apache php mysql

结构上,就是将 libco 和 libevent 两者的功能结合起来,所以我把我的工程,命名为 libcoevent,意为 “基于 libevent 的同步协程服务器编程框架”。名字中 co 的意思并不代表 libco,而是 coroutine。

编程语言上,我选择的是 C++,主要是因为 libco 只支持基于 x86 或 x64 架构的 Linux,而这样的架构,基本上都是 PC 机,或者是资源不缺、性能也不错的嵌入式系统,上 C++ 完全没有问题。本文解释代码实现的原理。

如果要使用该工程,请在链接选项中加入 -lco -levent -lcoevent 三个选项。

类关系及基本功能

类关系

类继承关系

类的基本继承关系图如下:

1.png

在实际调用中,只有处于继承关系树的叶子结点上的类才会被实际使用到,其他类均视为虚类。

类从属关系

各类的实例在程序运行中是有从属关系的,除了作为顶层的 Base 类之外,其他树叶类都需依附于其他的类所在的运行环境中才能执行。从属关系图如下:

2.png

  • Base 类提供最基本的运行环境,并管理 Server 对象;

  • Procedure 对象管理 Client 对象。在图中体现为 ServerSession 对象均管理 Client 对象。

    • Server 对象由应用程序创建并初始化到 Base 对象中运行。当服务器结束或当其从属的 Base 对象销毁时,可配置自动销毁 Server 对象。

    • Session 对象由处于会话模式(session mode)的 Server 对象自动创建,并调用应用程序指定的程序入口运行;当会话结束时(函数调用 return)或其从属的 Server 对象服务结束时,由 Server 对象自动销毁。

  • Client 对象由应用程序调用 Procedure 对象的接口创建,用于与第三方服务交互。应用程序可提前调用接口要求销毁 Client 对象,也可以待 Procedure 服务结束时自动统一销毁。

Base 和 Event 类

1.png

Base 类用于运行 libcoevent 的各个服务。每个 Base 类的实例应对应着一个线程,所有的服务以协程的方式在 Base 实例中运行。从上图可知,Base 类包含一个 libevent 库的 event_base 对象和本协程库的一系列 Event 对象。

1.png

Event 类其实是借用了 libevent 的 struct event 名称,因为每一个 Event 类的实例,对应着 libevent 的一个 event 对象。我们需要关注的重点,是 ProcedureClient 类。

Procedure 类

Procedure 类有两个关键特点:

  1. 每个对象都拥有一个 libco 协程,即拥有自己独立的上下文信息,可以用于编写一个独立的服务器过程(procedure);

  2. Procesure 的子类可以创建 Client 对象与第三方服务器通信和交互。

Procedure 类拥有两个子类,分别是 ServerSession

Server 类

Server 类由应用程序创建并初始化到 Base 对象中运行。Server 类有三个子类:

  • SubRoutine:实际上不作为任何服务器程序,但提供了最基本的 sleep() 函数,并支持 Procedure 类的创建 Client 对象的功能,因此应用程序可以用来作为临时创建或常驻的内部程序来使用。

  • UDPServer:应用程序创建并初始化 UDPServer 对象后,程序会自动绑定到一个数据报 socket 接口上。应用可以通过在网络接口中收发数据包来实现网络服务。UDPServer 同时提供普通模式会话模式

  • TCPServer:应用程序创建并初始化 TCPPServer 对象后,程序会自动绑定并监听流 socket。TCPServer 只支持会话模式

所谓的 “普通模式”,也就是应用程序注册 Server 对象的入口函数,并且由应用程序操作 Server 对象的行为。

所谓的 “会话模式”,指的是 UDPServer 或 TCPServer 对象,在接收到传入数据后,自动区分客户端,并单独创建 Session 对象进行处理。每个 Session 对象只服务于一个客户端。

Session 类

Session 对象不能由应用主动创建,而是由处于会话模式的 Server 类自动按需创建。Session 对象的特点是,只能与单一一个客户端(相比起 UDPServer 对象而言)进行通信,因此没有 send() 函数,只有 reply()

在头文件 coevent.h 声明的 Session 类及其子类均为纯虚类,目的是防止应用程序显式地构建 Session 对象并隐藏实现细节。

Client 类

Client 对象由 Procedure 对象创建,并且由 Procedure 对象进行回收。Client 对象的作用是主动向远程服务器发起通信。由于从客户-服务结构的角度,这个动作属于客户端,所以命名为 Client。

DNSClient

Client 的子类中比较特别的是 DNSClient 类,这个类的存在是为了解决在异步 I/O 中的 getaddrinfo() 阻塞问题。DNSClient 的实现原理请参见代码和我之前的文章《DNS 报文结构和个人 DNS 解析代码实现》。

而对于 DNSClient 类而言,具体实现原理,就是封装了一个 UDPClient 对象,通过该对象完成 DNS 报文的收发,并在类中实现报文的解析。

UDPServer——基于 libevent 的协程实现

UDPServer 类普通模式的原理,就是一个非常典型的基于 libevent 的同步协程服务器框架。其代码实现中,核心功能就是以下几个函数:

  • _libco_routine(),协程的入口函数,使用这个函数,转化成为 liboevent 的统一服务入口函数

  • _libevent_callback()libevent 时间回调函数,在这个函数里,实现协程上下文的恢复。

  • UDPServer::recv_in_timeval(),数据接收函数,在这个函数中,实现关键的数据等待功能,同时实现了协程上下文的保存

上述三个函数的代码总量,加上空行也不超过 200 行,我相信还是很容易看明白的。以下具体解释实现原理:

libco 协程接口

正如前文所说,我使用的是 libco 作为协程库。协程对于应用程序是透明的,但是对于库的实现而言,这才是核心。

下面解释一下 libco 的协程功能所提供的几个接口(libco 的文档数量简直 “感人”,这也是网上经常被吐槽的……):

创建和销毁协程

Libco 使用结构体 struct stCoRoutine_t * 保存协程,通过调用 co_create() 可以创建协程对象;使用 co_release() 销毁协程资源。

进入协程

创建了协程之后,调用 co_resume() 可以从协程函数的开头开始执行协程。

暂停协程

当协程到了需要交出 CPU 使用权的时候,可以调用 co_yield() 释放协程、切换掉上下文。调用之后,上下文会恢复到上一个调用 co_resume() 的协程中。调用 co_yield() 的位置可以视为一个 “断点”。

恢复协程

恢复协程和创建协程所用的函数都是 co_resume(),调用该函数,将当前堆栈切换为指定协程的上下文,协程会从上文提到的 “断点” 恢复执行。

协程调度实现

从上一小节可以看到,我们使用到的 libco 协程功能函数中,虽然包含了协程的切换函数,但什么时候切换、切换之后 CPU 如何分配,这是我们需要实现并封装起来的工作。

创建和销毁协程的时机,自然就是在 UDPServer 类初始化和析构的时候。下文重点解析进入、暂停和恢复协程的操作:

进入协程

进入 / 恢复协程的代码,是在 _libevent_callback() 中,有这么一行:

// handle control to user application
co_resume(arg->coroutine);
登录后复制

如果当前协程还没有被执行过,那么执行了这句代码之后,程序会切换到创建 libco 协程时指定的协程函数开始执行。对于 UDPServer,也就是 _libco_routine() 函数。这个函数非常简单,只有三行:

static void *_libco_routine(void *libco_arg)
{
    struct _EventArg *arg = (struct _EventArg *)libco_arg;
    (arg->worker_func)(arg->fd, arg->event, arg->user_arg);
    return NULL;
}
登录后复制

通过传入参数,将 libco 回调函数转换为应用程序指定的服务器函数执行。

但是如何实现第一次的 libevent 回调呢?这还是很简单的,只需要在调用 libevent 的 event_add()时,将超时时间设置为 0 即可,这会导致 libevent 事件立即超时。通过这个机制,我们也就实现了在 Base 运行之后立即执行各 Procedure 服务函数的目的。

暂停和恢复协程

在什么时候调用 co_yield是本协程实现的重点,调用 co_yield 的位置,是一个可能会导致上下文切换的地方,也是将异步编程框架转换为同步框架的关键技术点。这里可以参照 UDPServerrecv_in_timeval() 函数。函数的基本逻辑如下:

1.png

其中最重要的分支,就是对 libevent 事件标志的判断;而最重要的逻辑,就是 event_add()co_yield() 函数的调用。函数片段如下:

struct timeval timeout_copy;
timeout_copy.tv_sec = timeout.tv_sec;
timeout_copy.tv_usec = timeout.tv_usec;
    ...
event_add(_event, &timeout_copy);
co_yield(arg->coroutine);
登录后复制

这里,我们把 co_yield() 函数理解为一个断点,当程序执行到这里的时候,CPU 的使用权会被交出,程序回到调用 co_resume() 的上一级函数手中。这个 “上一级函数” 究竟是哪里呢?实际上就是前文提到的 _libevent_callback() 函数。

_libevent_callback() 的角度来看,程序会从 co_resume() 函数返回,并且继续往下执行。此时我们可以这么理解:协程的调度,实际上是借用了 libevent来进行的。这里我们要关注一下 co_resume() 上方的几句:

// switch into the coroutine
if (arg->libevent_what_ptr) {
    *(arg->libevent_what_ptr) = (uint32_t)what;
}
登录后复制

这里将 libevent 事件 flag 值传递给了协程,而这是前文进行事件判断的重要依据。当时间到来,_libevent_callback() 会在下面调用 co_resume() 的位置,将 CPU 使用权交回给协程。

销毁协程

除了 ci_yield() 之外,协程函数调用 return 也会导致从 co_resume() 返回,所以在 _libevent_callback() 中,我们还需要判断协程是否已经结束。如果协程结束,那么就应当销毁相关的协程资源了。参见 if (is_coroutine_end(arg->coroutine)) {...} 条件体内的代码。

会话模式(Session Mode)

在本工程的实现中,提供了被称为 “会话模式” 的一个服务器设计模式。会话模式指的是 UDPServer 或 TCPServer 对象,在接收到传入数据后,自动区分客户端,并单独创建 Session 对象进行处理。每个 Session 对象只服务于一个客户端。

对于 TCPServer 而言,实现上述的功能比较简单,因为监听一个 TCP socket 之后,当有传入连接的时候,只要调用 accept(),就可以获得一个新的文件描述符,为这个文件描述符创建一个新的 Server 的子类就行了——这就是 TCPSession 类。

但是 UDPServer 就比较麻烦了,因为 UDP 不能这么做。我们只能自行实现所谓的 session。

UDPSession 实现

设计目标

我们需要实现 UDPSession 类的如下效果:

  • 类调用 recv 函数时,只会接收到对应的远程客户端发来的数据

  • 类调用 send 函数(实际实现是 reply())时,可以使用 UDPServer 的端口进行回复

recv()

在工程中,UDPSession 是抽象类,实际实现是 UDPItnlSession。但是准确而言,UDPItnlSession 的实现,密切依赖于 UDPServer。这一部分,可以参照 UDPServer_session_mode_worker() 函数中的 do-while() 循环体代码。程序思路如下:

  • UDPServer 维护一个 UDPSession 字典,以远程 IP + 端口名的组合作为 key。

  • 当数据到来时,判断远程 IP + 端口的组合是否在字典中,如果在,那么就把数据复制给对应的 session;如果不存在,则创建 session

复制数据的代码,参见 UDPItnlSession 类的 forward_incoming_data() 函数实现。

reply()

发送数据其实就很简单,直接对 UDPServer 的 fd 进行 sendto() 就可以了。

quit

对于 session mode 的 Server 对象,代码中提供了一个可以由其 session 调用的、要求 server 退出并销毁资源的函数:quit_session_mode_server()。实现原理是向 server 触发一个 EV_SIGNAL 事件。对于普通的 I/O 事件而言,这是不应当出现的,我们这里活用来作为退出信号。如果 server 发现了这个信号,则触发退出逻辑。

应用示例

本工程的示例代码分为 server 和 client 两部分,其中 server 用到了 libcoevent,而 client 只是使用 Python 写的简单程序。本文就不说明 client 部分的代码了。

Server 的代码,分别针对 Server 类的三个子类做了应用示例。使用了包括空行、调试语句、错误判断等在内的逻辑,仅使用不到 300 行,就实现了一个过程和两个服务。应该说,逻辑还是很清晰的,而且也节省了大量代码。

SubRoutine

通过函数 _simple_test_routine(),展示了一个一次性的线性网络逻辑。程序中,routine 首先创建了一个 DNSClient 对象,向默认域名服务器请求了一个域名,然后 connect() 该服务器的 80 端口。成功后,直接返回。

这个函数展示了 SubRoutine 的使用场景,以及 Client 对象的使用方法,特别是 DNSClient 的简易使用方法。

UDPServer

UDPServer 的入口函数是 _udp_session_routine(),功能是为客户端提供域名查询服务。Clients 发送一段字符串作为待查询域名,然后 server 通过 DNSClient 对象请求后,将查询结果返回给客户端。

这个函数展示了 UDPSession 对象和 DNSClient 的(比较复杂和完整的)使用方法。

TCPServer

入口函数是 _tcp_session_routine(),逻辑比较简单,主要是展示 TCPSession 的用法。

后记

原理上,libcoevent 已经开发完了,实现了必须的功能,完全可以用来编写服务器程序。当然由于这是初版,所以很多代码看起来还是有点乱。这个库的意义在于,可以从教学角度,仔细地说明 C/C++ 协程更为本源的实现原理,也可以作为一个可用的协程服务器库来使用。

欢迎读者针对这个库多多批判,也欢迎读者提出新需求——比如我就决定加几个需求,算是 TODO 吧:

  1. 实现 HTTPServer,作为 TCPServer 的子类,提供 HTTP fcgi 服务;

  2. 实现 SSLClient 的类,处理对外的 SSL 请求。

相关文章:

C#网络编程系列文章(八)之UdpClient实现同步UDP服务器

C语言实现php服务器

相关视频:

C# 教程

以上是基于汇编的 C/C++ 协程(用于服务器)的实现的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何在C++中实现策略设计模式? 如何在C++中实现策略设计模式? Jun 06, 2024 pm 04:16 PM

策略模式在C++中的实现步骤如下:定义策略接口,声明需要执行的方法。创建具体策略类,分别实现该接口并提供不同的算法。使用上下文类持有具体策略类的引用,并通过它执行操作。

Golang 与 C++ 的异同 Golang 与 C++ 的异同 Jun 05, 2024 pm 06:12 PM

Golang和C++分别是垃圾回收和手动内存管理编程语言,语法和类型系统各异。Golang通过Goroutine实现并发编程,C++通过线程实现。Golang内存管理简单,C++性能更强。实战案例中,Golang代码更简洁,C++性能优势明显。

如何在C++中实现嵌套异常处理? 如何在C++中实现嵌套异常处理? Jun 05, 2024 pm 09:15 PM

嵌套异常处理在C++中通过嵌套的try-catch块实现,允许在异常处理程序中引发新异常。嵌套的try-catch步骤如下:1.外部try-catch块处理所有异常,包括内部异常处理程序抛出的异常。2.内部try-catch块处理特定类型的异常,如果发生超出范围的异常,则将控制权交给外部异常处理程序。

如何使用C++模板继承? 如何使用C++模板继承? Jun 06, 2024 am 10:33 AM

C++模板继承允许模板派生类重用基类模板的代码和功能,适用于创建具有相同核心逻辑但不同特定行为的类。模板继承语法为:templateclassDerived:publicBase{}。实例:templateclassBase{};templateclassDerived:publicBase{};。实战案例:创建了派生类Derived,继承了基类Base的计数功能,并增加了printCount方法来打印当前计数。

如何遍历C++ STL容器? 如何遍历C++ STL容器? Jun 05, 2024 pm 06:29 PM

要遍历STL容器,可以使用容器的begin()和end()函数获取迭代器范围:向量:使用for循环遍历迭代器范围。链表:使用next()成员函数遍历链表元素。映射:获取键值对迭代器,使用for循环遍历。

在Docker环境中使用PECL安装扩展时为什么会报错?如何解决? 在Docker环境中使用PECL安装扩展时为什么会报错?如何解决? Apr 01, 2025 pm 03:06 PM

在Docker环境中使用PECL安装扩展时报错的原因及解决方法在使用Docker环境时,我们常常会遇到一些令人头疼的问�...

如何处理跨线程的C++异常? 如何处理跨线程的C++异常? Jun 06, 2024 am 10:44 AM

在多线程C++中,异常处理通过std::promise和std::future机制实现:在抛出异常的线程中使用promise对象记录异常。在接收异常的线程中使用future对象检查异常。实战案例展示了如何使用promise和future在不同线程中捕获和处理异常。

char在C语言字符串中的作用是什么 char在C语言字符串中的作用是什么 Apr 03, 2025 pm 03:15 PM

在 C 语言中,char 类型在字符串中用于:1. 存储单个字符;2. 使用数组表示字符串并以 null 终止符结束;3. 通过字符串操作函数进行操作;4. 从键盘读取或输出字符串。

See all articles