Previous Up Next

 6  Modern communication: sockets

Communication with pipes has some limitations. First, it is local to a machine: with named pipes, communicating processes must run on the same machine and what’s more, with anonymous pipes, they must share a common ancestor. Furthermore, pipes are not very suitable for a particularly useful model of communication: the client-server model. In this model, only one program, the server, has direct access to a shared resource. The other programs, the clients, access the resource by connecting to the server. The server serializes and controls the access to the shared resource. (Example: the x-window windowing system — the shared resources are the screen, the keyboard and the mouse.)

The client-server model is difficult to implement with pipes. The major difficulty is to establish the connection between a client and the server. With anonymous pipes, it is impossible: the server and the client would need a common ancestor that allocated an arbitrarily large number of pipes in advance. With named pipes, the server could read connection requests on a particular pipe. These requests would contain the name of another named pipe created and used by the client to communicate with the server. The problem is to ensure the mutual exclusion of simultaneous connection requests performed by multiple clients.

Sockets are a generalization of pipes addressing these issues. The client-server model is shown in figure 3.

Figure 3 — Client-Server model
  1. The server U creates a socket s on a port p known to the clients and waits for connections on it (1).
  2. The client A creates a socket and connects to the server on the port p (2). On the server, the system allocates a new socket to communicate privately with the client A (3). In this example, the server forks off an auxiliary server V (4), closes its connection with the client A (represented by the dashed line) and lets its child V handle the connection with A (5).
  3. The server can then accept a new client B, establish another connection handled in parallel by another clone W (6), and so on.
  4. The server can close its service by closing the file descriptor associated with the socket s. After a while the system frees the port p which can then be reused, for example to install another service.

In the model described above, the server U and the client A establish a private connection (3) to communicate without interference from other clients. For that reason, this mode of communication is referred to as the connection-oriented mode. If the transaction is short the server can handle the request directly (without forking) through the connection (3). In this case, the next client must wait for the server to be available, either because it is handling the connection (3), or because it explicitly manages several connections via multiplexing.

Sockets also allow a connectionless communication mode. In this mode, less frequently used, the server does not establish a private connection with the client, but responds directly to the client’s requests. We will briefly comment on this model in section 6.10 but in the remainder of this chapter, we mainly describe connection-oriented communication.

6.1  Sockets

Sockets, an extension of pipes, were introduced in bsd 4.2. They are now found on all Unix machines connected to a network. Special system calls are provided to establish connections following the client-server model; they enable local and remote communication between processes in a (nearly) transparent way.

The communication domain of a socket limits the processes (and the format of their address) with which we can communicate on the socket. Different communication domains are available, for example:

The communication type of a socket indicates whether communication is reliable (no loss or duplication of data) and the way the data is sent and received (a stream of bytes, or a sequence of packets — small blocks of bytes). The communication type constrains the protocol used to transmit data. Different communication types are available, here are three of them with their properties:

TypeReliableData representation
Streamyesbyte stream
Datagramnopackets
Segmented packetsyespackets

The “stream” type is very similar to communication with pipes. It is used most frequently, in particular to transmit unstructured byte sequences (e.g. rsh). The “segmented packets” type transmits data as packets: each write delimits a packet, each read returns at most a packet. It is well suited for message-oriented communication. The “datagram” type is the closest to the hardware properties of an Ethernet network: data is transmitted with packets and there is no guarantee that they reach their destination. It is the most economical type in terms of network resources. Some programs use it to transmit data that is not of crucial importance (e.g. biff); others, to get more network performance but with the burden of managing data losses manually.

6.2  Socket creation

The socket system creates a new socket:

val socket : socket_domain -> socket_type -> int -> file_descr

The result is a file descriptor that represents the new socket. Initially, this descriptor is “disconnected”, it is not ready to accept any read or write.

The first argument is a value of type socket_domain, it specifies the socket’s communication domain:

PF_UNIXThe Unix domain.
PF_INETThe Internet domain.

The second argument, a value of type socket_type, specifies the desired communication type:

SOCK_STREAMByte streams, reliable.
SOCK_DGRAMPackets, unreliable.
SOCK_RAWDirect access to the lower layers of the network.
SOCK_SEQPACKETPackets, reliable.

The third argument is the communication protocol to use. It is usually 0 which selects the default protocol for the given communication domain and type (e.g. udp for SOCK_DGRAM or tcp for SOCK_STREAM). Other values allow to use special protocols, for example icmp (Internet Control Message Protocol) used by the ping command to send packets which return automatically to the sender. The numbers for these special protocols are in the /etc/protocols file or in the protocols table of the nis (Network Information Service) database, if any. The system call getprotobyname returns information about a protocol in a portable manner:

val getprotobyname : string -> protocol_entry

Given the name of a protocol the result is a record of type protocol_entry. The p_proto field of this record has the protocol number.

6.3  Addresses

Several socket operations use socket addresses, represented by the variant type sockaddr:

type sockaddr = | ADDR_UNIX of string | ADDR_INET of inet_addr * int

ADDR_UNIX f is an address in the Unix domain, f is the name of the corresponding file in the machine’s file system. ADDR_INET (a,p) is an address in the Internet domain, a is the Internet address of a machine and p a port number on this machine.

Internet addresses are represented by the abstract type inet_addr. The following functions convert strings of the form 128.93.8.2 to values of type inet_addr, and vice versa:

val inet_addr_of_string : string -> inet_addr val string_of_inet_addr : inet_addr -> string

Another way to obtain internet addresses is to look them up by host name in the /etc/hosts table, the nis database or in domain name servers. The system call gethostbyname does that. On modern machines, the domain name servers are consulted first and /etc/hosts is only used as a fallback but in general that may depend on the machine’s configuration.

val gethostbyname : string -> host_entry

The argument is the host name to look for and the result a record of type host_entry. The h_addr_list field of this record is an array of Internet addresses corresponding to the machine (the same machine can be connected to multiple networks under different addresses).

Regarding port numbers, the most common services are listed in the table /etc/services which can be read in a portable manner using the getservbyname function:

val getservbyname : string -> string -> service_entry

The first argument is the service name ("ftp" for ftp servers, "smtp" for email, "nntp" for news servers, "talk" and "ntalk" for commands of that name, etc.) and the second argument is the name of the protocol: usually "tcp" if the service is using the stream connection type or "udp" for the datagram type. The result of getservbyname is a record of type service_entry whose s_port field contains the desired number.

Example

To obtain the address of the ftp server pauillac.inria.fr:

ADDR_INET((gethostbyname "pauillac.inria.fr").h_addr_list.(0), (getservbyname "ftp" "tcp").s_port)
* * *

6.4  Connection to a server

The system call connect establishes a connection with a server on a socket.

val connect : file_descr -> sockaddr -> unit

The first argument is a socket descriptor and the second argument is the server’s address.

Once the connection is established, calls to write on the socket descriptor send data to the server and calls to read receive data from the server. Sockets behave like pipes for read and write operations. First, read blocks if no data is available and can return fewer bytes than requested. Second, whenever the server closes the connection read returns 0 and write sends a sigpipe signal to the calling process.

connect binds the socket to a local address chosen by the system. Sometimes, it is preferable to manually choose this address. This can be done by calling the function bind (see section 6.7) before connect.

The netstat Unix command lists the current connections on the machine and their status.

6.5  Disconnecting sockets

There are two ways to disconnect a socket. The first is to call close on the socket. This closes the read and write sides of the connection, and deallocates the socket. But sometimes this is too brutal, for example we may want to close the connection from the client to the server to indicate an end of file but keep the connection open in the other direction to get remaining data from the server. The system call shutdown allows to close the connection gradually.

val shutdown : file_descr -> shutdown_command -> unit

The first argument is the descriptor of the socket to close and the second a value of type shutdown_command indicating which direction to close:

SHUTDOWN_RECEIVECloses the socket for reading; write on the other end of the connection will send a sigpipe signal to the caller.
SHUTDOWN_SENDCloses the socket for writing; read on the other end of the connection returns an end of file.
SHUTDOWN_ALLCloses the socket for reading and writing; unlike close, the socket descriptor is not deallocated.

Note that disconnecting a socket can take some time whether done with close or shutdown.

6.6  Complete example: the universal client

We program a client command such that client host port establishes a connection on the port port of the machine named host, sends on the resulting socket the data it reads on its standard input and writes the data it receives on its standard output. For instance, the command

echo -e 'GET /~remy/ HTTP/1.0\r\n\r\n' | ./client pauillac.inria.fr 80

connects to the port 80 of pauillac.inria.fr and sends an http request for the web page /~remy/.

This command is a “universal” client application in the sense that it factors out the code to establish a connection common to many clients and delegates the implementation of the specific protocol to the program that calls client.

The library function Misc.retransmit fdin fdout reads data on the descriptor fdin and writes it on fdout. It terminates, without closing the descriptors, when the end of file is reached on the input descriptor. Note that retransmit may be interrupted by a signal.

let retransmit fdin fdout = let buffer_size = 4096 in let buffer = String.create buffer_size in let rec copy () = match read fdin buffer 0 buffer_size with | 0 -> () | n -> ignore (write fdout buffer 0 n); copy () in copy ();;

The serious matter starts here.

1 open Sys;; 2 open Unix;; 3 4 let client () = 5 if Array.length Sys.argv < 3 then begin 6 prerr_endline "Usage: client <host> <port>"; 7 exit 2; 8 end; 9 let server_name = Sys.argv.(1) 10 and port_number = int_of_string Sys.argv.(2) in 11 let server_addr = 12 try (gethostbyname server_name).h_addr_list.(0) 13 with Not_found -> 14 prerr_endline (server_name ^ ": Host not found"); 15 exit 2 in 16 let sock = socket PF_INET SOCK_STREAM 0 in 17 connect sock (ADDR_INET(server_addr, port_number)); 18 match fork () with 19 | 0 -> 20 Misc.retransmit stdin sock; 21 shutdown sock SHUTDOWN_SEND; 22 exit 0 23 | _ -> 24 Misc.retransmit sock stdout; 25 close stdout; 26 wait ();; 27 28 handle_unix_error client ();;

We start by determining the Internet address of the machine to which we want to connect. It can be specified by a host name or in numerical form, gethostbyname correctly handles both cases. Then, we create a socket of type stream in the Internet domain with the default protocol and connect it to the address of the machine.

The process is then cloned with fork. The child process copies the data from its standard input to the socket. Once the end of standard input is reached it closes the connection in the sending direction and terminates. The parent process copies the data it reads on the socket to its standard output. Once the end of file is reached on the socket, it closes the standard output, synchronizes with the child process and terminates.

The connection is closed either by the client or by the server:

6.7  Establishing a service

Having seen how a client connects to a server, we now show how a server can provide a service for clients. First we need to associate a particular address to a socket to make it reachable from the network. The system call bind does this:

val bind : file_descr -> sockaddr -> unit

The first argument is the socket descriptor and the second the address to bind. The constant Internet address inet_addr_any can be used to bind all the Internet addresses that the machine has (it may be on multiple sub-networks).

We then indicate that the socket can accept connections with the system call listen:

val listen : file_descr -> int -> unit

The first argument is the socket descriptor and the second is the number of request that can be be put on hold while the server is busy (ranges from a few dozen to several hundreds for large servers). When the number of waiting clients exceeds this number, additional client connection requests fail.

Finally, connection requests on a socket descriptor are received via the system call accept:

val accept : file_descr -> file_descr * sockaddr

When the call returns the socket given in argument is still free and can accept more connection request. The first component of the result is a new descriptor connected to the client, everything written (resp. read) on that socket can be read (resp. is written) on the socket the client gave to connect. The second component of the result is the address of the client. It can be used to check that the client is authorized to connect (for example this is what the x server does, xhost can be used to add new authorizations), or to establish a second connection from the server to the client (as ftp does for each file transfer request).

The general structure of a tcp server is as follows.

let install_tcp_server_socket addr = let s = socket PF_INET SOCK_STREAM 0 in try bind s addr; listen s 10; s with z -> close s; raise z;;

The library function Misc.install_tcp_server addr creates a socket of type stream in the Internet domain with the default protocol and prepares it to accept new connection requests on the address addr with bind and listen. Given that this is a library function, we close the socket in case of an error.

let tcp_server treat_connection addr = ignore (signal sigpipe Signal_ignore); let server_sock = install_tcp_server_socket addr in while true do let client = restart_on_EINTR accept server_sock in treat_connection server_sock client done;;

The library function Misc.tcp_server creates a socket with install_tcp_server and enters an infinite loop. At each iteration of the loop it waits for a connection request with accept and treats it with the function treat_connection. Since this is a library function we restart the accept call if it is interrupted. We also ignore the signal sigpipe so that unexpected disconnection raise an EPIPE exception that can be caught by treat_connection rather than killing the server. Note that it in any case it is treat_connection’s duty to close the client descriptor at the end of the connection.

The function treat_connection is also given the descriptor of the server so that if it forks or or double_forks it can be closed by the child.

Now suppose we have the following, application specific, service function that talks to the client end ends by closing the connection:

let service (client_sock, client_addr) = (* Handle the client on the descriptor client_sock *) (* And when we are done: *) close client_sock;;

The server itself can treat each connection sequentially. The following library function, in Misc, captures this pattern:

let sequential_treatment server service client = service client

However as the server cannot handle any other requests while serving a client, this scheme is only appropriate for quick services, where the service function always runs in a short, bounded, amount of time (for instance, a date server).

Most servers delegate the service to a child process: fork is called immediately after accept returns. The child process handles the connection and the parent process immediately retries to accept. We obtain the following library function in Misc:

let fork_treatment server service (client_sock, _ as client) = let treat () = match fork () with | 0 -> close server; service client; exit 0 | k -> () in try_finalize treat () close client_sock;;

Note that it is essential that the parent closes client_sock otherwise the close made by the child will not terminate the connection (besides the parent would also quickly run out of descriptors). The descriptor is also closed if the fork fails, for the server may eventually decide the error is not fatal and continue to operate.

Similarly, the child immediately closes the server descriptor on which the connection request was received. First, it does not need it. Second, the server may stop accepting new connections before the child has terminated. The call to exit 0 is important since it ensures that the child terminates after the execution of the service and that it does not start to execute the server loop.

So far we ignored the fact that children will become zombie processes and that we need to recover them. There are two ways to to so. The simple approach is to have a grandchild process handle the connection using a double fork (see page ??). This gives the following library function, also in Misc:

let double_fork_treatment server service (client_descr, _ as client) = let treat () = match fork () with | 0 -> if fork () <> 0 then exit 0; close server; service client; exit 0 | k -> ignore (restart_on_EINTR (waitpid []) k) in try_finalize treat () close client_descr;;

However with this approach the server loses all control on the grandchild process. It is better to have the processes handling services and the server in the same process group so that the whole group can be killed at once to terminate the service. For this reason servers usually keep the fork treatment but add children recovering code, for example in the handler of the sigchld signal (see the function Misc.free_children on page ??).

6.8  Tuning sockets

Sockets have numerous internal parameters that can be tuned: the size of the transfer buffer, the size of the minimum transfer, the behavior on closing, etc.

These parameters have different types, for this reason there are as many getsockopt and setsockopt OCaml functions as there are types. Consult the OCaml documentation of the function getsockopt and its variants to get a detailed list of those options and the posix reference for getsockopt and setsockopt for their exact meaning.

Example

The following two parameters apply only to sockets of type stream in the Internet domain.

In the tcp protocol, the disconnection of a socket is negotiated and hence takes some time. Normally a call to close returns immediately, and lets the system negotiates the disconnection. The code below turns close on the socket sock into a blocking operation. It blocks either until all the sent data has been transmitted or until 5 seconds have passed.

setsockopt_optint sock SO_LINGER (Some 5);;

The SO_REUSEADDR option allows the bind system call to allocate a new socket on a local address immediately after the socket sock bound on that address is closed (there is however the risk to get packets intended for the old connection). This option allows to stop a server and restart it immediately, very useful for testing purposes.

setsockopt sock SO_REUSEADDR;;
* * *

6.9  Complete example: the universal server

We program a server command such that:

./server port cmd arg1 ... argn

receives connection requests on the port port and, for each connection, executes cmd with the arguments arg1 ... argn and the socket connection as its standard input and output. For example, if we execute:

./server 8500 grep foo

on the pomerol machine and the universal client (see section 6.6) on an other machine as follows:

./client pomerol 8500 < /etc/passwd

the client displays the same result as if we had typed:

grep foo < /etc/passwd

except that grep is executed on pomerol, and not on the local machine.

This command is a “universal” server in the sense that it factors out the code common to many server and delegates the implementation of the specific service and communication protocol to the cmd program it launches.

1 open Sys;; 2 open Unix;; 3 4 let server () = 5 if Array.length Sys.argv < 2 then begin 6 prerr_endline "Usage: client <port> <command> [arg1 ... argn]"; 7 exit 2; 8 end; 9 let port = int_of_string Sys.argv.(1) in 10 let args = Array.sub Sys.argv 2 (Array.length Sys.argv - 2) in 11 let host = (gethostbyname(gethostname ())).h_addr_list.(0) in 12 let addr = ADDR_INET (host, port) in 13 let treat sock (client_sock, client_addr as client) = 14 (* log information *) 15 begin match client_addr with 16 | ADDR_INET(caller, _) -> 17 prerr_endline ("Connection from " ^ string_of_inet_addr caller); 18 | ADDR_UNIX _ -> 19 prerr_endline "Connection from the Unix domain (???)"; 20 end; 21 (* connection treatment *) 22 let service (s, _) = 23 dup2 s stdin; dup2 s stdout; dup2 s stderr; close s; 24 execvp args.(0) args 25 in 26 Misc.double_fork_treatment sock service client in 27 Misc.tcp_server treat addr;; 28 29 handle_unix_error server ();;

The address given to tcp_server contains the Internet address of the machine running the program; the usual way to get it (line 11) is by calling gethostname. But in general many addresses are referencing the same machine. For instance, the address of the pauillac machine is 128.93.11.35, it can also be accessed locally (provided we are already on the pauillac machine) with the address 127.0.0.1. To provide a service on all the addresses pointing to this machine, we can use the constant Internet address inet_addr_any.

The service is handled by a “double fork”. The service function, executed by the child, redirects standard input and the two standard output on the connection socket and executes the requested command (note that the handling of the service cannot be done sequentially).

The connection is closed without any intervention of the server program. One of the following cases occurs:

Precautions

Writing a server requires more care than writing a client. While the client usually knows the server to which it connects, the server knows nothing about its clients and particularly if the service is public, the client can be “hostile”. The server must therefore guard itself against all pathological cases.

A typical attack is to open connections and leave them open without transmitting requests. After accepting the connection the server is blocked on the socket as long as the client stays connected. An attacker can saturate the service by opening a lot of unused connections. The server must be robust against these attacks: it must only accept a limited number of simultaneous connections to avoid system resources exhaustion and it must terminate connections that remain inactive for too long.

A sequential server handling connections without forking is immediately exposed to this blocking issue. It will be unresponsive for further request even though it does nothing. A solution for a sequential server is to multiplex the connections, but it can be tricky to implement. The solution with a parallel server is more elegant, but it still needs a timeout, for example by programming an alarm (see section 4.2).

6.10  Communication in connectionless mode

The tcp protocol used by most connections of type SOCK_STREAM works only in connection-oriented mode. Conversely, the udp protocol used by most connections of type SOCK_DGRAM always works in connectionless mode, there is no established connection between the two machines. For this type of sockets, data is transmitted with the system calls recvfrom and sendto.

val recvfrom : file_descr -> string -> int -> int -> msg_flag list -> int * sockaddr val sendto : file_descr -> string -> int -> int -> msg_flag list -> sockaddr -> int

Their interface is similar to read and write, they return the size of the transferred data. The call recvfrom also returns the address of the sending machine.

We can call connect on a socket of type SOCK_DGRAM to obtain a pseudo-connection. This pseudo-connection is just an illusion, the only effect is that the address passed in argument is memorized by the socket and becomes the address used for sending and receiving data (messages coming from other addresses are ignored). It is possible to call connect more than once to change the address or disconnect the pseudo-connection by connecting to an invalid address like 0. In contrast, doing this with a socket of type stream would generally issue an error.

6.11  Low level reads and writes

The system calls recv and send respectively generalize read and write but they work only on socket descriptors.

val recv : file_descr -> string -> int -> int -> msg_flag list -> int val send : file_descr -> string -> int -> int -> msg_flag list -> int

Their interface is similar to read and write but they add a list of flags of type msg_flag whose semantics is:

MSG_OOBProcess out-of-band data.
MSG_DONTROUTEShort-circuit the default routing table.
MSG_PEEKExamines the data without reading it.

These primitives can be used in connection-oriented mode instead of read and write or in pseudo-connected mode instead of recvfrom and sendto.

6.12  High-level primitives

Examples like the universal client-server are so frequent that the Unix module provides higher-level functions to establish or use network services.

val open_connection : sockaddr -> in_channel * out_channel val shutdown_connection : Pervasives.in_channel -> unit

The open_connection function opens a connection to the given address and creates a pair of Pervasives input/output channels on the resulting socket. Reads and writes on these channels communicate with the server but since the output channel is buffered we must flush it to ensure that a request has been really sent. The client can shutdown the connection abruptly by closing either of the channels (this will close the socket) or more “cleanly” by calling shutdown_connection. If the server closes the connection, the client receives an end of file on the input channel.

A service can be established with the establish_server function.

val establish_server : (in_channel -> out_channel -> unit) -> sockaddr -> unit

The function establish_server f addr establishes a service on the address addr and handles requests with the function f. Each connection to the server creates a new socket and forks. The child creates a pair of Pervasives input/output channels on the socket to communicate with the client and gives them to f to provide the service. Once f returns the child closes the socket and exits. If the client closes the connection cleanly, the child gets and end of file on the input channel and if it doesn’t it may receive a sigpipe signal when f writes on the output channel. As for the parent, it has probably already handled another request! The establish_server function never terminates, except in case of error (e.g. of the OCaml runtime or the system during the establishment of the service).

6.13  Examples of protocols

In simple cases (rsh, rlogin, …), the data transmitted between a client and a server is naturally represented by two streams of bytes, one from the client to the server and the other in the reverse direction. In other cases, the data to transmit is more complex, and requires to be encoded and decoded to/from the streams of bytes. The client and the server must then agree on a precise transmission protocol, which specifies the format of requests and responses exchanged on the connection. Most protocols used by Unix commands are specified in documents called “rfc” (request for comments): these documents start as proposals open for discussion, and gradually become standards over time, as users adopt the described protocol.2

“Binary” protocols

Most binary protocols transmit data in a compact format, as close as possible to the in-memory representation, in order to minimize the encoding/decoding work needed for transmission and save network bandwidth. Typical examples of protocols of this type are the x-window protocol, which governs exchanges between the x server and x applications, and the nfs protocol (rfc 1094).

Binary protocols usually encode data as follows. An integer or floating point number is represented by its 1, 2, 4, or 8 bytes binary representation. A string by its length as an integer followed by its contents as bytes. A structured object (tuple, record) by the representation of its fields in order. A variable size structure (array, list) by its length as an integer followed by the representation of its elements. If the exact type of data being transmitted in known to a process it can easily recreate it in its memory. When different type of data is exchanged on a socket the data encoding can be preceded by an integer to identify the data that follows.

Example

The XFillPolygon call of the x library, which draws and fills a polygon, sends a message with the following structure to the x server:

* * *

With binary protocols we must pay attention to the computer architecture of the communicating machines. In particular for multi-byte integers, big-endian machines store the most significant byte first (that is, in memory, at the lower-address) and little-endian machines store the least significant byte first. For instance, the 16 bit integer 12345 = 48 × 256 + 57 is represented by the byte 48 at the address n and the byte 57 at the address n+1 on a big-endian machine, and by the byte 57 at the address n and the byte 48 at the address n+1 on a little-endian machine. Hence protocols must precisely specify which convention they use when multi-bytes integers are transmitted. Another option is to allow both and have it specified in the header of the transmitted message.

The OCaml system helps to encode and decode data structures (a procedure called marshalling, serialization or pickling in the literature) by providing two functions to convert an OCaml value into a sequence of bytes and vice versa:

val output_value : out_channel -> 'a -> unit val input_value : in_channel -> 'a

These function are defined to save values to a disk file and get them back but they can also be used to transmit any value on a pipe or a socket. They handle any OCaml values except functions, preserve sharing and circularities inside values and work correctly between machines of different endianness. More information can be found in the Marshal module.

Note that semantically, the type of input_value is incorrect. It is too general, it is not true that the result of input_value is of type 'a for any type 'a. The value returned by input_value belongs to a precise type, and not to all possible types. But this type cannot be determined at compile time, it depends on the content of the channel read at runtime. Type-checking input_value correctly requires an extension to the ML language known as dynamic objects: values are paired with a representation of their type allowing to perform runtime type checks. Consult [15] for a detailed presentation.

Example

If the x-window protocol was written in OCaml, we would define a variant type request for requests sent to the server and a reply type for server responses:

type request = | FillPolyReq of (int * int) array * drawable * graphic_context * poly_shape * coord_mode | GetAtomNameReq of atom | ... and reply = | GetAtomNameReply of string | ...

The core of the server would be a loop that reads and decodes a request and responds by writing a reply:

(* Get a connection request on the descriptor s *) let requests = in_channel_of_descr s and replies = out_channel_of_descr s in try while true do match input_value requests with ... | FillPoly(vertices, drawable, gc, shape, mode) -> fill_poly vertices drawable gc shape mode | GetAtomNameReq atom -> output_value replies (GetAtomNameReply(get_atom_name atom)) | ... done with End_of_file -> (* end of the connection *)

The functions of the x library, linked with each application would have the following structure:

(* First establish a connection with the server on the descriptor s *) ... let requests = out_channel_of_descr s and replies = in_channel_of_descr s;; let fill_poly vertices drawable gc shape mode = output_value requests (FillPolyReq(vertices, drawable, gc, shape, mode));; let get_atom_name atom = output_value requests (GetAtomNameReq atom); match input_value replies with | GetAtomNameReply name -> name | _ -> fatal_protocol_error "get_atom_name";;
* * *
Remote procedure call

Another typical incarnation of binary protocols is remote procedure calls (rpc). A user on machine A wants to call a function f on a machine B. This is obviously not directly possible. It can be programmed on a case by case basis using the system to open a connection to the machine B, execute the call and send the result back to the machine A.

Figure 4 — Remote procedure call

But since this is a common situation, an rpc service can handle that (see figure 4). An rpc server runs on both machine A and B. A user on machine A requests the rpc server on the machine to execute a function on the distant machine B. The server on A relays the request to the rpc server on machine B which executes the call to f, sends the result back to the server on A which gives the result to the user. The point is that another user can call another function on B by going through the same server on A. The connection work is shared by the rpc service installed on the machines A and B and from the perspective of the users, everything happens as if these calls were simple function calls (dashed arrows).

“Text” protocols

Network services where the efficiency of the protocol is not crucial are often “text” protocols. A “text” protocol is in fact a small command language. Requests are command lines, the first word identifies the request type and the possible remaining words the command’s arguments. Responses are also made of one or more lines of text, often starting with a numerical code to identify the kind of response. Here are some “text” protocols:

NameDescr.Purpose
smtp (Simple Mail Transfer Protocol)rfc 821Electronic mail
ftp (File Transfer Protocol)rfc 959File transfer
nttp (Network News Transfer Protocol)rfc 977News reading
http/1.0 (HyperText Transfer Protocol)rfc 1945Web navigation
http/1.1 (HyperText Transfer Protocol)rfc 2068Web navigation

The great advantage of these protocols is that the exchanges between the server and the client are human readable. For example we can just use the telnet command to talk directly to the server. Invoke telnet host service where host is the host name on which the server is running the service service (e.g. http, smtp, nntp, etc.) and then type in the requests as a client would, the server’s responses will be printed on standard output. This makes it easier to understand the protocol. However coding and decoding requests and responses is more involved than for binary protocols and the message size also tends to be larger which is less efficient.

Example

Here is an example of an interactive dialog, in the shell, to send an email on an smtp server. The lines preceded by >> go from the client to the server, and are typed in by the user. The lines preceded by << go from the server to the client.

telnet margaux smtp Trying 128.93.8.2 ... Connected to margaux.inria.fr. Escape character is '^]'. << 220 margaux.inria.fr Sendmail 5.64+/AFUU-3 ready at Wed, 15 Apr 92 17:40:59 >> HELO pomerol.inria.fr << 250 Hello pomerol.inria.fr, pleased to meet you >> MAIL From:<god@heavens.sky.com> << 250 <god@heavens.sky.com>... Sender ok >> RCPT To:<xleroy@margaux.inria.fr> << 250 <xleroy@margaux.inria.fr>... Recipient ok >> DATA << 354 Enter mail, end with "." on a line by itself >> From: god@heavens.sky.com (Himself) >> To: xleroy@margaux.inria.fr >> Subject: Hello! >> >> Is everything ok down there? >> . << 250 Ok >> QUIT << 221 margaux.inria.fr closing connection Connection closed by foreign host.

The commands HELO, MAIL and RCPT respectively send to the server: the name of the client machine, the address of the sender and the address of the recipient. The DATA command asks to send the body of the email. The body of the message is then entered and ended by a line containing the single character '.' (would the body of the email contain such a line, we just double the initial '.' on that line, this additional period is then suppressed by the server).

The responses from the server are all made of a 3 digit numerical code followed by a comment. Responses of the form 5xx indicate an error and those with 2xx, that everything is fine. When the client is a real program it only interprets the response code, the comment is only to help the person who develops the mail system.

* * *

6.14  Complete example: http requests

The http protocol (HyperText Transfer Protocol) is primarily used to read documents over the famous “world wide web”. This domain is a niche area of client-server examples: between the client that reads a page and the server that writes it there is a myriad of intermediary relays that act as virtual servers for the real client or delegated clients for the real server. These relay often provide additional service like caching, filtering, etc..

There are several versions of the http protocol. To allow us to focus on the essentials, namely the architecture of clients or relays, we use the simple protocol inherited from the very first versions of the protocol. Even if dust-covered it is still understood by most servers. At the end of the section we describe a more modern, but also more complex, version which is needed to make real tools to explore the web. We do however leave the translation of the examples to this new version as an exercise.

Version 1.0 of the http protocol specified in rfc 1945 defines simple requests of the form:

GET sp uri crlf

where sp represents a space and crlf the string "\r\n" (“return” followed by “linefeed”). The response to a simple request is also simple: the content of the url is sent directly, without any headers and the end of the request is signaled by the end of file, which closes the connection. This form of request, inherited from version 0.9 of the protocol, limits the connection to a single request.

Fetching a url

We write a geturl program that takes a single argument, a url, retrieves the resource it denotes on the web and displays it.

The first task is to parse the url to extract the name of the protocol (here, necessarily "http"), the address of the server, the optional port and the absolute path of the document on the server. This is done with Str, OCaml’s regular expression library.

open Unix;; exception Error of string let error err mes = raise (Error (err ^ ": " ^ mes));; let handle_error f x = try f x with Error err -> prerr_endline err; exit 2 let default_port = "80";; type regexp = { regexp : Str.regexp; fields : (int * string option) list; } let regexp_match r string = let get (pos, default) = try Str.matched_group pos string with Not_found -> match default with Some s -> s | _ -> raise Not_found in try if Str.string_match r.regexp string 0 then Some (List.map get r.fields) else None with Not_found -> None;; let host_regexp = { regexp = Str.regexp "\\([^/:]*\\)\\(:\\([0-9]+\\)\\)?"; fields = [ 1, None; 3, Some default_port; ] };; let url_regexp = { regexp = Str.regexp "http://\\([^/:]*\\(:[0-9]+\\)?\\)\\(/.*\\)"; fields = [ 1, None; 3, None ] };; let parse_host host = match regexp_match host_regexp host with | Some (host :: port :: _) -> host, int_of_string port | _ -> error host "Ill formed host";; let parse_url url = match regexp_match url_regexp url with | Some (host :: path :: _) -> parse_host host, path | _ -> error url "Ill formed url";;

Sending a simple request is a trivial task, as the following function shows.

let send_get url sock = let s = Printf.sprintf "GET %s\r\n" url in ignore (write sock s 0 (String.length s));;

Note that the url can be complete, with the address and port of the server, or just contain the requested path on the server.

Reading the response is even easier, since only the document is returned, without any additional information. If there’s an error in the request, the error message returned by the server as an html document. Thus we just print the response with the function Misc.retransmit without indicating whether this is an error or the desired document. The rest of the program establishes the connection with the server.

let get_url proxy url fdout = let (hostname, port), path = match proxy with | None -> parse_url url | Some host -> parse_host host, url in let hostaddr = try inet_addr_of_string hostname with Failure _ -> try (gethostbyname hostname).h_addr_list.(0) with Not_found -> error hostname "Host not found" in let sock = socket PF_INET SOCK_STREAM 0 in Misc.try_finalize begin function () -> connect sock (ADDR_INET (hostaddr, port)); send_get path sock; Misc.retransmit sock fdout end () close sock;;

We conclude, as usual, by parsing the command line.

let geturl () = let len = Array.length Sys.argv in if len < 2 then error "Usage:" (Sys.argv.(0) ^ " [ proxy [:<port>] ] <url>") else let proxy, url = if len > 2 then Some Sys.argv.(1), Sys.argv.(2) else None, Sys.argv.(1) in get_url proxy url stdout;; handle_unix_error (handle_error geturl) ();;

http relay

We program an http relay (or proxy), which is a server that redirects http requests from a client to another server (or relay…) and forwards responses from that server back to the client.

The role of a relay is shown in in figure 5. When a client uses a relay, it addresses its requests to the relay rather than to the individual http servers located around the world. A relay has multiple advantages. It can store the responses to the most recent or frequent requests and serve them without querying the remote server (e.g. to avoid network overload or if the server is down). It can filter the responses (e.g. to remove advertisements or image, etc.). It can also simplify the development of a program by making it see the whole world wide web through a single server.

Figure 5 — http relay

The proxy port command launches the server on the port port (or if omitted, on the default port for http). We reuse the code of the get_url function (we assume that the functions above are available in a Url module). It only remains to write the code to analyze the requests and set up the server.

open Unix open Url let get_regexp = { regexp = Str.regexp "^[Gg][Ee][Tt][ \t]+\\(.*[^ \t]\\)[ \t]*\r"; fields = [ 1, None ] } let parse_request line = match regexp_match get_regexp line with | Some (url :: _) -> url | _ -> error line "Ill formed request"

We establish the service with the establish_server function, thus we just need to define the function to handle a connection:

let proxy_service (client_sock, _) = let service () = try let in_chan = in_channel_of_descr client_sock in let line = input_line in_chan in let url = parse_request line in get_url None url client_sock with End_of_file -> error "Ill formed request" "End_of_file encountered" in Misc.try_finalize (handle_error service) () close client_sock

and the rest of the program just establishes the service:

let proxy () = let http_port = if Array.length Sys.argv > 1 then try int_of_string Sys.argv.(1) with Failure _ -> error Sys.argv.(1) "Incorrect port" else try (getservbyname "http" "tcp").s_port with Not_found -> error "http" "Unknown service" in let treat_connection s = Misc.double_fork_treatment s proxy_service in let addr = ADDR_INET(inet_addr_any, http_port) in Misc.tcp_server treat_connection addr;; handle_unix_error (handle_error proxy) ();;

The http/1.1 protocol

Simple http requests need one connection per request. This is inefficient because most requests on a server are followed by others (e.g. if a client gets a web page with images, it will subsequently request the images) and the time to establish a connection can easily exceed the time spent in handling the request itself (chapter 7 show how we can reduce this by handling the requests with threads rather than processes). Version 1.1 of the http described in rfc 2068 uses complex requests that allow to make multiple requests on a single connection3.

In complex requests, the server precedes every response with a header describing the format of the response and possibly the size of the document transmitted. The end of the document is no longer indicated by an end of file, since we know its size. The connection can therefore stay open to handle more requests. Complex requests have the following form:

GET sp uri sp HTTP/1.1 crlf header crlf

The header part defines a list of key-value fields with the following syntax:

field : value crlf

Superfluous spaces are allowed around the ':' separator and any space can always be replaced by a tab or a sequence of spaces. The header fields can also span several lines: in this case, and in this case only, the crlf end of line lexeme is immediately followed by a space sp. Finally, uppercase and lowercase letters are equivalent in the keyword of fields and in the values of certain fields.

Mandatory and optional fields depend on the type of request. For instance, a GET request must have a field indicating the destination machine:

Host : hostname crlf

For this type of request, we may also request, using the optional field If-Modified, that the document be returned only if it has been modified since a given date.

If-Modified : date crlf

The number of fields in the header is not fixed in advance but indicated by the end of the header: a line containing only the characters crlf.

Here is a complete request (on each line an implicit \n follows the \r):

GET /~remy/ HTTP/1.1\r Host:pauillac.inria.fr\r \r

A response to a complex request is also a complex response. It contains a status line, a header, and the body of the response, if any.

HTTP/1.1 sp status sp message crlf header crlf body

The fields of a response header have a syntax similar to that of a request but the required and optional fields are different (they depend on type of request and the status of the response — see the full documentation of the protocol).

The body of the response can be transmitted in a single block, in chunks or be empty:

Here is an example of a single block response:

HTTP/1.1 200 OK\r Date: Sun, 10 Nov 2002 09:14:09 GMT\r Server: Apache/1.2.6\r Last-Modified: Mon, 21 Oct 2002 13:06:21 GMT\r ETag: "359-e0d-3db3fbcd"\r Content-Length: 3597\r Accept-Ranges: bytes\r Content-Type: text/html\r \r <html> ... </html>

The status 200 indicates that the request was successful. A 301 means the url was redirected to another url defined in the Location field of the response. The 4XX statuses indicate errors on the client side while 5XX errors on the server side.

Exercise 15

Write a relay that works with the http/1.1 protocol.

* * *
Exercise 16

Add a cache to the relay. Pages are saved on the hard drive and when a requested page is available in the cache, it is served unless too old. In that case the server is queried again and the cache updated.

* * *
Exercise 17

Write a program wget such that wget u1 u2 ... un makes the requests u1, u2, …, un and saves the responses in the files ./m1/p1, ./m2/p2, …, ./mn/pn where mi and pi are respectively the name of the server and the absolute path of the request ui. The program should take advantage of the protocol to establish a single connection to a machine m when it is the same for consecutive requests. Permanent url redirections should be followed. The following options can also be added:

-NDo not download the url if the file ./mi/ui does not exist or is older than the url.
-rDownload recursively all the urls embedded in the responses which are documents in html format.
* * *

1
The Internet network is made of local networks, usually Ethernet networks, connected by specialized links. It connects millions of machines world-wide. In the Internet domain, there is no difference between programs that communicate with a neighboring machine, connected to the same Ethernet cable, and those that communicate with a machine on the other side of the planet, through a dozen routers and a satellite link.
2
rfcs are available via anonymous ftp on numerous sites. In France: ftp.inria.fr, in the directory rfc. The reference site is http://www.faqs.org/rfcs/.
3
The http/1.0 protocol already allows complex requests but we prefer to describe the http/1.1 protocol which deals exclusively with these.

Previous Up Next