llpdrv [-dyY] [-a|-h|-m] [[-i|-u|-p|-t] from] [-i to] [-i dist]
Background
The LLP support that is currently provided with ProtoGen is somehow a hack. Driven by the urgent need for an implementation of ANSI X3.28 protocol, I decided to write an LLP translator that transforms ANSI X3.28 sessions to a simpler LLP. For the latter, I chose what I called "Internal LLP" (ILLP) (see section The Internal Protocol) later, since it was designed for the communication among HL7 applications built upon ProtoGen/HL7. The ILLP is a very simple LLP designed for the use with C++ IOstream classes. Basically it signals the end of a message by an end-of-file token. Thus, reading a HL7 message from a communication channel becomes just the same as reading it from a file (one message per file). No need to track end-of-message delimiter characters as when using the MLLP or HLLP or more.
This can easily be done with BSD sockets (i.e. TCP/IP or UNIX-Stream sockets): The a stream socket, which is a full-duplex communication channel, can, once it is connected, be shut down on just one half. I.e. you can close just your writable half of the channel and still receive data on the readable half. The peer will recognize that you have closed your writable half by receiving and end-of-file token (i.e. read() returns 0). Now the peer can send it's reply data through the still open half that you are reading on.
Thus, the ILLP favors a specific view on a (HL7) communication session which is outlined in the figure below. A request message is sent from the client to the server after which the client closes it's write-half of the connection signaling end of message to the server. Now the server interprets the request message, performs the service and sends the reply message (typically ACK) via it's still usable write-half of the connection (which is the read-half for the client). This is now closed as well, which signals end of message to the client. By now, the connection became unusable, because either party has closed it's write-half. Since this is so, the socket is deleted and the session is finished.
C S L -------- REQUEST -----------> E ----+ I R PERFORM E V SERVICE N <------- REPLY -------------- E <---+ T R
The LLP Driver
Rather than being linked into the program that needs to use the LLP, the
LLP driver is a separate program, called llpdrv
. It translates
the session and transport protocol mechanisms of the ANSI X3.28, the
"hybrid lower layer protocol" (HLLP), or the "minimal lower layer
protocol" (MLLP), which are described in HL7 v2.1, Appendix B or
the HL7 Implementation Guide, to the ILLP. Programs that need to
communicate in either of these LLPs do only need to implement the simple
ILLP and communicate to sites that require the other LLPs via an
llpdrv
process.
The llpdrv
process is started once for each communication channel
that should be driven by one of the supported LLP. For example: consider
you have a serial line `/dev/cua00', which is connected to a HL7
application `CaVe' that requires ANSI X3.28 LLP. Your application,
called `MyApp' can communicate via ILLP/TCP. You would start the LLP
driver once as follows:
llpdrv -a -t /dev/cua00 -i mahl7%myhost -i cvhl7
This starts an LLP-driver process that listens on the channel `/dev/cua00' (referred to as "the line") for any ANSI X3.28 communication to start (e.g. ENQ). When this happens, it connects your application on the host `myhost' and TCP service/port `mahl7' via ILLP/TCP. MyApp will receive the message that was sent by CaVe but packed into ILLP instead of ANSI X3.28 LLP. MyApp will reply to the message just received in the same thread.
So far we have only shown how the LLP driver is used if the CaVe System
is the client and the MyApp is the server. If the two need to change
roles, the client (MyApp) can connect the server (CaVe) via the TCP
service/port `cvhl7' on the host where the llpdrv
process is
running. An ILLP session is now directed to the CaVe System via ANSI
X3.28.
An llpdrv
process is thus two servers in one: The server that
serves the port `cvhl7' is called the distal server, while the server
that serves "the line" is called the proximal server. The proximal
client is the first process to contact the LLP driver. The destination
server is the second process that the LLP driver gets contact with and,
third, multiple distal clients can connect subsequently to the distal
server.
................................. (1) . . (2) PROXIMAL--->>---PROXIMAL--LLPDRV----------->>---DESTINATION CLIENT . SERVER | . SERVER . | . . +---DISTAL---<<---DISTAL . SERVER . CLIENT . . (3-n) ...........LLP DRIVER............
Roles and Processes
It is important to identify four different "roles" that the LLP driver processes can assume: Startup process, proximal server process, proximal serving process and distal server process.
First of all, there is the "startup process", which is the one that is usually called by execve(2) from a shell. The setup process exits as soon as it has:
There is nothing else printed to the standard output, thus the server process' id can be stored easily for an eventual shutdown of the LLP driver at a later point in time.
The "server process" is the process that accepts connections by a client. However, it does not serve the client itself but invokes a "serving process" that does the real work, before it loops back to accept more connections. This is because the Berkeley sockets are capable of handling multiple client-server connections that rendez-vous at the same socket in parallel (actually, rendez-vous is sequential but interaction is parallel). Since the tty(or pty)-socket abstraction is not a real Berkeley socket, multiple interactions are not possible with tty-sockets. Anyway, the server process stays resident throughout the lifetime of a running LLP driver. Thus the proximal server process is the main process which has to be sent a termination signal in order to shut down the whole LLP driver.
* | setup | bind | fork------+ | | exit disassociate ==== from terminal . | +--------do | +-------do | | | | | accept | | | | | connection | | | If socket is a tty don't fork but act like a child. | | fork--------------+ From here on we must decide if | | | | we exit or continue on error | | close allocate conditions depending on if we | | connection semaphore... did fork or not. | | | | : | +-------+ fork--------------+ A distal server is | . . | : | always established | . . proximal : distal except if we only | . . server...mutex...server want a single trans- | . . | | action. | . . kill distal | reopen tty . server ......... exit | . . | ==== +-------------------<if we didn't fork . . . else . . . | . . . exit . . . ==== . . . . . . . . . startup > proximal > proximal > distal process server serving server (and serving) process process process transient resident transient transient
The "serving process" is the process that actually serves the proximal client. The proximal connection is regarded as a full duplex serial line. This line is connected to another server, which is the destination of any transaction that is initiated from the proximal client. The destination server is connected by an INET socket and communication to it is driven by the so called "internal protocol" (see section The Internal Protocol). The protocol used at the proximal connection can be either a kind of ANSI X3.28, the HLLP, or the MLLP.
The capability of the proximal client to act as a server is propagated by the LLP driver by means of the "distal server" which is a child process of the proximal serving process. The distal server binds an INET socket which can however serve only one transaction at a time (non-multiplexed). The distal connection is driven by the "internal protocol" since the "line" is regarded as a non-multiplexed line. Because the proximal and distal clients use the line concurrently, there is a need for a mutual exclusion mechanism that guarantees that either the proximal serving process or the distal serving process may use the line. The process that has permissions is said to "have the line", while the other process is blocked in a wait state.
Security
Only processes on the localhost may connect to the distal server of the LLP driver.
LLP to use at the proximal connection (the line):
-a
-h
-m
The medium of the line:
-i
-u
-p
-t
The destination address (to), always ILLP driven using TCP:
-i service%host
The distal server (dist), always ILLP driven using TCP:
-i service
Miscellaneous options
-I
-1
-y
-Y
llpdrv
forks directly into the
background, disassociates from the controlling terminal and establishes
a new process group to which any of it's sub-processes belong.
`/etc/ttys'
ttyd0 llpdrv -YIa -t /dev/ttyd0 -i discard%localhost -i carevuefsi
The ANSI X3.28 protocol implementation has difficulties with very hasty peers, i.e. those whose timeout timer values are small. 1 s is safe.
The ANSI X3.28 protocol options are only adjustable at compile time.
The HLLP protocol is not actually implemented but only it's empty "slot" is provided.
The MLLP protocol was lat tested long ago, it might be broken in the meantime.
The LLP driver is more complex than necessary.
The LLP driver is less complex than necessary.
The LLP driver introduces too much overhead involving the ILLP communication.
The LLP driver will be replaced by a library approach which is more comprehensive and flexible. Thus the LLP driver itself is not supported on the long run.
hosts(5), services(5), section The Internal Protocol.