DICOM医学图像处理:fo-dicom网络传输之 C-Echo and C-Store
背景:
上一篇博文对DICOM中的网络传输进行了介绍。主要參照DCMTK Wiki中的英文原文。通过对照DCMTK与fo-dicom两个开源库对DICOM标准的详细实现,对理解DICOM标准有一个更直观的认识。此篇博文是对上一篇博文的补充。由于专栏前面的演示样例大多是利用DCMTK工具包来进行的,此次借着分析fo-dicom源代码结构的机会,參照fo-dicom的README.md,给出C-ECHO 和C-STORE服务的详细实现。在实现的同一时候给出DICOM3.0标准中的相关介绍,帮助我们理解。
C-ECHO的fo-dicom实现:
1)C-ECHO參数说明:
C-ECHO又叫验证服务(即Verification),是用来验证DICOM服务两端的交流是否畅通。DICOM3.0的第7部分给出了C-ECHO服务的參数。例如以下图1所看到的:
【注意】:这里解说一下DICOM3.0标准的阅读方法。
以DICOM3.0标准的第7、8部分为例,【第7部分】中第9章開始解说DIMSE-C的各种服务。依次为C-STORE、C-FIND、C-GET、C-MOVE、C-ECHO(上图1就是我在该部分的C-ECHO小节中截取的),当中前半部分主要给出了DIMSE-C各种服务的參数。这里不过罗列出DICOM3.0标准的要求,目的是让你明确各个服务參数是否是必要的(分别用M、U、=表示);后半部分開始解说DIMSE-C各种服务的协议及实现流程(即Protocol和Procedures)。在PROTOCOL中给出的是详细的DIMSE-C服务的各种指令在传输过程中的格式,该部分也就是你利用抓包工具可以直接抓取的真实数据流;在Procedures中给出的是SCU和SCP之间的交互流程。通常为了说明服务是由谁发起的,由谁响应。在介绍Protocol的时候对于比較复杂的、可变的区域(Variables Fields)一般会放在附录中。比如第7部分的附录C和E等。【第8部分】与【第7部分】类似,从第7章開始介绍ACSE的各种服务的參数(例如以下图2所看到的),依次为A-ASSOCIATE、A-RELEASE、A-ABORT、A-P-ABORT、P-DATA;第9章给出的是ACSE中各种服务的结构,即STRUCTURE。该部分与【第7部分】中的PROTOCOL相同,给出的是详细ACSE PDU在传输时刻的数据格式,该部分也是能够通过抓包工具直接获得的;相同对于比較复杂的STRUCTURE介绍也会单独放到附录中,比如第8部分的附录E。
fo-dicom对于DIMSE消息的实现基类是DicomMessage。针对请求和响应分别派生出了DicomRequest和DicomResponse。最后依据不同的DIMSE服务派生对应的类。C-ECHO是当中最简单的,fo-dicom已经给出了SCP和SCU的详细实现。
參照fo-dicom中的README.md文件,给出C-ECHO SCP和SCU的代码,详情例如以下:
2)C-ECHO代码实例:
C-ECHO SCP的代码是直接利用了fo-dicom给出的DicomCEchoProvider类,通过创建DicomServer<DicomCEchoProvider>(12345)对象,开启C-ECHO SCP服务,当中參数12345表示C-ECHO服务的port号。C-ECHO SCU和C-ECHO SCP的代码分别例如以下所看到的:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Dicom; using Dicom.Network; namespace CEchoSCU { class Program { static void Main(string[] args) { var client = new DicomClient(); client.NegotiateAsyncOps(); client.AddRequest(new DicomCEchoRequest()); client.Send("127.0.0.1", 12345, false, "SCU", "ANY-SCP"); Console.ReadLine(); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; using Dicom; using Dicom.Network; namespace CEchoSCP { class Program { static void Main(string[] args) { var server = new DicomServer<DicomCEchoProvider>(12345); Console.ReadLine(); } } }
实际执行结果例如以下:
C-STORE的fo-dicom实现:
1)C-STORE參数说明:
C-STORE就是存储服务。在医疗信息系统中最常见的服务之中的一个。尤其是PACS系统中。与C-ECHO服务同样,DICOM3.0标准第7部分也给出了C-STORE服务的參数列表,例如以下图4所看到的:
该參数列表的目的相同是为了介绍C-STORE服务中各參数的必要性。真正的參数消息格式在兴许的C-STORE PROTOCOL中介绍,例如以下图5所看到的:
图5中给出的不过C-STORE RQ的实际消息格式,该消息由C-STORE服务的SCU(client)流向C-SOTRE服务的SCP(服务端);与之相相应的C-STORE-RSP消息是从SCP流向SCU。DICOM3.0标准中也有C-STORE-RSP的具体介绍,例如以下图6所看到的。
2)C-STORE代码实例:
在fo-dicom的说明文档README.md中仅仅给出了C-STORE的SCU演示样例,例如以下图7所看到的:
上一篇博文对fo-dicom源代码结构分析的基础上可知。实现DIMSE众多服务的SCU端非常easy,首先创建DicomClient实体类,代表一个client。然后通过AddRequest加入不同的请求就可以实现各种DIMSE的client,如图7中C-STORE SCU的实现为:
client.AddRequest(new DicomCStoreRequest(@"test.dcm"));
DicomCStoreRequest类是DicomRequest的派生类,上述代码通过制定DCM文件路径来构建了一个DicomCStoreRequest对象,在DicomCStoreRequest内部通过打开指定的DCM文件提取获得上述參数中的Affected SOP Instance UID等參数。
既然fo-dicom中没有提供线程的C-STORE SCP实现,我们先利用DCMTK的storescp.exe工具来验证一下fo-dicom给出的C-STORE SCU的正确性,測试代码例如以下:
- SCP端利用storescp.exe,在控制台下输入:storescp.exe –d –od c:\ 12345
- SCU端利用fo-dicom中的C-STORE SCU。详细代码如上图7所看到的。然后双击生成后的storescu.exe。
最后能够得到例如以下结果,如图8所看到的:
同一时候在C盘根文件夹下能够看到被重命名的test.dcm文件,例如以下图9所看到的:
之所以被重命名我们在之前分析DCMTK开源库源代码时提到过,通常DCMTK会依据SOP Instance UID(-uf,默认的)对接收到的DCM文件进行重命名,当然也能够通过选项设置重命名的方式。比如依照时间(-tn)、特定前缀(-fe)等等,例如以下图10所看到的。
由此说明fo-dicom中给出的C-STORE SCU功能正常。接下来我们尝试利用fo-dicom构建C-STORE SCP。
3)构建C-STORE SCP
打开C-ECHO SCP的实现DicomCEchoProvider.cs文件,我们看到DicomCEchoProvider类通过派生DicomService服务类来实现了Dicom服务的基本框架。然后通过实现IDicomServiceProvider和IDicomCEchoProvider接口,完毕了C-ECHO 的服务端。细致查看DicomCEchoProvider的代码能够发现,事实上就是在接收到A-ASSOCIATE-RQ消息后。判别Presentation Context中的Abstract Syntax。依据实际请求消息来决定是否建立连接,另外当接收到C-ECHO SCU发起的C-ECHO Request时,向其会送DicomCEchoResponse确认信息就可以。
既然通过实现两个接口函数就能够完毕C-ECHO SCP的构建,那么我们就自己尝试来完毕C-STORE SCP的搭建,仿照DicomCEchoProvider的方式,DicomCStoreProvider的代码例如以下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Dicom; using Dicom.Log; using Dicom.Network; using System.Threading; using System.IO; namespace CStoreSCP { class CStoreSCPProvider : DicomService, IDicomServiceProvider, IDicomCStoreProvider { public CStoreSCPProvider(Stream stream, Logger log) : base(stream, log) { } public DicomCStoreResponse OnCStoreRequest(DicomCStoreRequest request) { return new DicomCStoreResponse(request,DicomStatus.Success); } public void OnCStoreRequestException(string tempFileName, Exception e) { } public void OnReceiveAssociationRequest(DicomAssociation association) { foreach (var pc in association.PresentationContexts) { if (pc.AbstractSyntax == DicomUID.Verification) pc.SetResult(DicomPresentationContextResult.Accept); else { //pc.SetResult(DicomPresentationContextResult.RejectAbstractSyntaxNotSupported); } if (pc.AbstractSyntax == DicomUID.CTImageStorage) { pc.SetResult(DicomPresentationContextResult.Accept); } } SendAssociationAccept(association); } public void OnReceiveAssociationReleaseRequest() { SendAssociationReleaseResponse(); } public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason) { } public void OnConnectionClosed(int errorCode) { } } }
然后通过var server = new DicomServer<CStoreSCPProvider>(12345);Console.ReadLine(); 来构建一个C-STORE SCP应用。
下图11是先执行CStoreSCP.exe,然后执行CStoreSCU.exe得到的结果:
从图11的输出结果能够看出。此次C-STORE SCP和SCU两端的通讯顺利完毕,那么我们发送的C:\test.dcm文件会被CStoreSCP.exe存储到那里呢?由上一篇博文分析我们知道fo-dicom库中将DICOM的服务基本框架放在了DicomService类中。查看当中处理P-DATA服务的核心函数ProcessPDataTF,能够看到例如以下代码:
var file = new DicomFile(); file.FileMetaInfo.MediaStorageSOPClassUID = pc.AbstractSyntax; file.FileMetaInfo.MediaStorageSOPInstanceUID = _dimse.Command.Get<DicomUID>(DicomTag.AffectedSOPInstanceUID); file.FileMetaInfo.TransferSyntax = pc.AcceptedTransferSyntax; file.FileMetaInfo.ImplementationClassUID = Association.RemoteImplemetationClassUID; file.FileMetaInfo.ImplementationVersionName = Association.RemoteImplementationVersion; file.FileMetaInfo.SourceApplicationEntityTitle = Association.CallingAE; _dimseStream = CreateCStoreReceiveStream(file);
转到CreateCStoreReceiveStream函数内部,通过函数的说明就能够知道fo-dicom对C-STORE服务默认情况下是在系统中创建了一个暂时文件,用来接收C-STORE SCU的数据。因此能够判断我们的test.dcm文件应该也在暂时目录中,打开我本机的temp目录。能够看到有一个后缀为tmp的暂时文件。例如以下图12所看到的。
文件大小与我们測试用的test.dcm同样。尝试改动.tmp的扩展名,改动后能够使用DICOM Viewer软件正常打开。因此说明我们的C-STORE SCP顺利成功。
DICOM数据流分析:
C-ECHO服务数据流分析:
1)工具:
在本地測试。为了抓取127.0.0.1回路数据包,须要使用RawCap.exe工具包。
RawCap.exe是控制台程序,在抓取本地回路数据包时非常便捷。
当抓取完毕后我们须要借助于WireShark的强大分析功能,来实现C-ECHO数据流的具体分析。WireShark能够直接打开RawCap.exe抓取的.pcap数据包。
WireShark是功能强大的数据包统计分析工具。当然本身也能够抓取网络数据包(本地回路数据包不方便)。WireShark支持众多协议,当中包含DICOM协议。以下以C-ECHO的数据包为例,简介一下怎样使用WireShark来自己主动识别并解析DICOM数据包。首先打开抓取的本地C-ECHO数据包cecho.pcap。如图13,在Protocol中右键选择"Protocol Preferences “中的"Data Preferences…”,会弹出一个协议设置窗体如图13。在左側列表中找到DICOM协议,勾选图14中红色部分。该部分的意思是除了检測DICOM协议默认port104的数据包的同一时候也检測其它port的数据包。之所以须要选择此项是由于非常多DICOM服务并未使用协议默认的104port。设置完毕后,又一次查看Protocol列,能够看到出现了DICOM字样。如图15所看到的。最上方的带DICOM字样的数据包就是我们抓取到的C-ECHO服务的本地回路数据包。
2)C-ECHO数据流分析:
利用RawCap.exe和WireShark两大强大的工具,我们已经能够直观的看到抓取的DICOM数据包了。接下来我就依照DICOM标准第7部分和第8部分中的内容,逐个数据包来分析一下,通过观察真实的数据包来加深一下对DICOM协议的理解。
从图15中能够看到。最顶部DICOM协议包括6个数据包,各自是连接建立(A-ASSOCIATE RQ/A-ASSOCIATE AC)、数据交互(P-DATA-TF)、连接释放(A-RELEASE RQ/A-RELEASE RP),这与DICOM协议第8部分中介绍的ACSE控制流程相符。
A-ASSOCIATE RQ/A-ASSOCATE AC分析:
双击第一个DICOM数据包,该数据包是A-ASSOCIATE RQ的真实数据流,如图16所看到的:
依照DICOM协议第8部分中第9章对A-ASSOCIATE RQ PDU的描写叙述。我们来逐项对照(DICOM协议可參照图17):第一项1个字节的PDU-type,图中为01H,说明该数据包代表的是A-ASSOCIATE RQ;第二项一个字节的保留,数据流为00H;第三项是四个字节的PDU-length,图中为00 00 00 ff,转换为无符号整数正好为255。这也是整个图中蓝色部分兴许的数据包长度;第四项是两个字节的Protocol-Version,图中为00 01。相应版本号为1;第五项为两字节保留;第六项和第七项是我们熟悉的AE Title,从WireShark的数据流中也能够看出各自是ANY-SCP和ECHOSCU;第8项又是一堆保留字节。用00H填充;第9项是一个可变区域(Variable Fields),该项是复合项。内部包括多个独立的子项。由图16能够看出该复合项内部含有Application Context、Presentation Context(2个,ID各自是1、3)、UserInfo三个子项;而UserInfo又是一个复合项,其内部又包括了Max PDU Length、ImplentationUID、ImplentationVersion三个子项。从WireShark的分析来看,Application Context子项类型为10H、Presentation Context子项类型为20H、UserInfo子项为50H(其内部的嵌套子项的类型分别为,Max PDU Length-51H、Implentation UID-52H、Implentation Version-55H)。各个子项的类型与DICOM协议第7、8两部分中的附录D相相应。比如图19中我截取的是Max PDU Length子项的格式。A-ASSOCIATE AC的数据包分析与A-ASSOCIATE RQ类似,仅仅是A-ASSOCIATE AC的数据流更简单一些,这里就不做具体介绍了。(终于数据域DICOM协议的相应结果如图18)。
A-RELEASE RQ/A-RELEASE RP分析:
连接释放的数据包格式简单,以下图20和图21各自是DICOM协议第8部分中给出的连接释放请求和应答数据包的格式:
双击WireShark中的连接释放数据包,能够看到两者的数据包类型分别为05H和06H,这与上图中DICOM协议的规定全然一致。
P-DATA-TF:
在上一篇博文中(http://blog.csdn.net/zssureqh/article/details/41016091)我已经分析了。DICOM协议第7部分中规定的DIMSE消息(Command和Dataset)是通过第8部分中ACSE协议中的P-DATA-TF服务以PDV的形式来传输的。
以下就让我们来分析一下DIMSE消息中C-ECHO RQ 和C-ECHO RSP的格式:
双击WireShark数据包中间两个,从数据流向能够断定一个是C-ECHO RQ消息。一个是C-ECHO RSP消息。
先打开第一个。依照上一篇博文的分析。首先该数据包是一个P-DATA-TF PDU,因此须要符合下图23中的格式。
通过分析最外层的是代表P-DATA-TF类型的04H。然后是由DIMSE消息填充的PDV区域,该项是复合项,第一子项是Item-length,此处为46H;第二子项为Presentation-context-ID,此处为01H;第三子项又是一个复合项。是DICOM标准第4部分中给出的DIMSE消息结构。包含Message Control Header、Command和DataSet三部分。此处的MessageControlHeader为03H,即表示是Command数据而不是DataSet,且是最后一个PDV,即Last Fragment。详细的相应关系如图24所看到的:
C-STORE服务数据流分析:
1)工具:
依旧使用RawCap.exe+WireShark来解决。
2)C-STORE数据流分析:
依照C-ECHO中的分析方式,相同能够看到DICOM数据包,如图25所看到的:
A-ASSOCIATE RQ/A-ASSOCIATE AC:
对于A-ASSOCIATE RQ/A-ASSOCIATE AC的分析与C-ECHO中基本类似,唯一不同的就是对于C-STORE服务须要不同的Presentation Context描写叙述上下文,如图26所看到的,此处C-STORE须要的是CT Image Storage服务,其SOP Class UID为1.2.840.10008.5.1.4.1.1.2。
A-RELEASE RQ/A-RELEASE RP:
与C-ECHO中的同样,这也说明了博文中的C-ECHO 和C-STORE服务实现成功。连接可以正常释放。
P-DATA-TF:
此处着重分析一下C-STORE服务中的P-DATA-TF数据包,由于传输一个DCM文件须要多个PDU,自然也须要多个PDV。
所以我们通过分析C-STORE的P-DATA-TF数据包能够更形象的学习Message Control Header和DIMSE的知识。
相同传输的每一个数据包首先符合P-DATA-TF的格式要求。第一项是PDU类型,即04H。随后是保留项、PDU-length、PDV复合项……,这与C-ECHO中的分析相同。依照上一篇博文的分析,C-STORE PROTOCOL的流程是CSTORE SCU向SCP发送C-STORE RQ消息。可是打开图中的第一个P-DATA数据包时我们看到的却不是C-STORE RQ,而是当中的一个数据片段,例如以下图27所看到的。
依次查看后面的几个P-DATA数据包,都是类似的情况。最后倒数两个各自是C-STORE RQ中DCM文件数据的最后一个数据包(Last Fragment)和SCP向SCU发送的C-STORE RSP,详细分析如图28所看到的:
从最后数据包Command中的(0000,0100)的值域8001H可知该指令就是C-STORE RSP。
看到这里你也许会非常兴奋,由于我们最终也看到了C-STORE服务的真实数据流。可是在上图中的全部DICOM相应的数据包中我们并未找到C-STORE SCU发起的C-STORE RQ数据包,那么C-STORE RQ数据包在哪里呢?
让我们将cstore.pcap的全部数据包依照时间排序,出现了大量标记为[TCP segment of a reassembled PDU]的TCP数据包。
打开第一个标记为[TCP segment of a reassembled PDU]的TCP数据包。其内部的真实数据分析例如以下图30所看到的:
至此我们顺利找到了C-STORE SCU端发送的C-STORE RQ消息,之所以没有在WireShark中以DICOM协议显示,可能是因为WireShark在识别多个连续分片的数据时不够智能。博文中的演示样例图和文字较多,细致阅读后应该对DICOM3.0中的协议会有更进一步的了解。通过分析数据包的方式在更直观的学习和掌握DICOM3.0标准的同一时候,对后期排查DICOM网络传输相关错误也会有帮助。
备注:
再次说明一下阅读DICOM3.0标准的方式:
以DICOM3.0标准的第7、8部分为例,【第7部分】中第9章開始解说DIMSE-C的各种服务,依次为C-STORE、C-FIND、C-GET、C-MOVE、C-ECHO(上图1就是我在该部分的C-ECHO小节中截取的),当中前半部分主要给出了DIMSE-C各种服务的參数,这里不过罗列出DICOM3.0标准的要求。目的是让你明确各个服务參数是否是必要的(分别用M、U、=表示);后半部分開始解说DIMSE-C各种服务的协议及实现流程(即Protocol和Procedures)。在PROTOCOL中给出的是详细的DIMSE-C服务的各种指令在传输过程中的格式,该部分也就是你利用抓包工具可以直接抓取的真实数据流;在Procedures中给出的是SCU和SCP之间的交互流程。通常为了说明服务是由谁发起的。由谁响应。在介绍Protocol的时候对于比較复杂的、可变的区域(Variables Fields)一般会放在附录中。比如第7部分的附录C和E等;【第8部分】与【第7部分】类似。从第7章開始介绍ACSE的各种服务的參数(如图2所看到的),依次为A-ASSOCIATE、A-RELEASE、A-ABORT、A-P-ABORT、P-DATA;第9章给出的是ACSE中各种服务的结构,即STRUCTURE,该部分与【第7部分】中的PROTOCOL相同,给出的是详细ACSE PDU在传输时刻的数据格式,该部分也是能够通过抓包工具直接获得的。相同对于比較复杂的STRUCTURE介绍也会单独放到附录中。比如第8部分的附录E。
实例project及抓取的数据包:
代码:搜索我上传的资源
数据包:搜索我上传的资源
兴许专栏博文介绍:
利用PHP Skel结合DCMTK开发WEB PACS应用
利用oracle直接操作DICOM数据
C#的异步编程模式在fo-dicom中的应用
VMWare三种网络连接模式的实际測试
时间:2014-11-18