一、前言
前面的文章,我们介绍了fo-dicom是一个怎样的开源库等一些内容:
- fo-dicom,第一个基于.NET Standard 2.0 开发的DICOM开源库
- fo-dicom开源库是如何满足 DICOM标准的基本要求
- fo-dicom开发之DICOM数据解析:常见数据类型及处理方法详解
今天我们分享下 fo-dicom是如何实现DICOM 的网络通信功能。
二、DICOM3.0标准的通用通信模型
下图显示了DICOM3.0标准的通用通信模型,该模型跨越了 网络(在线)和媒体存储交换(离线)通信。应用程序可利用以下任一传输机制:
- DICOM 消息服务和上层服务,它们独立于特定的物理网络通信支持和协议(如 TCP/IP)。
- DICOM Web 服务 API 和 HTTP 服务,允许使用通用超文本和相关协议来传输 DICOM 服务
- 基本 DICOM 文件服务,它提供独立于特定媒体存储格式和文件结构的存储介质访问
- DICOM 实时通信,提供基于 SMPTE 和 RTP 的 DICOM 元数据的实时传输。
DICOM的通用通信模型旨在为医疗图像和相关数据的传输和存储提供灵活和多样化的解决方案。DICOM的通用通信模型不仅仅是简单地传输和存储医疗图像和相关数据,而是提供了一种多层次、多种方式的灵活解决方案,以满足不同场景下的需求和要求。这种多样化的传输和存储机制使得DICOM成为医疗行业中不可或缺的通信标准,为医疗图像和相关数据的交换和共享提供了可靠和高效的技术支持。
DICOM的通用通信模型的重要性和价值在于其能够满足医疗行业不同方面的需求,为医疗图像和相关数据的传输和存储提供了全面而可靠的解决方案,从而推动了医疗信息技术的发展和应用。
三、基于.NET 的网络通信功能
fo-dicom
使用了Socket和TcpClient等底层网络通信类来与DICOM服务器进行连接和通信,从而实现 DICOM 的网络通信功能。下面是 fo-dicom
网络通信的实现基本原理:
-
传输协议选择:
fo-dicom
支持多种传输协议,如 TCP/IP、UDP 和 WebSocket。用户可以根据需要选择适合的传输协议。 -
连接建立:对于服务器端应用程序,使用
DicomServer
类监听指定的端口号,等待客户端连接请求。一旦有客户端连接请求到达,服务器将建立一个与客户端的网络连接。 -
数据传输:在 DICOM 通信中,数据通过 DIMSE(DICOM Message Service Element)进行传输。DIMSE 是基于消息的通信模式,包括 C-STORE(存储服务)、C-FIND(查询服务)、C-MOVE(移动服务)等。
-
数据编码:
fo-dicom
使用 DICOM 标准定义的数据格式和编码规则对数据进行编码。DICOM 数据集使用一系列的标签(Tag)来组织和描述不同的信息,例如患者姓名、图像序列等。fo-dicom
将数据集编码为字节流以进行传输。 -
数据解码:在接收方,
fo-dicom
将接收到的字节流解码为 DICOM 数据集,以便进行后续的处理和分析。 -
数据处理:根据具体的应用需求,可以对 DICOM 数据集进行查询、存储、检索等操作。
fo-dicom
提供了一组 API 来处理 DICOM 数据集,以便用户能够方便地访问和操作数据。 -
响应发送:在服务器端应用程序中,一旦完成对 DICOM 请求的处理,将向客户端发送一个响应。响应中包含请求的执行结果、状态信息等。
-
连接断开:通信完成后,可以关闭服务器端的监听或断开客户端与服务器的连接。
fo-dicom
通过使用 .NET 平台的网络通信库来实现底层的网络传输,并且遵循 DICOM 标准的数据格式和编码规则。它提供了一组简洁而强大的 API,使得用户可以方便地进行 DICOM 数据的传输和处理。
四、案例说明
以下是一个使用 fo-dicom
进行C-STORE命令的网络通信的简单案例:案例来源于官网的示例,https://github.com/fo-dicom/fo-dicom-samples
1. SCP处理C-STORE请求
假设我们有一个服务器端应用程序,它监听在本地的端口号 11112上,等待客户端的连接请求。一旦接收到来自客户端的 C-STORE 请求,服务器将把接收到的 DICOM 图像数据保存到本地磁盘。
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using Microsoft.Extensions.Logging;
namespace Samples
{
internal class Program
{
private const string _storagePath = @".\DICOM";
private static void Main(string[] args)
{
// start DICOM server on port from command line argument or 11112
var port = args != null && args.Length > 0 && int.TryParse(args[0], out int tmp) ? tmp : 11112;
Console.WriteLine($"Starting C-Store SCP server on port {port}");
using (var server = DicomServerFactory.Create<CStoreSCP>(port))
{
// end process
Console.WriteLine("Press <return> to end...");
Console.ReadLine();
}
}
private class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider
{
private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[]
{
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ExplicitVRBigEndian,
DicomTransferSyntax.ImplicitVRLittleEndian
};
private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[]
{
// Lossless
DicomTransferSyntax.JPEGLSLossless,
DicomTransferSyntax.JPEG2000Lossless,
DicomTransferSyntax.JPEGProcess14SV1,
DicomTransferSyntax.JPEGProcess14,
DicomTransferSyntax.RLELossless,
// Lossy
DicomTransferSyntax.JPEGLSNearLossless,
DicomTransferSyntax.JPEG2000Lossy,
DicomTransferSyntax.JPEGProcess1,
DicomTransferSyntax.JPEGProcess2_4,
// Uncompressed
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ExplicitVRBigEndian,
DicomTransferSyntax.ImplicitVRLittleEndian
};
public CStoreSCP(INetworkStream stream, Encoding fallbackEncoding, ILogger log, DicomServiceDependencies dependencies)
: base(stream, fallbackEncoding, log, dependencies)
{
}
public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
{
if (association.CalledAE != "STORESCP")
{
return SendAssociationRejectAsync(
DicomRejectResult.Permanent,
DicomRejectSource.ServiceUser,
DicomRejectReason.CalledAENotRecognized);
}
foreach (var pc in association.PresentationContexts)
{
if (pc.AbstractSyntax == DicomUID.Verification)
{
pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes);
}
else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
{
pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
}
}
return SendAssociationAcceptAsync(association);
}
public Task OnReceiveAssociationReleaseRequestAsync()
{
return SendAssociationReleaseResponseAsync();
}
public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason)
{
/* nothing to do here */
}
public void OnConnectionClosed(Exception exception)
{
/* nothing to do here */
}
public async Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStoreRequest request)
{
var studyUid = request.Dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID).Trim();
var instUid = request.SOPInstanceUID.UID;
var path = Path.GetFullPath(Program._storagePath);
path = Path.Combine(path, studyUid);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
path = Path.Combine(path, instUid) + ".dcm";
await request.File.SaveAsync(path);
return new DicomCStoreResponse(request, DicomStatus.Success);
}
public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e)
{
// let library handle logging and error response
return Task.CompletedTask;
}
public Task<DicomCEchoResponse> OnCEchoRequestAsync(DicomCEchoRequest request)
{
return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success));
}
}
}
}
在上面的代码中,首先从命令行参数中获取端口号,然后创建一个 CStoreSCP 对象作为 DICOM 服务器,并将其绑定到指定的端口。在 CStoreSCP 类中,实现了 IDicomServiceProvider、IDicomCStoreProvider 和 IDicomCEchoProvider 接口,分别处理 DICOM 关联请求、C-Store 请求和 C-Echo 请求。其中,
OnReceiveAssociationRequestAsync() 方法会检查 Called AE 是否为 STORESCP,如果不是则拒绝关联请求。OnCStoreRequestAsync() 方法则会将接收到的 DICOM 数据保存到本地文件系统中。其他的方法实现通常为空实现,因为并不需要对其进行特殊处理。
2. SCU发送C-STORE请求
对于客户端应用程序,我们可以使用 DicomClient
类来发送 C-STORE 请求到服务器。以下是一个简单的客户端示例:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using FellowOakDicom.Network.Client;
namespace Samples
{
internal static class Program
{
private static string _storeServerHost = "127.0.0.1";
private static int _storeServerPort = 11112;
private const string _storeServerAET = "STORESCP";
private const string _aet = "FODICOMSCU";
static async Task Main(string[] args)
{
var storeMore = "";
_storeServerHost = GetServerHost();
_storeServerPort = GetServerPort();
Console.WriteLine("***************************************************");
Console.WriteLine("Server AE Title: " + _storeServerAET);
Console.WriteLine("Server Host Address: " + _storeServerHost);
Console.WriteLine("Server Port: " + _storeServerPort);
Console.WriteLine("Client AE Title: " + _aet);
Console.WriteLine("***************************************************");
var client = DicomClientFactory.Create(_storeServerHost, _storeServerPort, false, _aet, _storeServerAET);
client.NegotiateAsyncOps();
do
{
try
{
Console.WriteLine();
Console.WriteLine("Enter the path for a DICOM file:");
Console.Write(">>>");
string dicomFile = Console.ReadLine();
while (!File.Exists(dicomFile))
{
Console.WriteLine("Invalid file path, enter the path for a DICOM file or press Enter to Exit:");
dicomFile = Console.ReadLine();
if (string.IsNullOrWhiteSpace(dicomFile))
{
return;
}
}
var request = new DicomCStoreRequest(dicomFile);
request.OnResponseReceived += (req, response) => Console.WriteLine("C-Store Response Received, Status: " + response.Status);
await client.AddRequestAsync(request);
await client.SendAsync();
}
catch (Exception exception)
{
Console.WriteLine();
Console.WriteLine("----------------------------------------------------");
Console.WriteLine("Error storing file. Exception Details:");
Console.WriteLine(exception.ToString());
Console.WriteLine("----------------------------------------------------");
Console.WriteLine();
}
Console.WriteLine("To store another file, enter \"y\"; Othersie, press enter to exit: ");
Console.Write(">>>");
storeMore = Console.ReadLine().Trim();
} while (storeMore.Length > 0 && storeMore.ToLower()[0] == 'y');
}
private static string GetServerHost()
{
var hostAddress = "";
var localIP = GetLocalIPAddress();
do
{
Console.WriteLine("Your local IP is: " + localIP);
Console.WriteLine("Enter \"1\" to use your local IP Address: " + localIP);
Console.WriteLine("Enter \"2\" to use defult: " + _storeServerHost);
Console.WriteLine("Enter \"3\" to enter custom");
Console.Write(">>>");
string input = Console.ReadLine().Trim().ToLower();
if (input.Length > 0)
{
if (input[0] == '1')
{
hostAddress = localIP;
}
else if (input[0] == '2')
{
hostAddress = _storeServerHost;
}
else if (input[0] == '3')
{
Console.WriteLine("Enter Server Host Address:");
Console.Write(">>>");
hostAddress = Console.ReadLine();
}
}
} while (hostAddress.Length == 0);
return hostAddress;
}
private static int GetServerPort()
{
Console.WriteLine("Enter Server port, or \"Enter\" for default \"" + _storeServerPort + "\":");
Console.Write(">>>");
var input = Console.ReadLine().Trim();
return string.IsNullOrEmpty(input) ? _storeServerPort : int.Parse(input);
}
public static string GetLocalIPAddress()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip.ToString();
}
}
return "";
}
}
}
首先定义了一些变量,包括存储服务器的主机地址、端口号,以及客户端和服务器的 AE(Application Entity)标题。然后,在 Main 方法中创建了一个 DicomClient 对象,并通过调用 NegotiateAsyncOps 方法进行异步操作的协商。接下来,进入一个循环,用户可以输入要发送的 DICOM 文件的路径。程序会检查路径是否有效,如果无效则提示用户重新输入,直到输入为空或用户选择退出。然后,创建一个 DicomCStoreRequest 对象,传入要发送的 DICOM 文件路径作为参数。并通过订阅 OnResponseReceived 事件来处理响应。最后,调用 AddRequestAsync 方法将请求添加到客户端的请求队列中,并调用 SendAsync 方法发送请求。
其中GetServerHost 方法用于获取服务器主机地址,它会提示用户选择使用本地 IP 地址、默认地址还是自定义地址。GetServerPort 方法用于获取服务器端口号,用户可以输入自定义端口号,或者直接回车使用默认端口号。GetLocalIPAddress 方法用于获取本地 IP 地址。
3. 小结
这个案例展示了一个简单的基于 fo-dicom
的 DICOM 网络通信示例,即SCU和SCP对CStore的通信的简单处理,服务器接收到客户端发送的 C-STORE 请求并保存图像到本地磁盘。
总结
关于C-STORE的处理,如果不是特别清楚,后续,我们在一起学习关于通信协议相关的内容,重点分析,C-ECHO,C-Store,C-Find,C-Move等等,加深理解和使用场景。