DICOM:C-GET与C-MOVE对比剖析(续)

背景:

专栏之前剖析过DICOM协议的C-GET服务与C-MOVE服务,两者最大的区别在于C-GET是基于单个TCP连接的点对点的两方服务,而C-MOVE是基于两个TCP连接的三方服务,详情参见之前的专栏博文DICOM:C-GET与C-MOVE对比剖析
近期在将相关DICOM服务,例如CStoreSCP、CMoveSCP,MppsSCP等,Docker化并Web发布时又遇到了一个问题,大致情形如下:

通过Tomcat的Servlet来响应DICOM的C-MOVE-RQ请求,但是由于CMove操作完成耗时较长,易导致页面超时失效。

尝试解决方案:

基于之前对于C-GET和C-MOVE服务的剖析,了解了C-MOVE的三方通信机制,如下图所示:

DICOM:C-GET与C-MOVE对比剖析(续)_第1张图片
DICOM:C-GET与C-MOVE对比剖析(续)_第2张图片
图中所示CMOVE是基于两个独立的TCP连接进行通信,其中真正进行数据转移的(即CSTORE服务)TCP连接(即上图TCP2、Socket2)是导致上述 “页面超时失效”的问题的根源,单纯传输CMOVE结果状态的效率是很高的。 既然如此,是否可以在发送了C-MOVE-RQ请求后,直接关闭TCP1连接,使得CMove函数直接返回,而CSTORE服务依然在继续,当然该做法会导致无法掌握CSTORE服务的完成状况。

DICOM:C-GET与C-MOVE对比剖析(续)_第3张图片

本地测试:

本地使用基于dcm4che3.X的storescp.bat、movescu.bat工具和基于fo-dicom的movescp.bat工具来分别模拟CMOVE服务中的三方,各方启动过程如下:

#启动storescp.bat
storescp -b STORESCP:11112 --directory d:\t
#启动movescp.bat
movescp -b ZSSURE@localhost:11110
#启动movescu.bat
movescu -c ZSSURE@localhost:11110 -m StudyInstanceUID=1.2.3.4.5.6.7.8.9.10.xxxxxxx --dest STORESCP

运行效果如下所示:
DICOM:C-GET与C-MOVE对比剖析(续)_第4张图片
从上图左下角可以看出,启动服务后新建了两个TCP连接,分别是CSTORE服务的11112 to 27491CMOVE的11110 to 27490
但是在右侧使用Ctrl+C停止CMOVE操作后,发现原本运行的CSTORE服务也停止了(如上图左上角所示)。

fo-dicom库分析:

按照之前的设想,断开基于CMOVE的连接后,CSTORE服务应当继续运行才是?但是上面本地验证的结果却是CSTORE服务也终止了。让我们剖析fo-dicom库源码来排查一下原因。
fo-dicom库中(确切的说应该是fo-dicom最早版本,即mDCM)的服务端和客户端组织关系大致如下:

DICOM:C-GET与C-MOVE对比剖析(续)_第5张图片
MoveSCP是基于DcmServiceBase的SCP服务端,在内部绑定的OnReceiveCMoveRequest事件中调用了CStoreClient发起CSTORE子操作,与此同时在CStoreClient的OnReceiveCStoreResponse事件绑定中调用了MoveSCP的SendCMoveResponse函数,大致绑定关系如下所示:
DICOM:C-GET与C-MOVE对比剖析(续)_第6张图片
这里需要注意的是SenCMoveResponse函数是DcmNetworkBase基类的方法,MoveSCP和CStoreClient中都继承了该方法,但是上述绑定关系中C#的匿名函数会将MoveSCP的SendCMoveResponse方法打包传入CStoreClient的OnReceiveCStoreResponse事件中,由此可以得出整个CMOVE服务的消息流顺序如下:
DICOM:C-GET与C-MOVE对比剖析(续)_第7张图片
即每一次CStore操作完成后会通知CMoveSCP发送C-MOVE-RSP消息给CMoveSCU。再次翻阅DICOM标准PS3.8第八部分的9.1.4,也并未明确给出如果中断CMOVE连接,CSTORE服务不可运行。
按照上述消息流继续跟踪fo-dicom的代码,发现当movescu命令工具被中断后,程序会进入到DcmNetworkBase.cs中的ShundownNetwork函数内部,

        protected void ShutdownNetwork() {
            _stop = true;
            if (_thread != null)
            {
                if (Thread.CurrentThread.ManagedThreadId != _thread.ManagedThreadId)
                    _thread.Join();
                _thread = null;
            }
        }

由代码可知,_thread线程是整个CMoveSCP的主线程,即DIMSE的FSM状态机运行在其中;而每次中止后Thread.CurrentThread是CStore的响应线程,即上文提到的在MoveSCP绑定的OnReceiveCMoveRequest事件中创建CStoreClient而生成的CSTOER子操作的线程。
由上述代码可知,每次命令行中断movescu操作时,进入到ShundownNetwork函数内部的负责C-STORE服务的Thread.CurrentThread线程都会被CMoveSCP主线程阻塞,而无法继续我们设想的CSTORE操作。为了验证我们的想法,可以使用Windows自带的procexp以及netstat持续观察中止movescu后的线程数和TCP连接数,如下图所示:
DICOM:C-GET与C-MOVE对比剖析(续)_第8张图片
由上图可以看出,多次中断movescu.bat命令行工具后,会导致诸多TCP连接以及CStoreClient线程处于阻塞状态,短时间内并不会关闭和退出。
至此,我们找到了中断movescu操作,CSTORE服务同时停止的原因了,即fo-dicom的DcmNetworkBase基类阻塞了CSTORE服务子线程。

方案验证:

为了验证我们剖析的结果,注释掉fo-dicom的DcmNetworkBase.cs类的ShundownNetwork中的子线程阻塞代码,

        protected void ShutdownNetwork() {
            _stop = true;
            //if (_thread != null)
            //{
                //if (Thread.CurrentThread.ManagedThreadId != _thread.ManagedThreadId)
                    //_thread.Join();
                //_thread = null;
            //}
        }

重新生成,并验证我们的想法,验证结果如下图所示:
DICOM:C-GET与C-MOVE对比剖析(续)_第9张图片
由上图可以看出,右侧movescu已经停止,但是左上角的storescp依然会接收到C-STORE-RQ请求(上图中的错误是由于我执行了多次,文件重复覆盖导致的)。至此验证了我们的想法,即

DICOM的CMOVE服务中可以在发送完C-MOVE-RQ请求后即可返回,不等待所有CSTORE子操作完成。

结论:

上述分析过程从TCP底层出发,提出了解决起初遇到的“Servlet响应超时”问题的一种方案,但该方案不严格遵守DICOM标准,因此现实部署中不建议使用。此文仅是为了更好的了解DICOM协议在fo-dicom库中的实现,为后续撰写DICOM的FSM有限状态机博文做准备,会详解介绍关于DICOM协议FSM有限状态机的各项内容,诸如DIMSE操作与子操作的关联线程的调度状态的转换等等,敬请期待!




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

你可能感兴趣的:(DICOM,CMOVE,CSTORE)