背景:
5月份的前半段好懒惰,手里积攒了好多篇文章,也有之前答应过博友要写的,迟迟未动笔。究其根源,有些许懒惰,但更多的是迷惑和一知半解,虽想写但却不知如何入手,零星的感悟要积累成文还是需要时间去沉淀的,以期尽量做到每篇博文有理有据。
今天正好借着手头新任务的机会,介绍一下DICOM3.0标准中的又一新内容。继之前两篇博文基于JMeter+dcm4che2测试PACS服务器性能的解决方案(前/续篇)中提到的DICOM标准第15部分中的TransferCapability概念,本篇介绍标准第8部分附录D中A-ASSOCIATE-RQ中的UserInformation,以及其扩展UserIdentity(DICOM3.0标准第7部分附录D)
此外,在文章后半段,会通过对比dcm4che2与fo-dicom(mDCM)的实现,同时借助于RawCap+WireShark对实际数据包进行分析,给出扩展UserInformation的实现。
UserInformation:
关于UserInformation的详细介绍在标准第8部分附录D,其属于A-ASSOCIATE-RQ PDU结构子项。A-ASSOCIATE-RQ PDU的Application Context以及Presentation Context两个子项之前介绍比较多,且与DICOM连接Association建立密切相关。对于User Information子项介绍的不多,该子项以0x50H为标志,内部可进一步扩展子项,范围从0x51H-0xFFH。在第8部分的附录D中只是简略的介绍了Maximum length子项,起始标志为0x51H。该项用于约束连接双方传递数据包P-DATA-TF的最大长度。
关于UserInformation项,标准中有两条注意事项:
1. The values of the Sub-Items types in the User Information Field are assigned by this standard in the range of 51H through FFH. Sub-Item values are defined by PS 3.7 and PS 3.8.
2. Succeeding editions of the Standard may define additional user information Sub-Items in a manner that does not affect the semantics of previously defined Sub-Items. Association acceptors compliant to
an earlier edition of the Standard are required to ignore such unrecognized user information Sub-Items and not reject an Association because of their presence.
由上述注意事项可知,之前鲜有涉猎UserInformation字段而并未影响我们完成DIMSE中的各种服务是因为“标准中提到后续更新的版本中会加入对自定义扩展的说明,对于符合早期DICOM标准的终端(即A-ASSOCIATE-RQ接收方)需要自动忽略无法识别的UserInformation子项,而不应该因为存在无法识别的子项而拒绝连接请求。”
UserIdentity:
DICOM标准第8部分在A-ASSOCIATE-RQ PDU结构体中介绍了User Information项目,作为A-ASSOCIATE-RQ PDU的一部分,对于用户自定义扩展并未做深入讨论。既然标准提到了扩展子项可以从0x51H-0xFFH,是不是我们可以任意扩展呢?原则上是可以任意扩展的,只要确保Associate连接双方约定好解析方式即可。
其实在DICOM3.0标准第7部分的附录中对于UserInformation中常见的扩展有更详细的介绍。在附录中D.3中详解介绍了几种常见的扩展,诸如以0x51H开头的Max PDU Length,以0x52H开头的Implentation Identification Notification,以0x53H开头的Asynchronous Operations Windows Negotiation,以0x54H开头的SCP/SCU Role Selection Negotiation,以0x56H开头的Service-Object Pair Class Extended Negotiation,以0x57H开头的Service-Object Pair Class Common Extended Negotiation,以0x58H开头的User identity Negotiation等等,
【注1】:在与0x52H开始的Implentation Identification Notification包含了两个子项,其中0x52H对应于Implentation Class UID,0x55H对应于Implentation Version Name。
【注2】:在关于UserIdentity扩展字段中,其实包括以0x58H开始的UserIdentityRQ,和以0x59H开始的UserIdentityAC。
近期在项目中要同时使用到fo-dicom(前身是mDCM)和dcm4che2工具包,且需要完成两者之间数据互传,按照之前对DICOM开源库的了解,以为只要配置完成AE Title、IP和Port后即可顺利实现,但是在尝试了几天未果。通过仔细分析dcm4che2附带工具包dcmsnd.bat以及dcmqr.bat,又让我重新阅读了DICOM协议的部分章节,因此也就有了此篇博文。
问题描述:使用fo-dicom开源库实现的客户端,诸如CStoreClient,向基于dcm4che2实现的dcm4chee服务器发送DCM数据,在配置完成AETitile、IP以及Port后,体会无法建立连接。
扩展fo-dicom(mDCM):
RawCap+WireShark抓包分析:
如上图所示是使用RawCap+WireShark抓取的dcmsnd.bat工具包向基于fo-dicom搭建的PACS服务器发送数据数据包。之所以如此操作,谁因为使用fo-dicom构建的CStoreClient无法顺利向基于dcm4che2的dcm4chee服务器发送数据。我们反向操作发现,竟然可以顺利成功。这说明dcm4che实现的dcmsnd.bat工具包发送的数据量可能比我们使用fo-dicom的CStoreClient要大,可能包含了额外的数据。
通过分析上图可以发现,多出来的部分就是以0x58H开头的UserIdentity自扩展子项,从我的专栏文章也可以知道之前在介绍dcmtk、fo-dicom、mDCM时并未涉及到该自定义字段。而dcm4che2工具包内部却对其进行了实现,并且在基于dcm4che2搭建的dcm4chee服务端中还通过该扩展字段进行了连接屏蔽(貌似这不符合我们之前看到的标准备注中提到的“服务端要忽略无法识别字段,而不应该拒绝连接”的说明。想想dcm4chee之所以如是操作,大抵也是为了增加安全性。
dcm4che2工具分析:
既然已经找到了问题原因,接下来让我们分析一下dcm4che2,找出解决方案。dcm4che2工具包中对于PDU数据结构的定义在PDUEncoder.java文件中。此外在ItemType.java类中预定了各种扩展字段类型枚举变量,具体代码如下:
class ItemType {
public static final int APP_CONTEXT = 0x10;
public static final int RQ_PRES_CONTEXT = 0x20;
public static final int AC_PRES_CONTEXT = 0x21;
public static final int ABSTRACT_SYNTAX = 0x30;
public static final int TRANSFER_SYNTAX = 0x40;
public static final int USER_INFO = 0x50;
public static final int MAX_PDU_LENGTH = 0x51;
public static final int IMPL_CLASS_UID = 0x52;
public static final int ASYNC_OPS_WINDOW = 0x53;
public static final int ROLE_SELECTION = 0x54;
public static final int IMPL_VERSION_NAME = 0x55;
public static final int EXT_NEG = 0x56;
public static final int COMMON_EXT_NEG = 0x57;
public static final int RQ_USER_IDENTITY = 0x58;
public static final int AC_USER_IDENTITY = 0x59;
}
标准中规定的0x58H扩展项的结构如下:
通过分析PDUEncoder.java文件,可以看出dcm4che2是严格按照DICOM标准中对UserIdentity子项的约束来构建的,具体构建过程如下:
至此,我们找到了dcm4che2工具中如何向A-ASSOCIATE-RQ PDU中写入UserIdentity扩展项了。接下来就是解决问题的时刻了,让我们看看fo-dicom实现中是否留有接口,让我们来写入UserIdentity这一扩展字段。
实施fo-dicom(mDCM)扩展:
通过分析发现fo-dicom以及其早期版本mDCM中并未留有接口,让用户扩展UserIdentity字段。由于手中搭建的建议客户端以及PACS使用的是早期版本的mDCM,因此这里就直接给出如何修改mDCM代码,fo-dicom中的修改大致相同,这里就不详细介绍了。
主要的修改有以下几处:
第一:在Dicom.Network文件夹下添加自定义的UserIdentity类,可仿照dcm4che2中的实现来进行,此处为了方便,我只向A-ASSOCIATE-RQ PDU中写入了Useridentity,而并未向A-ASSOCIATE-AC PDU中扩展。因此我定义的UserIdentity类结构如下:
public class UserIdentity
{
//密码类型
public static int USERNAME = 1;
public static int USERNAME_PASSCODE = 2;
public static int KERBEROS = 3;
public static int SAML = 4;
private int userIdentityType;
private bool positiveResponseRequested;
private string username = null;
private string passcode = null;
#region Properties
public string UserName
{
get { return username; }
set { username = value; }
}
public string PassCode
{
get { return passcode; }
set { passcode = value; }
}
public int UserIdentityType
{
get { return userIdentityType; }
set { userIdentityType = value; }
}
public bool bPositiveResponseRequested
{
get { return positiveResponseRequested; }
set { positiveResponseRequested = value; }
}
#endregion
#region Constructors
public UserIdentity()
{
this.username = "zssure";
this.passcode = "zssure";
this.positiveResponseRequested = false;
this.userIdentityType = UserIdentity.USERNAME_PASSCODE;
}
public UserIdentity(string user, string passwd)
{
this.username = user;
this.passcode = passwd;
this.positiveResponseRequested = false;
this.userIdentityType = UserIdentity.USERNAME_PASSCODE;
}
#endregion
}`<br>
第二:扩展PDU.cs类,添加WriteAddingUserIdentity函数,便于向socket流数据体中添加UserIdentity子项目,主要代码如下:
//User Indentity
//http://medical.nema.org/medical/dicom/current/output/html/part07.html#sect_D.3.3.7
pdu.Write("Item-Type", (byte)0x58);
pdu.Write("Reserved", (byte)0x00);
pdu.MarkLength16("Item-Length");
pdu.Write("User Identity Type", (byte)userIdentity.UserIdentityType);
pdu.Write("Positive Response Requested", (userIdentity.bPositiveResponseRequested?(byte)0x01:(byte)0x00));
pdu.Write("Primary Field Length", (ushort)userIdentity.UserName.Length);
pdu.Write("Primary Field", userIdentity.UserName);
pdu.Write("Secondary Field Length", (ushort)userIdentity.PassCode.Length);
pdu.Write("Secondary Field", userIdentity.PassCode);
pdu.WriteLength16();
//zssure:end.
第三:扩展DicomNetworkBase.cs基类,添加WriteAddingUserIdentity函数,便于向socket流数据体中添加UserIdentity子项目。
/// <summary>
///Add UserIdentity Information
/// http://medical.nema.org/medical/dicom/current/output/html/part07.html#sect_D.3.3.7
/// </summary>
/// <param name="associate"></param>
/// <param name="userIdentity"></param>
protected void SendAssociateRequest(DcmAssociate associate, UserIdentity userIdentity)
{
_assoc = associate;
if (UseRemoteAeForLogName)
{
LogID = Associate.CalledAE;
Log = LogManager.GetLogger(LogID);
}
Log.Info("{0} -> Association request:\n{1}", LogID, Associate.ToString());
AAssociateRQ pdu = new AAssociateRQ(_assoc);
SendRawPDU(pdu.WriteAddingUserIdentity(userIdentity));
}
第四:修改CStoreClient的OnConnected函数,添加写入UserIdentity子项目。另外为了兼容之前未写入UserIdentity字段的情况,此处做了一下分类讨论。
//zssure:2015/05/26
//Adding UserIdentity Information
//http://medical.nema.org/medical/dicom/current/output/html/part07.html#sect_D.3.3.7
if (userIdentity != null)
{
SendAssociateRequest(associate, userIdentity);
}
else
SendAssociateRequest(associate);
//zssure:end
测试:
重新编译mDCM,将新版Dicom.dll库添加到工程引用,重新使用CStoreClient向dcm4chee服务器发送数据,顺利完成数据上传任务。
【注】:在实验前要在dcm4chee服务端提前注册一个用户名和密码,比如我这里使用的用户名是zssure,密码是2015。
源码下载:
目前mDCM版本已经少有人维护,所以我就讲本地扩展了UserIdentity的版本上传到了Github上,博文中提到的几个主要修改,诸如UserIdentity.cs、DicomNetworkBase.cs、CStoreClient.cs以及PDU.cs都可以从mDCM仓库中找到。
作者:[email protected]
时间:2015-05-27