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消息才能在其上传输。
五类核心服务的交互差异
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配置,但把接收压力集中在请求端。
用 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目标未知)。
测试用例与结果判读
| 测试项 | SCU 命令 | 预期状态码 | 失败时优先检查 |
|---|---|---|---|
| C-STORE 单张图像 | storescu | 0x0000 | 表示上下文、SOP类、传输语法 |
| C-FIND 患者级查询 | findscu -P | 0xFF00 × N → 0x0000 | QueryRetrieveLevel、匹配键格式 |
| C-MOVE 转发到第三方 | movescu | 0xFF00 × N → 0x0000 | 目标AE Title注册、出站关联 |
| C-GET 同关联回传 | getscu | 0xFF00 × N → 0x0000 | SCU是否声明Storage SOP类 |