DICOM医学图像处理:AETitle在C-FIND和C-MOVE请求中的设置问题

背景:

最近去医院部署设备,调试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响应信息会出现在看图端的日志文件中?为了对问题进行一个全面的分析,找到问题出现的根源。因此回来后决定复原“现场场景”,希望通过分析本地的源码找到问题的根源。

利用VMWare复原现场

为了在同一台电脑中同时模拟出多台看图端与PACS服务端,我们需要借助于VMWare虚拟机工具(最近很火的Docker貌似也可以完成类似的功能,但是由于相关工程是基于Windows开发的,所以估计使用Docker来模拟还有一定的困难,如果有大神曾做过类似的模拟,还请不吝赐教^_^)。

一、VMWare本机模拟结构图

如上图所示,利用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,但是并未获得图像信息;

至此成功复原了当时的现场,为本地调试做好了准备。

二、VMWare虚拟机GuestOS与HostOS之间网络连接(局域网)

为了实现上述结构图中的模拟,需要在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.

Dicom Network源码分析(基于mDCM)

此处的DicomViewer看图端和PACSServer服务端采用的是C#编写的mDCM开源库。为了解决上述问题,此处对mDCM开源库中Dicom Network的相关类进行分析,对mDCM中DICOM网络服务的实现流程有一个宏观的认识,期望能够快速找到上述问题的根源。

从Github上下载mDCM的源码,利用VS打开(如下图)。找到mDCM中关于DICOM网络服务的文件夹Network,可以看到mDCM按照Client和Server将DICOM网络服务的实现类分成了两部分。

上图中的各个类之间的继承关系如下所示,

此处通过对PACSServer服务端的实现来剖析一下mDCM的相关类,对于客户端分支的相关分析此处就不做介绍了,客户端的流程比服务端要简单,主要是一个主动连接及被动接收服务端应答的过程,相应的处理函数也比较少,有兴趣的同学可自己浏览mDCM源码。


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类来决定。

从上述表格中我们对mDCM实现DICOM网络服务的流程有了一个基本的把握,下面从真实的PACSServer实现代码入手,给出具体的相关DICOM网络服务流的流向示意图,如下图所示:

上图中ServerProc到Process到ProcessNextPDU到ProcessPDataTF到ProcessDimse的流向就是mDCM对DICOM网络服务实现的控制流,并且上述四步控制,即ServerProc、Process、ProcessNextPDU、ProcessPDataTF、ProcessDimse,都是表格中所提到的各个类中的私有函数,言外之意就是我们不能够随意更改整体处理流程,但是对于各处理部分相应的处理函数可自由定制。

通过上述的剖析我们对mDCM的具体实现有了一个深刻的了解,那么接下来就是分析问题根源的时候了。对于Query/Retrieve请求,发出的是C-MOVE Request,那么从上图流向中我们可以定位到PacsSCPImpl类中的OnReceiveCMoveRequest函数内部,插入断点,单步调试。具体流程为:1)启动PACSServer进入调试模式;2)利用GuestOS-1虚拟机中的DicomViewer发起Query/Retrieve查询;3)PACSServer进入到OnReceiveCMoveRequest函数内部;按照同样的流程启动GuestOS-2的看图端发起Query/Retrieve请求,通过对比观察两次调试时刻的成员变量和局部变量发现,由于GuestOS-1和GuestOS-2中的AETitle相同,PACSServer服务端内部在数据库检索时刻搜索到第一个对应的AETitle对应的客户端IP地址后就退出了,因此对于GuestOS-1和GuestOS-2中的AETitle相同的情况,永远是在服务器数据库中顺序靠前的那一个看图端可以顺利完成Query/Retrieve查询请求。通过修改PACSServer服务端数据库中GuestOS-1和GuestOS-2的顺序得到了验证。

那么还有另外一个问题就是:失败的GuestOS-2中的DicomViewer日志文件中为什么会接收到C-MOVE Response信息呢?再次按照上述方法进入到单步调试模式,发现在相应客户端发起的Query/Retrieve请求时,对于应答的发送使用的是DcmNetworkBase基类中的SendCMoveResponse函数,该函数对于C-MOVE response信息的发送是根据PACSServer服务端监听套接字接收到的客户端套接字来完成的,因此并未通过查询服务端数据库中的AETitle来获取客户机的IP地址

至此对于此次奇葩问题有了一个完美的解答,最后总结一下。虽然mDCM库中对于信息的发送有的是通过查询数据库来获取目的地IP地址,有的是直接利用客户机的套接字来获取IP地址,按理来说如果是直接利用套接字中的IP地址来发送消息(如上述的SendCMoveResponse)看图端中的AETitle是可以相同的,但是随着医院信息系统的扩展,看图端数量的增加,建议还是按照DICOM3.0标准来设置不同的AETitle以区分网络中的各终端设备。如果不是此次现场部署的医院看图端足够多(原本的设计是根据IP地址的最后一位添加“SCU_”前缀来命名各个看图端,没想到的是医院每个楼层都有上百台看图端,因此导致只以IP地址最后一部分为后缀的AETitle命名方式出现了重复)也不会发现PACSServer服务端程序中的问题。

备注:

1)VMWare虚拟机安装Win7出现“GCDROM not loaded”错误

搜索了网络上的部分资料,最后从WinPE启动进入后,对虚拟机的硬盘进行格式化和分区,然后利用WinPE中的安装工具成功在VMWare虚拟机中安装Win7操作系统。

2)后续博文介绍

Dicom中的MPPS服务介绍

C#的异步编程模式在fo-dicom中的应用

VMWare三种网络连接模式的实际测试

作者:[email protected]

时间:2014-09-28

时间: 2024-11-03 22:44:38

DICOM医学图像处理:AETitle在C-FIND和C-MOVE请求中的设置问题的相关文章

DICOM医学图像处理:DCMTK的wiki资料学习之PACS调试

背景: 前段时间着重从dcmtk和fo-dicom(mDCM)源码角度进行剖析,期望加深对DICOM协议的理解.知其然,知其所以然.如果"所以然"很不好懂,那我们还是先多多"知其然"吧.搞清楚原理的目的不也是为了更好的运用于实践么?所以理论和实践应该彼此交错进行,理论搞不动了就搞搞应用,应用久了就钻研钻研理论. 以前上DCMTK官网仅仅是浏览关于开源库中各个类的设计模式.依赖关系.最近在打开DCMTK官网的wiki时,才发现OFFIS对DCMTK的介绍是如此的详细.

DICOM医学图像处理:DICOM网络传输

背景: 专栏取名为DICOM医学图像处理原因是:博主是从医学图像处理算法研究时开始接触DICOM协议的.当初认识有局限性,认为DICOM只是一个简单的文件格式约定,简而言之,我当时认为DICOM协议就是扩展名为DCM文件的格式说明.其实不然,随着对医疗行业的深入,对DICOM协议也有了更全面的认识.而今才发现DCM文件只是DICOM协议一部分中的一小节,仅仅是整个协议中的一个数据结构,而DICOM协议更多的是关于医疗行业各种服务及相关流程的约定,因此其实DICOM协议中最主要的是信息流,是对医院

DICOM医学图像处理:Dcmtk与fo-dicom保存文件的不同设计模式之“同步VS异步”+“单线程VS多线程”

一.背景: 最近一直在做DCM相关的编程工作,以前项目使用C++居多,所以使用DCMTK开源库,而目前团队使用C#居多,所以需要转向使用fo-dicom库,由于前一篇专栏文章DICOM医学图像处理:利用fo-dicom发送C-Find查询Worklist在调试过程中需要对DIMSE信息进行手动保存,偶然间发现了dcmtk开源库与fo-dicom开源库在保存dcm文件时使用的方式差异很大,因此决定研究一下,期望通过对比分析来看一下孰优孰劣. 二.dcmtk与fo-dicom保存文件函数的源码剖析:

DICOM医学图像处理:DCMTK在VS2012中的配置

背景: 最近由于项目需要,将原本的开发IDE环境由VS2008升级到了VS2012.本以为编译完成后的DCMTK开源库可以直接从VS2008移植到VS2012.但是通过项目属性添加完包含目录和依赖库后,编译会出现大量的链接错误(大多是跟dcmdata.lib.oflog.lib有关). 解决方法: 重新按照原本的博客前辈柳北风儿(大神目前已经博客转移到网易:http://blog.163.com/[email protected]/),利用CMake工具,选择VS2012本地编译器对DCMTK3

DICOM医学图像处理:DICOM存储操作之 “多幅JPG图像数据存入DCM文件”

背景: 续上篇,继续介绍如何将多幅JPG图像数据存入DCM文件.即将有损压缩数据直接写入DCM文件,存储为Multi-frame形式. 多幅JPG图像数据存入DCM文件: 为了避免引起歧义,这里着重说明一下.本博文的描述的场景是:假设我们手中有多张JPG文件,想把JPG文件写入DCM文件,即单个DCM文件包含多幅图像信息的Multi-Frame形式.该问题之前与CSDN博友y317215133y也讨论过,当时我在OFFIS论坛中找到了一个帖子直接给了y317215133y答复.今天重新梳理了一下

DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间worklist查询进行了介绍,并着重对比分析了DICOM3.0中各部分对DICOM网络通讯服务的定义.此次通过结合早些时间的博文DICOM医学图像处理:基于DCMTK工具包学习和分析worklist,对DCMTK开源库中提供的storescp.exe和storescu.exe工具的源码进行剖析,从底层深入了解C-STORE服务的触发及响应. 分析思路: storescp.exe和storescu.exe分别充当着

DICOM医学图像处理:开源库mDCM与DCMTK的比较分析(一),JPEG无损压缩DCM图像(续)

背景: 上周通过单步调试,找出了开源库mDCM与DCMTK在对DICOM图像进行JPEG无损压缩时的细小区别,并顺利实现了在C++和C#环境下对DICOM图像的压缩.但是问题接踵而至啊,随着项目的深入,发现在单独的测试工程中可以实现的mDCM版本,在嵌入到项目整体中后,却意外地出现了错误,并未顺利实现DICOM图像的JPEG无损压缩.因此需要继续详细对比分析mDCM与DCMTK两者,期望寻找原因. 问题分析: 开启项目的日志功能后,得到的信息反馈为: No registered codec fo

DICOM医学图像处理:DICOM存储操作之“多幅BMP图像数据存入DCM文件”

背景: 本专栏"DICOM医学图像处理"受众较窄,起初只想作为自己学习积累和工作经验的简单整理.前几天无聊浏览了一下,发现阅读量两极化严重,主要集中在"关于BMP(JPG)与DCM格式转换"和"DICOM 通讯协议",尤其是许久前的第一篇博文DCMTK开源库的学习笔记1:将DCM文件保存成BMP文件或数据流(即数组).因此在2014年底前打算写几篇关于DCM格式转换的文章,此次主要聚焦"如何将BMP.JPG等常规图像保存成DCM文件&q

DICOM医学图像处理:DIMSE消息发送与接收“大同小异”之DCMTK fo-dicom mDCM

背景: 从DICOM网络传输一文开始,相继介绍了C-ECHO.C-FIND.C-STORE.C-MOVE等DIMSE-C服务的简单实现,博文中的代码给出的实例都是基于fo-dicom库来实现的,原因只有一个:基于C#的fo-dicom库具有高封装性.对于初学者来说实现大多数的DIMSE-C.DIMSE-N服务几乎都是"傻瓜式"操作--构造C-XXX-RQ.N-XXX-RQ然后绑定相应的OnResponseReceived处理函数即可.本博文希望在前几篇预热的基础上,对比DCMTK.fo