最近去医院部署设备,调试PACS系统,遇到了一个奇葩的问题。基本场景是:医院内部网络情况复杂,多个楼层的诊室都安装了看图端,都需要访问顶楼机房的PACS服务器。起初为了调试关闭了防火墙,并确保各楼层的看图端与PACS服务器之间可以ping通,端口也顺利开放。但是具体部署调试过程中发现“有些楼层可正常进行worklist查询和Query/Retrieve查询,而有些楼层只能正常进行worklist查询,Query/Retrieve查询后本地并未获得图像数据”;第二天尝试后发现“原本正常进行worklist和Query/Retrieve查询的看图端,只能正常进行worklist查询,Query/Retrieve查询后本地无图像数据,而原本Query/Retrieve查询失败的竟然奇迹的可以下载图像了”。
在看图端和PACS服务端已经通过ping和talnet指令分别检测了网络和端口的连通性,所以说明网络硬件环境因素基本可以排除。那么问题多半出在DICOM服务端和看图端配置方面,最糟糕的是系统内部的bug导致(最不希望看到的就是系统的bug,^_^)。首先拷贝PACS服务端与正常看图端的日志文件,与异常的看图端日志文件进行对比分析,如下图所示:
上图中是正常的看图端,可以看到在看图端本地有保存图像的日志记录;而下图中是异常看图端的日志记录,对比上图发现PACS服务端响应看图端的C-Move请求的消息,即C-Move response,顺利到达了看图端,而看图端保存图像数据的信息并未出现。由于发现C-Move response信息能够顺利返回,而图像却不能保存所以猜测有可能是医院网络环境中对于影像数据的传输进行了限制,因为影像传输的数据量较大,但是在跟网管沟通后发现网络方面并未进行任何流量方面的限制。因此第一次排查尝试失败。
既然网络环境没有限制,那么会是哪一部分出了问题呢?为了对问题有一个更全面的把握,决定对医院的现有看图端的情况进行统计,希望能够从中找到线索,所以开始逐楼层进行排查。首先从最底层楼层开始排查,此时确保其他楼层并未有人使用看图端。逐个排查后发现有部分看图端依然只能实现worklist查询,Query/Retrieve查询仍然失败,记录失败看图端的IP地址和AETitle,耗费了一整天的时间统计了多个楼层的看图端连接情况。经过整理发现大多数Query/Retrieve查询失败的看图端的AETitle竟然有大面积的重合,因此可以断定大多是由于AETitle的重复而导致的Query/Retrieve查询失败。
随意挑选了AETitle重复的两台看图端,通过修改PACS服务器端和看图端的AETitle发现问题竟然奇迹般的解决了。于是通过观察PACS服务端Dicom节点数据库文件对AETitle出现重复的看图端进行了修改,顺利解决了此次部署中出现的奇葩问题。
现场虽然解决了背景中介绍的奇葩问题,但是并未对问题进行深究,例如既然AETitle有重复,那么为什么所有看图端worklist查询都可以成功,唯独Query/Retrieve查询会失败?既然Query/Retrieve查询失败,那么为什么PACS服务端的C-Move response响应信息会出现在看图端的日志文件中?为了对问题进行一个全面的分析,找到问题出现的根源。因此回来后决定复原“现场场景”,希望通过分析本地的源码找到问题的根源。
为了在同一台电脑中同时模拟出多台看图端与PACS服务端,我们需要借助于VMWare虚拟机工具(最近很火的Docker貌似也可以完成类似的功能,但是由于相关工程是基于Windows开发的,所以估计使用Docker来模拟还有一定的困难,如果有大神曾做过类似的模拟,还请不吝赐教^_^)。
如上图所示,利用VMWare WorkStation构建两个虚拟机,模拟现场中出现重复AETitle的看图端;本地安装PACSServer模拟医院的PACS服务器。基本的模拟流程如上图所示,
1)利用GuestOS-1虚拟机向HostOS上传一组数据,因为本地HostOS安装完PACSServer后其中并未存储相关数据,所以需要利用GuestOS-1上传来向PACSServer数据库写入一条数据;
2)利用GuestOS-1查询自己上传的数据,测试环境PACSServer是否正常运行。经测试证明HostOS中的PACSServer运行正常。
随后利用GuestOS-1和GuestOS-2来复原医院现场的情况,
3)GuestOS-1看图端向服务器发起worklist查询服务;
4)GuestOS-1看图端顺利获取到PACSServer中的患者信息;
5)GuestOS-2看图端向服务器发起worklist查询服务;
6)GuestOS-2看图端顺利获取到PACSServer中的患者信息;
7)GuestOS-1看图端向服务器发起Query/Retrieve请求;
8)GuestOS-1看图端顺利获得C-Move response及图像信息;
9)GuestOS-2看图端向服务器发起Query/Retrieve请求;
10)GuestOS-2看图端顺利获得C-Move response,但是并未获得图像信息;
至此成功复原了当时的现场,为本地调试做好了准备。
为了实现上述结构图中的模拟,需要在GuestOS虚拟机与HostOS之间建立连接。VMWare虚拟机的网络连接有三种方式:Bridge模式、Host-Only模式、NAT模式。博文(http://www.cnblogs.com/xiaochaohuashengmi/archive/2011/03/15/1985084.html)和博文(http://zhaisx.iteye.com/blog/458671)中对该三种模式有简单的介绍,大家可以仔细阅读,此处我选用的是Host-Only模式,HostOS主机的VMware Network Adapter VMnet1的IP地址是192.168.24.1,GuestOS-1的IP地址是192.168.24.100,GuestOS-2的IP地址是192.168.24.200.
此处的DicomViewer看图端和PACSServer服务端采用的是C#编写的mDCM开源库。为了解决上述问题,此处对mDCM开源库中Dicom Network的相关类进行分析,对mDCM中DICOM网络服务的实现流程有一个宏观的认识,期望能够快速找到上述问题的根源。【注】:专栏的前几篇博文中提到过mDCM与fo-dicom、DCMTK的关系,有兴趣的可以进入我的专栏阅读一下。
从Github上下载mDCM的源码,利用VS打开(如下图)。找到mDCM中关于DICOM网络服务的文件夹Network,可以看到mDCM按照Client和Server将DICOM网络服务的实现类分成了两部分。
上图中的各个类之间的继承关系如下所示,
此处通过对PACSServer服务端的实现来剖析一下mDCM的相关类,对于客户端分支的相关分析此处就不做介绍了,客户端的流程比服务端要简单,主要是一个主动连接及被动接收服务端应答的过程,相应的处理函数也比较少,有兴趣的同学可自己浏览mDCM源码。
DcmNetworkBase |
DcmNetworkBase是mDCM实现DICOM网路服务的基类,类中给出了基类网络服务的基础函数,主要由以下几类: 1)可重载的各类请求和应答的响应函数,如OnReceiveEchoRequest/OnReceiveEchoResponse、OnReceiveCMoveRequest/OnReceiveCMoveResponse等等。作为基类,各响应函数内部并未真正实现相应的操作,只是简单的发送终止应答,即调用SendAbort函数。 2)不可重载的,可派生的发送各种请求和应答的函数,如SendEchoRequest/SendEchoResponse、SendCMoveRequest/SendCMoveResponse等等。该类函数按照DICOM网络服务协议的要求,封装好了相应的发送操作,派生后可自由使用。 3)私有的,限定关键流程的函数,如Process、ProcessNetPDU、ProcessPDataTF、ProcessDimse。这四个函数表明了DICOM服务中信息流的流向,用户不可修改该流程,但是可通过重载相应的请求和应答函数向该流程中添加自己的操作。 4)私有的,限定底层网络操作流程的函数,如Connect。用户可通过重载OnInitializeNetwork函数来向连接流程中添加自己的操作。 |
CStoreService | CStoreService是服务端用来相应C-STORE 请求的类,即C-STORE-SCP类,派生自DcmNetworkBase基类,并对基类中关于C-STORE请求和C-ECHO请求进行了定制。 重载实现的函数有: 1)OnReceiveAssociateRequest 2)OnReceiveCStoreRequest,函数中调用了OnReceiveCStore委托,通过绑定自己的函数可对CStore请求进行定制化操作。例如数据库的写入等。 3)OnReceiveEchoRequest 4)OnReceiveDimseBegin 5)OnReceiveDimseProgress 后两个函数可以在DcmNetworkBase基类对DIMSE消息进行处理之前添加自己的操作,两个函数中调用的是OnCStoreRequestBegin和OnCStoreRequestProgress两个委托,例如可进行相关日志的写入操作。 |
CEchoService | 该类是一个连接测试类,比较简单。重载了OnReceiveAssociationRequest和OnReceiveEchoRequest两个函数。 |
DcmServer<T> where T:DcmSeriveBase |
该类是我们以后编写PACSServer服务端具体用到了泛型类,该类包含派生自DcmServiceBase的服务类成员,然后通过制定基本的连接流程来实现PACSServer服务端。关于流程的相关函数有, 1)共有的,可访问的启动、关闭和端口等相关操作函数,如AddPort、Start、Stop等 2)私有的,不可更改的核心流程控制函数,ServerProc。该函数选用Select模式来接收客户端的响应,然后针对获得的客户端socket对象调用派生自DcmServiceBae的服务类来实现PACSServer的功能,具体PACSServer提供的功能由T:DcmServiceBase类来决定。 |
搜索了网络上的部分资料,最后从WinPE启动进入后,对虚拟机的硬盘进行格式化和分区,然后利用WinPE中的安装工具成功在VMWare虚拟机中安装Win7操作系统。
Dicom中的MPPS服务介绍
C#的异步编程模式在fo-dicom中的应用
VMWare三种网络连接模式的实际测试
博文最后提到了"虽然mDCM库中对于信息的发送有的是通过查询数据库来获取目的地IP地址,有的是直接利用客户机的套接字来获取IP地址",其本质原因应该是:
——DICOM网络协议是建立在OSI的TCP/IP协议之上的,之间的信息发送利用Socket套接字完全可以明确双方彼此之间的唯一通路(在C#中有TcpClient和NetworkStream类),详情可参见关于网络传输的相关书籍资料。但是DICOM协议之所以添加了Application Entity Title约束,主要用在C-MOVE请求中,由于C-MOVE服务可能发生三方交互的情景,因此再第三方首次加入会话时刻需要确定其IP地址和端口的,因此采用预登记Application Entity Title方式来回溯查找到第三方的IP地址和端口。
后记2: 2015.07.14
后记1中提到了“利用Socket套接字完全可以明确双方彼此之间的唯一通路”这种说法也是不完全准确的,因为互联网真实情况比较复杂,在局域网中基本可以断定上述说法是正确的,但是放到外网中,经过各级路由情况就发生了变化,例如近期博文“DICOM:C-GET和C-MOVE对比剖析”中提到的内网与外网的区别,以及C-GET与C-MOVE的区别。这一点要格外注意,尤其是在互联网+深入医疗大环境下,原有医院局域网的情况已经被打破。
时间:2014-09-28