Sockets are a mechanism for interprocess communication designed at U.C. Berkeley for use in their version of Unix. Sockets have been added to many other versions of Unix and there is an implementation of sockets for Windows called Winsock. This document describes the Allegro interface to sockets. This interface works on Unix and on Windows (95 and NT).
The socket code is found the in package acl-socket which has the nickname socket.
There are three independent characteristics of sockets:
type | Valid values: :stream or :datagram.
A :stream socket offers a reliable, two-way, stream
connection between sockets. Reliable means that what you send is received at the
other end in the exact order you sent it. Stream means that the receiver reads a
stream of bytes and sees no record boundaries. It uses the internet protocol TCP. A :datagram socket offers unreliable, one-way, connectionless packet communication. Unreliable means that the packet may or may not be delivered. Packets may be delivered in an order other than in the order they were sent. Record boundaries are maintained: if the sender sends two ten byte packets and if the packets get through, the receiver will receive two ten byte packets rather than one twenty byte packet. For each packet you send you must give the destination address. It uses the internet protocol UDP. |
address family | Valid values: :internet or :file.
In order to send to another socket the socket must have a name. An :internet socket is named by a 32-bit host number and a 16-bit
port number. On Unix, port numbers less than 1024 can only be allocated by a process with
the user id of root. A :file socket is named by a file on a local disk. This is called the Unix address family but we've chosen to call it the :file address family since it really isn't Unix specific. This address family can only permit processes on the same machine to communicate. Note that the current version of the socket interface on Windows (Winsock, version 1.1), does not support the :file address family. |
format | Valid values: :text or :binary. This isn't a property of the Unix socket implementation but is instead something we've added for the Common Lisp implementation since a Lisp stream is either binary (supports read-byte, etc.) or text (supports read-char, etc.). |
Stream sockets have a fourth characteristic called connect, with a value :active or :passive. In order to use stream sockets you have to set up a link between two of them. That link is called a connection. You setup a connection in this way:
Machine A: create a passive socket at port port-b:
|
Machine B: create an active socket telling it to connect to Machine A, port port-b:
|
Machine A: wait for a connect request from anyone and when it occurs return a stream
for I/O:
|
Note that steps 2 and 3 can occur in either order.
Note the asymmetry: a passive socket is not a Lisp stream (you can't do read and write to it). An active socket is a Lisp stream.
When accept-connection is called on a passive socket, it does not return until a connection is made to the passive socket. The value accept-connection returns is a stream.
As long as the passive socket is not closed, new connections can still be made to the port of that socket.
An active socket can be used for only one connection. Once that connection has been made, the socket should be closed and a new active socket created.
Host naming conventions: this package supports three conventions for naming a host:
hostname | A string using the domain naming convention, e.g. ftp.franz.com. |
dotted | A string which is the printed representation of the numeric address: e.g. "192.132.95.84". We also support the non standard Berkeley extensions to this format for class A addresses: "23.3" (which is the same as "23.0.0.3") and class B addresses "128.1.3" (which is the same as "128.1.0.3"). |
ipaddr | An unsigned 32-bit number, representing the IP address in the native byte order for the host. |
The variables defined by the interface:
*print-hostname-in-stream*
*print-hostname-in-stream* controls whether the socket printing code converts the ip address of a socket into a hostname. This is usually what you want, however this can be a slow process (taking up to a minute to accomplish). The default value for this variable is t.
The functions defined by the interface:
(accept-connection (sock passive-socket) &key wait)
If wait is true, waits for there to be a connection to this socket. If wait is nil and there is no connection pending it returns nil immediately. Finally it returns a new stream object that can be used to do I/O to the connected socket. Wait defaults to true.
(dotted-to-ipaddr dotted &key errorp)
If dotted is a string like "192.132.95.84" (or one of the extensions to this format), this function converts the string to an unsigned 32-bit IP address. If there are any invalid characters in the string then an error will be signaled unless the value of :errorp keyword is nil.
(initialize-socket)
On Unix systems this is unnecessary and always does nothing. On Windows, if the socket system has already been initialized then this does nothing. If it hasn't, then the socket system is initialized. If, on Windows, this function hasn't been called before the first network operation, then it will be called automatically.
(ipaddr-to-dotted ipaddr &key values)
Convert a 32-bit unsigned IP address, ipaddr, to a string in dotted form. The value is always converted to the format "a.b.c.d", even if it is a class A or B address. If values is non-nil, then return a, b, c and d as multiple values instead of a single string.
(ipaddr-to-hostname ipaddr)
Return, as a string, the hostname of the machine with the given 32-bit IP address, ipaddr.
(lookup-hostname hostname)
Given a string naming a host, a 32-bit IP address, or a string in dotted form, return the 32-bit IP address for the host. An error is signaled if the host can't be found. The mechanism used to lookup the host's IP address can vary from machine to machine and is determined by the system administrator of the machine. Typically, the Domain Name Service is used, but in cases where that isn't available a file that maps host names to IP addresses is consulted.
(lookup-port portname protocol)
A port number is a 16-bit unsigned number (except port 0 is not used). Certain commonly used ports on machines are given symbolic names. The mapping between name and port number is kept in a file in the system area of the machine (often /etc on Unix and the Windows system directory on MS Windows). Given a symbolic name and a protocol (either "tcp" or "udp"), lookup-port returns the associated port number (or signals an error if such a port doesn't exist). The reason that the protocol type is supplied is that the port N for protocol "tcp" is distinct from port N for protocol "udp". j
(make-socket &key type format address-family connect &allow-other-keys)
The keywords arguments have the following possible values (with the default value given first):
:type :stream or :datagram :format :text or :binary :address-family :internet or :file :connect :active or :passive
All of the various kinds of sockets are created with make-socket, which determines the kind of socket you want based on the values of the type, format, connect, and address-family arguments. The value of the :address-family keyword can't be :file on Windows because Windows does not support it.
make-socket calls a specialized socket creation function and that function looks for other keywords designed just for that socket type. We describe next the extra keywords that are permitted for given values of :address-family and :type
:address-family :internet :type :stream
These additional keyword arguments are valid: :local-port, :remote-host , :remote-port, :backlog, :reuse-address, :broadcast and :keepalive.
The port values are 16-bit integer or strings naming ports found in the operating system's services file and labeled as being "tcp" services. On Unix the file is called /etc/services. On Windows, it is in the Windows directory and is called services.
The host value can be a 32-bit internet address or a string naming a host. Currently it cannot be a string with a dotted IP address, e.g. "192.132.95.1".
If the :local-port argument is not given, one will be selected by the system. You can use the local-port function to determine which port the system selected.
Note: The remote-host and remote-port values aren't used for :passive sockets.
The :backlog value is used by :passive sockets to tell the operating system how many connections can be pending (connected but for which an accept-connection hasn't been done). The default is 5.
:reuse-address sets the SO_REUSEADDR flag. This allows this a particular port to be reopened in :connect :passive mode even if there is an existing connection for the port. This is very useful when debugging a server program since without it you may have to wait up to a minute after closing a particular port to reopen the same port again (due to certain port-non-reuse requirements found in the TCP/IP protocol).
:broadcast requests permission to send broadcast packets from this socket. Whether permission is granted depends on the policy of the operating system. [Currently this is not implemented for datagram sockets which is the only place it makes sense.]
:keepalive if non-nil then continue to verify that the the connection is alive by sending empty packets to the receiving end.
A passive internet address family socket can now be created with a specific :local-host value. Normally the :local-host doesn't need to be specified as the operating system will determine that when a connection is made. There may be times when you want to specify the local-host. For example, a convention has been established that every machine running tcp/ip has at least two IP addresses: one is associated with the ethernet card and one is for a local-to-the-machine network called the loopback network. The loopback IP address is usually 127.1 (it's a Class A address so it is written as two numbers). If you open up a passive socket and specify "127.1" as the local-host, then that means that only programs on your machine can connect to that socket. Naturally, this could very important for security reasons.
:address-family :file :type :stream
These additional keyword arguments are valid: :local-filename, :remote-filename, and :backlog.
These are the files that name the local and remote filenames for the connection.
For :passive sockets the :local-filename must be specified (and :remote-filename will be ignored). For :active sockets :local-filename can be omitted but :remote-filename must be specified.
The filename specified must not already exist in the filesystem (or you'll get an error).
:address-family :internet :type :datagram
These additional keyword arguments are valid: :local-port, :remote-host, and :remote-port.
See the :internet :stream case above for the meaning of the keywords. A datagram socket is never connected to a remote socket, it can send a message to a different host and port each time data is sent through it. However if you know that you'll be sending data to a particular host and port with this socket, then you can specify that :remote-host and :remote-port when you create the socket. If you've done that then you can omit the :remote-host and :remote-port arguments to the send-to function. In other words, specifying the :remote-host and :remote-port just sets the default values for the :remote-host and :remote-port arguments when a send-to is done.
:address-family :file :type :datagram
These additional keyword arguments are valid: :local-filename and :remote-filename.
See the :file :stream case above for the meaning of the keywords. As in the description just above, if you specify a :remote-filename then you are merely setting the default value for the :remote-filename argument when a send-to is done.
(with-pending-connect &body body)
A call to socket:make-socket can take a while (maybe a minute or two) to make or fail to make a connection.
If you want to limit the amount of time spent in the socket:make-socket call you can write it with a mp:with-timeout. If the timeout occurs, then the program gives up on trying to make the connection. However the connection is offically still pending and operating system resources will continue to be used. Thus, it's important to clean up after a timed out attempt to make a connection.
The best way to ensure that things are cleaned up is to use socket:with-pending-connect. Just write your code something like this:
(socket:with-pending-connect (mp:with-timeout (10 (error "connect failed")) (socket:make-socket :remote-host "foo.com" :remote-port 1234)))The with-pending-connect wraps the body with an unwind-protect that will clean up the pending connection as control passes through it.
(receive-from (sock datagram-socket) size &key buffer extract)
This is used to read from a datagram socket. You must specify a size bigger than the size of the message you expect (usually the rest of the datagram will be thrown away if it doesn't all fit).
If the buffer argument is given then it should be a Lisp vector of some raw type (like character or (unsigned-byte 8) but definitely not t) of at least size bytes that will be used to read in the datagram. If buffer is not given then a fresh buffer will be created (of the type appropriate to the format of the socket).
If extract is unspecified or nil then the first value returned is the buffer used to read the data. If extract is true then the first value returned is a subsequence of the buffer, that subsequence being that part of the buffer that contains the data just read.
The second value returned is the number of bytes read by receive-from.
In the case of an :internet socket, the third value returned is the 32-bit internet address of the sender of the data and the fourth value is the port number.
In the case of a :file socket, the third (and last) value returned is the filename of the sender's socket (or "" if there is no filename).
(send-to (sock internet-datagram-socket) buffer size &key remote-host remote-port)
Sends the bytes in the buffer to the socket at :remote-host and :remote-port.
For datagram sockets you can't do normal Lisp stream I/O. The data is written to the socket using this function. buffer is a Lisp vector type and size is the number of bytes of data to send. :remote-host and :remote-port describe where to send the data. If they aren't given then the values saved when the socket was created with make-socket are used.
(send-to (sock file-datagram-socket) buffer size &key remote-filename)
This is like the previous send-to except this is used for sockets within the address family :file.
(socket-os-fd sock)
Return the operating system file descriptor associated with this socket. This is useful if you want to do some socket processing in foreign code and that code needs to access the socket directly. Note: Such accessing will likely confuse the buffering done by Lisp.
These functions retrieve slot values from socket instances. The values of these slots are set when the socket is created.
(remote-host socket) (local-host socket)
Returns an IP address. You can use ipaddr-to-dotted or ipaddr-to-hostname to make a more readable version of this value if you plan to print it out.
(local-port socket) (remote-filename socket) (local-filename socket) (remote-port socket) (socket-address-family socket) (socket-connect socket) (socket-format socket) (socket-type socket)
Note: Both internet stream and internet datagram sockets use 16-bit port numbers. Note that stream (tcp) port N is totally distinct from datagram (udp) port N.
When errors are raised by the socket interface, Lisp conditions are signalled. This section describes those conditions.
A condition is a Clos class and thus fits into the hierarchy of Clos classes. The condition excl::socket-error is a subclass of the condition error. The condition excl::system-socket-error is a subclass of excl::socket-error.
excl::socket-error is the superclass for all socket related errors. It add no slots to the error condition class. In the future we may add subclasses to excl::socket-error, but at present it is merely a placeholder condition and is never signalled.
excl::system-socket-error denotes operating system detected socket errors. It adds the following slots
Name | Reader function | What |
excl::identifier | excl::socket-error-identifier | Symbol denoting this error (see table below) |
excl::code | excl::socket-error-code | Operating system dependent error code (if any) |
excl::situation | excl::socket-error-situation | String describing the operation in progress when the the error occured |
Handling socket error is difficult because the error returned in exceptional situations can depend on the operating system and the address of the other side of the connection. For example, attempting to make a connection to a machine that is down may result in a "Connection Timed Out" or a "Host Unreachable" error, or maybe something else on certain systems. The error codes assigned to socket errors vary from operating system to operating system so to make it easier to write portable code we translate a large set of the common error codes from a machine dependent number to a symbol which we call the identifier. Condition handling code should check the identifier field (using excl::socket-error-identifier). If the identifier value is :unknown then this is not a common socket error and the operating system dependent code value of the condition must be used. Here are the possible identifier values and their meanings:
Identifier | Meaning |
:address-in-use | Local socket address already in use |
:address-not-available | Local socket address not available |
:network-down | Network is down |
:network-reset | Network has been reset |
:connection-aborted | Connection aborted |
:connection-reset | Connection reset by peer |
:no-buffer-space | No buffer space |
:shutdown | Connection shut down |
:connection-timed-out | Connection timed out |
:connection-refused | Connection refused |
:host-down | Host is down |
:host-unreachable | Host is unreachable |
:unknown | Unknown error |
Create an active stream socket connection to a socket that just prints characters to
whomever connects to it. After connecting, read the first five characters and print them
out. USER(1): (let ((s (make-socket :remote-host "vapor" :remote-port "chargen"))) (dotimes (i 5) (print (read-char s))) (close s)) #\space #\! #\" #\# #\$ |
Sending a message from frisky to vapor: on vapor: USER(1): (print (read (accept-connection (make-socket :connect :passive :local-port 9933)))) .. this hangs ... on frisky: USER(1): (let ((s (make-socket :remote-host "vapor" :remote-port 9933))) (format s "Secret-message~%") (close s)) Then you see on vapor: Secret-message Secret-message USER(2): A flaw in this example is that on vapor we've left the socket and the stream open and we lost track of the objects to close them. So, while concise, this is not a good programming style. Another problem with this example is that when we created the port on vapor we used a specific port number (9933). This means our program will fail if port 9933 is already in use. If possible, it is best to let the system choose a port number (this is done by not specifying a :local-port argument) and then using the local-port function to find out which port was chosen. |
If we just want to send a simple message then datagrams might be more appropriate
(although the program must guarantee that the message made it because datagram
communication is unreliable). on vapor: user(2): (setq s (make-socket :type :datagram :local-port 9999)) #<text datagram socket waiting for connection at */9999 @ #x20664e82> user(3): on frisky: user(10): (setq x (make-socket :type :datagram)) #<text datagram socket waiting for connection at */45602 @ #x20717fb2> user(11): (send-to x "foo-the-bar" 11 :remote-host "ultra" :remote-port 9999) 11 user(12): on vapor: user(3): (receive-from s 100 :extract t) "foo-the-bar" 11 ;; length of result 3229900653 ;; frisky's IP address 45602 ;; the port number chosen for the socket by frisky user(4): |