DICOM:C-GET与C-MOVE对比剖析

转载自:http://blog.csdn.net/zssureqh/article/details/46868695

背景:

之前专栏中介绍最多的两款PACS分别是基于dcmtk的dcmqrscp以及Orthanc,和基于fo-dicom的DicomService(自己开发的),该类应用场景都是针对于局域网,因此在使用DIMSE-C各项服务时并未遇到的复杂问题,学习和使用成本相对较低。通过近一年的时间也已经对C-ECHO、C-FIND、C-STORE、C-MOVE、N-PRINT等各项服务都进行了详细介绍,并且从DICOM标准来看C-MOVE服务可以包含C-GET,因此今天之前专栏中从未介绍过C-GET服务。
近期由于项目需要,开始频繁接触基于dcm4che的dcm4chee服务。通过托管于JBoss AS应用服务器中dcm4chee可以为互联网用户提供DICOM服务(注意区别于之前介绍的Orthanc的RESTful服务,以及DICOM标准中的WADO协议)。通过直接将DICOM服务部署到外网主机中来实现web的PACS服务,因为DICOM协议本身是建立在TCP上,所以从理论上这种DICOM服务的web化是可行的。
起初在对接C-FIND、C-STORE等服务时也很顺利,与常规的局域网操作完全相同,无非就是访问的对端AE节点IP地址是公网IP而已。但是在进行C-MOVE操作时,却总是无法成功。

dcm4chee的AE Title配置服务:

起初以为dcm4chee对C-MOVE的实现有问题,于是自己在本地Ubuntu虚拟机中搭建部署了一套dcm4chee服务。通过单步调试,以及检索dcm4che官网,找到了部分线索:dcm4che的AE Title Configuration Service
如官网中所述,dcm4chee由于部署到互联网中,相较于局域网需要配置SCU和SCP各自AETitle信息的情形,dcm4chee额外添加了一种自动配置模式,即如果请求端(即任何SCU)开放的端口是104或11112,dcm4chee会自动将该SCU客户端的AETitle添加到配置中,允许其与dcm4chee建立连接。按照官方说明,修改请求端的端口为11112,再次尝试。C-FIND依然可以返回结果,而C-MOVE却始终无法下载数据到本地。通过查看dcm4chee后台日志发现,总是提示无法建立连接。那么到底是不是dcm4chee服务出的问题呢?。通过使用dcm4che源码中自带的工具包dcmqr.bat发现可以顺利从部署到外网的dcm4chee服务器中下载数据到本地,具体操作指令如下:
dcmqr DCM4CHEE@XXX.XXX.XXX.XXX:11112 -cget -nocfind -q0020000D=1.3.6.1.4.1.30071.6.10987654321.1234567890 -cstore CT -cstoredest c:\test
输出日志截图如下:
这里写图片描述 
在本地c:\test目录下可以看到下载成功的dcm文件。

C-GET与C-MOVE:

既然dcmqr.bat工具可以顺利将数据下载到本地,可以确信dcm4chee服务端实现没有问题。那么到底是哪里出现了问题呢?让我们接着往下看:
同样使用dcmqr.bat工具,输入下述指令:
dcmqr DCM4CHEE@XXX.XXX.XXX.XXX:11112 -cmove ZSSURE -nocfind -q0020000D=1.3.6.1.4.1.30071.6.198690283870599.4896581024566519 -cstore CT -cstoredest c:\test
却总是提示无法识别Move Destination AETitle:ZSSURE,如下截图所示分别是dcm4chee服务端和dcmqr.bat客户端的额输出日志:
这里写图片描述

这里写图片描述 
至此应该可以大致确定问题出现在C-MOVE服务的实现机制上,通过dcmqr.bat工具的测试,可以确定C-GET服务可以顺利从部署到互联网的dcm4chee服务器下载数据,而C-MOVE服务无法实现。
下面就详细分析一下C-GET与C-MOVE的区别:
这里写图片描述

这里写图片描述 
从DICOM3.0标准官方描述中可以看出,两者的目的是相同的,且主动发起请求方(即SCU)的起始操作是相同的——都是从SCP端查询匹配项。但是随后的子操作不同,在C-GET SERVICE中的描述是子操作的触发在同一个连接中(on the same Association);而在C-MOVE SERVICE中的描述是子操作的触发在单独的、另外的一个连接中(on a separate Association)。为了更清楚的描述两者的区别,请看下面两幅示意图:
这里写图片描述

这里写图片描述 
图中用双向箭头来示意具体C-GET和C-MOVE服务中所建立的TCP连接数;单项箭头分别表示具体服务中的RQ和RSP,按照官方说法C-GET和C-MOVE服务都是确认服务(Confirmed Service),所以单项箭头都是一一配对的,正常情况下一个RQ对应一个RSP;单项箭头标记的数字表明C-GET和C-MOVE服务具体请求过程中的各消息发送时序。

fo-dicom(mDCM)实现C-GET:

之前分析过fo-dicom(mDCM)库中实现DICOM协议的各种关系。简单来说类就是对某种操作以及操作所需的数据的一种封装,从上面的结构图分析以及官方DICOM3.0标准来看,DICOM协议中的操作主要就是建立&关闭TCP连接、发送&接收请求、读取&解析套接字信息(DIMSE),以及相关异常处理等等,所以整体的操作都被放到了DcmNetworkBase类中;然后根据不同的角色(即SCU和SCP),分别派生出DcmServiceBase和DcmClientBase,直至最底层的DcmServer和DcmClient。简单的类图如下所示:
根据背景中的描述,我们这里扮演的是C-GET SCU角色,即需要派生DcmClient。仔细查看fo-dicom(mDCM)源码发现其中并未实现C-GET相关的任何客户端(SCU)和服务端(SCP)类的封装,为了借鉴现有经验又查看了一下dcmtk和dcm4che的源码,只有dcm4che提供的dcmqr.bat工具包中嵌入了c-get服务。
通过查看DICOM标准第7部分可知,C-GET服务于C-MOVE服务除了在Association上不同以外,其他参数以及操作流程几乎完全相同。既然fo-dicom(mDCM)中给出了CMoveClient类,那么我么直接借用CMoveClient类来编写CGetClient类。
起初直接拷贝了CMoveClient类源码,去掉了其中关于第三方DestinationAE的相关成员变量,在测试过程中总是出现Association.Available==0的情况,如下图所示:
这里写图片描述 
后来通过对比DICOM标准以及上述分析发现,在CMoveClient类中并未实现DcmNetworkBase类的OnReceiveCStoreRequest方法,而C-GET服务按照我们之前的分析需要在同一个Association中处理来C-GET SCP的C-STORE SCU,因此对CGetClient类的OnReceiveCStoreRequest进行扩展,添加保存DcmDataset到.dcm文件的代码(注:这里为了方便用户后期扩展,将具体的实现放到了代理delegate中,由用户在Dicom.dll库外部自定义实现)
大致的代码如下所示:

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">OnReceiveCStoreRequest</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">byte</span> presentationID, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ushort</span> messageID, DicomUID affectedInstance,
            DcmPriority priority, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">string</span> moveAE, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ushort</span> moveMessageID, DcmDataset dataset, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">string</span> fileName)
        {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">try</span>
            {
                <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (OnCStoreRequest != <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>)
                    OnCStoreRequest(presentationID, messageID, affectedInstance, priority, moveAE, moveMessageID, dataset, fileName);
                SendCStoreResponse(presentationID, messageID, affectedInstance, DcmStatus.Success);

            }
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">catch</span> (System.Exception ex)
            {
                SendCStoreResponse(presentationID, messageID, affectedInstance, DcmStatus.ProcessingFailure);

            }
            Console.WriteLine(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"c-move c-store RQ!"</span>);
        }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li></ul>

添加OnReceiveCStoreRequest实现代码后,再一次连接dcm4chee服务器后可以看到顺利接收到了DcmDataset并保存到本地.dcm文件。至此CGetClient类的封装工作就完成了。

知识点补充:

TCP全双工:

全双工协议(即Full-Duplex Transmissions),是指通信时允许两个方向同时传递数据,他相当于两个单工通信,它要求发送设备和接收设备都有独立的接收和发送能力。通熟点来讲在通信时能保证在任意时刻双方都能听到对方的声音。(摘自《全双工和半双工的区别》)。TCP协议就是全双工协议,因此在上述实现C-GET请求时可以再同一个TCP连接中(即Association)实现C-STORE和C-GET。

外网VS内网:

IP地址资源紧张,255.255.255.255形式的32位地址已经远不能标识互联网中的所有主机,而外网和内网的划分在一定程度上解决了IP地址资源紧张的问题。每个内网通过路由映射到一个外网IP地址,内网中的主机经过路由器之后才能访问外网,而对外他们只耗费了一个外网IP地址资源。常见的内网IP有以下几种类型:
10.0.0.0~10.255.255.255
172.16.0.0~172.31.255.255
192.168..0.0~192.168.255.255

所以只要拥有外网IP,就可以直接连接互联网,而不需要经过路由器或交换机。由于外网IP资源宝贵,且属于全球化资源,因此外网IP一般都是用于公司企业、学校、政府等机构。除了上述三类内网IP以外的地址都是外网IP

外网访问内网:

自媒体的火热,以及Github、Gitlib等的普及,人人都希望建立自己的独立博客、独立网站。之前博文介绍的就是一种发布个人网站的一种方式,这种方式费用较高,需要购买服务器(拥有外网IP的主机),可以简单的认为就是购买外网IP地址,以及域名。大家可以访问www.zssure.me体验一下我搭建的个人主页。
那么除此以外有没有其他方法可以发布个人主页呢?搜索一下就可以看到很多博文介绍如何使用路由的虚拟服务器(即端口映射)来实现外网访问内网的功能呢,如是也可以简单的发布个人主页。当然这种方式的危险系数高,易受到黑客攻击,不建议大家使用。关于从外网直接访问内网的设置可参见路由器实现外网访问内网
大致的流程是利用路由器的虚拟服务器功能,进行端口映射,实现外网IP地址到内网IP+端口的一一映射,从而实现外网直接访问内网的目的。示意图如下(图片来源于路由器实现外网访问内网):
这里写图片描述

这里写图片描述

内网访问外网:

内网访问外网的情况就比较常见,诸如公司、学校等机构局域网内能够上网的都是内网访问外网。通常通过NAT(Network Address Translation)来实现。百度百科对NAT的描述如下:

NAT(Network Address Translation,网络地址转换)是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。
这种方法需要在专用网连接到因特网的路由器上安装NAT软件。装有NAT软件的路由器叫做NAT路由器,它至少有一个有效的外部全球IP地址。这样,所有使用本地地址的主机在和外界通信时,都要在NAT路由器上将其本地地址转换成全球IP地址,才能和因特网连接。
另外,这种通过使用少量的公有IP 地址代表较多的私有IP 地址的方式,将有助于减缓可用的IP地址空间的枯竭。在RFC-1632中有对NAT的说明。

利用NAT有两大好处,分别是宽带分享和安全防护。对内可以多台主机共享一条宽带;对外多带主机对应一个公网IP,因此当受到外网攻击时无法确定对应的单一主机。(这也就是C-MOVE请求访问基于WEB的dcm4chee服务器最终失败的原因。

虚拟服务器:

VPN:

对于虚拟服务器和VPN的介绍暂时搁置一下,由于后期需要手动搭建VPN服务因此放到下一篇博文中再介绍,敬请期待!

PS:可以对比之前专栏中的博文DICOM医学图像处理:AETitle在C-FIND和C-MOVE请求中的设置问题 
来分析一下内网与外网的区别,这里特此说明当时的一种解决方案(即利用TcpClient来直接反推IP地址)的做法是有局限性的,在局域网中是可行的,在互联网大环境下会失败。

修正:博文中C-GET服务示意图中C-GET-RSP、C-STORE-RQ、C-STORE-RSP等消息的流程是错误的,详情可以参见后续的博文DICOM:C-GET服务


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值