对于初学者来说,使用 HL7 标准构建自定义应用程序可能是一项非常艰巨的任务。多年来,我已经构建了许多自定义 HL7 服务器应用程序,如果没有该领域其他人的帮助,我无法通过 Internet 与他们进行多次讨论。随着我越来越精通,我还收到了许多其他开发人员/客户的电子邮件,他们提出的问题与我开始使用该标准时所问的问题相同。因此,本着分享的精神,我决定整理一个示例/教程,说明任何处理 HL7 V2 标准的程序员在开始时应该知道的所有最基本的基础知识。
这是我的 HL7 文章系列的一部分。在我们开始本教程之前,请快速浏览一下我之前的文章“HL7 2.x 标准的简短介绍”。请注意,本教程假设您了解 Java 或任何等效的面向对象语言。对网络和线程编程的基本了解会很有用,但不是必需的。
“一个问题说得好,问题就解决了一半。” ~查尔斯凯特林
在我们继续之前,有几件事需要注意。尽管 HL7 标准本身不推荐用于消息通信的任何特定协议,但大多数 HL7 系统使用最小底层协议进行通信。本教程将向您展示如何使用此协议在 Java 中构建自定义 HL7 服务器。本教程不涉及诸如强大的错误处理、错误记录和通知、消息转换以及数据持久性等高级主题。但是,我确实计划很快就这些主题编写另一个教程。
最低层协议(通常缩写为“MLLP”)是最流行的协议,用于使用 TCP/IP 传输 HL7 消息。因为 TCP/IP 将信息作为连续的字节流传输,所以通信代码需要包装协议才能识别每条消息的开始和结束。MLLP 通常用于满足这一要求,通常使用不可打印的字符作为核心 HL7 消息信息的环绕字符。这些包装字符有助于将 HL7 消息有效负载有效地封装为数据“块”,然后将其传输到接收系统。接收系统然后解开消息有效载荷,解析消息内容,然后通过返回响应消息(称为“ACK”或“NACK”)来确认收到此消息 HL7 中的消息)也包装其消息内容。在基于 MLLP 的系统中,源系统通常不会发送任何新消息,直到接收到先前传输的消息的确认消息。
需要记住的是,我上面描述的 MLLP 块是通过使用具有单字节值的字符来构建的。这些字符必须由交换信息的站点达成一致,因为消息内容中的数据可能与用于构建承载消息所需的 MLLP 块的字节值冲突。此外,在这些情况下,使用多字节字符编码(如 UTF-16 或 UTF-32)并不理想,因为这可能导致数据的字节值可能等于 MLLP 成帧字符,从而导致与 MLLP 包络相关的错误。因此,建议仅使用单字节值编码,例如 UTF-8、ISO-8859-x 等。有关此领域的更多信息,请参阅官方 HL7 文档。
您可以将套接字视为通过 TCP 或 UDP 协议传输信息的两台机器之间连接的“终端”的抽象。大多数现代语言(如 Java、C# 和 Ruby)通过丰富的类和/或接口库提供套接字编程支持。这减轻了应用程序员必须处理 ISO(国际标准组织)标准的 OSI(开放系统互连)模型的较低层的负担。使用套接字时,程序员正在使用“会话”层,因此无需处理 OSI 堆栈较低层提供的实时数据通信服务。Java 为套接字编程提供了极好的支持(通过 java.net 包)。
我们将采取小步骤,以便涵盖所有核心要素
构建一个能够传输和接收信息的简单 TCP/IP 客户端。
构建一个简单的 TCP/IP 服务器,能够接收和回应接收到的信息
构建简单的线程支持来处理来自客户端的许多并发连接
将 MLLP 和消息确认功能添加到服务器中
编写一个小的 TCP/IP 客户端,它将传输一个虚拟的 MLLP 包装的 HL7 消息
修改服务器类以构建基于 MLLP 的解析功能
修改服务器类,构建简单的确认功能
把它们放在一起
这是一个非常简单的 TCP 客户端,我们将使用它来理解套接字的工作原理。我们将使用此客户端通过各个步骤主要测试我们的服务器应用程序。
package com.saravanansubramanian;
import java.net.*;
import java.io.*;
public class SimpleTCPEchoClient {
public static void main(String[] args) throws IOException {
String testMessage = "This is a test message that the client will transmit";
byte[] byteBuffer = testMessage.getBytes();
// Create socket that is connected to a server running on the same machine on port 1080
Socket socket = new Socket("localhost", 1080);
System.out.println("Connected to Server");
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// Send the message to the server
out.write(byteBuffer);
in.read(byteBuffer);
System.out.println("Message received from Server: " + new String(byteBuffer));
// Close the socket and its streams
socket.close();
}
}
从代码中可以看出,“Socket”类用于与本地运行的另一个侦听器应用程序建立连接,侦听端口 1080。一旦建立连接,就可以使用输入和写入通过此连接进行读写。输出流。完成后,我们只需关闭套接字即可。
我了解到人们会忘记你说过的话,人们会忘记你做过的事,但人们永远不会忘记你给他们带来的感受。~ 玛雅安吉洛
提示:在工业强度的应用程序中,如果不能立即建立连接,或者如果现有连接由于不可预见的原因(如果服务器应用程序崩溃,或意外关闭)而断开,客户端应用程序应重试一定次数之前放弃。我不会说明这个功能,因为它不在本教程的范围内;但是,将其包含在您的 HL7 应用程序中是一项非常有用的功能。
现在我们已经在上一步中看到了一个 TCP 客户端示例,这里是一个简单的 TCP 服务器,它可以通过指定的端口接受来自客户端的连接,接收它传输的任何信息,并简单地将其回显。它将永远循环运行。我们通常使用“ServerSocket”类在 Java 中构建服务器应用程序。我们服务器的这个版本一次只能接受和处理一个连接。一旦客户端连接被服务器接受,我们就可以通过这个连接提供的输入和输出流进行读写。
package com.saravanansubramanian;
import java.net.*;
import java.io.*;
public class SimpleTCPEchoServer {
private static final int BUFFER_SIZE = 200;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(1080);
int receivedMessageSize;
byte[] receivedByeBuffer = new byte[BUFFER_SIZE];
while (true) {
Socket clientSocket = serverSocket.accept(); // Get client connection
System.out.println("Handling client at " +
clientSocket.getInetAddress().getHostAddress() + " through port " +
clientSocket.getPort());
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
receivedMessageSize = in.read(receivedByeBuffer);
out.write(receivedByeBuffer, 0, receivedMessageSize);
clientSocket.close(); // Close the socket. We are done serving this client
}
}
}
提示:在生产 HL7 服务器应用程序中,您必须通过实施坚固的错误处理以及“自动重启”功能来确保服务器不会崩溃,以防服务器崩溃。此外,如果需要快速手动干预或写入操作系统的事件日志,您将希望通过电子邮件发送错误通知。
到目前为止所展示的客户端和服务器示例都是极其简化的示例,可帮助您了解套接字通信的基础知识。我们实现的服务器一次只能处理一个连接。要处理多个连接,您必须使用线程。线程在 Java 中很容易实现。但是,它们也很容易被滥用。在 Java 中实现线程时,您必须确保各个线程不会干扰彼此的工作。这是因为线程共享相同的内存。在服务器的这个修改版本中,我们实例化一个新线程,并将服务器接受的每个传入连接传递给它自己的 ConnectionHandler 类。这将确保服务器快速准备好接受下一个等待连接到服务器的客户端,而不会出现任何明显的延迟。
package com.saravanansubramanian;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
public class SimpleThreadedEchoServer {
private int listenPort;
public SimpleThreadedEchoServer(int aListenPort) {
listenPort = aListenPort;
}
public static void main(String[] args) {
SimpleThreadedEchoServer server = new SimpleThreadedEchoServer(1080);
server.acceptIncomingConnections();
}
private void acceptIncomingConnections() {
try {
ServerSocket server = new ServerSocket(listenPort, 5); //Accept up to 5 clients in the queue
System.out.println("Server has been started");
Socket clientSocket = null;
while (true) {
clientSocket = server.accept();
handleIncomingConnection(clientSocket);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}
protected void handleIncomingConnection(Socket aConnectionToHandle) {
new Thread(new ConnectionHandler(aConnectionToHandle)).start();
}
private static class ConnectionHandler implements Runnable {
private Socket connection;
private int receivedMessageSize;
private byte[] receivedByeBuffer = new byte[BUFFER_SIZE];
private static final int BUFFER_SIZE = 32;
public ConnectionHandler(Socket aClientSocket) {
connection = aClientSocket;
}
public void run() {
try {
System.out.println("Handling client at " + connection.getInetAddress().getHostAddress()
+ " on port " + connection.getPort());
InputStream in = connection.getInputStream();
OutputStream out = connection.getOutputStream();
receivedMessageSize = in.read(receivedByeBuffer);
out.write(receivedByeBuffer, 0, receivedMessageSize);
connection.close(); // Close the socket. We are done serving this client
} catch (IOException e) {
System.out.println("Error handling a client: " + e);
}
}
}
}
提示:在世界各地使用的许多 HL7 系统中,多个客户端可能会同时尝试连接到服务器(在不同端口或同一端口上)。服务器应用程序必须能够在没有太多延迟的情况下接受所有这些连接,然后接收、处理和确认从每个客户端收到的任何信息。
尽管到目前为止所展示的示例允许客户端和服务器来回传输简单的文本消息,但这还不足以与 HL7 系统进行通信。与 HL7 系统的通信需要使用 MLLP 协议和消息确认功能。在这一步中,我们将使用我们现有的服务器并添加代码以启用 MLLP 支持。我们还将在服务器中实现功能,以创建一个简单的确认消息并将其传输回客户端应用程序。
在本教程的开头,我将 MLLP 协议描述为一种包装协议,其中核心 HL7 消息被包裹在特殊字符中,以向接收应用程序发送消息信息传输的开始和结束信号。在处理 MLLP 时,您将配置和使用基本上三个字符组。它们是开始块、结束块和段分隔符. 使用 MLLP 协议传输的 HL7 消息以起始块字符为前缀,消息段以段分隔符终止,然后消息本身以两个字符终止,即结束块和回车符。大多数情况下,通常用于表示起始块的字符是 VT(垂直制表符),即 ASCII 11。FS(文件分隔符)、ASCII 28 用于表示结束块,使用 CR(ASCII 13)用于回车。
以下是使用与 MLLP 相关的包装字符“封装”传输的观察请求 HL7 消息的示例:
MSH^~\&199809091533ORU^R01856369D2.2
PID139385000343456
ORC123112312RAD19980909-0005
这是修改后的原始 TCP 客户端以传输示例 HL7 消息。注意测试消息是如何用特殊字符包装的,如前所述(以粗体突出显示)。
package com.saravanansubramanian;
import java.net.*;
import java.io.*;
public class SimpleMLLPBasedTCPClient {
private static final char END_OF_BLOCK = '\u001c';
private static final char START_OF_BLOCK = '\u000b';
private static final char CARRIAGE_RETURN = 13;
public static void main(String[] args) throws IOException {
// Create a socket to connect to server running locally on port 1080
Socket socket = new Socket("localhost", 1080);
System.out.println("Connected to Server");
StringBuffer testHL7MessageToTransmit = new StringBuffer();
testHL7MessageToTransmit.append(START_OF_BLOCK)
.append("MSH|^~\\&|AcmeHIS|StJohn|CATH|StJohn|20061019172719||ORM^O01|MSGID12349876|P|2.3")
.append(CARRIAGE_RETURN)
.append("PID|||20301||Durden^Tyler^^^Mr.||19700312|M|||88 Punchward Dr.^^Los Angeles^CA^11221^USA|||||||")
.append(CARRIAGE_RETURN)
.append("PV1||O|OP^^||||4652^Paulson^Robert|||OP|||||||||9|||||||||||||||||||||||||20061019172717|20061019172718")
.append(CARRIAGE_RETURN)
.append("ORC|NW|20061019172719")
.append(CARRIAGE_RETURN)
.append("OBR|1|20061019172719||76770^Ultrasound: retroperitoneal^C4|||12349876")
.append(CARRIAGE_RETURN)
.append(END_OF_BLOCK)
.append(CARRIAGE_RETURN);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// Send the MLLP-wrapped HL7 message to the server
out.write(testHL7MessageToTransmit.toString().getBytes());
byte[] byteBuffer = new byte[200];
in.read(byteBuffer);
System.out.println("Received from Server: " + new String(byteBuffer));
// Close the socket and its streams
socket.close();
}
}
在这一步中,我们将向服务器(在连接处理程序类中)添加一个方法来处理使用套接字流从客户端接收到的字符流的解析。此解析例程的要点是,它将期望 MLLP 以正确的顺序/序列包装字符。如果没有,它将抛出异常并关闭与客户端的连接。
public String getMessage(InputStream anInputStream) throws IOException {
boolean end_of_message = false;
StringBuffer parsedMessage = new StringBuffer();
int characterReceived = 0;
try {
characterReceived = anInputStream.read();
} catch (SocketException e) {
System.out
.println("Unable to read from socket stream. "
+ "Connection may have been closed: " + e.getMessage());
return null;
}
if (characterReceived == END_OF_TRANSMISSION) {
return null;
}
if (characterReceived != START_OF_BLOCK) {
throw new RuntimeException(
"Start of block character has not been received");
}
while (!end_of_message) {
characterReceived = anInputStream.read();
if (characterReceived == END_OF_TRANSMISSION) {
throw new RuntimeException(
"Message terminated without end of message character");
}
if (characterReceived == END_OF_BLOCK) {
characterReceived = anInputStream.read();
if (characterReceived != CARRIAGE_RETURN) {
throw new RuntimeException(
"End of message character must be followed by a carriage return character");
}
end_of_message = true;
} else {
parsedMessage.append((char) characterReceived);
}
}
return parsedMessage.toString();
}
接下来,我们将向服务器添加另外两个方法(同样,在连接处理程序类中)以构建简单的 HL7 消息确认以传输回客户端。第一种方法是帮助构建基本的消息确认功能。下面显示的方法是一个验证简化示例,以帮助理解如何构造确认消息。实际上,您将重新传输 MSH 和 MSA 段中的其他字段,例如发送设施、发送应用程序、应用程序版本和其他有用信息。
private String getSimpleAcknowledgementMessage(String aParsedHL7Message) {
if (aParsedHL7Message == null)
throw new RuntimeException("Invalid HL7 message for parsing operation. Please check your inputs");
String messageControlID = getMessageControlID(aParsedHL7Message);
StringBuffer ackMessage = new StringBuffer();
ackMessage = ackMessage.append(START_OF_BLOCK)
.append("MSH|^~\\&|||||||ACK||P|2.2")
.append(CARRIAGE_RETURN)
.append("MSA|AA|")
.append(messageControlID)
.append(CARRIAGE_RETURN)
.append(END_OF_BLOCK)
.append(CARRIAGE_RETURN);
return ackMessage.toString();
}
第二种支持方法是在确认消息中帮助解析接收到的需要重传的消息的消息控制id。下面第二个例程的某些部分可能看起来有点矫枉过正,但我故意这样做是为了说明如何使用字段分隔符解析 HL7 消息中的字段(一旦您可以解析一个字段,您就可以应用相同的概念来执行其他)。您可能还想在未找到消息控件 ID 时引发异常。
private String getMessageControlID(String aParsedHL7Message) {
int fieldCount = 0;
StringTokenizer tokenizer = new StringTokenizer(aParsedHL7Message, FIELD_DELIMITER);
while (tokenizer.hasMoreElements())
{
String token = tokenizer.nextToken();
fieldCount++;
if (fieldCount == MESSAGE_CONTROL_ID_LOCATION){
return token;
}
}
return "";
}
我们完了。如果您需要的话,我们基本上已经看到了从头开始组装自定义 HL7 应用程序所需的所有细节。通常,HL7 程序员不会重新发明轮子,而是使用免费软件或商业工具包来构建他/她的 HL7 消息传递应用程序。但是,即使在处理工具包时,也需要对套接字、MLLP 协议以及本教程中介绍的消息确认基础有基本的了解。我计划使用一个名为“HAPI”的流行 HL7 库编写关于 HL7 编程的整个系列文章。所以,请尽快在我的博客上注意这一点。
您可以在此处在GitHub上找到本教程中使用的完整源代码
构建能够 24/7 全天候运行且无人值守的强大 HL7 系统并不容易。对于大多数程序员来说,一旦他们克服了最初的套接字编程障碍,HL7 2.x 相关编程中的大部分问题将围绕支持任何解析、转换、格式化和存储要求。随着 HL7 3.0* 标准越来越多地开始使用,这些问题应该会慢慢消失。但是,HL7 2.x 标准目前不会有任何进展。我们应该期望它在他们完全退休之前至少再坚持 10 年。在此之前,我们将不得不继续寻求为 HL7 消息传递构建更优雅、更坚固的解决方案。
提示:您需要设计系统,以便 MLLP 字符分组是可配置的。这样,客户可以将它们更改为他们需要的任何内容。您还应该允许围绕消息确认功能进行一些配置,例如传输的有关处理系统及其位置的信息(进入“MSH”段)。此外,还有用于指示系统是处于“测试”模式还是“生产模式”的配置。过去,我设置的可配置内容还包括消息控制 ID 起始编号或编号模式、电子邮件地址和错误日志位置(用于错误通知),以及连接重试尝试。
* - HL7 3.0 (RIM) 从未像许多人想象的那样在 2000 年代初及以后得到广泛采用,包括替换基于 V2 的消息接口。请参阅我关于 V3 标准的文章,以获取有关该标准尝试做什么、其优势、劣势以及它在现在漫长的医疗保健信息学历史中的遗产的更多信息。
原文链接:HL7 Programming using Java - Saravanan Subramanian