首页 > 代码库 > DICOM医学图像处理:fo-dicom网络传输之 C-Echo and C-Store
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服务的端口号。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(客户端)流向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端很容易,首先创建DicomClient实体类,代表一个客户端,然后通过AddRequest添加不同的请求即可实现各种DIMSE的客户端,如图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协议默认端口104的数据包的同时也检测其他端口的数据包。之所以需要选择此项是因为很多DICOM服务并未使用协议默认的104端口。设置完成后,重新查看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。
实例工程及抓取的数据包:
代码:搜索我上传的资源
数据包:搜索我上传的资源
后续专栏博文介绍:
利用PHP Skel结合DCMTK开发WEB PACS应用
利用oracle直接操作DICOM数据
C#的异步编程模式在fo-dicom中的应用
VMWare三种网络连接模式的实际测试
作者:zssure@163.com
时间:2014-11-18
DICOM医学图像处理:fo-dicom网络传输之 C-Echo and C-Store