4.6. Example: A simple HTTP client

The HTTP protocol is used to get web pages from web servers. Its principle is very simple: A request is sent to the server, and the server replies with the document (well, actually HTTP can be very complicated, but it can also still be used in this simple way). For example, the request could be

  GET / HTTP/1.0
  --empty line--
Note there is a second line which is empty. The server responds with a header, an empty line, and the document. In HTTP/1.0 we can assume that the server sends EOF after the document.

The first part of our client connects to the web server. This is not new:

  let ues = Unixqueue.create_unix_event_system();;
  let c = connector (`Socket(`Sock_inet_byname(Unix.SOCK_STREAM,
					       "www.npc.de", 80),
			     default_connect_options
		    )) ues;;
Furthermore, we need an asynchronous output channel that stores the incoming server reply. This is also a known code snippet:
   class async_buffer b =
   object (self)
     inherit Netchannels.output_buffer b
     method can_output = true
     method request_notification (f : unit->bool) = ()
   end
We also create a buffer:
  let b = Buffer.create 10000;;
Now we are interested in the moment when the connection is established. In this moment, we set up an output_async_descr object that copies its contents to the connection, so we can asynchronously send our HTTP request. Furthermore, we create an async_buffer object that collects the HTTP response, which can arrive at any time from now on.
  when_state
    ~is_done:(fun connstat ->
		match connstat with
		    `Socket(fd, _) ->
		      prerr_endline "CONNECTED";
		      let printer = new output_async_descr ~dst:fd ues in
		      let buffer = new async_buffer b in
		      let receiver = new receiver ~src:fd ~dst:buffer ues in
		      let s = "GET / HTTP/1.0\n\n" in
		      ignore(printer # output s 0 (String.length s));
	              when_state
                        ~is_done:(fun _ ->
                                    prerr_endline "HTTP RESPONSE RECEIVED!")
	                ~is_error:(fun _ ->
                                    prerr_endline "ERROR!")
                        receiver
		  | _ -> assert false
	     )
    c
  ;;
Some details: We can ignore the result of printer#output because the printer has unlimited capacity (the default of output_async_descr channels). Because printer is not closed, this channel does not close the destination descriptor fd (which would be fatal). The receiver, however, closes the file descriptor when it finds the end of the input stream.

One important line is missing: Up to now we have only set up the client, but it is not yet running. To invoke it we need:

  Unixqueue.run ues;;

This client is not perfect, not only, because it is restricted to the most basic form of the HTTP protocol. The error handling could be better: In the case that printer transitions to error state, it will close fd. But the file descriptor is in use by receiver at the same time, causing a followup error that is finally reported. A better solution would use a duplicate of the file descriptor for receiver, so both engines can independently close their descriptors. Furthermore, the error state of printer would be trapped.