Chapter 9 |
Interprocess Communication under LISP |
|
by William Lott and Bill Chiles
CMUCL offers a facility for interprocess communication (IPC)
on top of using Unix system calls and the complications of that level
of IPC. There is a simple remote-procedure-call (RPC) package build
on top of TCP/IP sockets.
The remote package provides simple RPC facility including
interfaces for creating servers, connecting to already existing
servers, and calling functions in other Lisp processes. The routines
for establishing a connection between two processes,
create-request-server and connect-to-remote-server,
return wire structures. A wire maintains the current state of
a connection, and all the RPC forms require a wire to indicate where
to send requests.
9.1.1 |
Connecting Servers and Clients |
|
Before a client can connect to a server, it must know the network address on
which the server accepts connections. Network addresses consist of a host
address or name, and a port number. Host addresses are either a string of the
form VANCOUVER.SLISP.CS.CMU.EDU or a 32 bit unsigned integer. Port
numbers are 16 bit unsigned integers. Note: port in this context has
nothing to do with Mach ports and message passing.
When a process wants to receive connection requests (that is, become a
server), it first picks an integer to use as the port. Only one server
(Lisp or otherwise) can use a given port number on a given machine at
any particular time. This can be an iterative process to find a free
port: picking an integer and calling create-request-server. This
function signals an error if the chosen port is unusable. You will
probably want to write a loop using handler-case, catching
conditions of type error, since this function does not signal more
specific conditions.
[Function]
wire:create-request-server port &optional on-connect
create-request-server sets up the current Lisp to accept
connections on the given port. If port is unavailable for any
reason, this signals an error. When a client connects to this port,
the acceptance mechanism makes a wire structure and invokes the
on-connect function. Invoking this function has a couple of
purposes, and on-connect may be nil in which case the
system foregoes invoking any function at connect time.
The on-connect function is both a hook that allows you access
to the wire created by the acceptance mechanism, and it confirms the
connection. This function takes two arguments, the wire and the
host address of the connecting process. See the section on host
addresses below. When on-connect is nil, the request server
allows all connections. When it is non-nil, the function returns
two values, whether to accept the connection and a function the
system should call when the connection terminates. Either value may
be nil, but when the first value is nil, the acceptance mechanism
destroys the wire.
create-request-server returns an object that
destroy-request-server uses to terminate a connection.
[Function]
wire:destroy-request-server server
destroy-request-server takes the result of
create-request-server and terminates that server. Any
existing connections remain intact, but all additional connection
attempts will fail.
[Function]
wire:connect-to-remote-server host port &optional on-death
connect-to-remote-server attempts to connect to a remote
server at the given port on host and returns a wire
structure if it is successful. If on-death is non-nil, it is
a function the system invokes when this connection terminates.
After the server and client have connected, they each have a wire
allowing function evaluation in the other process. This RPC mechanism
has three flavors: for side-effect only, for a single value, and for
multiple values.
Only a limited number of data types can be sent across wires as
arguments for remote function calls and as return values: integers
inclusively less than 32 bits in length, symbols, lists, and
remote-objects (see section 9.1.3). The system sends symbols
as two strings, the package name and the symbol name, and if the
package doesn't exist remotely, the remote process signals an error.
The system ignores other slots of symbols. Lists may be any tree of
the above valid data types. To send other data types you must
represent them in terms of these supported types. For example, you
could use prin1-to-string locally, send the string, and use
read-from-string remotely.
[Macro]
wire:remote wire {call-specs}*
The remote macro arranges for the process at the other end of
wire to invoke each of the functions in the call-specs.
To make sure the system sends the remote evaluation requests over
the wire, you must call wire-force-output.
Each of call-specs looks like a function call textually, but
it has some odd constraints and semantics. The function position of
the form must be the symbolic name of a function. remote
evaluates each of the argument subforms for each of the
call-specs locally in the current context, sending these
values as the arguments for the functions.
Consider the following example:
(defun write-remote-string (str)
(declare (simple-string str))
(wire:remote wire
(write-string str)))
The value of str in the local process is passed over the wire
with a request to invoke write-string on the value. The
system does not expect to remotely evaluate str for a value
in the remote process.
[Function]
wire:wire-force-output wire
wire-force-output flushes all internal buffers associated
with wire, sending the remote requests. This is necessary
after a call to remote.
[Macro]
wire:remote-value wire call-spec
The remote-value macro is similar to the remote macro.
remote-value only takes one call-spec, and it returns
the value returned by the function call in the remote process. The
value must be a valid type the system can send over a wire, and
there is no need to call wire-force-output in conjunction
with this interface.
If client unwinds past the call to remote-value, the server
continues running, but the system ignores the value the server sends
back.
If the server unwinds past the remotely requested call, instead of
returning normally, remote-value returns two values, nil
and t. Otherwise this returns the result of the remote
evaluation and nil.
[Macro]
wire:remote-value-bind wire ({variable}*) remote-form
{local-forms}*
remote-value-bind is similar to multiple-value-bind
except the values bound come from remote-form's evaluation in
the remote process. The local-forms execute in an implicit
progn.
If the client unwinds past the call to remote-value-bind, the
server continues running, but the system ignores the values the
server sends back.
If the server unwinds past the remotely requested call, instead of
returning normally, the local-forms never execute, and
remote-value-bind returns nil.
The wire mechanism only directly supports a limited number of data
types for transmission as arguments for remote function calls and as
return values: integers inclusively less than 32 bits in length,
symbols, lists. Sometimes it is useful to allow remote processes to
refer to local data structures without allowing the remote process
to operate on the data. We have remote-objects to support
this without the need to represent the data structure in terms of
the above data types, to send the representation to the remote
process, to decode the representation, to later encode it again, and
to send it back along the wire.
You can convert any Lisp object into a remote-object. When you send
a remote-object along a wire, the system simply sends a unique token
for it. In the remote process, the system looks up the token and
returns a remote-object for the token. When the remote process
needs to refer to the original Lisp object as an argument to a
remote call back or as a return value, it uses the remote-object it
has which the system converts to the unique token, sending that
along the wire to the originating process. Upon receipt in the
first process, the system converts the token back to the same
(eq) remote-object.
[Function]
wire:make-remote-object object
make-remote-object returns a remote-object that has
object as its value. The remote-object can be passed across
wires just like the directly supported wire data types.
[Function]
wire:remote-object-p object
The function remote-object-p returns t if object
is a remote object and nil otherwise.
[Function]
wire:remote-object-local-p remote
The function remote-object-local-p returns t if
remote refers to an object in the local process. This is can
only occur if the local process created remote with
make-remote-object.
[Function]
wire:remote-object-eq obj1 obj2
The function remote-object-eq returns t if obj1 and
obj2 refer to the same (eq) lisp object, regardless of
which process created the remote-objects.
[Function]
wire:remote-object-value remote
This function returns the original object used to create the given
remote object. It is an error if some other process originally
created the remote-object.
[Function]
wire:forget-remote-translation object
This function removes the information and storage necessary to
translate remote-objects back into object, so the next
gc can reclaim the memory. You should use this when you no
longer expect to receive references to object. If some remote
process does send a reference to object,
remote-object-value signals an error.
The wire package provides for sending data along wires. The
remote package sits on top of this package. All data sent
with a given output routine must be read in the remote process with
the complementary fetching routine. For example, if you send so a
string with wire-output-string, the remote process must know
to use wire-get-string. To avoid rigid data transfers and
complicated code, the interface supports sending
tagged data. With tagged data, the system sends a tag
announcing the type of the next data, and the remote system takes
care of fetching the appropriate type.
When using interfaces at the wire level instead of the RPC level,
the remote process must read everything sent by these routines. If
the remote process leaves any input on the wire, it will later
mistake the data for an RPC request causing unknown lossage.
When using these routines both ends of the wire know exactly what types are
coming and going and in what order. This data is restricted to the following
types:
-
8 bit unsigned bytes.
- 32 bit unsigned bytes.
- 32 bit integers.
- simple-strings less than 65535 in length.
[Function]
wire:wire-output-byte wire byte
[Function]
wire:wire-get-byte wire
[Function]
wire:wire-output-number wire number
[Function]
wire:wire-get-number wire &optional
signed
[Function]
wire:wire-output-string wire string
[Function]
wire:wire-get-string wire
These functions either output or input an object of the specified
data type. When you use any of these output routines to send data
across the wire, you must use the corresponding input routine
interpret the data.
When using these routines, the system automatically transmits and interprets
the tags for you, so both ends can figure out what kind of data transfers
occur. Sending tagged data allows a greater variety of data types: integers
inclusively less than 32 bits in length, symbols, lists, and remote-objects
(see section 9.1.3). The system sends symbols as two strings, the
package name and the symbol name, and if the package doesn't exist remotely,
the remote process signals an error. The system ignores other slots of
symbols. Lists may be any tree of the above valid data types. To send other
data types you must represent them in terms of these supported types. For
example, you could use prin1-to-string locally, send the string, and use
read-from-string remotely.
[Function]
wire:wire-output-object wire object &optional cache-it
[Function]
wire:wire-get-object wire
The function wire-output-object sends object over
wire preceded by a tag indicating its type.
If cache-it is non-nil, this function only sends object
the first time it gets object. Each end of the wire
associates a token with object, similar to remote-objects,
allowing you to send the object more efficiently on successive
transmissions. cache-it defaults to t for symbols and
nil for other types. Since the RPC level requires function
names, a high-level protocol based on a set of function calls saves
time in sending the functions' names repeatedly.
The function wire-get-object reads the results of
wire-output-object and returns that object.
9.2.3 |
Making Your Own Wires |
|
You can create wires manually in addition to the remote
package's interface creating them for you. To create a wire, you need
a Unix file descriptor. If you are unfamiliar with Unix file
descriptors, see section 2 of the Unix manual pages.
[Function]
wire:make-wire descriptor
The function make-wire creates a new wire when supplied with
the file descriptor to use for the underlying I/O operations.
[Function]
wire:wire-p object
This function returns t if object is indeed a wire,
nil otherwise.
[Function]
wire:wire-fd wire
This function returns the file descriptor used by the wire.
The TCP/IP protocol allows users to send data asynchronously, otherwise
known as out-of-band data. When using this feature, the operating
system interrupts the receiving process if this process has chosen to be
notified about out-of-band data. The receiver can grab this input
without affecting any information currently queued on the socket.
Therefore, you can use this without interfering with any current
activity due to other wire and remote interfaces.
Unfortunately, most implementations of TCP/IP are broken, so use of
out-of-band data is limited for safety reasons. You can only reliably
send one character at a time.
The Wire package is built on top of CMUCLs networking support. In
view of this, it is possible to use the routines described in section
10.6 for handling and sending out-of-band data. These
all take a Unix file descriptor instead of a wire, but you can fetch a
wire's file descriptor with wire-fd.