上一篇博文对 DICOM 中的网络传输进行了介绍。主要參照 DCMTK Wiki 中的英文原文。通过对照 DCMTK 与 fo-dicom 两个开源库对 DICOM 标准的详细实现,对理解 DICOM 标准有一个更直观的认识。此篇博文是对上一篇博文的补充。由于专栏前面的演示样例大多是利用 DCMTK 工具包来进行的,此次借着分析 fo-dicom 源代码结构的机会,參照 fo-dicom 的 README.md,给出 C-ECHO 和 C-STORE 服务的详细实现。在实现的同一时候给出 DICOM3.0 标准中的相关介绍,帮助我们理解。
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 的代码,详情例如以下:
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 就是存储服务。在医疗信息系统中最常见的服务之中的一个。尤其是 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 所看到的。
在 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 的正确性,測试代码例如以下:
最后能够得到例如以下结果,如图 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。
打开 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 顺利成功。
在本地測试。为了抓取 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 服务的本地回路数据包。
利用 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 控制流程相符。
双击第一个 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)。
连接释放的数据包格式简单,以下图 20 和图 21 各自是 DICOM 协议第 8 部分中给出的连接释放请求和应答数据包的格式:
双击 WireShark 中的连接释放数据包,能够看到两者的数据包类型分别为 05H 和 06H,这与上图中 DICOM 协议的规定全然一致。
在上一篇博文中(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 所看到的:
依旧使用 RawCap.exe+WireShark 来解决。
依照 C-ECHO 中的分析方式,相同能够看到 DICOM 数据包,如图 25 所看到的:
对于 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。
与 C-ECHO 中的同样,这也说明了博文中的 C-ECHO 和 C-STORE 服务实现成功。连接可以正常释放。
此处着重分析一下 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 三种网络连接模式的实际測试
作者:[email protected]
时间:2014-11-18
来源: http://www.bubuko.com/infodetail-2135009.html