有同步式效劳器编程的递次思绪,便于功用设想和代码调试——我运用了 libco 中的协程部份
有异步 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
三个选项。
类关联及基础功用
类关联
类继承关联
类的基础继承关联图以下:
在现实挪用中,只需处于继承关联树的叶子结点上的类才会被现实运用到,其他类均视为虚类。
类隶属关联
各种的实例在顺序运转中是有隶属关联的,除了作为顶层的 Base 类以外,其他树叶类都需依附于其他的类地点的运转环境中才实行。隶属关联图以下:
Base 类供应最基础的运转环境,并治理 Server 对象;
Procedure 对象治理 Client 对象。在图中体现为 Server 和 Session 对象均治理 Client 对象。
Server 对象由运用顺序建立并初始化到 Base 对象中运转。当效劳器完毕或当其隶属的 Base 对象烧毁时,可设置自动烧毁 Server 对象。
Session 对象由处于会话形式(session mode)的 Server 对象自动建立,并挪用运用顺序指定的顺序进口运转;当会话完毕时(函数挪用
return
)或其隶属的 Server 对象效劳完毕时,由 Server 对象自动烧毁。Client 对象由运用顺序挪用 Procedure 对象的接口建立,用于与第三方效劳交互。运用顺序可提早挪用接口要求烧毁 Client 对象,也能够待 Procedure 效劳完毕时自动一致烧毁。
Base 和 Event 类
Base 类用于运转 libcoevent 的各个效劳。每一个 Base 类的实例应对应着一个线程,一切的效劳以协程的体式格局在 Base 实例中运转。从上图可知,Base 类包含一个 libevent 库的 event_base
对象和本协程库的一系列 Event 对象。
Event 类现实上是借用了 libevent 的 struct event
称号,因为每一个 Event 类的实例,对应着 libevent 的一个 event
对象。我们须要关注的重点,是 Procedure 和 Client 类。
Procedure 类
Procedure 类有两个症结特性:
每一个对象都具有一个 libco 协程,即具有本身自力的上下文信息,能够用于编写一个自力的效劳器历程(procedure);
Procesure 的子类能够建立 Client 对象与第三方效劳器通讯和交互。
Procedure 类具有两个子类,离别是 Server 和 Session。
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
的位置,是一个可能会致使上下文切换的处所,也是将异步编程框架转换为同步框架的症结技术点。这里能够参照 UDPServer 的 recv_in_timeval()
函数。函数的基础逻辑以下:
个中最主要的分支,就是对 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 吧:
完成 HTTPServer,作为 TCPServer 的子类,供应 HTTP fcgi 效劳;
完成 SSLClient 的类,处置惩罚对外的 SSL 要求。
相干文章:
C#收集编程系列文章(八)之UdpClient完成同步UDP效劳器
C言语完成php效劳器
相干视频:
C# 教程
以上就是基于汇编的 C/C++ 协程(用于效劳器)的完成的细致内容,更多请关注ki4网别的相干文章!