4. DICOM网络服务与通信模型

DICOM网络通信的核心是“服务类”(Service Class)与“角色”(Role)的组合。任何一次网络交互,都发生在两个DICOM应用实体(AE)之间:一方发起请求,另一方响应。发起请求的一方称为SCU(Service Class User,服务类用户),响应请求的一方称为SCP(Service Class Provider,服务类提供者)。同一个AE在不同服务中角色可以互换——PACS归档服务器在接收图像时是Storage SCP,在响应查询时是Query/Retrieve SCP,而在向外转发图像时又变成Storage SCU。

理解SCU/SCP的关键在于:它描述的是单次服务交互中的相对角色,而不是设备的固定属性。DICOM采用客户端-服务器模型,但“客户端”与“服务器”是动态的。建立连接时,SCU通过A-ASSOCIATE请求提出自己希望使用的表示上下文(Presentation Context),即“SOP类 + 传输语法”的组合;SCP在A-ASSOCIATE响应中逐条接受或拒绝。只有被接受的表示上下文,后续的DIMSE消息才能在其上传输。

DIMSE(DICOM Message Service Element)是网络服务的消息层。每个操作由一对消息构成:请求(RQ)与响应(RSP)。例如C-STORE-RQ / C-STORE-RSP、C-FIND-RQ / C-FIND-RSP。命令集(Command Set)承载操作类型与状态码,数据集(Data Set)承载实际的SOP实例内容。

五类核心服务的交互差异

C-STORE是最基础的推送服务:SCU把整个SOP实例(如一张CT图像)作为数据集发送,SCP接收后返回状态码。成功为0x0000,警告如0xB000/0xB006表示部分元素被强制转换或数据集不一致,失败如0xA700表示拒绝存储。C-STORE是单向数据流,SCU不索取任何数据。

C-FIND用于查询,SCU发送一个“查询标识符”(Query/Retrieve Level + 匹配键),SCP返回0个或多个匹配结果。它采用“多响应”机制:每个匹配项作为一条C-FIND-RSP返回,状态为0xFF00(Pending),最后以0x0000(Success)结束。C-FIND只返回属性,不传输像素数据。

C-MOVE是“第三方转发”服务:SCU向SCP指定目标AE Title,SCP把匹配的实例通过C-STORE推送到该目标。SCU本身不接收图像,只接收每个子操作的进度响应。这要求SCP能作为Storage SCU主动连接目标AE。

C-GET与C-MOVE目标相同,但图像直接通过同一个已建立的关联回传给请求方。SCU既是Query/Retrieve SCU,又临时充当Storage SCP,需要在关联协商时声明支持Storage SOP类。C-GET避免了第三方AE配置,但把接收压力集中在请求端。

DICOM 网络服务交互模型(SCU ⇄ SCP) C-STORE(推送存储) Storage SCU Storage SCP C-STORE-RQ(SOP实例) C-STORE-RSP(0x0000/0xA700) 单向数据流,SCU不索取数据 C-FIND(查询) Query SCU Query SCP C-FIND-RQ(匹配键) C-FIND-RSP(0xFF00 Pending × N → 0x0000) 多响应,仅返回属性,无像素 C-MOVE(第三方转发) QR SCU QR SCP C-MOVE-RQ(目标AE Title) C-MOVE-RSP(进度 0xFF00 → 0x0000) 目标 Storage SCP SCP 作为 Storage SCU 主动推送 C-GET(同关联回传) QR SCU QR SCP C-GET-RQ(匹配键) C-GET-RSP + C-STORE-RQ(同关联) SCU 临时充当 Storage SCP 无需第三方 AE 配置 关键状态码与判断依据 0x0000 Success 0xFF00 Pending(多响应) 0xA700 拒绝存储 0xB000/0xB006 警告 C-FIND/C-MOVE/C-GET 均以 Pending 逐条返回,最终以 Success 或 Failure 收尾;C-STORE 单次请求单次响应。 关联协商阶段被拒绝的表示上下文,对应 SOP 类无法执行,需检查 AE 配置与传输语法。

用 DCMTK 与 pynetdicom 做通信测试

理论必须落到可执行的验证上。DCMTK 提供了一套命令行工具,适合快速搭建测试环境。常用组合如下:

# 启动一个 Storage SCP,监听 11112 端口,AE Title 为 PACS_SCP
storescp -v -d 11112 -aet PACS_SCP --output-directory ./received

# 用 storescu 向该 SCP 发送一张 CT 图像
storescu -v -d 127.0.0.1 11112 -aet TEST_SCU -aec PACS_SCP ./ct_slice.dcm

# 启动 Query/Retrieve SCP(需配合 dcmqrscp 与索引配置)
dcmqrscp -v -d 11113 -aet QR_SCP --config ./dcmqrscp.cfg

# 执行 C-FIND 查询
findscu -v -d 127.0.0.1 11113 -aet TEST_SCU -aec QR_SCP -P -k "QueryRetrieveLevel=PATIENT" -k "PatientName=ZHANG*"

# 执行 C-MOVE,把结果转发到 PACS_SCP
movescu -v -d 127.0.0.1 11113 -aet TEST_SCU -aec QR_SCP -P -k "QueryRetrieveLevel=STUDY" -k "StudyInstanceUID=1.2.840..." PACS_SCP

# 执行 C-GET,图像回传到当前 SCU
getscu -v -d 127.0.0.1 11113 -aet TEST_SCU -aec QR_SCP -P -k "QueryRetrieveLevel=SERIES" -k "SeriesInstanceUID=1.2.840..."

使用 pynetdicom 则更适合编写自动化测试脚本。下面是一个最小化的 Storage SCP 示例,它接收 C-STORE 请求并把数据集写入文件:

from pynetdicom import AE, evt, StoragePresentationContexts
from pydicom.filewriter import write_file

def handle_store(event):
    ds = event.dataset
    ds.file_meta = event.file_meta
    ds.save_as(f"recv_{ds.SOPInstanceUID}.dcm", write_like_original=False)
    return 0x0000  # Success

handlers = [(evt.EVT_C_STORE, handle_store)]
ae = AE(ae_title='PACS_SCP')
ae.supported_contexts = StoragePresentationContexts
ae.start_server(('0.0.0.0', 11112), evt_handlers=handlers)

测试时最值得观察的是关联协商日志与状态码序列。以 C-FIND 为例,如果 SCP 返回 0xA900(数据集与SOP类不匹配),通常说明查询级别或匹配键写错;如果返回 0x0122(SOP类不支持),则要检查表示上下文是否被接受。C-MOVE 的常见问题是目标 AE Title 未在 SCP 的已知 AE 列表中注册,导致 SCP 无法发起出站关联,此时 C-MOVE-RSP 会返回 0xA801(Move目标未知)。

建议按“先 C-STORE 验证连通性 → 再 C-FIND 验证查询 → 最后 C-MOVE/C-GET 验证检索”的顺序测试。每一步都先用 -v 或 -d 打开详细日志,确认 A-ASSOCIATE 的表示上下文协商结果,再观察 DIMSE 消息的状态码变化。

测试用例与结果判读

测试项SCU 命令预期状态码失败时优先检查
C-STORE 单张图像storescu0x0000表示上下文、SOP类、传输语法
C-FIND 患者级查询findscu -P0xFF00 × N → 0x0000QueryRetrieveLevel、匹配键格式
C-MOVE 转发到第三方movescu0xFF00 × N → 0x0000目标AE Title注册、出站关联
C-GET 同关联回传getscu0xFF00 × N → 0x0000SCU是否声明Storage SOP类