DICOM:剖析Orthanc中的Web Server,Mongoose之“连接请求触发的事件序列”(二)
背景:
Orthanc是本专栏中介绍过的一款新型DICOM服务器,具有轻量级、支持REST的特性,可将任意运行Windows和Linux系统的计算机变成DICOM服务器,即miniPACS。Orthanc内嵌多种模块,数据库管理简单,且不依赖于第三方软件。因此通过剖析Orthanc源码可以学习到搭建DICOM系统中的各个环节,例如SQLite嵌入型数据库、GoogleLog日志库、DCMTK医学DICOM库,以及近期要介绍的开源Web Server,Mongoose。
题记:
近期计划参照官网剖析Mongoose源码的同时进行实例讲解。开源软件的一大优势就是源代码公开,通过剖析源码可以学习其优良的设计架构和实现模式,然而对于代码上万行,甚至十几万行开源软件,单纯的钻研源代码可能会陷入其中而无法自拔。往往在感叹作者设计巧妙时,迷失了方向;在为各种逻辑求根溯源时,失去了信心;最终在悔恨自己能力有限的同时,放弃了努力。很多良好的架构和模式,其实不一定是设计出来的,至少可以说不是“提前”设计出来的。任何开源项目都是从无到有,从起步到成熟,一开始就阅读已成熟的源码,享受别人思想果实自然会“无福消受”。要想搞清楚为何设计这种数据结构?为何设计如此运行逻辑?——要从应用场景(context)出发,考虑项目的起源和应用,切记任何设计都是为应用服务的。因此后续博文会采用源码分析+实例讲解的方式来进行剖析和学习,还是那句话“看源码累了就调试调试实例,调试累了就钻研钻研源码”,劳(源码)逸(实例)结合,脚踏实地,Keep Running……
HTTP请求:
当今互联网和移动互联网已经人人皆知,平时我们上网最用的就是HTTP请求,即从客户端(PC、手机)到服务器的请求消息。至于HTTP请求的具体格式就不介绍了,既然是协议就是用来约定双方交互的规则,让服务端知道客户端想做啥,同时让客户端理解服务端给了啥。
HTTP是基于TCP/IP的应用层协议,因此在服务端和客户端交流信息之前首先要建立TCP连接。关于连接建立的方式有很多,比如每次请求都建立新的连接、网页不同部分请求建立不同的连接等等。借用博文Tornado HTTP服务器的基本流程中的一张HTTP流程图,大多数HTTP服务端该流程基本相似,Mongoose也一样,如下图所示:
Mongoose与Fossa:
上一篇专栏博文介绍了Mongoose是一个小型的、可嵌入的HTTP Server。只要在自己工程中引入mongoose.h和mongosoe.c源码文件即可快速开启一个HTTP Server。Mongoose支持REST和JSON-RPC服务(这也是我打算分析Mongoose的根本原因,希望仿照Orthanc将Mongoose嵌入到传统PACS系统中,对传统PACS系统进行分布式改造)。
打开Mongoose在Github上的仓库你会发现Mongoose是建立在NS Skeleton框架之上的,而NS Skeleton者的就是Fossa。官方对Fossa的描述为:Fossa是一个用C语言编写的支持多协议的网络编程库。采用基于事件驱动的模式,方便用户实现各种网络协议,开发可扩展网络应用。
截至目前我们可以认为Fossa是底层网络开发库,Mongoose是对Fossa的一次封装,强化了事件驱动,方便用户快速上手,而Orthanc是将Mongoose进行了再一次封装,强化了REST功能,并提供了与SQLite数据库和DICOM协议相关的接口。
Mongoose与Fossa的事件:
如上所述Fossa是基于事件驱动的,Mongoose是对Fossa的封装,因此同样采用事件驱动方式。但是两者的事件略有不同,如下表所示:
Fossa | Mongoose |
NS_CONNECT |
MG_CONNECT |
NS_RECV |
MG_REQUEST |
NS_SEND |
MG_POLL |
NS_POLL |
MG_REPLY |
NS_ACCEPT |
MG_CLOSE |
NS_CLOSE |
MG_HTTP_ERROR |
Fossa官网中对于HTTP请求中的事件做了较详细的介绍,如下表。可能是Mongoose对Fossa封装变动不大的原因,在Mongoose官网中并未找到关于MG_XXX事件的详细说明,文档API.md中只是介绍了各个事件的使用方式。
NS_ACCEPT:当监听端口接收到新的请求连接时,触发NS_ACCEPT事件 |
NS_CONNECT:当使用ns_connect创建新的连出连接时,无论新的连接创建失败还是成功,都触发NS_CONNECT事件 |
NS_RECV:当新数据接收完成且写入到接收缓冲区后,触发NS_RECV事件 |
NS_SEND:当数据被写入远程节点且从输出缓冲区清除后,触发NS_SEND事件 |
NS_POLL:NS_POLL会被发送到ns_mgr_poll的连接链表中的每个连接,用于进行清理工作(housekeeping),例如检测是否超时、发送心电包等等。 |
NS_CLOSE:连接断开时触发。【注】:具体的NS_CLOSE事件何时出发官方文档中并未给出,后续通过多组实例测试再详细介绍事件触发条件。 |
实例讲解:
实例测试:
简单的理论介绍后,已经大致了解了HTTP请求的流程。首先,需要绑定端口(在Windows下就是socket操作),建立TCP连接;然后再根据不同的事件进行相应的处理。 为了更直观的了解Mongoose中HTTP请求的具体流程,我们以安装说明文档Embbed.md中的例子为基础,在VS中添加NS_ENDALBE_DEBUG预定义,开启Mongoose源码中原本存在的调试信息;另外修改mongoose.c中的mg_ev_handler函数,对Fossa的每一种事件添加调试语句,输出当前事件类型。
1)重新生成MongooseEmbbed工程,开启调试状态;
2)打开谷歌浏览器,输入:http://localhost:8080,调试结果如下:
默认情况下Mongoose与其他Http Server相同,会默认显示当前目录下的文件。并添加了超链接。上图右侧就是MongooseEmbbed工程所在的主目录下所有文件的索引,并且已经给出了各个索引的超链接。
Mongoose事件触发流程调试:
从控制台输出的信息可以看出启动初期浏览器还未发起任何请求之前,程序处于主函数内的for循环中,循环输出调试信息;
当浏览器地址栏中输入http://localhost:8080后,接下来的for循环中,mg_poll_server函数输出了NS_ACCEPT调试信息,说明触发了NS_ACCEPT事件;
下一个循环中,同时输出了NS_POLL和NS_RECV两种事件的调试信息;
然后同时输出了NS_POLL、NS_SEND、NS_CLOSE三种事件调试信息;
最后进入NS_POLL的轮询状态,
从目前的调试信息来看,Mongoose对于浏览器发起的连接请求处理的基本流程遵循:NS_ACCEPT->NS_RECV->NS_SEND->NS_POLL->NS_CLOSE。这与Fossa官方说明基本相同,但是在整个流程中会出现多次NS_POLL和NS_ACCEPT,这究竟是什么原因呢?想要搞清楚整个问题,需要对HTTP Web Server的整体响应流程有详细的了解,需要分析ns_poll_server源码中的具体流程。目前该部分思路还未整理完成,打算放到下一篇博文中,敬请关注