基于JMeter+dcm4che2测试PACS服务器性能的解决方案(续篇)

背景:


前一篇博文通过扩展JMeter的java请求,结合dcm4che2现有的工具包dcmsnd.bat实现了简单的测试DICOM服务器C-STORE SCP性能的尝试。由于借用了现有的dcmsnd.bat命令行工具,会有诸多的局限性,比如:

1)必须构造命令行中的参数,才能调用dcmsnd.bat,操作多此一举

2)无法准确跟踪一张图像上传完成后的准确时间

3)既然要模拟海量用户并发,需要准备对应的数量的文件,无法通过自动生成dcm的三级UID来自动生成海量测试文件。


针对上述情况,本篇博文在剖析dcm4che2的dcmsnd工具源码的基础上,给出了自己的DcmSend类,通过该类可自由控制dcm文件发送,与此同时可通过修改单一文件的各级UID来自动模拟出海量dcm文件,提高了测试效率和准确性。下面就给出具体过程。

dcm4che2介绍:


相较于之前常用的dcmtk和fo-dicom,在dcm4che中对于DICOM标准的实现更全面一些,出现了部分之前少有提及的概念,但不必担心,这些新的概念都是对以往概念的总结和归纳 。在dcmsnd.bat工具包源码中用到的几个新概念主要是DICOM3.0标准第15部分,这里主要介绍附录H中的几个核心概念:

1)Device:这里指的是从服务概念来划分的,并不一定指的是物理上的一台设备,有可能是多台设备(比如PET-CT,我也不是很确定?)。但是通常Device可以认为是物理上的一台设备。

2)Transfer Capability:这个在DICOM3.0标准的附录H中被划分成了NetworkAE、NetworkConnection和Transfer Capability三部分,三者的具体关系如下图:

这里写图片描述
简单来说,Transfer Capability类同于我们之前介绍的Presentation Context(即描述上下文),两者含有的内容是一样的,都是AbstractSyntax+TransferSyntax,即服务类别+编码格式。但两者所描述的范围不同,Transfer Capability是用于描述设备(暂且认为Device对应一台物理设备)的功能,是设备说明书的一部分;而Presentation Context是我们在讲解DICOM网络传输时用到的概念,是在连接建立的握手过程中用于表明连接双方请求的服务和编码格式的,因此从概念上来说Transfer Capability用于宏观,Presentation Context用于微观;Transfer Capability对应于实体(即设备),而PresentationContext对应于服务(即通讯)

总之简单来说,在编码时,可以通过Transfer Capability来设置连接具体交互时的PresentationContext。这一点在DcmSnd.java类的configureTransferCapability,以及的NetworkApplicationEntity.javamakeAAssociateRQ函数中表现的很明显,在makeAAssociateRQ函数中就是通过TransferCapability来构造AssociateRQ的PresentationContext的。剩下的NetworkApplicationEntity和NetworkConnection两个概念就比较好理解了。

3)NetworkApplicationEntity:代表提供服务的主体,这里要与Device区别开来。举个例子,一台安装Window 7的电脑我们可以称之为Device,而电脑中我们安装了Office、PhotoShop、VisualStudio,分别提供不同的服务,这些安装在Windows7下的软件可以类比做NetworkApplicationEntity,即提供单项服务的主体(即软件)。

4)NetworkConnection:指的就是DICOM中Association对应的底层TCP连接。

编写自己的DcmSnd类:


关于DICOM中Associaton建立的过程,之前我在专栏中介绍过多次DICOM医学图像处理:DICOM网络传输DICOM医学图像处理:全面分析DICOM3.0标准中的通讯服务模块。dcm4che2与之前使用的dcmtk和fo-dicom相同,只不过开发语言换成了java而已。下面就直奔主题,直接介绍如何抽取DcmSnd.java现有类库中的相关代码,构造自己的DcmSnd类,我这里称之为ZSDcmSend

DcmSnd.java类分析:


具体流程跟之前博文中对于DICOM Association建立完全一样,知道了DcmSnd.java具体构造流程后,我们就直接裁剪掉关于cmd命令行解析的代码,构造我们自己的精简版ZSDcmSend.java。

这里写图片描述
核心部分代码粘贴如下,流程就是按照上图所示:

public void SendDicomFile()
{
    try {

        //set parameters for SCP, remote
        setCalledAET("PACS_SCP");
        setRemoteHost("192.168.24.1");
        setRemotePort(11110);
        //set parameters for SCU, local
        setCalling("ZSSURE");
        setLocalHost("127.0.0.1");
        setLocalPort(11112);


        DimseRSPHandler rspHandler = new DimseRSPHandler() {
            @Override
            public void onDimseRSP(Association as, DicomObject cmd,
                    DicomObject data) {
               ZSDcmSend.this.onDimseRSP(cmd);
            }
        };

        FileInfo info = new FileInfo(dcmFile);
        DicomObject dcmObj = new BasicDicomObject();
        DicomInputStream in = null;
        try {
            in = new DicomInputStream(dcmFile);
            in.setHandler(new StopTagInputHandler(Tag.StudyDate));
            in.readDicomObject(dcmObj, PEEK_LEN);
            info.tsuid = in.getTransferSyntax().uid();
            info.fmiEndPos = in.getEndOfFileMetaInfoPosition();
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("WARNING: Failed to parse " + dcmFile + " - skipped.");
            System.out.print("Failure");
            return;
        } finally {
            CloseUtils.safeClose(in);
        }
        info.cuid = dcmObj.getString(Tag.MediaStorageSOPClassUID,
                dcmObj.getString(Tag.SOPClassUID));
        if (info.cuid == null) {
            System.err.println("WARNING: Missing SOP Class UID in " + dcmFile
                    + " - skipped.");
            System.out.print("Failure");
            return;
        }
        info.iuid = dcmObj.getString(Tag.MediaStorageSOPInstanceUID,
                dcmObj.getString(Tag.SOPInstanceUID));
        if (info.iuid == null) {
            System.err.println("WARNING: Missing SOP Instance UID in " + dcmFile
                    + " - skipped.");
            System.out.print("Failure");
            return;
        }
        addTransferSyntaxs(info.cuid,info.tsuid);
        configureTransferCapability();

        try {
            assoc=ae.connect(remoteAE, executor);
        } catch (ConfigurationException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }

        DataWriter data=new DataWriter(info);
        String test=new DicomInputStream(info.f).readDicomObject().getString(Tag.SOPInstanceUID);
        System.out.println(test);
        try {
            assoc.cstore(info.cuid, info.iuid, 0, data, info.tsuid,rspHandler);
        } catch (NoPresentationContextException e) {
            System.err.println("WARNING: " + e.getMessage()
                    + " - cannot send ");
            System.out.print('F');
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("ERROR: Failed to send - " + ": "
                    + e.getMessage());
            System.out.print('F');
        } catch (InterruptedException e) {
            // should not happen
            e.printStackTrace();
        }catch(NullPointerException e)
        {
            e.printStackTrace();
        }



    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

}

自定义修改DCM文件UID:


dcm4che2官网的cookbook中有关于DICOM文件读写的示例,这里有一个“坑”需要注意一下——之前我一直认为跟dcmtk和fo-dicom操作类似,加载完DicomObject对象后,先remove掉指定Tag元素,然后根据类型采用putString、putInt、putDouble等在添加即可。但是下面一段代码一直没有发生变化:

DicomInputStream temp=new DicomInputStream(info.f);
DicomObject tempObj=temp.readDicomObject();
tempObj.remove(Tag.StudyInstanceUID);
tempObj.putString(Tag.StudyInstanceUID, VR.UI,"123.123.123.123.1.1");
tempObj.remove(Tag.SeriesInstanceUID);
tempObj.putString(Tag.SeriesInstanceUID,VR.UI,"123.123.123.123.1.1.1");
tempObj.remove(Tag.SOPInstanceUID);
tempObj.putString(Tag.SOPInstanceUID,VR.UI,"123.123.123.123.1.1.1.1");
temp.close();
info.iuid=tempObj.getString(Tag.SOPInstanceUID);
FileOutputStream fos=new FileOutputStream(info.f);
BufferedOutputStream bos=new BufferedOutputStream(fos);
DicomOutputStream dos=new DicomOutputStream(bos);
dos.writeDicomFile(tempObj);


后来自己浏览了dcm4che2的Cookbook,发现Java中有FilterInputStream和FilterOutputStream两个字节流,对于DICOM文件修改要想生效应该采用派生自FilterOutputStream的DicomOutputStream。官网实例如下图:

这里写图片描述
然后我添加了一个修改UID的函数changeTagbyString即可解决自定义修改DICOM文件中UID的目的

    public int changeTagbyString(int[] tag,VR[] vr,String[] strValue)
{
    if(tag.length!=vr.length || tag.length!=strValue.length ||strValue.length!=vr.length)
    {
        System.out.println("parameters set error!");
        return -1;
    }
    if(tag==null || vr==null ||strValue==null || dcmFile==null)
    {
        System.out.println("parameter set error!");
        return -2;
    }
    try {
        DicomInputStream temp=new DicomInputStream(dcmFile);
        DicomObject tempObj=temp.readDicomObject();
        for(int i=0;i<tag.length;++i)
        {
            tempObj.remove(tag[i]);
            tempObj.putString(tag[i], vr[i],strValue[i]);
        }

        temp.close();
        //save your changes
        FileOutputStream fos=new FileOutputStream(dcmFile);
        BufferedOutputStream bos=new BufferedOutputStream(fos);
        DicomOutputStream dos=new DicomOutputStream(bos);
        dos.writeDicomFile(tempObj);
        dos.close();
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return 0;
}

至此,自定义ZSDcmSend.java类就算完成了,配置一个PACS服务器测试一下看看结果吧。

后记:


写好了自定义的ZSDcmSend.java类还只是扩展JMeter的Java请求的第一步,后续好需要配合AbstractJavaSamplerClient类在合适的地方添加采样开关,同时需要统计相关数据才能具体测算出每幅图像传输的时间和服务器响应时间,进而实现测试PACS服务器的性能的目标。后续还会继续更新。

(未完……)







作者:zssure@163.com

时间:2015-05-24