Dcmtk 源码解读SCP交互过程- CEcho Scp

Posted 恒哥的爸爸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dcmtk 源码解读SCP交互过程- CEcho Scp相关的知识,希望对你有一定的参考价值。

图1 Echo SCP源码动态流程图

1 创建过程

 

       首先,创建一个DcmStorageSCPProxy的代理类,此类继承于 OFThread。整个StoreSCP服务的链接过程,可以简单的描述为在主线程中(上图中的蓝色区域)初始化链接对象(包括创建socket);主要的创建代码在dul.cc文件中,这里需要注意的是,在服务端绑定的ip地址的时候,INADDR_ANY表示本机所有的网卡,都要进行监听。这里和127.0.0.1还有有很大区别的,127.0.0.1表示只有本机客户端可以连接此监听端口。然后,开始监听此端口。整个过程比较简单。

static OFCondition initializeNetworkTCP(PRIVATE_NETWORKKEY ** key, void *parameter)

    struct linger sockarg;
    int reuse = 1;
    (*key)->networkSpecific.TCP.tLayer = NULL;
    (*key)->networkSpecific.TCP.tLayerOwned = 0;
    (*key)->networkSpecific.TCP.port = -1;
    (*key)->networkSpecific.TCP.listenSocket = -1;

    int sock;
    struct sockaddr_in server;
    (*key)->networkSpecific.TCP.port = *(int *) parameter;
    (*key)->networkSpecific.TCP.listenSocket = socket(AF_INET, SOCK_STREAM, 0); // 表示创建ip4 stream类型的socket
    sock = (*key)->networkSpecific.TCP.listenSocket;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *) &reuse, sizeof(reuse));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = (unsigned short) htons((*key)->networkSpecific.TCP.port);
    bind(sock, (struct sockaddr *) & server, sizeof(server)); // 这里要注意,绑定的ip地址,INADDR_ANY表示本机所有的网卡,都要进行监听。这里和127.0.0.1还有有很大区别的,127.0.0.1表示只有本机客户端可以连接此监听端口
    /* Listen on the socket */
    listen(sock, PRV_LISTENBACKLOG);
    return EC_Normal;

2 监听线程       

        其次,启动监听线程(上图中绿色区域), 等待SCU来连接。 服务端一旦监听到SCU的连接,就会对比请求的Presetation Context,是否SCP端支持,如果支持,就会向SCU发送
A-ASSOCIATE-AC消息。这里要注意,在和客户端交互的过程中,有一个很重要的概念,就是dicom通讯中的状态机流程。在交互的过程中,有13个状态

Sta1 - Idle
Sta2 - Transport connection open
Sta3 - Awaiting local A-ASSOCIATE response primitive
Sta4 - Awaiting transport connection opening to complete
Sta5 - Awaiting A-ASSOCIATE-AC or A-ASSOCIATE-RJ PDU
Sta6 - Association established and ready for data transfer
Sta7 - Awaiting A-RELEASE-RP PDU
Sta8 - Awaiting local A-RELEASE response primitive
Sta9 - Release collision requestor side; awaiting A-RELEASE response
Sta10 - Release collision acceptor side; awaiting A-RELEASE-RP PDU
Sta11 - Release collision requestor side; awaiting A-RELEASE-RP PDU
Sta12 - Release collision acceptor side; awaiting A-RELEASE response primitive
Sta13 - Awaiting Transport Connection Close Indication

另外,还有19中事件Event,来触发这些状态的跃迁。

#define    A_ASSOCIATE_REQ_LOCAL_USER    0
#define    TRANS_CONN_CONFIRM_LOCAL_USER    1
#define    A_ASSOCIATE_AC_PDU_RCV        2
#define    A_ASSOCIATE_RJ_PDU_RCV        3
#define    TRANS_CONN_INDICATION        4
#define    A_ASSOCIATE_RQ_PDU_RCV        5
#define    A_ASSOCIATE_RESPONSE_ACCEPT    6
#define    A_ASSOCIATE_RESPONSE_REJECT    7
#define    P_DATA_REQ            8
#define    P_DATA_TF_PDU_RCV        9
#define    A_RELEASE_REQ            10
#define    A_RELEASE_RQ_PDU_RCV        11
#define    A_RELEASE_RP_PDU_RCV        12
#define    A_RELEASE_RESP            13
#define    A_ABORT_REQ            14
#define    A_ABORT_PDU_RCV            15
#define    TRANS_CONN_CLOSED        16
#define    ARTIM_TIMER_EXPIRED        17
#define    INVALID_PDU             18
#define DUL_NUMBER_OF_EVENTS        19

在图1中的绿色区域,首先是TRANS_CONN_INDICATION=4事件下,将SCP当前的连接的状态从state = 1(Idle) next state = 2(Transport connection open)的转换,马上又通过A_ASSOCIATE_RQ_PDU_RCV=5  事件,将current state = 2 (Transport connection open)next state = 3(Awaiting local A-ASSOCIATE response primitive) 。然后,Scp代码通过本地的一些配置,判断当前服务是否支持SCU提交的PC,是否是合法的AE,如果所有的验证都通过后,将发送A-ASSOCIATE-AC信息,通过事件A_ASSOCIATE_RESPONSE_ACCEPT=6 将本地状态从  current state = 3 ( Awaiting local A-ASSOCIATE response primitive)转化为next state = 6(Association established and ready for data transfer)的状态。

3 创建DIMSE处理线程

       ASC连接成功后,启动第三个线程,处理P-DATA-TF 格式的PDU,也就是DIMSE层来处理具体的请求(在图1中的黄色区域)。

      整个过程是通过一个循环调用DIMSE_readNextPDV函数来获取PDV包。

for (last = OFFalse, bytesRead = 0, type = DUL_COMMANDPDV;
         type == DUL_COMMANDPDV && !last;)
    
        /* get next PDV (in detail, in order to get this PDV, a */
        /* PDU has to be read from the incoming socket stream) */
        cond = DIMSE_readNextPDV(assoc, blocking, timeout, &pdv);

        /* if this is the first loop iteration, get the presentation context ID which is captured in the */
        /* current PDV. If this is not the first loop iteration, check if the presentation context IDs in */
        /* the current PDV and in the last PDV are identical. If they are not, return an error. */
        // 如果是PDV的第一个包,将表示上下文记录下来;后续的PDV如果表达上下的ID和第一个PDV的表达上下文不同的话,也不符合规范,返回;
        if (pdvCount == 0)
        
            pid = pdv.presentationContextID;
        
        else if (pdv.presentationContextID != pid)
        
            delete cmdSet;
            return makeDcmnetSubCondition(DIMSEC_RECEIVEFAILED, OF_error, "DIMSE Failed to receive message", subCond);
        

        /* check if the fragment length of the current PDV is odd. This should */
        /* never happen (see DICOM standard (year 2000) part 7, annex F) (or */
        /* the corresponding section in a later version of the standard.) */
        //保证PDV的长度是2的整数倍  ... ... 可以查看第7章的 附件F


        /* if information is contained the PDVs fragment, we want to insert this information into the buffer */
        if (pdv.fragmentLength > 0) 
            cmdBuf.setBuffer(pdv.data, pdv.fragmentLength);
        

        /* if this fragment contains the last fragment of the DIMSE command, set the end of the stream */
        //如果是最后一个PDV,那么,将命令buffer设置结束标志量
        if (pdv.lastPDV) 
            cmdBuf.setEos();
        

        /* insert the information which is contained in the buffer into the DcmDataset */
        /* variable. Mind that DIMSE commands are always specified in the little endian */
        /* implicit transfer syntax. Additionally, we want to remove group length tags. */
        econd = cmdSet->read(cmdBuf, EXS_LittleEndianImplicit, EGL_withoutGL);
        if (econd != EC_Normal && econd != EC_StreamNotifyClient)
        
          delete cmdSet;
          return makeDcmnetSubCondition(DIMSEC_RECEIVEFAILED, OF_error, "DIMSE: receiveCommand: cmdSet->read() Failed", econd);
        

        /* update the counter that counts how many bytes were read from the incoming socket */
        /* stream. This variable will only be used for dumping general information. */
        bytesRead += pdv.fragmentLength;

        /* update the following variables which will be evaluated at the beginning of each loop iteration. */
        last = pdv.lastPDV;
        type = pdv.pdvType;

        /* update the counter that counts how many PDVs were received on the incoming */
        /* socket stream. This variable will be used for determining the first */
        /* loop iteration and dumping general information. */
        pdvCount++;
    

        当通过DIMSE_readNextPDV函数接收到从SCU发送的A-ASSOCIATE-RELEASE-RQ的请求, SCP内部发生Event:A_RELEASE_RQ_PDU_RCV=11的事件,将 current state = 6(Association established and ready for data transfer) next state = 8  (Awaiting local A-RELEASE response primitive) 状态。

       最后程序调用ASC_acknowledgeRelease(assoc)和DUL_AcknowledgeRelease函数,将A-ASSOCIATE-RESPONSE发送会SCU端。这时,内部状态机发生A_RELEASE_RESP=13 事件,将将 current state = 8(Awaiting local A-RELEASE response primitive)改变为 next state = 13  (Awaiting Transport Connection Close Indication) 状态。

       源代码中,注意文件dulfsm.cc中的FSM_ENTRY,有限状态机类,和dcm4che中的dcm4che-net.jar的State类作用相同,这里的实现过程,设计都很巧妙,有兴趣的可以研读源代码。

以上是关于Dcmtk 源码解读SCP交互过程- CEcho Scp的主要内容,如果未能解决你的问题,请参考以下文章

Dcmtk 源码解读SCP交互过程- CEcho Scp

Dcmtk 源码解读SCP交互过程- CEcho Scp

[Java随笔]冰蝎2.0-jsp马交互部分源码解读及其特征检测

[Java随笔]冰蝎2.0-jsp马交互部分源码解读及其特征检测

Spring源码解读---启动过程源码解析

Spring源码解读---启动过程源码解析