DICOM:剖析Orthanc中的Web Server,Mongoose之 Flag bit & Event(三)_orthanc 源码解析-程序员宅基地

技术标签: DICOM医学图像处理  Fossa  Orthanc  DICOM  Mongoose  

背景:

Orthanc是本专栏中介绍过的一款新型DICOM服务器,具有轻量级、支持REST的特性,可将任意运行Windows和Linux系统的计算机变成DICOM服务器,即miniPACS。Orthanc内嵌多种模块,数据库管理简单,且不依赖于第三方软件。因此通过剖析Orthanc源码可以学习到搭建DICOM系统中的各个环节,例如SQLite嵌入型数据库、GoogleLog日志库、DCMTK医学DICOM库,以及近期要介绍的开源Web Server,Mongoose。

上一篇博文中简单的分析了Mongoose中连接请求触发的事件序列,调试输出结果大致上符合Fossa官网给出的*NS_ACCEPT->(NS_RECV->NS_SEND->NS_POLL…)->NS_CLOSE 流程,但是实际运行时刻由于网络环境实时变化,因此输出的调试日志中偶尔会出现多次NS_ACCEPT或者多次NS_POLL等等。博文末尾提到要想了解事件触发的真正原因,需要分析ns_poll_server函数源码,接下来通过分析Mongoose和Fossa的设计来对事件触发有一个更全面的了解。*

Mongoose事件

官方说明文档中对于Mongoose的描述是:

Mongosoe has single-threaded, event-driven, asynchronous, non-blocking core. 这个内核就是Fossa。这其中单线程(single-threaded)指的就是主线程中的mg_poll_server循环。

mg_poll_server函数内部遍历所有的有效连接,通过 select 异步方式监控各连接套接字来完成一次IO迭代操作。如此反复直至处理完毕。每当select返回,针对状态发生变化的套接字(有数据要发送或接收)进行IO操作。但是mg_poll_server本身并不完成循环遍历,需要外部循环调用mg_poll_server来实现实时监控连接状态。

查看代码的话发现mg_poll_server内部就是简单的调用了ns_mgr_poll函数,那么我们看一下Fossa对该函数的描述:

Fossa是一个支持多种协议的网络库,实现了非阻塞、异步IO处理,提供基于事件的API。Fossa的使用方式是,先声明并初始化事件处理程序,创建连接;最后通过循环调用ns_mgr_poll函数实现事件监控。ns_mgr_poll迭代遍历所有套接字,接收新连接、发送和接收数据、关闭连接,并根据 具体事件 调用相应的 事件处理函数

Fossa中要求每个连接需要绑定事件处理函数,即event handler function,由用户自定义实现。事件处理是Fossa应用的核心元素——设定了程序的功能。Mongoose就是对Fossa的一次封装,规定了各种事件的默认处理程序,因此直接复制粘贴Mongoose官方文档中的示例代码就可以开启一个简单的Web Server,具体代码如下:

    #include "mongoose.h"
    int main(void) {

      struct mg_server *server = mg_create_server(NULL, NULL);
      mg_set_option(server, "document_root", ".");  // Serve current directory
      mg_set_option(server, "listening_port", "8080");  // Open port 8080

      for (;;) {
        mg_poll_server(server, 1000);   // Infinite loop, Ctrl-C to stop
      }
      mg_destroy_server(&server);

      return 0;
    }

上述代码中并未像Fossa官网所述,给出用户自定义的事件处理函数却能顺利开启Web Server( 详情可参考博文DICOM:剖析Orthanc中的Web Server, Mongoose ),这恰恰说明了Mongoose在对Fossa进行封装时给出了默认的事件处理函数,即mg_ev_handler,在该函数内部规定了Fossa中各种事件的处理流程,由于代码过长此处就不贴出来了,详情可参考Link:mg_ ev_handler。所以在使用Mongoose时主要关注的是自定义事件,另外Mongoose对Fossa的事件进行了简单的再封装,以MG_开头来标记事件,诸如MG_AUTH、MG_REQUEST、MG_CONNECT、MG_REPLY。

Fossa标志

由上面介绍了解到Mongoose重点是对Fossa的自定义事件进行二次封装,其主要贡献是设计了Fossa事件的默认处理流程函数,即上面提到的mg_ev_handler。因此要想解决上一篇博文中的疑问事件真正触发的原因是什么? 。通过分析mg_ev_handler源码只能是了解了Fossa事件的触发机制,并未真正了解原因。因此要想解决疑惑,需要分析Fossa的处理核心,即ns_mgr_poll 。在Fossa中对于每一个连接都包含相应的flag bit field,即标志位状态位特征位。而flag bit目的就是用于区别连接(这里的连接是名词,代表所有与Fossa相关的请求。在Fossa中将连接分为三类,即Inbound、Outbound和Listening)整个生命周期所处的不同阶段,对每个阶段用一个flag来表示。 ns_mgr_poll正是根据flag bit来分情况处理各种连接,比如添加新连接、开始读取数据、开始发送数据、关闭连接等等;也正是由于flag bit将连接的各个阶段区分开来,才使得能够将不同阶段的处理进行模式化,也就是事件化

所以要想搞清楚之前博文中事件的触发流程,根本是需要了解Fossa和Mongoose是如何用flag bit来表示连接的各个阶段的。Fossa官方文档指出每个连接都有标志位域。Fossa针对不同协议定义了多种标志,其中一些标志由Fossa来设置,部分标志需要由外部用户自定义的事件处理函数来设置(以此来完成用户与Fossa的交互)。下面列举主要的标志:

  • NSF_FINISHED_SENDING_DATA
  • NSF_BUFFER_BUT_DONT_SEND
  • NSF_CLOSE_IMMEDIATELY
  • NSF_USER1/NSF_USER2/NSF_USER3/NSF_USER4

以上标志位都是由外部用户自定义的事件处理函数来设置的,下面看一下Fossa内部设置的标志位,

  • NSF_SSL_HANDSHAKE_DOWN
  • NSF_CONNECTING
  • NSF_LISTENING
  • NSF_WEBSOCKET_NO_DEFRAG
  • NSF_IS_WEBSOCKET

根据标志位名称大致猜测出标志位是跟连接建立过程 连接具体状态 相关,由此可知标志位域(flag bit field) 关乎http web server的处理流程,是Fossa和Mongoose开源库内部的核心逻辑,所以需要Fossa内部自己实现——开源库中往往都会将协议规定的流程化部分自己实现,只将可定制化部分交由用户自定义。

找准了问题入手的方向,下面就以Mongoose官方文档为例进行实例测试:

实例测试

为了方便查看,再一次将官方安装说明中的代码贴在此处,如下:

        
    #include "mongoose.h"
    int main(void) {

      struct mg_server *server = mg_create_server(NULL, NULL);
      mg_set_option(server, "document_root", ".");  // Serve current directory
      mg_set_option(server, "listening_port", "8080");  // Open port 8080

      for (;;) {
        mg_poll_server(server, 1000);   // Infinite loop, Ctrl-C to stop
      }
      mg_destroy_server(&server);

      return 0;
    }

另外为了跟踪Fossa的ns_mgr_poll函数中各阶段标志位的情况,对mongoose.c中的ns_mgr_poll代码修改,添加相应的调试输出信息。具体修改如下:

      time_t ns_mgr_poll(struct ns_mgr *mgr, int milli) {
      int loop=0;

      struct ns_connection *conn, *tmp_conn;
      struct timeval tv;
      fd_set read_set, write_set;
      sock_t max_fd = INVALID_SOCKET;
      time_t current_time = time(NULL);

      FD_ZERO(&read_set);
      FD_ZERO(&write_set);
      ns_add_to_set(mgr->ctl[1], &read_set, &max_fd);

      for (conn = mgr->active_connections; conn != NULL; conn = tmp_conn) {
          printf("The for loop in adding sock or conn section is %d times\n",loop++);//Just for debugging
    tmp_conn = conn->next;
    if (!(conn->flags & (NSF_LISTENING | NSF_CONNECTING))) {
            printf("For the Flag --%d-- ,For the sock --%d--,Call user ev_handler for NS_POLL\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_call(conn, NS_POLL, ¤t_time);
    }
    if (!(conn->flags & NSF_WANT_WRITE)) {
      //DBG(("%p read_set", conn));
            printf("For the Flag --%d--,For the sock --%d--, call ns_add_to_set function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_add_to_set(conn->sock, &read_set, &max_fd);
    }
    if (((conn->flags & NSF_CONNECTING) && !(conn->flags & NSF_WANT_READ)) ||
    (conn->send_iobuf.len > 0 && !(conn->flags & NSF_CONNECTING) &&
     !(conn->flags & NSF_BUFFER_BUT_DONT_SEND))) {
      //DBG(("%p write_set", conn));
                 printf("For the Flag --%d--2--,For the sock --%d-- call ns_add_to_set function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_add_to_set(conn->sock, &write_set, &max_fd);
    }
    if (conn->flags & NSF_CLOSE_IMMEDIATELY) {
            printf("For the Flag --%d--, For the sock --%d-- call ns_close_conn function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_close_conn(conn);
    }
      }

      tv.tv_sec = milli / 1000;
      tv.tv_usec = (milli % 1000) * 1000;
      loop=0;
      if (select((int) max_fd + 1, &read_set, &write_set, NULL, &tv) > 0) {
    // select() might have been waiting for a long time, reset current_time
    // now to prevent last_io_time being set to the past.
    current_time = time(NULL);

    // Read wakeup messages
    if (mgr->ctl[1] != INVALID_SOCKET &&
    FD_ISSET(mgr->ctl[1], &read_set)) {
      struct ctl_msg ctl_msg;
      int len = (int) recv(mgr->ctl[1], (char *) &ctl_msg, sizeof(ctl_msg), 0);
      send(mgr->ctl[1], ctl_msg.message, 1, 0);
      if (len >= (int) sizeof(ctl_msg.callback) && ctl_msg.callback != NULL) {
    struct ns_connection *c;
    for (c = ns_next(mgr, NULL); c != NULL; c = ns_next(mgr, c)) {
      ctl_msg.callback(c, NS_POLL, ctl_msg.message);
    }
      }
    };
    for (conn = mgr->active_connections; conn != NULL; conn = tmp_conn) {
        printf("The for loop in select section is %d times\n",loop++);
      tmp_conn = conn->next;
      if (FD_ISSET(conn->sock, &read_set)) {
    if (conn->flags & NSF_LISTENING) {
                printf("For the Flag --%d--, For the sock --%d--, NSF_LISTENING!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      if (conn->flags & NSF_UDP) {
                  printf("For the Flag --%d--,For the sock --%d--, call ns_handler_udp function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
    ns_handle_udp(conn);
      } else {
    // We're not looping here, and accepting just one connection at
    // a time. The reason is that eCos does not respect non-blocking
    // flag on a listening socket and hangs in a loop.
                  printf("For the Flag --%d--,For the sock --%d-- call accept_conn function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
    accept_conn(conn);
      }
    } else {
      conn->last_io_time = current_time;
              printf("For the Flag --%d--,For the sock --%d-- call ns_read_from_socket function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_read_from_socket(conn);
    }
      }

      if (FD_ISSET(conn->sock, &write_set)) {
    if (conn->flags & NSF_CONNECTING) {
                printf("For the Flag --%d--,For the sock --%d-- call ns_read_from_socket function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_read_from_socket(conn);
    } else if (!(conn->flags & NSF_BUFFER_BUT_DONT_SEND)) {
      conn->last_io_time = current_time;
              printf("For the Flag --%d--,For the sock --%d-- call ns_write_to_socket function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_write_to_socket(conn);
    }
      }
    }
      }
      loop=0;
      for (conn = mgr->active_connections; conn != NULL; conn = tmp_conn) {
          printf("The for loop in the close section is %d times\n",loop++);
    tmp_conn = conn->next;
    if ((conn->flags & NSF_CLOSE_IMMEDIATELY) ||
    (conn->send_iobuf.len == 0 &&
      (conn->flags & NSF_FINISHED_SENDING_DATA))) {
                  printf("For the Flag --%d--2--,For the sock --%d-- call ns_close_conn function!\n",conn->flags,(conn->sock!=NULL?conn->sock:-1));//Just for debugging
      ns_close_conn(conn);
    }
      }

      return current_time;
    }

【注】:代码中的printf输出语句就是为了方便调试添加的,待测试完毕请自行删除,以免影响Mongoose服务器性能。

测试结果

ns_mgr_poll函数内部结构可知,函数内部主要分成三大功能模块:
1. 链接配置阶段(即添加新连接到服务端链表,及设置对各个连接的可读或可写性检查)
2. 链接监控阶段(利用select异步模型监控各连接读写状态)
3. 链接清理阶段(根据连接实际状态查看是否需要关闭)

在调试信息中的结果与上述三大类一一对应,为了方便查看调试结果对不同的调试信息使用了不同的背景色。如下图所示:
这里写图片描述

该图表明Mongoose Web Server初始化完成后的状态,此刻mg_server的连接链表中只包含初始化时的监听端口,即listening connection

接下来在浏览器中输入http://localhost:8080,回车后调试日志结果如下图:
这里写图片描述

从图中我们可以看出原本的listening connection在select时检测到有新的链接接入,即Inbound connection。在链接配置阶段将新接受的Inbound添加到服务端的连接链表中,且插入位置为表头,由下一阶段链接监控阶段的for循环输出日志可以确定插入位置是表头。
这里写图片描述
对于数据的接收和发送,Fossa内部使用了缓冲机制,其缓冲结构如下图所示:
Fossa缓冲结构

待数据接收和发送处理完成后,起初Mongoose服务的连接链表中还保存三个连接,随着时间的推移,除了listening connection以外的两个连接逐个关闭,Mongoose Web Server又恢复到初始化状态。
这里写图片描述

从上面的调试日志中可以看出在ns_mgr_poll函数内部的三大模块中主要是根据连接链表中各连接的flag bit来进行分类处理,实现端口监控连接接入接收和发送数据连接关闭等功能。这也正是我们上篇博文中希望深入研究的部分,此次博文中只是分析调试了Mongoose官网的测试实例,至于原理性的东西可能要牵扯到HTTP协议和具体的实现时序图等内容,具体细节会在后续文章中给出,敬请期待。

第一次使用MarkDown写CSDN博文,不知道效果如何,^_^。用浏览器查看了一下原来CSDN博文的源码,看来原本的代码格式在常见的MarkDown编辑器不存在,只能手动拷贝CSDN博文的自由格式,如下图:






作者:[email protected]

时间:2015-02-10

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zssureqh/article/details/43716909

智能推荐

Python 与 JavaScript 语法差异点_js python 语法 区别-程序员宅基地

文章浏览阅读1.2k次。随着人工智能技术的普及,越来越多的前端程序员开始关注相关技术。Python 作为人工智能领域最常用的语言,与前端程序员日常使用的语言 JavaScript 同属脚本语言,且在两者发展过程中,社区也多有相互借鉴之处,因此有很多相似。一个熟悉 JavaScript 语言的前端程序员,通过掌握了他们之间的不同之处,可以快速上手 Python 。以下是我学习过程中记录的 Python 不同于 JavaSc..._js python 语法 区别

深谈德国车和日本车的区别--觉得分析的还算冷静客观-程序员宅基地

文章浏览阅读744次。《德系VS日系》比较客观的文章作者:颜宇鹏,新车评网创办人之一、总编辑、首席车评人、视频版主持人。从业超过十年,曾任专业汽车杂志试车总监、主编。阅车无数,对全球车型发展、中国汽车市场、试车驾驶技术有深厚积累,其见解独到的车评备受读者喜爱和业界推崇。在讨论“德系VS日系”这个话题时,我想先确立以下几点基础原则。..._2019新君威的前后防碰梁

ajax上传文件、FormData上传文件、html5上传文件、多文件上传_ajajx提交文件到php-程序员宅基地

文章浏览阅读448次。首先简单了解一下 FormData,点击链接:https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/Using_FormData_ObjectsIE浏览器版本最好为:IE10+,因为 FormData 兼容性问题然后查看示例,代码如下↓html 代码:<!DOCTYPE html><html lan..._ajajx提交文件到php

端口扫描技术_在端口扫描中隐蔽扫描能够更精准的判断出端口是否开启-程序员宅基地

文章浏览阅读3.6k次。(79条消息) 端口扫描原理和划分_HideInTime的博客-程序员宅基地_扫描器通过什么来判断端口的状态一、端口分类固定端口(0-1023):TCP的21(ftp),80(http),139(netbios),UDP的7(echo),69(tftp)等等动态端口(1024-49151):这些端口并不被固定的捆绑于某一服务,操作系统将这些端口动态的分配给各个进程, 同一进程两次分配有可能分配到不同的端口二、常用端口扫描技术1. 全TCP连接Connect()扫描此扫描试图与每一..._在端口扫描中隐蔽扫描能够更精准的判断出端口是否开启

Android apk 分析工具:APK Analyzer_什么工具可以比较两个apk 之间的差异-程序员宅基地

文章浏览阅读1.2w次,点赞2次,收藏6次。前言Android studio 2.2 版本后自带有一个分析工具:APK Analyzer。这是一个专门分析 apk 的神器,主要作用如下: 1. (重要) 直观的看到 apk 中各个文件的大小 (比如 DEX,resource 等等)。我们可根据文件大小信息,减小 apk 的大小; 2. (重要) 学习大企业 app 的命名规范和目录架构规范,还可以查看大公司 app 使用了什么技术和第三方_什么工具可以比较两个apk 之间的差异

web使用自动扫描器_web目录扫描器-程序员宅基地

文章浏览阅读448次。4、有了关于现有漏洞的信息,我们可以跟踪引用并搜索已发布的漏洞,例如,如果我们在用户注册表中搜索CVE-2007-5106,这是一个XSS 漏洞,我们将在Security Focus中找到一个漏洞:https://www.securityfocus.com/bid/25769/exploit。1、我们将扫描Pruggia 漏洞应用程序,并将结果导出到一个HTML报告文件,该命令为:nikto -h http://192.168.120.129/peruggia/-o result.html。_web目录扫描器

随便推点

云锁linux宝塔安装,【最新版】宝塔面板下为Nginx自编译云锁Web防护模块教程-程序员宅基地

文章浏览阅读639次。相信很多站长在使用宝塔面板的同时也会安装云锁用于加固服务器安全性,不过有时因为Nginx版本过高等问题导致安装云锁时无法自动安装Web防护模块,所以还需要我们在Linux系统下额外将云锁Web防护模块编译进Nginx才可以。之前也转载过一篇一、上传云锁Web防护模块压缩包并解压Ps:其实宝塔添加模块功能里可以通过配置shell脚本实现这些前置准备,但我还是喜欢用手动的方式上传,这样使步骤看起来更直..._宝塔 云锁自编译 测试

Android 笔记:Error:A problem occurred configuring project ':app'.-程序员宅基地

文章浏览阅读8.4k次。原文作者:雪飘碧鸳 在github上导入项目,或其他地方导入Android Studio,出现Error:A problem occurred configuring project ‘:app’.的错误。其实这种错误有很多种原因,需要对每种情况进行不同的处理才行,这里说的一种情况是JNI的情况,即该项目使用到C/C++库,此时需要引入NDK才行,先看下错误提示Gradle ‘trunk’ ..._error:a problem occurred configuring project ':app'.

dataframe的groupby,agg,unstack应用_group by unstack-程序员宅基地

文章浏览阅读603次。groupby一个索引的比较简单,这里主要讲两个索引的:这里先设dataframe为下图然后根据第0列和第1列来进行分组,再对第二列进行数量统计,这里用了nunique函数来进行数量统计上图中最上面的2表示是根据原表的第3列即序号2的那一列使用的agg函数得到的结果。unstack是把一维表转换成二维表,即把(0,1)这对分组条件分别写成表的行列索引,一一对应agg函数得到的结果,如下图:stack是把二维表转换成一维表,类似花括号形式(就像是思维导图一层一层括出去),第一列写上原本的行索引_group by unstack

树莓派宝塔搭建NAS私有云盘nextcloud_宝塔做nas-程序员宅基地

文章浏览阅读2.2k次。宝塔新建网站:假设文件夹根目录为/home/nextcloud创建FTP,数据集。并且选择php版本。删除文件夹根目录/home/nextcloud下的两个html文件。下载nextcloud文件:官网链接可以使用wget:wget https://download.nextcloud.com/server/releases/nextcloud-20.0.0.zip,或者本地端下载后拖过去。多线程下载:sudo apt install axel axel -n 20 ht.._宝塔做nas

快学Scala 第一课 (变量,类型,操作符)-程序员宅基地

文章浏览阅读41次。Scala 用val定义常量,用var定义变量。常量重新赋值就会报错。变量没有问题。注意:我们不需要给出值或者变量的类型,scala初始化表达式会自己推断出来。当然我们也可以指定类型。多个值和变量可以一起声明:Scala 类型:Byte, Char, Short, Int, Long, Float, Double, BooleanScala不区分基...

Oracle分区查询_oracle查询分区数据-程序员宅基地

文章浏览阅读1.4w次,点赞4次,收藏19次。根据分区查询速度会快很多:分区查询:SELECT * FROM USER_TAB_PARTITIONS WHERE TABLE_NAME = '表名'分区键查询:SELECT * FROM all_PART_KEY_COLUMNS where name='表名';根据分区名查询:select * from 表名 partition('分区名');根据分区键查询:select * from 表名 partition where ACTDATETIME>=to_date('2022_oracle查询分区数据

推荐文章

热门文章

相关标签