Author: Christian.Queinnec@lip6.fr

## Client-server

Interprocess communication between processes on the same machine or on different machines through TCP/IP sockets is a mode of point-to-point asynchronous communication. The reliability of such transmissions is assured by the TCP protocol. It is nonetheless possible to simulate the broadcast to a group of processes through point-to-point communication to all receivers.
The roles of different processes communicating in an application are asymmetric, as a general rule. That description holds for client-server architectures. A server is a process (or several processes) accepting requests and trying to respond to them. The client, itself a process, sends a request to the server, hoping for a response.

### Client-server Action Model

A server provides a service on a given port by waiting for connections from future clients. Figure 20.1 shows the sequence of principal tasks for a server and a client.

> Figure 20.1: Model of a server and client

A client can connect to a service once the server is ready to accept connections (`accept`). In order to make a connection, the client must know the IP number of the server machine and the port number of the service. If the client does not know the IP number, it needs to request name/number resolution using the function `gethostbyname`. Once the connection is accepted by the server, each program can communicate via input-output channels over the sockets created at both ends.

### Client-server Programming

The mechanics of client-server programming follows the model described in Figure 20.1. These tasks are always performed. For these tasks, we write generic functions parameterized by particular functions for a given server. As an example of such a program, we describe a server that accepts a connection from a client, waits on a socket until a line of text has been received, converting the line to CAPITALS, and sending back the converted text to the client.
Figure 20.2 shows the communication between the service and different clients1.

> Figure 20.2: CAPITAL service and its clients

Certain tasks run on the same machine as the server, while others are found on remote machines.
We will see

1. How to write the code for a ``generic server'' and instantiate it for our particular capitalization service.
2. How to test the server, without writing the client, by using the `telnet` program.
3. How to create two types of clients:
   - a sequential client, which waits for a response after sending a request;
   - a parallel client, which separates the send and receive tasks.
   Therefore, there are two processes for this client.

### Code for the Server

A server may be divided into two parts: waiting for a connection and the following code to handle the connection.

#### A Generic Server

The generic server function `establish_server` described below takes as its first argument a function for the service (`server_fun`) that handles requests, and as its second argument, the address of the socket in the Internet domain that listens for requests. This function uses the auxiliary function `domain_of`, which extracts the domain of a socket from its address.
In fact, the function `establish_server` is made up of high-level functions from the `Unix` library. This function sets up a connection to a server.

    # let establish_server server_fun sockaddr =
       let domain = domain_of sockaddr in
       let sock = Unix.socket domain Unix.SOCK_STREAM 0
       in Unix.bind sock sockaddr ;
          Unix.listen sock 3;
          while true do
            let (s, caller) = Unix.accept sock
            in match Unix.fork() with
                   0 -> if Unix.fork() <> 0 then exit 0 ;
                        let inchan = Unix.in_channel_of_descr s
                        and outchan = Unix.out_channel_of_descr s
                        in server_fun inchan outchan ;
                           close_in inchan ;
                           close_out outchan ;
                           exit 0
                 | id -> Unix.close s; ignore(Unix.waitpid [] id)
          done ;;
    val establish_server :
      (in_channel -> out_channel -> 'a) -> Unix.sockaddr -> unit = <fun>

To finish building a server with a standalone executable that takes a port number parameter, we write a function `main_server` which takes a parameter indicating a service. The function uses the command-line parameter as the port number of a service. The auxiliary function `get_my_addr`, returns the address of the local machine.

    # let get_my_addr () =
       (Unix.gethostbyname(Unix.gethostname())).Unix.h_addr_list.(0) ;;
    val get_my_addr : unit -> Unix.inet_addr = <fun>

    # let main_server  serv_fun =
       if Array.length Sys.argv < 2 then Printf.eprintf "usage : serv_up port\n"
       else try
              let port =  int_of_string Sys.argv.(1) in
              let my_address = get_my_addr()
              in establish_server serv_fun  (Unix.ADDR_INET(my_address, port))
            with
              Failure("int_of_string") ->
                Printf.eprintf "serv_up : bad port number\n" ;;
    val main_server : (in_channel -> out_channel -> 'a) -> unit = <fun>

#### Code for the Service

The general mechanism is now in place. To illustrate how it works, we need to define the service we're interested in. The service here converts strings to upper-case. It waits for a line of text over an input channel, converts it, then writes it on the output channel, flushing the output buffer.

    # let uppercase_service ic oc =
       try while true do
             let s = input_line ic in
             let r = String.uppercase s
             in output_string oc (r^"\n") ; flush oc
           done
       with _ -> Printf.printf "End of text\n" ; flush stdout ; exit 0 ;;
    val uppercase_service : in_channel -> out_channel -> unit = <fun>

In order to correctly recover from exceptions raised in the `Unix` library, we wrap the initial call to the service in an ad hoc function from the `Unix` library:

    # let go_uppercase_service () =
       Unix.handle_unix_error main_server uppercase_service ;;
    val go_uppercase_service : unit -> unit = <fun>

#### Compilation and Testing of the Service

We group the functions in the file `serv_up.ml`, adding an actual call to the function `go_uppercase_service`. We compile this file, indicating that the `Unix` library is linked in

    ocamlc -i -custom -o serv_up.exe unix.cma serv_up.ml -cclib -lunix

The transcript from this compilation (using the option `-i`) gives:

    val establish_server :
      (in_channel -> out_channel -> 'a) -> Unix.sockaddr -> unit
    val main_server : (in_channel -> out_channel -> 'a) -> unit
    val uppercase_service : in_channel -> out_channel -> unit
    val go_uppercase_service : unit -> unit

We launch the server by writing:

    serv_up.exe 1400

The port chosen here is 1400. Now the machine where the server was launched will accept connections on this port.

### Testing with `telnet`

We can now begin to test the server by using an existing client to send and receive lines of text. The `telnet` utility, which normally is a client of the `telnetd` service on port 23, and used to control a remote connection, can be diverted from this role by passing a machine name and a different port number. This utility exists on several operating systems. To test our server under Unix, we type:

    $ telnet boulmich 1400
    Trying 132.227.89.6...
    Connected to boulmich.ufr-info-p6.jussieu.fr.
    Escape character is '^]'.

The IP address for `boulmich` is 132.227.89.6 and its complete name, which contains its domain name, is `boulmich.ufr-info-p6.jussieu.fr`. The text displayed by `telnet` indicates a successful connection to the server. The client waits for us to type on the keyboard, sending the characters to the server that we have launched on `boulmich` on port 1400. It waits for a response from the server and displays:

    The little cat is dead.
    THE LITTLE CAT IS DEAD.
    We obtained the expected result.
    WE OBTAINED THE EXPECTED result.

The phrases entered by the user are in lower-case and those sent by the server are in upper-case. This is exactly the role of this service, to perform this conversion.
To exit from the client, we need to close the window where it was run, by executing the `kill` command. This command will close the client's socket, causing the server's socket to close as well. When the server displays the message ```End of text`,'' the process associated with the service terminates.

### The Client Code

While the server is naturally parallel (we would like to handle a particular request while accepting others, up to some limit), the client may or may not be so, according to the nature of the application. Below we give two versions of the client. Beforehand, we present two functions that will be useful for writing these clients.
The function `open_connection` from the `Unix` library allows us to obtain a couple of input-output channels for a socket.
The following code is contained in the language distribution.

    # let open_connection sockaddr =
       let domain = domain_of sockaddr in
       let sock = Unix.socket domain Unix.SOCK_STREAM 0
       in try Unix.connect sock sockaddr ;
              (Unix.in_channel_of_descr sock , Unix.out_channel_of_descr sock)
         with exn -> Unix.close sock ; raise exn ;;
    val open_connection : Unix.sockaddr -> in_channel * out_channel = <fun>

Similarly, the function `shutdown_connection` closes down a socket.

    # let shutdown_connection inchan =
       Unix.shutdown (Unix.descr_of_in_channel inchan) Unix.SHUTDOWN_SEND ;;
    val shutdown_connection : in_channel -> unit = <fun>

#### A Sequential Client

From these functions, we can write the main function of a sequential client. This client takes as its argument a function for sending requests and receiving responses. This function analyzes the command line arguments to obtain connection parameters before actual processing.

    # let main_client client_fun  =
       if Array.length Sys.argv < 3
       then Printf.printf "usage :  client server port\n"
       else let server = Sys.argv.(1) in
            let server_addr =
              try  Unix.inet_addr_of_string server
              with Failure("inet_addr_of_string") ->
                     try  (Unix.gethostbyname server).Unix.h_addr_list.(0)
                     with Not_found ->
                            Printf.eprintf "%s : Unknown server\n" server ;
                            exit 2
            in try
                 let port = int_of_string (Sys.argv.(2)) in
                 let sockaddr = Unix.ADDR_INET(server_addr,port) in
                 let ic,oc = open_connection sockaddr
                 in client_fun ic oc ;
                    shutdown_connection ic
               with Failure("int_of_string") -> Printf.eprintf "bad port number";
                                                exit 2 ;;
    val main_client : (in_channel -> out_channel -> 'a) -> unit = <fun>

All that is left is to write the function for client processing.

    # let client_fun ic oc =
       try
         while true do
           print_string  "Request : " ;
           flush stdout ;
           output_string oc ((input_line stdin)^"\n") ;
           flush oc ;
           let r = input_line ic
           in Printf.printf "Response : %s\n\n" r;
              if r = "END" then ( shutdown_connection ic ; raise Exit) ;
         done
       with
           Exit -> exit 0
         | exn -> shutdown_connection ic ; raise exn  ;;
    val client_fun : in_channel -> out_channel -> unit = <fun>

The function `client_fun` enters an infinite loop which reads from the keyboard, sends a string to the server, gets back the transformed upper-case string, and displays it. If the string is `"END"`, then the exception `Exit` is raised in order to exit the loop. If another exception is raised, typically if the server has shut down, the function ceases its calculations.
The client program thus becomes:

    # let go_client () = main_client client_fun ;;
    val go_client : unit -> unit = <fun>

We place all these functions in a file named `client_seq.ml`, adding a call to the function `go_client`. We compile the file with the following command line:

    ocamlc -i -custom -o client_seq.exe unix.cma client_seq.ml -cclib -lunix

We run the client as follows:

    $ client_seq.exe boulmich 1400
    Request : The little cat is dead.
    Response: THE LITTLE CAT IS DEAD.

    Request : We obtained the expected result.
    Response: WE OBTAINED THE EXPECTED RESULT.

    Request : End
    Response: END

#### The Parallel Client with `fork`

The parallel client mentioned divides its tasks between two processes: one for sending, and the other for receiving. The processes share the same socket. The functions associated with each of the processes are passed to them as parameters.
Here is the modified program:

    # let main_client client_parent_fun client_child_fun  =
       if Array.length Sys.argv < 3
       then Printf.printf "usage :  client server port\n"
       else
         let server = Sys.argv.(1) in
         let server_addr =
           try  Unix.inet_addr_of_string server
           with Failure("inet_addr_of_string")
                  -> try  (Unix.gethostbyname server).Unix.h_addr_list.(0)
                     with Not_found ->
                            Printf.eprintf "%s : unknown server\n" server ;
                            exit 2
         in try
              let port = int_of_string (Sys.argv.(2)) in
              let sockaddr = Unix.ADDR_INET(server_addr,port) in
              let ic,oc = open_connection sockaddr
              in match Unix.fork () with
                     0 -> if Unix.fork() = 0 then client_child_fun oc ;
                          exit 0
                  | id -> client_parent_fun ic ;
                          shutdown_connection ic ;
                          ignore (Unix.waitpid [] id)
            with
               Failure("int_of_string") -> Printf.eprintf "bad port number" ;
                                           exit 2 ;;
    val main_client : (in_channel -> 'a) -> (out_channel -> unit) -> unit = <fun>

The expected behavior of the parameters is: the (grand)child sends the request and the parent receives the response.
This architecture has the effect that if the child needs to send several requests, then the parent receives the responses to requests as each is processed. Consider again the preceding example for capitalizing strings, modifying the client side program. The client reads the text from one file, while writing the response to another file. For this we need a function that copies from one channel, `ic`, to another, `oc`, respecting our little protocol (that is, it recognizes the string `"END"`).

    # let copy_channels ic oc =
       try while true do
             let s = input_line ic
             in if s = "END" then raise End_of_file
                else (output_string oc (s^"\n"); flush oc)
           done
       with End_of_file -> () ;;
    val copy_channels : in_channel -> out_channel -> unit = <fun>

We write the two functions for the child and parent using the parallel client model:

    # let child_fun in_file out_sock =
       copy_channels in_file out_sock ;
       output_string out_sock ("FIN\n") ;
       flush out_sock ;;
    val child_fun : in_channel -> out_channel -> unit = <fun>
    # let parent_fun out_file in_sock = copy_channels in_sock out_file ;;
    val parent_fun : out_channel -> in_channel -> unit = <fun>

Now we can write the main client function. It must collect two extra command line parameters: the names of the input and output files.

    # let go_client () =
      if Array.length Sys.argv < 5
        then Printf.eprintf "usage : client_par server port filein fileout\n"
      else let in_file = open_in Sys.argv.(3)
           and out_file = open_out Sys.argv.(4)
           in main_client (parent_fun out_file) (child_fun in_file) ;
              close_in in_file ;
              close_out out_file ;;
    val go_client : unit -> unit = <fun>

We gather all of our material into the file `client_par.ml` (making sure to include a call to `go_client`), and compile it. We create a file `toto.txt` containing the text to be converted:

    The little cat is dead.
    We obtained the expected result.

We can test the client by typing:

    client_par.exe boulmich 1400 toto.txt result.txt

The file `result.txt` contains the text:

    $ more result.txt
    THE LITTLE CAT IS DEAD.
    WE OBTAINED THE EXPECTED RESULT.

When the client finishes, the server always displays the message `"End of text"`.

### Client-server Programming with Lightweight Processes

The preceding presentation of code for a generic server and a parallel client created processes via the `fork` primitive in the `Unix` library. This works well under Unix; many Unix services are implemented by this technique. Unfortunately, the same cannot be said for Windows. For portability, it is preferable to write client-server code with lightweight processes, which were presented in Chapter 19. In this case, it becomes necessary to examine the interactions among different server processes.

#### Threads and the `Unix` Library

The simultaneous use of lightweight processes and the `Unix` library causes all active threads to block if a system call does not return immediately. In particular, reads on file descriptors, including those created by `socket`, are blocking.
To avoid this problem, the `ThreadUnix` library reimplements most of the input-output functions from the `Unix` library. The functions defined in that library will only block the thread which is actually making the system call. As a consequence, input and output is handled with the low-level functions `read` and `write` found in the `ThreadUnix` library.
For example, the standard function for reading a string of characters, `input_line`, is redefined in such a way that it does not block other threads while reading a line.

    # let my_input_line  fd =
       let s = " "  and  r = ref ""
       in while (ThreadUnix.read fd s 0 1 > 0) && s.[0] <> '\n' do r := !r ^s done ;
          !r ;;
    val my_input_line : Unix.file_descr -> string = <fun>

#### Classes for a Server with Threads

Now let us recycle the example of the CAPITALIZATION service, this time giving a version using lightweight processes. Shifting to threads poses no problem for our little application on either the server side or the client side, which start processes independently.
Earlier, we built a generic server parameterized over a service function. We were able to achieve this kind of abstraction by relying on the functional aspect of the Objective CAML language. Now we are about to use the object-oriented extensions to the language to show how objects allow us to achieve a comparable abstraction.
The server is organized into two classes: `serv_socket` and `connection`. The first of these handles the service startup, and the second, the service itself. We have introduced some print statements to trace the main stages of the service.

##### The `serv_socket` class.

has two instance variables: `port`, the port number for the service, and `socket`, the socket for listening. When constructing such an object, the initializer opens the service and creates this socket. The `run` method accepts connections and creates a new `connection` object for handling requests. The serv_socket uses the connection class described in the following paragraph. Usually, this class must be defined before the serv_socket class.

    # class serv_socket p =
       object (self)
         val port = p
         val mutable sock = ThreadUnix.socket Unix.PF_INET Unix.SOCK_STREAM 0

         initializer
           let my_address = get_my_addr ()
           in Unix.bind sock (Unix.ADDR_INET(my_address,port)) ;
              Unix.listen sock 3

         method private client_addr = function
             Unix.ADDR_INET(host,_) -> Unix.string_of_inet_addr host
           | _ -> "Unexpected client"

         method run () =
           while(true) do
             let (sd,sa) = ThreadUnix.accept sock in
             let connection = new connection(sd,sa)
             in Printf.printf "TRACE.serv: new connection from %s\n\n"
                              (self#client_addr sa) ;
             ignore (connection#start ())
           done
       end ;;
    class serv_socket :
      int ->
      object
        val port : int
        val mutable sock : Unix.file_descr
        method private client_addr : Unix.sockaddr -> string
        method run : unit -> unit
      end

It is possible to refine the server by inheriting from this class and redefining the `run` method.

##### The `connection` class.

The instance variables in this class, `s_descr` and `s_addr`, are initialized to the descriptor and the address of the socket created by `accept`. The methods are `start`, `run`, and `stop`. The `start` creates a thread calling the two other methods, and returns its thread identifier, which can be used by the calling instance of `serv_socket`. The `run` method contains the core functionality of the service. We have slightly modified the termination condition for the service: we exit on receipt of an empty string. The `stop` service just closes the socket descriptor for the service.
Each new connection has an associated number obtained by calling the auxiliary function `gen_num` when the instance is created.

    # let gen_num = let c = ref 0 in (fun () -> incr c; !c) ;;
    val gen_num : unit -> int = <fun>
    # exception Done ;;
    exception Done
    # class connection (sd,sa) =
       object (self)
         val s_descr = sd
         val s_addr = sa
         val mutable number = 0
         initializer
           number <- gen_num();
           Printf.printf "TRACE.connection : object %d created\n" number ;
           print_newline()

         method start () =  Thread.create (fun x -> self#run x ; self#stop x) ()

         method stop() =
           Printf.printf "TRACE.connection : object finished %d\n" number ;
           print_newline () ;
           Unix.close s_descr

        method run () =
          try
            while true do
              let line =  my_input_line s_descr
              in if (line = "") or (line = "\013") then raise Done ;
                 let result = (String.uppercase line)^"\n"
                 in ignore (ThreadUnix.write s_descr result 0 (String.length result))
            done
          with
             Done -> ()
           | exn  -> print_string (Printexc.to_string exn) ; print_newline()
       end ;;
    class connection :
      Unix.file_descr * 'a ->
      object
        val mutable number : int
        val s_addr : 'a
        val s_descr : Unix.file_descr
        method run : unit -> unit
        method start : unit -> Thread.t
        method stop : unit -> unit
      end

Here again, by inheritance and redefinition of the `run` method, we can define a new service.
We can test this new version of the server by running the `protect_serv` function.

    # let go_serv () = let s = new serv_socket 1400 in s#run () ;;
    # let protect_serv () = Unix.handle_unix_error go_serv () ;;

### Multi-tier Client-server Programming

Even though the client-server relation is asymmetric, nothing prevents a server from being the client of another service. In this way, we have a communication hierarchy. A typical client-server application might be the following:

- a mail client presents a friendly user interface;
- a word-processing program is run, followed by an interaction with the user;
- the word-processing program accesses a database.

One of the goals of client-server applications is to alleviate the processing of centralized machines. Figure 20.3 shows two client-server architectures with three tiers.

> Figure 20.3: Different client-server architectures

Each tier may run on a different machine. The user interface runs on the machine running the user mail application. The processing part is handled by a machine shared by a collection of users, which itself sends requests to a remote database server. With this application, a particular piece of data may be sent to the user mail application or to the database server.

### Some Remarks on the Client-server Programs

In the preceding sections, we constructed servers for a simple CAPITALIZATION service. Each server used a different approach for its implementation. The first such server used the Unix fork mechanism. Once we built that server, it became possible to test it with the telnet client supplied with the Unix, Windows, and MacOS operating systems. Next, we built a simple first client. We were then able to test the client and server together. Clients may have tasks to manage between communications. For this purpose, we built the `client_par.exe` client, which separates reading from writing by using forks. A new kind of server was built using threads to clearly show the relative independence of the server and the client, and to bring input-output into this setting. This server was organized into two easily-reused classes. We note that both functional programming and object-oriented programming support the separation of ``mechanical,'' reusable code from code for specialized processing.