This is an appendix to Understanding Unix/Linux Programming written for people interested in Ada programming on POSIX systems.

Connections and Protocols: Writing a Web Server

Section 12.3.1, page 385-386

[...] Note that every stream-based server sets up a service with these three steps:

Create a socket:
GNAT.Sockets.Create_Socket (Socket => Server);
Give the socket an address:
GNAT.Sockets.Bind_Socket
  (Socket  => Server,
   Address => (Family => GNAT.Sockets.Family_Inet,
               Addr   => GNAT.Sockets.Any_Inet_Addr,
               Port   => Port));
Arrange to take incoming calls:
GNAT.Sockets.Listen_Socket (Socket => Server,
                            Length => Queue_Length);

Rather than retype these steps for every server we write, we combine this three-step procedure into a single function: Make_Server. Code for this function is included in the package Socket_Shortcuts (specification, body). When we write servers, we can just call this function:

Server := Make_Server (Port => 8080);   

Section 12.3.3, page 386-387

[...] Stream-based network client connect to servers with these two steps:

Create a socket:
GNAT.Sockets.Create_Socket (Socket => Client);
Use the socket to connect to a server:
GNAT.Sockets.Connect_Socket
  (Socket => Client,
   Server => Server_Address);

We abstract these two steps nto a single function: Connect_To_Server, included in the package Socket_Shortcuts (specification, body). When we write clients, we can just call this function:

Client := Connect_To_Server (Host => "www.dmusyd.edu",
                             Port => 80);   

Section 12.4, page 388-393

The Generic Client

procedure Generic_Client is
   Connection : GNAT.Sockets.Socket_Type;
begin
   Connection := 
     Socket_Shortcuts.Connect_To_Server (Host => [...],
                                         Port => [...]);
   Talk_With_Server (Connection);
   GNAT.Sockets.Close_Socket (Socket => Connection);
end Generic_Client;

The Generic Server

procedure Generic_Server is
   Receiver, Connection : GNAT.Sockets.Socket_Type;
   Client               : GNAT.Sockets.Sock_Addr_Type;
begin
   Receiver := 
     Socket_Shortcuts.Make_Server (Port => [...]);
   loop
      GNAT.Sockets.Accept_Socket (Server  => Receiver,
                                  Socket  => Connection,
                                  Address => Client);
      Process_Request (Connection);
      GNAT.Sockets.Close_Socket (Socket => Connection);
   end loop;
end Generic_Server;

Section 12.4.1, page 389-390

We can use the generic client to recreate our time client by inserting an appropriate procedure Talk_With_Server:

procedure Talk_With_Server (Connection : in GNAT.Sockets.Socket_Type) is
   Stream : GNAT.Sockets.Stream_Access;
   Buffer : String (1 .. 80);
   Filled : Natural;
begin
   Stream := GNAT.Sockets.Stream (Connection);
   Stream_Shortcuts.Get_Line (Source => Stream,
                              Item   => Buffer,
                              Last   => Filled);
   Ada.Text_IO.Put_Line (Buffer (Buffer'First .. Filled));
end Talk_With_Server;

Similarly we can use the generic server to recreate our time server by inserting an appropriate procedure Process_Request:

procedure Process_Request (Connection : in GNAT.Sockets.Socket_Type) is
   function Hours_And_Minutes (S : Duration) return String is
      function Two_Digits is new Zero_Filled_Image (Num   => Natural,
                                                    Width => 2);
      Hours   : Natural := Natural (S) / 3600;
      Minutes : Natural := (Natural (S) - Hours * 3600) / 60;
   begin
      return Two_Digits (Hours) & ":" & Two_Digits (Minutes);
   end Hours_And_Minutes;

   package Latin_1 renames Ada.Characters.Latin_1;

   Stream : GNAT.Sockets.Stream_Access;
begin
   Stream := GNAT.Sockets.Stream (Connection);
   Stream_Shortcuts.Put
     (Target => Stream,
      Item   => "The time here is " &
                Hours_And_Minutes (Ada.Calendar.Seconds (Ada.Calendar.Clock)) &
                "." & Latin_1.LF);
end Process_Request;

Section 12.4.2, page 390-391

Instead of handling the connection in the main time server process, we can also use Fork to create a child process for talking to each client. In the child process we can then use POSIX.Unsafe_Process_Primitives.Exec to make an external program (for example /bin/date) do the actual work: forking_time_server.adb

Section 12.5, page 393-404

Section 12.5.4, page 397-398

The main loop of our web server looks like this:

      loop
         GNAT.Sockets.Accept_Socket (Server  => Server,
                                     Socket  => Connection,
                                     Address => Client);
         Channel := GNAT.Sockets.Stream (Socket => Connection);
         File := GNAT.Sockets.Compatibility.Posix_File_Descriptor (Connection);

         HTTP_Request := Stream_Shortcuts.Get_Line (Source => Channel);
         Stream_Shortcuts.Skip_Until_Empty_Line (Source => Channel);

         Process (Request         => HTTP_Request,
                  Connection      => Channel,
                  File_Descriptor => File);

         GNAT.Sockets.Close_Socket (Socket => Connection);
      end loop;

Processing a request consists of identifying the command, and then acting on the argument:

   procedure Process
     (Request         : in     Unbounded.Unbounded_String;
      Connection      : access Ada.Streams.Root_Stream_Type'Class;
      File_Descriptor : in     POSIX.IO.File_Descriptor) is
      use type Interfaces.C.int;
      Request_Copy, Command, Argument : Unbounded.Unbounded_String;
   begin
      if Fork = 0 then
         Request_Copy := Request;
         Extract_First_Non_Whitespace_Slice (Source  => Request_Copy,
                                             Extract => Command);
         Extract_First_Non_Whitespace_Slice (Source  => Request_Copy,
                                             Extract => Argument);
         Sanitize (Argument);

         if Command /= "GET" then
            Cannot_Do (Connection => Connection);
         elsif not Exists (File_Name => Argument) then
            Do_404 (Argument   => Argument,
                    Connection => Connection);
         elsif Is_A_Directory (Argument) then
            Do_Ls (Directory_Name => Argument,
                   Connection     => File_Descriptor);
         elsif Extension (Argument) = "cgi" then
            Do_CGI (CGI_Program     => Argument,
                    Connection      => Connection,
                    File_Descriptor => File_descriptor);
         else
            Do_Cat (File_Name       => Argument,
                    Connection      => Connection,
                    File_Descriptor => File_descriptor);
         end if;
      end if;
   end Process;

The procedure Do_Ls handles directory listings:

   procedure Do_Ls (Directory_Name : in     Unbounded.Unbounded_String;
                    Connection     : in     POSIX.IO.File_Descriptor) is
      use type POSIX.IO.File_Descriptor;
      Directory : constant POSIX.POSIX_String := POSIX.To_POSIX_String 
                    (Unbounded.To_String (Directory_Name));
      Written   : POSIX.IO_Count;
      Arguments : POSIX.POSIX_String_List;
   begin
      POSIX.IO.Write
        (File   => Connection,
         Buffer => POSIX.To_POSIX_String
           (Header (Content_Type => "text/plain; charset=iso-8859-1")),
         Last   => Written);

      if POSIX.IO.Duplicate_And_Close (File   => Connection,
                                       Target => POSIX.IO.Standard_Output) /=
        POSIX.IO.Standard_Output then
         POSIX.Process_Primitives.Exit_Process (Status => 2);
      end if;
      if POSIX.IO.Duplicate_And_Close (File   => Connection,
                                       Target => POSIX.IO.Standard_Error) /=
        POSIX.IO.Standard_Error then
         POSIX.Process_Primitives.Exit_Process (Status => 2);
      end if;
      POSIX.IO.Close (File => Connection);

      POSIX.Append (List => Arguments, Str  => "ls");
      POSIX.Append (List => Arguments, Str  => "-l");
      POSIX.Append (List => Arguments, Str  => Directory);
      POSIX.Unsafe_Process_Primitives.Exec (Pathname => "/bin/ls",
                                            Arg_List => Arguments);
      POSIX.Process_Primitives.Exit_Process (Status => 1);
   end Do_Ls;

Section 12.5.5, page 398-399

Compile the source code and run the program at a port:

% make webserv
[...]
% ./webserv 12345

Put an HTML file in the directory /tmp/ and open it with http://localhost:12345/filename.html.

Section 12.5.6, page 399-403

The complete source code for the web server: webserv.adb

-- 

Go to next chapter.

Collected examples:
posix-in-ada.zip
Based on:
Understanding Unix/Linux Programming, Bruce Molay, ISBN 0-13-008396-8.
Main page:
http://edb.jacob-sparre.dk/Posix_in_Ada/
Written by:
Jacob Sparre Andersen.
Latest update:
12th of November, 2007