背景:
之前专栏中介绍最多的两款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应用server中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)开放的port是104或11112。dcm4chee会自己主动将该SCUclient的AETitle加入到配置中,同意其与dcm4chee建立连接。依照官方说明。改动请求端的port为11112,再次尝试。
C-FIND依旧能够返回结果。而C-MOVE却始终无法下载数据到本地。通过查看dcm4chee后台日志发现,总是提示无法建立连接。那么究竟是不是dcm4chee服务出的问题呢?。通过使用dcm4che源代码中自带的工具包dcmqr.bat发现能够顺利从部署到外网的dcm4cheeserver中下载数据到本地,详细操作指令例如以下:
dcmqr [email protected]: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 [email protected]: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.batclient的额输出日志:
至此应该能够大致确定问题出如今C-MOVE服务的实现机制上,通过dcmqr.bat工具的測试。能够确定C-GET服务能够顺利从部署到互联网的dcm4cheeserver下载数据,而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相关的不论什么client(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库外部自己定义实现)。
大致的代码例如以下所看到的:
protected override void OnReceiveCStoreRequest(byte presentationID, ushort messageID, DicomUID affectedInstance,
DcmPriority priority, string moveAE, ushort moveMessageID, DcmDataset dataset, string fileName)
{
try
{
if (OnCStoreRequest != null)
OnCStoreRequest(presentationID, messageID, affectedInstance, priority, moveAE, moveMessageID, dataset, fileName);
SendCStoreResponse(presentationID, messageID, affectedInstance, DcmStatus.Success);
}
catch (System.Exception ex)
{
SendCStoreResponse(presentationID, messageID, affectedInstance, DcmStatus.ProcessingFailure);
}
Console.WriteLine("c-move c-store RQ!");
}
加入OnReceiveCStoreRequest实现代码后。再一次连接dcm4cheeserver后能够看到顺利接收到了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等的普及,人人都希望建立自己的独立博客、独立站点。
之前博文介绍的就是一种公布个人站点的一种方式。这样的方式费用较高,须要购买server(拥有外网IP的主机),能够简单的觉得就是购买外网IP地址,以及域名。大家能够訪问www.zssure.me体验一下我搭建的个人主页。
那么除此以外有没有其它方法能够公布个人主页呢?搜索一下就能够看到非常多博文介绍怎样使用路由的虚拟server(即port映射)来实现外网訪问内网的功能呢,如是也能够简单的公布个人主页。当然这样的方式的危急系数高,易受到黑客攻击,不建议大家使用。
关于从外网直接訪问内网的设置可參见路由器实现外网訪问内网
大致的流程是利用路由器的虚拟server功能,进行port映射,实现外网IP地址到内网IP+port的一一映射。从而实现外网直接訪问内网的目的。示意图例如以下(图片来源于路由器实现外网訪问内网):
内网訪问外网:
内网訪问外网的情况就比較常见。诸如公司、学校等机构局域网内能够上网的都是内网訪问外网。通常通过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的dcm4cheeserver终于失败的原因。
虚拟server:
VPN:
对于虚拟server和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服务
作者:[email protected]
时间:2015-07-13