This is an appendix to Understanding Unix/Linux
Programming
written for people interested in Ada programming on POSIX
systems.
[...] Note that every stream-based server sets up a service with these three steps:
GNAT.Sockets.Create_Socket (Socket => Server);
GNAT.Sockets.Bind_Socket
(Socket => Server,
Address => (Family => GNAT.Sockets.Family_Inet,
Addr => GNAT.Sockets.Any_Inet_Addr,
Port => Port));
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);
[...] Stream-based network client connect to servers with these two steps:
GNAT.Sockets.Create_Socket (Socket => Client);
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);
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;
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;
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;
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
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;
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.
The complete source code for the web server: webserv.adb
Go to next chapter.
Understanding Unix/Linux Programming, Bruce Molay,
ISBN 0-13-008396-8.