DcmSCP:dicom service class provider,相当于服务器
DcmSCU:dicom service class user,相当于客户端
DIMSE:dicom message service element,dicom连接中传递的消息单元
一、建立连接
DICOM网络连接建立在TCP基础上,使用IP地址和端口号通信。
1. SCP开始监听端口
2. 初始化TCP连接
3. SCU向SCP发送连接请求
4. SCP接收连接请求消息,查找是否有支持的服务
5. 若有支持的服务,SCP向SCU发送连接确认消息,SCU收到确认消息后DICOM连接建立。
6. 否则SCP向SCU发送连接拒绝消息,断开TCP连接
二、消息类型
DIMSE有C-Style风格和N-Style风格两种,PACS系统之间传输文件一般使用C-Style消息。
1. C-ECHO 用于确认连接是否建立
2. C-STORE 用于发送文件并存储
3. C-MOVE 用于查询和移动文件
4. C-GET 用于查询和拉取文件
5. C-FIND 用于查询文件
每种消息都有请求和确认两种。部分服务流程如下:
·C-STORE
SCU向SCP发送请求消息,消息中带有待存储的dicom数据文件,SCP收到消息后将数据文件存储在服务器,然后向SCU返回确认消息,包含处理结果。
·C-MOVE
SCU向SCP发送请求消息,消息中带有查询数据信息和移动目标的AETitle,SCP收到消息后,从服务器文件中查询是否有符合条件的文件,如果有,另外创建一个SCU,通过该SCU向目标发送C-STORE请求,等待C-STORE回应。一次C-MOVE操作中可能会包含多次C-STORE子操作。待所有符合条件的dicom文件都发送完毕后,关闭其创建的SCU,释放连接,然后向最初发送C-MOVE请求的SCU返回C-MOVE确认,包含C-MOVE的处理结果。(实际上对于每次C-STORE子操作都应当返回一次C-MOVE确认消息,但编写程序时也可只在最后返回确认消息,这取决于你的实际需求。)
三、基于DCMTK的示例
·头文件
#pragma once #include "dcmtk/config/osconfig.h" #include "dcmtk/dcmnet/scp.h" #include "dcmtk/dcmnet/scu.h" #include "dcmtk/dcmnet/diutil.h" #include "dcmtk/ofstd/offname.h" //OFFilenameCreator 类 class DataItem :public DcmDataset { public: //构造函数获取dataset DataItem(DcmDataset& old) :DcmDataset(old) {}; ~DataItem() {}; DcmList* getElementList() const { return this->elementList; } }; class SCP :public DcmSCP { public: SCP(); SCP(const SCP& old); ~SCP(); void initSCU(); void setOutputDirectory(OFString path); // 处理收到的命令,C-ECHO、C-STORE、C-GET、C-MOVE等 virtual OFCondition handleIncomingCommand(T_DIMSE_Message* incomingMsg, const DcmPresentationContextInfo& presInfo); OFCondition generateSTORERequestFilename(const T_DIMSE_C_StoreRQ& reqMessage, OFString& filename, OFString studyInstanceUID); OFCondition generateDirAndFilename(OFString& filename, OFString& directoryName, OFString& sopClassUID, OFString& sopInstanceUID, OFString studyInstanceUID); // 处理C-MOVE服务 OFCondition handleMOVE(DcmDataset* dataset, OFString dest); OFCondition queryFilewithDataset(OFList<OFString>& files, DcmDataset dataset); OFCondition ConnectToDest(); //该函数控制退出listen循环,只需重载,会在listen函数中被调用。 virtual OFBool stopAfterCurrentAssociation(); void setIsTmp(bool stat); private: OFString OutputDirectory = "D:/DICOMSTORE"; OFString QueryDirectory = "D:/DICOMSTORE/1.2.276.0.7230010.3.1.4.3707881089.6120.1625463501.901"; DcmSCU scu; OFString moveDest; bool isTmp = false; }; void getFiles(OFString path, OFList<OFString>& files);
·源文件
#include "SCP.h" SCP::SCP() { } SCP::~SCP() { } void SCP::initSCU() { scu.setPeerAETitle(moveDest); scu.setPeerPort(11114); scu.setPeerHostName("127.0.0.1"); setVerbosePCMode(OFTrue); OFList<OFString> ts; ts.push_back(UID_LittleEndianExplicitTransferSyntax); ts.push_back(UID_BigEndianExplicitTransferSyntax); ts.push_back(UID_LittleEndianImplicitTransferSyntax); scu.addPresentationContext(UID_CTImageStorage, ts); scu.addPresentationContext(UID_SecondaryCaptureImageStorage, ts); scu.addPresentationContext(UID_VerificationSOPClass, ts);// 响应C-ECHO } OFCondition SCP::handleIncomingCommand(T_DIMSE_Message* incomingMsg, const DcmPresentationContextInfo& presInfo) { //该函数尚未接收来自scu的dataset,只接收了命令信息 OFCondition cond; OFCondition status = EC_IllegalParameter; // 处理 C-ECHO 请求 if ((incomingMsg->CommandField == DIMSE_C_ECHO_RQ) && (presInfo.abstractSyntax == UID_VerificationSOPClass)) { DCMNET_DEBUG("C-ECHO"); cond = handleECHORequest(incomingMsg->msg.CEchoRQ, presInfo.presentationContextID); } else if ((incomingMsg->CommandField == DIMSE_C_STORE_RQ)) { // 处理 C-STORE 请求 DCMNET_DEBUG("C-STORE"); // 接收数据 T_DIMSE_C_StoreRQ& storeReq = incomingMsg->msg.CStoreRQ; Uint16 rspStatusCode = STATUS_STORE_Error_CannotUnderstand; DcmFileFormat fileformat; DcmDataset* reqDataset= fileformat.getDataset(); status = receiveSTORERequest(storeReq, presInfo.presentationContextID, reqDataset); OFString studyInstanceUID; reqDataset->findAndGetOFString(DCM_StudyInstanceUID, studyInstanceUID); // 直接保存为文件 OFString filename; // 生成文件名(包含目录) status = generateSTORERequestFilename(storeReq, filename, studyInstanceUID); if (status.good()) { if (OFStandard::fileExists(filename)) DCMNET_WARN("file already exists, overwriting: " << filename); // 调用 receiveSTORERequest 函数接收并保存 dataset 为文件 //status = receiveSTORERequest(storeReq, presInfo.presentationContextID, filename); status = fileformat.saveFile(filename); if (status.good()) { rspStatusCode = STATUS_Success; } } // 发送回应消息 if (status.good()) status = sendSTOREResponse(presInfo.presentationContextID, storeReq, rspStatusCode); else if (status == DIMSE_OUTOFRESOURCES) { sendSTOREResponse(presInfo.presentationContextID, storeReq, STATUS_STORE_Refused_OutOfResources); } } else if ((incomingMsg->CommandField == DIMSE_C_MOVE_RQ)) { // 处理 C-MOVE 请求 /*接收C-MOVE消息 * 服务器数据查询符合条件的文件 * 向指定sop发送C-STORE,发送符合条件的文件 * 收到C-STORE回应 * 发送C-MOVE回应 */ DCMNET_DEBUG("C-MOVE"); DcmFileFormat fileformat; DcmDataset* reqDataset = fileformat.getDataset(); T_DIMSE_C_MoveRQ& moveReq = incomingMsg->msg.CMoveRQ; Uint16 rspStatusCode = STATUS_MOVE_Failed_UnableToProcess; //接收查询条件和移动目的地,moveDest为sop目标的aetitle status = receiveMOVERequest(moveReq, presInfo.presentationContextID, reqDataset, moveDest); if (status.good()) { if (moveDest.empty()) { //目标AEtitle为空 sendMOVEResponse(presInfo.presentationContextID, moveReq.MessageID, moveReq.AffectedSOPClassUID, NULL, STATUS_MOVE_Failed_MoveDestinationUnknown); } //处理C-MOVE请求 status = handleMOVE(reqDataset, moveDest); if (status.bad()) { //发送处理成功信息 //有失败的子操作 sendMOVEResponse(presInfo.presentationContextID, moveReq.MessageID, moveReq.AffectedSOPClassUID, reqDataset, STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures); } //操作成功时不应返回任何dataset sendMOVEResponse(presInfo.presentationContextID, moveReq.MessageID, moveReq.AffectedSOPClassUID, NULL, STATUS_Success); } } else { // 其他请求全部拒绝 OFString tempStr; DCMNET_ERROR("Cannot handle this kind of DIMSE command (0x" << STD_NAMESPACE hex << STD_NAMESPACE setfill('0') << STD_NAMESPACE setw(4) << OFstatic_cast(unsigned int, incomingMsg->CommandField) << ")"); DCMNET_DEBUG(DIMSE_dumpMessage(tempStr, *incomingMsg, DIMSE_INCOMING)); cond = DIMSE_BADCOMMANDTYPE; } return cond; } OFCondition SCP::generateSTORERequestFilename(const T_DIMSE_C_StoreRQ& reqMessage, OFString& filename, OFString studyInstanceUID) { OFString directoryName; OFString sopClassUID = reqMessage.AffectedSOPClassUID; OFString sopInstanceUID = reqMessage.AffectedSOPInstanceUID; // 生成文件名 OFCondition status = generateDirAndFilename(filename, directoryName, sopClassUID, sopInstanceUID, studyInstanceUID); if (status.good()) { DCMNET_DEBUG("generated filename for object to be received: " << filename); // 创建存储目录 status = OFStandard::createDirectory(directoryName, OutputDirectory /* rootDir */); if (status.bad()) DCMNET_ERROR("cannot create directory for object to be received: " << directoryName << ": " << status.text()); } else DCMNET_ERROR("cannot generate directory or file name for object to be received: " << status.text()); return status; } OFCondition SCP::generateDirAndFilename(OFString& filename, OFString& directoryName, OFString& sopClassUID, OFString& sopInstanceUID, OFString studyInstanceUID) { OFCondition status = EC_Normal; // 生成目录名 OFString generatedDirName; if (!studyInstanceUID.empty()) { OFOStringStream stream; stream << studyInstanceUID<< OFStringStream_ends; OFSTRINGSTREAM_GETSTR(stream, tmpString) generatedDirName = tmpString; OFSTRINGSTREAM_FREESTR(tmpString); } // 连接文件路径 OFStandard::combineDirAndFilename(directoryName, OutputDirectory, generatedDirName); // 生成文件名 OFString generatedFileName; if (sopClassUID.empty()) status = NET_EC_InvalidSOPClassUID; else if (sopInstanceUID.empty()) status = NET_EC_InvalidSOPInstanceUID; else { OFOStringStream stream; stream << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN") << '.' << sopInstanceUID << ".dcm" << OFStringStream_ends; OFSTRINGSTREAM_GETSTR(stream, tmpString) generatedFileName = tmpString; OFSTRINGSTREAM_FREESTR(tmpString); // 连接文件路径和文件名 OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName); } return status; } OFCondition SCP::handleMOVE(DcmDataset* dataset, OFString dest) { OFList<OFString> files; queryFilewithDataset(files, *dataset); OFCondition result = ConnectToDest(); if (result.bad()) { return result; } if (files.empty()) { result = scu.sendECHORequest(0);//建立一次连接,用于关闭tmpSCP监听 return EC_Normal; } else { Uint16 rsp; for (auto file : files) { //逐个发送文件到dest目的 //需要先初始化scu与目标sop的连接 result = scu.sendSTORERequest(0, file, NULL, rsp); if (result.bad()) { DCMNET_ERROR(result.text()); //sendMOVEResponse(); } } scu.closeAssociation(DCMSCU_RELEASE_ASSOCIATION); return result; } } OFCondition SCP::queryFilewithDataset(OFList<OFString>& files, DcmDataset dataset) { OFList<OFString> allFiles; //获取查询目录下的所有文件 getFiles(QueryDirectory, allFiles); DataItem queryItem(dataset); DcmList* queryList = queryItem.getElementList(); if (queryList->empty() || allFiles.empty()) { return OFCondition(EC_Normal); } for (auto file : allFiles) { DcmFileFormat fileformat; OFString val;//查询条件 OFString value; OFCondition result = fileformat.loadFile(OFFilename(file)); // 待查询的dataset DcmDataset* dataset = fileformat.getDataset(); DcmObject* object; DcmTag tag; bool isequal = true; //遍历每个element queryList->seek(ELP_first); do { object = queryList->get(); tag = object->getTag();//获取当前tag DcmElement* element; queryItem.findAndGetElement(tag, element);//获取tag对应的element element->getOFString(val, 0);//获取tag对应的value dataset->findAndGetOFString(tag, value);//获取当前查询文件的相同tag对应的value if (val != value) { isequal = false; break; } } while (queryList->seek(ELP_next)); if (isequal) { files.push_back(file); } } return OFCondition(EC_Normal); } OFCondition SCP::ConnectToDest() { initSCU(); OFCondition result; /*初始化连接*/ result = scu.initNetwork(); if (result.bad()) { DCMNET_ERROR("Unable to set up the network: " << result.text()); return result; } result = scu.negotiateAssociation(); if (result.bad()) { DCMNET_ERROR("Unable to negotiate association: " << result.text()); return result; } /*发送C-ECHO测试连接*/ result = scu.sendECHORequest(0); if (result.bad()) { DCMNET_ERROR("Could not process C-ECHO with the server:" << result.text()); return result; } else { DCMNET_INFO("连接成功。\n"); } return result; } OFBool SCP::stopAfterCurrentAssociation() { if (isTmp) return OFTrue; else return OFFalse; } void getFiles(OFString path, OFList<OFString>& files) { intptr_t hFile = 0; //文件信息 struct _finddata_t fileinfo; OFString p; if ((hFile = _findfirst(p.assign(path).append("/*").c_str(), &fileinfo)) != -1) { do { //如果是目录,递归查找 //如果不是,把文件绝对路径存入vector中 if ((fileinfo.attrib & _A_SUBDIR)) { if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0) getFiles(p.assign(path).append("/").append(fileinfo.name), files); } else { files.push_back(p.assign(path).append("/").append(fileinfo.name)); } } while (_findnext(hFile, &fileinfo) == 0); _findclose(hFile); } } void SCP::setOutputDirectory(OFString path) { OutputDirectory = path; } void SCP::setIsTmp(bool stat) { isTmp = stat; }
调用SCP类的listen函数开启端口监听(这会阻塞线程)。在另一个线程中创建DcmSCU对象,向该SCP发送命令(仅支持C-ECHO、C-MOVE、C-STORE)。发送C-MOVE时需要另外启动一个线程并创建另一个SCP对象用于接收数据。IP地址和端口号请根据实际情况设置。
一、建立连接
DICOM网络连接建立在TCP基础上,使用IP地址和端口号通信。
1. SCP开始监听端口
2. 初始化TCP连接
3. SCU向SCP发送连接请求
4. SCP接收连接请求消息,查找是否有支持的服务
5. 若有支持的服务,SCP向SCU发送连接确认消息,SCU收到确认消息后DICOM连接建立。
6. 否则SCP向SCU发送连接拒绝消息,断开TCP连接
二、消息类型
DIMSE有C-Style风格和N-Style风格两种,PACS系统之间传输文件一般使用C-Style消息。
1. C-ECHO 用于确认连接是否建立
2. C-STORE 用于发送文件并存储
3. C-MOVE 用于查询和移动文件
4. C-GET 用于查询和拉取文件
5. C-FIND 用于查询文件
每种消息都有请求和确认两种。部分服务流程如下:
·C-STORE
SCU向SCP发送请求消息,消息中带有待存储的dicom数据文件,SCP收到消息后将数据文件存储在服务器,然后向SCU返回确认消息,包含处理结果。
·C-MOVE
SCU向SCP发送请求消息,消息中带有查询数据信息和移动目标的AETitle,SCP收到消息后,从服务器文件中查询是否有符合条件的文件,如果有,另外创建一个SCU,通过该SCU向目标发送C-STORE请求,等待C-STORE回应。一次C-MOVE操作中可能会包含多次C-STORE子操作。待所有符合条件的dicom文件都发送完毕后,关闭其创建的SCU,释放连接,然后向最初发送C-MOVE请求的SCU返回C-MOVE确认,包含C-MOVE的处理结果。
标签:DICOM,MOVE,文件传输,发送,DCMTK,SCU,OFString,SCP,STORE From: https://www.cnblogs.com/cc-qt-dcmtk/p/17735580.html