Author: Christian.Queinnec@lip6.fr

## Client-server Toolbox

We present a collection of modules to enable client-server interactions among Objective CAML programs. This toolbox will be used in the two applications that follow.
A client-server application differs from others in the protocol that it uses and in the processing that it associates with the protocol. Otherwise, all such applications use very similar mechanisms: waiting for a connection, starting a separate process to handle the connection, and reading and writing sockets.
Taking advantage of Objective CAML's ability to combine modular genericity and extension of objects, we will create a collection of functors which take as argument a communications protocol and produce generic classes implementing the mechanisms of clients and of servers. We can then subclass these to obtain the particular processing we need.

### Protocols

A communications protocol is a type of data that can be translated into a sequence of characters and transmitted from one machine to another via a socket. This can be described using a signature.

    # module type PROTOCOL =
       sig
         type t
         val to_string : t -> string
         val of_string : string -> t
       end ;;

The signature requires that the data type be monomorphic; yet we can choose a data type as complex as we wish, as long as we can translate it to a sequence of characters and back. In particular, nothing prevents us from using objects as our data.

    # module Integer =
       struct
         class integer x =
           object
             val v = x
             method x = v
             method str = string_of_int v
           end
         type t = integer
         let to_string o = o#str
         let of_string s = new integer (int_of_string s)
       end ;;

By making some restrictions on the types of data to be manipulated, we can use the module `Marshal`, described on page ??, to define the translation functions.

    # module Make_Protocol = functor ( T : sig type t end ) ->
       struct
         type t = T.t
         let to_string (x:t) = Marshal.to_string x [Marshal.Closures]
         let of_string s = (Marshal.from_string s 0 : t)
       end ;;

### Communication

Since a protocol is a type of value that can be translated into a sequence of characters, we can make these values persistent and store them in a file.
The only difficulty in reading such a value from a file when we do not know its type is that a priori we do not know the size of the data in question. And since the file in question is in fact a socket, we cannot simply check an end of file marker. To solve this problem, we will write the size of the data, as a number of characters, before the data itself. The first twelve characters will contain the size, padded with spaces.
The functor `Com` takes as its parameter a module with signature `PROTOCOL` and defines the functions for transmitting and receiving values encoded using the protocol.

    # module Com = functor (P : PROTOCOL) ->
       struct
         let send fd m =
           let mes = P.to_string m in
           let l = (string_of_int (String.length mes)) in
           let buffer = String.make 12 ' ' in
             for i=0 to (String.length l)-1 do buffer.[i] <- l.[i] done ;
             ignore (ThreadUnix.write fd buffer 0 12) ;
             ignore (ThreadUnix.write fd mes 0 (String.length mes))

         let receive fd =
           let buffer = String.make 12 ' '
           in
             ignore (ThreadUnix.read fd buffer 0 12) ;
             let l = let i = ref 0
             in while (buffer.[!i]<>' ') do incr i done ;
                int_of_string (String.sub buffer 0 !i)
           in
             let buffer = String.create l
             in ignore (ThreadUnix.read fd buffer 0 l) ;
                P.of_string buffer
       end ;;
    module Com :
      functor(P : PROTOCOL) ->
        sig
          val send : Unix.file_descr -> P.t -> unit
          val receive : Unix.file_descr -> P.t
        end

Note that we use the functions `read` and `write` from module `ThreadUnix` and not those from module `Unix`; this will permit us to use our functions in a thread without blocking the execution of other processes.

### Server

A server is built as an abstract class parameterized by the type of data in the protocol. Its constructor takes as arguments a port number and the maximum number of simultaneous connections allowed. The method for processing a request is abstract; it must be implemented in a subclass of `server` to obtain a concrete class.

    # module Server = functor (P : PROTOCOL) ->
       struct
         module Com = Com (P)

         class virtual ['a] server p np =
           object (s)
             constraint 'a = P.t
             val port_num = p
             val nb_pending = np
             val sock = ThreadUnix.socket Unix.PF_INET Unix.SOCK_STREAM 0

             method start =
               let host = Unix.gethostbyname (Unix.gethostname()) in
               let h_addr = host.Unix.h_addr_list.(0) in
               let sock_addr = Unix.ADDR_INET(h_addr, port_num) in
                 Unix.bind sock sock_addr ;
                 Unix.listen sock nb_pending ;
                 while true do
                   let (service_sock, client_sock_addr) = ThreadUnix.accept sock
                   in ignore (Thread.create s#process service_sock)
                 done
             method send = Com.send
             method receive = Com.receive
             method virtual process : Unix.file_descr -> unit
           end
       end ;;

In order to show these ideas in use, let us revisit the capital service, adding the capability of sending lists of strings.

    # type message = Str of string | LStr of string list ;;
    # module Cap_Protocol = Make_Protocol (struct type t=message end) ;;
    # module Cap_Server = Server (Cap_Protocol) ;;

    # class cap_server p np =
       object (self)
         inherit [message] Cap_Server.server p np
         method process fd =
           match self#receive fd with
               Str s  -> self#send fd (Str (String.uppercase s)) ;
                         Unix.close fd
             | LStr l -> self#send fd (LStr (List.map String.uppercase l)) ;
                         Unix.close fd
       end ;;
    class cap_server :
      int ->
      int ->
      object
        val nb_pending : int
        val port_num : int
        val sock : Unix.file_descr
        method process : Unix.file_descr -> unit
        method receive : Unix.file_descr -> Cap_Protocol.t
        method send : Unix.file_descr -> Cap_Protocol.t -> unit
        method start : unit
      end

The processing consists of receiving a request, examining it, processing it and sending the result. The functor allows us to concentrate on this processing while constructing the server; the rest is generic. However, if we wanted a different mechanism, such as for example using acknowledgements, nothing would prevent us from redefining the inherited methods for communication.

### Client

To construct clients using a given protocol, we define three general-purpose functions:

- `connect`: establishes a connection with a server; it takes the address (IP address and port number) and returns a file descriptor corresponding to a socket connected to the server.
- `emit_simple`: opens a connection, sends a message and closes the connection.
- `emit_answer`: same as `emit_simple`, but waits for the server's response before closing the connection.

    # module Client = functor (P : PROTOCOL) ->
       struct
         module Com = Com (P)

         let connect addr port =
           let sock = ThreadUnix.socket Unix.PF_INET Unix.SOCK_STREAM 0
           and in_addr = (Unix.gethostbyname addr).Unix.h_addr_list.(0)
           in ThreadUnix.connect sock (Unix.ADDR_INET(in_addr, port)) ;
              sock

         let emit_simple addr port mes =
           let sock = connect addr port
           in Com.send sock mes ;  Unix.close sock

         let emit_answer addr port mes =
           let sock = connect addr port
           in Com.send sock mes ;
              let res = Com.receive sock
              in Unix.close sock ; res
       end ;;
    module Client :
      functor(P : PROTOCOL) ->
        sig
          module Com :
            sig
              val send : Unix.file_descr -> P.t -> unit
              val receive : Unix.file_descr -> P.t
            end
          val connect : string -> int -> Unix.file_descr
          val emit_simple : string -> int -> P.t -> unit
          val emit_answer : string -> int -> P.t -> P.t
        end

The last two functions are of a higher level than the first: the mechanism linking the client and the server does not appear. The caller of `emit_answer` does not even need to know that the computation it is requesting is carried out by a remote machine. As far as the caller is concerned, it invokes a function that is represented by an address and port, with an argument which is the message to be sent, and a value is returned to it. The distributed aspect can seem entirely hypothetical.
A client of the capital service is extremely easy to construct. Assume that the `boulmich` machine provides the service on port number 12345; then the function `list_uppercase` can be defined by means of a call to the service.

    # let list_uppercase l =
       let module Cap_client = Client (Cap_Protocol)
       in match Cap_client.emit_answer "boulmich" 12345 (LStr l)
          with Str x -> [x]
            | LStr x -> x ;;
    val list_uppercase : string list -> string list = <fun>

### To Learn More

The first improvement to be made to our toolbox is some error handling, which has been totally absent so far. Recovery from exceptions which arise from a broken connection, and a mechanism for retrying, would be most welcome.
In the same vein, the client and the server would benefit from a timeout mechanism which would make it possible to limit the time to wait for a response.
Because we have constructed the generic server as a class, which moreover is parameterized by the type of data to be transmitted over the network, it is easy to extend it to augment or modify its behavior in order to implement any desired improvements.
Another approach is to enrich the communication protocols. One can for example add requests for acknowledgement to the protocol, or accompany each request by a checksum allowing verification that the network has not corrupted the data.