DICOM:DICOM3.0网络通信协议之“开源库实现剖析”

背景:

日前,通过对比fo-dicom与dcm4che两种开源库(也是C#与Java两大语言体系)的不同实现来实战学习了DICOM的网络传输,博文中列举了两大开源库各自的实现特点,以及使用的语言特性。本篇继续对比两大开源库,从宏观整体来分析各自DICOM Protocol的实现,聚焦各自使用的线程池,以及管理方式。

ACSE vs DIMSE

DICOM协议是建立在TCP全双工稳定连接之上的,依托于DICOM Upper Layer服务。之前博文DICOM医学图像处理:全面分析DICOM3.0标准中的通讯服务模块中介绍过DICOM协议的整体通信框架,DICOM Upper Layer服务包括A-ASSOCIATEA-RELEASEA-ABORTA-P-ABORTP-DATA(如下图所示)。
DICOM:DICOM3.0网络通信协议之“开源库实现剖析”_第1张图片
DICOM:DICOM3.0网络通信协议之“开源库实现剖析”_第2张图片
从上图中可以看出除了DICOM Upper Layer的几种服务之外,还有专栏中经常提到的DIMSE-C和DIMSE-N服务,包括C-CTORE、C-GET、C-MOVE、C-FIND、C-ECHO、N-EVENT-REPORT、N-GET、N-SET、N-ACTON、N-CREATE、N-DELETE等11种。那么DIMSE服务与DICOM Upper Layer服务之间有何区别?两者的关系如何呢?——这是初学者最容易混淆的,也最难懂的地方。
为了后面行文方便,直接给出两者关系的整体示意图,详情如下:
DICOM:DICOM3.0网络通信协议之“开源库实现剖析”_第3张图片
该图以fo-dicom为背景,对PDU和DIMSE进行了划分。之前在博文DICOM医学图像处理:DICOM网络传输中介绍过PDU(Protocol Data Unit)和DIMSE(DICOM Message Exchange)的概念,以及简单的对比。从上图可以看出,PDU是Upper Layer 服务层的信息单位,直接建立在TCP连接之上(此处就是.NET中的NetworkStream),其中A-ASSOCIATE、A-RELEASE、A-ABORT等服务直接与TCP连接建立相关。除此以外,有一个特殊的Upper Layer服务,即P-DATA,该服务提供的(或者说该服务具体传输)的就是DIMSE,即DIMSE-C和DIMSE-N。
博文DICOM医学图像处理:DICOM网络传输曾介绍过,DIMSE消息分为Command(必须)和Dataset(可选)两部分, Command用于指出具体服务种类,诸如C-FIND查询、C-STORE存储、C-MOVE提取等等;Dataset给出具体服务的操作数据。言外之意,上图右侧(浅绿色背景)的DIMSE消息是通过P-DATA服务中的PDU传输的,与此同时PDU在具体传输过程中会分成多个PDV(Presentation Data Value)单元。
至此,我们搞清楚了DICOM Upper Layer服务(ACSE)与DIMSE(Command and DataSet)的区别了。

【注】:
在DIMSE(Dicom Message)中有一个字段表示服务状态,即Status,取值为Pending、Success、Failure等(详情见PS3.7附录C),用于两个AE实体间进行状态交互,从而给出相关的处理操作,指的是整个实体交互操作。

而PDV中Command和Dataset的MessageControlHeader的isLastFragmentisCommand是用在具体的数据包重组时刻,是在单次DicomMessage中进行的,用于拼接Command+Dataset,或者拼接多个Dataset片段。

两者所用的范围不同,切记。

线程池:fo-dicom vs dcm4che

了解了DICOM通信协议的整体框架,以及消息结构,我们可以分析一下fo-dicom与dcm4che开源库的具体实现方式,如题所言此次从宏观入手,介绍整体的处理流程,尤其是线程池的管理。

1)fo-dicom使用的线程池

在上一篇博文DICOM:DICOM3.0网络通信协议(三)中提到过fo-dicom在实现时并非完全采用.NET的ThreadPool自有线程池,而是在此基础上又添加了一层自己的任务管理,即封装了ThreadPoolQueue< T >任务管理类。这里给出其用意,如下图所示:
DICOM:DICOM3.0网络通信协议之“开源库实现剖析”_第4张图片
DicomService服务使用ThreadPool.QueueUserWorkItem来实现多线程并发。由于DICOM是基于全双工的TCP协议,因此分为客户端和服务端两部分。分为两种情况:
1. 对于客户端发送来的请求:DicomRequest,直接响应并添加到ThreadPool系统线程池中,并不控制执行顺序(这是由于我们无法控制ThreadPool系统线程池的具体执行顺序);
2. 对于发送响应到客户端:DicomResponse,使用ThreadPoolQueue< int >来控制多个响应的顺序执行,具体原理是使用Queue缓存响应消息直至上一个消息处理完成后,具体代码在ThreadPoolQueue类的Execute方法中。

fo-dicom开源库的整个响应流程时序图如下,
DICOM:DICOM3.0网络通信协议之“开源库实现剖析”_第5张图片
由上图可以看出,之所以需要实现自己的任务管理来限定响应的执行顺序,其原因在于fo-dicom开源库使用TCPListener接收到客户端请求后,借用了.NET平台的异步I/O来读取TCP的数据流,.NET平台对于BeginXXX/EndXXX开始的函数也是采用ThreadPool系统线程池来实现的,因此如果后续对于发送到客户端的响应不做约束直接添加到ThreadPool系统线程池中,无法控制各个消息返回客户端的先后顺序(尤其在C-MOVE、C-GET等请求附带C-STORE子操作时)。

【注】:在上一篇博文介绍fo-dicom和dcm4che各自使用的语言特性时提到过MemoryStream。如上图所示,fo-dicom中正是巧妙利用了.NET平台下Stream的seek方法(可自由控制数据流的读取位置)完成PDV的拼接工作。然而Java语言的I/OStream中除了RandomAccessFile以外,无法使用类似seek的指令自由控制流数据读取位置。因此dcm4che开源库中并未出现PDV拼接工作,而是使用的顺序读取解析的方式。——后续有机会会对该部分进行详细介绍。

2)dcm4che使用的线程池

直接给出dcm4che开源库的具体实现时序图,如下所示:
DICOM:DICOM3.0网络通信协议之“开源库实现剖析”_第6张图片
由上图可以看出,dcm4che开源库直接使用的是java语言提供的Executor线程池(具体的是newCachedThreadPool或其他),并且整个方案结构简单。首先与fo-dicom开源库相同,第一层都是依托于TCPListener来响应客户端的请求,不同于fo-dicom采用异步I/O读取NetworkStream的是,dcm4che在接收到客户端的请求后,直接将socket流封装到Association中,随后添加到Executor线程池中,使用DICOM有限状态机(FSM,Finite State Machine)来处理整个DICOM数据流,由于将操作以TCP连接请求为单位进行封装放到Executor线程池的单个线程中执行,外加内部实现的DICOM协议状态机可确保单个连接之上的执行逻辑,因此不需要像fo-dicom中自定义任务管理来控制执行流程。

【注】:dcm4che巧妙的利用的Java语言的enum特性,实现了DICOM协议有限状态机(详情参考博文DICOM:DICOM3.0网络通信协议(续)),以此来管理整个DICOM请求和响应的执行流程——后续有机会同样会对该部分进行详细介绍。




作者:[email protected]
时间:2015-11-26

你可能感兴趣的:(ThreadPool,FSM,DICOM,ACSE,DIMSE)