~/bin/codemd ../../toolkit/adalib/src/numlib.ads -x Spec -l
0069 | type Fx is interface;
0070 | type FxPtr is access all Fx'Class;
0071 |
0072 | function Val (F : Fx; x : RealType) return RealType is abstract;
In the last chapter object orientation was introduced as a means to build a rich logging support system. A particular example was logging over a network utilizing the udp discipline.
In this chapter, we build ourselves a server framework, supporting stream socket communications. A server framework that receives requests for services over a network channel would imply providing the services concurrently to many clients. A webserver is a typical server working in this fashion.
The server framework will also illustrate a far simpler form of object orientation supported by Ada in order to help realize diverse applications. A particular example will be a logging server - a marriage of the logging framework from the previous chapter and the server framework of this chapter; thus realizing a logging server that can receive log messages from many clients concurrently to present a uniform system log - invaluable in monitoring any application.
Tasks and the concurrency framework
Simple object orientation with Interfaces
Stream Socket based client server implementations.
In this projectlet, a function with 1 argument returning a value is the subject of study. Such a function is defined as:
~/bin/codemd ../../toolkit/adalib/src/numlib.ads -x Spec -l
0069 | type Fx is interface;
0070 | type FxPtr is access all Fx'Class;
0071 |
0072 | function Val (F : Fx; x : RealType) return RealType is abstract;
and can be used as:
~/bin/codemd ../../toolkit/adalib/src/numlib-integration.ads -x Appl -l
0004 | function Newton_Coates_3 (f : FxPtr; X1, X2 : RealType) return RealType;
0005 |
0006 | function Simpsons (f : FxPtr; X1, X2 : RealType) return RealType renames
0007 | Newton_Coates_3;
0008 |
0009 | function Newton_Coates_4 (f : FxPtr; X1, X2 : RealType) return RealType;
0010 |
0011 | function Newton_Coates_5 (f : FxPtr; X1, X2 : RealType) return RealType;
Numerical Integration with 3 different algorithms (Newton Coates) are specified in the above with any arbitrary function. An application can provide the function as:
~/bin/codemd ../../toolkit/examples/diffint/src/funcs.ads -x Spec -l
0013 | type PolySinusoid is new numlib.Fx with null record ;
0014 |
0015 | overriding
0016 | function Val( f : PolySinusoid ; x : numlib.RealType ) return numlib.RealType ;
0017 |
0018 | tpf : aliased PolySinusoid ;
and its implementation:
~/bin/codemd ../../toolkit/examples/diffint/src/funcs.adb -x Body -l
and an application utilizes this as:
~/bin/codemd ../../toolkit/examples/diffint/src/diffint.adb -x Appl -l
0114 | for n in 1..numSteps
0115 | loop
0116 | Put( x ); Put(" ; ");
0117 | Put( funcs.Val(funcs.tpf , x )); Put(" ; ");
0118 | y := y + numlib.integration.Newton_Coates_5(funcs.tpf'access , x , x + xdelta) ;
0119 | Put(y) ; Put(" ; ");
0120 | New_Line ;
0121 | x := x + xdelta ;
0122 | end loop ;
At runtime, these produce CSV files of the computations:
~/bin/diffint t3
~/bin/diffint t4
Diffint /Users/rajasrinivasan/bin/diffint Jan 13 2025 05:44:56
Diffint.T3
Diffint /Users/rajasrinivasan/bin/diffint Jan 13 2025 05:44:56
Diffint.T4
and plotting the output
using CSV
using DataFrames
using Plots
=DataFrames.DataFrame(CSV.File("Diffint.T4.csv",delim=";",header=false));
dfplot(df.Column1 , [df.Column2,df.Column3] ,
=["x" "Integral"] ,
label="Integral" , show=true) title
The chart shows the original function and its integral.
In this projectlet Stream sockets are used as the communication channel. There are of course quite a few options and choices particularly for within a node but Stream based channels are the staple for distriputed systems. These channels provide reliable communication, bidirectional but have no clear markers between messages; in addition, the lower layers may break up a stream. Different standard tools/protocols such as ftp, http have used different techniques to address this challenge. Simple solutions such as a standard message size for all messages will work but will not scale very well.
In this projectlet, each message is preceded by a length field so that the receiver can mark the message boundaries correctly.
~/bin/codemd ../../toolkit/adalib/src/server.adb -x SendReceive -l
0114 | procedure Send (sock : GS.Socket_Type; payload : AS.Stream_Element_Array) is
0115 | hdr : constant HeaderType := HeaderType (payload'Length);
0116 | begin
0117 | HeaderType'Write (GS.Stream (sock), hdr);
0118 | AS.Stream_Element_Array'Write (GS.Stream (sock), payload);
0119 | end Send;
0120 |
0121 | function Receive (sock : GS.Socket_Type) return AS.Stream_Element_Array is
0122 | hdr : HeaderType;
0123 | buffer : AS.Stream_Element_Array (1 .. MAX_MESSAGE_SIZE);
0124 | begin
0125 | HeaderType'Read (GS.Stream (sock), hdr);
0126 | AS.Stream_Element_Array'Read
0127 | (GS.Stream (sock), buffer (1 .. AS.Stream_Element_Offset (hdr)));
0128 | return buffer (1 .. AS.Stream_Element_Offset (hdr));
0129 | end Receive;
The quintessential server framework operates concurrently / independently or as a task in Ada terminology, with any other application, accepting requests for services from different clients. For each client, a proxy is created also as an independent task that carries on a conversation with its client.
~/bin/codemd ../../toolkit/adalib/src/server.ads -x Server -l
0026 | task type SockServer_Type is
0027 | entry Serve (port : Integer; svc : ServicePtr_Type);
0028 | end SockServer_Type;
0029 | type SockServer_PtrType is access all SockServer_Type;
0030 |
0031 | -- proxy for each client
0032 | task type ClientSock_Type is
0033 | entry Serve
0034 | (handler : ServicePtr_Type; sock : GS.Socket_Type;
0035 | addr : GS.Sock_Addr_Type);
0036 | end ClientSock_Type;
0037 | type ClientSockPtr_Type is access all ClientSock_Type;
In the above, the port identifies the service by number - used by a client to connect to the server and the argument svc the application the implements the service. The frame work expects an independent service to handle the initial connection request (procedure ServiceConnection) and subsequently every message received (procedure Message). Exactly how the message is handled is irrelevant to the server task.
The service itself is not too germane to the server framework except that it should support the handling of messages received from the client. Simply stated, the service is described as an Interface that meets some criteria:
~/bin/codemd ../../toolkit/adalib/src/server.ads -x Interface -l
0013 | type ServiceType is interface;
0014 | type ServicePtr_Type is access all ServiceType'Class;
0015 |
0016 | -- Services that can be provided by a server
0017 | procedure ServiceConnection
0018 | (svc : in out ServiceType; Sock : GS.Socket_Type;
0019 | From : GS.Sock_Addr_Type) is null;
0020 | procedure Message
0021 | (svc : in out ServiceType; msgbytes : AS.Stream_Element_Array;
0022 | Sock : GS.Socket_Type; From : GS.Sock_Addr_Type) is null;
Thus the implementation relies on a singleton task to await connection requests:
~/bin/codemd ../../toolkit/adalib/src/server.adb -x Server -l
0039 | loop
0040 | GS.Accept_Socket
0041 | (mysocket, clientsocket, clientaddr, 5.0, Status => accstatus);
0042 | if accstatus = GS.Completed then
0043 | pragma Debug (Put_Line ("Received a connection"));
0044 | sockclient := new ClientSock_Type;
0045 | sockclient.Serve (handler, clientsocket, clientaddr);
0046 | end if;
0047 | end loop;
As shown above, the server receives handles the connection request by creating a distinct task as a proxy for each client to handle the requests:
~/bin/codemd ../../toolkit/adalib/src/server.adb -x Clientproxy -l
0069 | -- Let the handler do its thing
0070 | myhandler.ServiceConnection (mysock, myaddr);
0071 | loop
0072 | HeaderType'Read (GS.Stream (mysock), msglen);
0073 | AS.Stream_Element_Array'Read
0074 | (GS.Stream (mysock),
0075 | buffer (1 .. AS.Stream_Element_Offset (msglen)));
0076 | myhandler.Message
0077 | (buffer (1 .. AS.Stream_Element_Offset (msglen)), mysock, myaddr);
0078 | end loop;
The proxy itself uses the Interface specification to invoke the service. The service is now very simple, just handling the message as required without having to worry about networking aspects.
A simple service that simply echoes the input message except converting the payload into a hexadecimal dump can be:
~/bin/codemd ../../toolkit/adalib/src/server.adb -x Debugserv -l
0086 | procedure ServiceConnection
0087 | (svc : in out DebugService; Sock : GS.Socket_Type;
0088 | From : GS.Sock_Addr_Type)
0089 | is
0090 | begin
0091 | Put (GSI.Source_Location);
0092 | Put (" > New Connection from ");
0093 | svc.cn := To_Unbounded_String (GS.Image (From));
0094 | Put (To_String (svc.cn));
0095 | New_Line;
0096 | end ServiceConnection;
0097 |
0098 | procedure Message
0099 | (svc : in out DebugService; msgbytes : AS.Stream_Element_Array;
0100 | Sock : GS.Socket_Type; From : GS.Sock_Addr_Type)
0101 | is
0102 | resp : aliased constant String :=
0103 | Hex.Image (msgbytes'Address, msgbytes'Length);
0104 | begin
0105 | Put (To_String (svc.cn));
0106 | Put (" sent > Message of length ");
0107 | Put (msgbytes'Length'Image);
0108 | New_Line;
0109 | Send (Sock, resp);
0110 | end Message;
With the framework taking care of much of the network details, the clients can focus on the application. But first, the framework is wrapped into an executable with the wrapper code:
~/bin/codemd ../../toolkit/adalib/src/server.adb -x Debugserv -l
0086 | procedure ServiceConnection
0087 | (svc : in out DebugService; Sock : GS.Socket_Type;
0088 | From : GS.Sock_Addr_Type)
0089 | is
0090 | begin
0091 | Put (GSI.Source_Location);
0092 | Put (" > New Connection from ");
0093 | svc.cn := To_Unbounded_String (GS.Image (From));
0094 | Put (To_String (svc.cn));
0095 | New_Line;
0096 | end ServiceConnection;
0097 |
0098 | procedure Message
0099 | (svc : in out DebugService; msgbytes : AS.Stream_Element_Array;
0100 | Sock : GS.Socket_Type; From : GS.Sock_Addr_Type)
0101 | is
0102 | resp : aliased constant String :=
0103 | Hex.Image (msgbytes'Address, msgbytes'Length);
0104 | begin
0105 | Put (To_String (svc.cn));
0106 | Put (" sent > Message of length ");
0107 | Put (msgbytes'Length'Image);
0108 | New_Line;
0109 | Send (Sock, resp);
0110 | end Message;
and a simple connection to this server can be made by a client as:
~/bin/codemd ../../toolkit/examples/clserv/src/clserv.adb -x DClient -l
0042 | procedure T2 is
0043 | pl : Long_Float;
0044 | begin
0045 | Put_Line (GSI.Enclosing_Entity);
0046 | for i in 1 .. 20 loop
0047 | pl := Long_Float (i);
0048 | server.Send (sock, pl'Address, pl'Size / 8);
0049 | declare
0050 | resp : AS.Stream_Element_Array := server.Receive (sock);
0051 | resps : String (1 .. resp'Length);
0052 | for resps'Address use resp'Address;
0053 | begin
0054 | Put (resps);
0055 | New_Line;
0056 | end;
0057 | end loop;
0058 | end T2;
Which produces the following output when run:
~/bin/clserv t2
Clserv clserv.adb Jan 08 2025 22:40:37
Clserv.T2
000000000000f03f
0000000000000040
0000000000000840
0000000000001040
0000000000001440
0000000000001840
0000000000001c40
0000000000002040
0000000000002240
0000000000002440
0000000000002640
0000000000002840
0000000000002a40
0000000000002c40
0000000000002e40
0000000000003040
0000000000003140
0000000000003240
0000000000003340
0000000000003440
Clserv.T1
01000000
02000000
03000000
04000000
05000000
06000000
07000000
08000000
09000000
0a000000
the latter half being from a similar Integer exchange with the server. Note that both hex dumps indicate that the lowest order byte travels first which is a characteristic of little endian storage as opposed to big endian storage where the highest order octet would have traveled first.
The server framework inserts the payload length in the data stream; the main purpose being the ability to mark the message boundaries. Perhaps other conventions can be used such as in http. Modify the framework to enable building a webserver.
Combine the logging framework and the server framework to realize a log server for a distributed application.
Develop a file transfer utility similar to ftp utilizing the framework. This tool will use the same channel ie the stream to transfer the request and the payload ie the file itself - unlike the ftp.
Repository: toolkit
Directory: examples/clserv
Directory:examples/diffint