Selasa, 16 Desember 2025

Creating Simple Web Server Daemon with Lazarus

 


I use this blog to help me memories useful programming techniques that I discover from any source in the world and combined with my creativity. So that if I need it in the future I can read it here.

Web server is a program or sub program that provide data to client program not limited to web browser using HTTP or HTTPS (HTTP over SSL) protocol. HTTP is a simple, fast and robust protocol for client-server application. The HTTP protocol works by having the client send a request to the server, then the server processes the request and sends back a response to the client.

Example of HTTP Client's Request:

GET / HTTP/1.1 Host: www.example.com

The first line of a HTTP request consist of a command, a url and the protocol version. Followed by header lines and terminated by empty line. Each header line consist of header name and value separated by colon and one space. After the header data is the content data that contain n bytes where n is the value of Content-Length header.

Example of HTTP Server's Response:

HTTP/1.1 200 OK Date: Mon, 23 May 2005 22:38:34 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 155 Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) ETag: "3f80f-1b6-3e1cb03b" Accept-Ranges: bytes Connection: close <html> <head> <title>An Example Page</title> </head> <body> <p>Hello World, this is a very simple HTML document.</p> </body> </html>

The first line of a HTTP response consist of protocol version and result code followed by error message. Followed by header lines and terminated by empty line. Each header line consist of header name and value separated by colon and one space. After the header data is the content data that contain n bytes where n is the value of Content-Length header.

Free Pascal, Delphi and Lazarus share the same programming language the Object Pascal with little differentiation. So many Delphi component can be used in Lazarus and vice versa because it only requires minor changes in the source code to support both programming. If you don't want to use visual component you can use operating system API like winsock (Windows Socket) for Windows or unisock (Unix Socket) for Linux. I will use synapse for Free Pascal library in this example.

To help parsing input data I create TStringSplitter :

type
  { TStringSplitter }
  TStringSplitter = class(TObject)
  private
    FLine: String;
    FCol: Integer;
    procedure SetCol(AValue: Integer);
    procedure SetLine(AValue: String);
  public
    constructor Create;
    function Fetch(const ATerminator: String=''): String;
    property Line: String read FLine write SetLine;
    property Col: Integer read FCol write SetCol;
  end;

function PosAfter(const SubText, Text: String; StartAt: Integer): Integer;
var
  I, L1, L2: Integer;
begin
  Result := -1;
  L1 := Length(SubText);
  L2 := Length(Text);
  if ((L1 > L2) or (StartAt < 1)) then exit;
  for I := StartAt to L2-L1+1 do
    if (CompareMem(@SubText[1], @Text[I], L1)) then
    begin
      Result := I;
      exit;
    end;
end;

procedure TStringSplitter.SetCol(AValue: Integer);
begin
  FCol := AValue;
end;
 
procedure TStringSplitter.SetLine(AValue: String);
begin
  FLine := AValue;
  FCol := 1;
end;
 
constructor TStringSplitter.Create;
begin
  FLine := '';
  FCol := 1;
end;
 
function TStringSplitter.Fetch(const ATerminator: String): String;
var
  I, J: Integer;
begin
  Result := '';
  I := FCol;
  if (I > Length(FLine)) then exit;
  if (ATerminator <> '') then
  begin
    J := PosAfter(ATerminator, FLine, I);
    if (J > 0) then
    begin
      Result := Copy(FLine, I, J-I);
      FCol := J + Length(ATerminator);
      exit;
    end;
  end;
  Result := Copy(FLine, FCol, MaxInt);
  FCol := Length(FLine) + 1;
end;

When the server is started it initialize the socket to listen to the specified port. Then it repeatly check for incoming connection and start HTTP handler worker thread until the server is terminated. 

procedure TDaemon1.WorkServerListener(Thread: TWorkerThread);
var
  sock: TTCPBlockSocket;
  h: TSocket;
begin
  try
    InitPatchMan;
    sock := TTCPBlockSocket.Create;
    try
      sock.Bind('0.0.0.0',IntToStr(Port));
      if (sock.LastError <> 0) then
        raise ENetworkError.Create('Cannot bind to port '+IntToStr(Port));
      sock.Listen;
      if (sock.LastError <> 0) then
        raise ENetworkError.Create('Cannot listen to port '+IntToStr(Port));
      while (not Thread.Terminated) do
      begin
        if (sock.CanRead(0)) then
        begin
          h := sock.Accept;
          {$HINTS OFF}
          if (h <> INVALID_SOCKET) then
            WorkerThreads.AddNew(@WorkRequestHandler,Pointer(h));
          {$HINTS ON}
        end
        else
          Sleep(1);
      end;
      sock.CloseSocket;
    finally
      sock.Free;
    end;
  except
    on E: Exception do
      // DebugLog('HTTP Server terminated by error '+E.ClassName+': '+
      //   E.Message);
  end;
end;

The HTTP handler worker thread read HTTP request from incoming connection and send back HTTP Respond.

procedure TDaemon1.WorkRequestHandler(Thread: TWorkerThread);
var
  sock: TTCPBlockSocket;
  S1: TStringSplitter;
  s, cmd, url, http_ver, nm, vl, error_msg, respond_text, respond_data: AnsiString;
  I, result_code: Integer;
  close_socket, can_read: Boolean;
  headers, respond_headers: TStrings;
begin
  result_code := 500;
  error_msg := 'Internal Server Error';
  close_socket := True;
  sock := TTCPBlockSocket.Create;
  S1 := TStringSplitter.Create;
  headers := TStringList.Create;
  respond_headers := TStringList.Create;
  try
    {$HINTS OFF}
    sock.Socket := TSocket(Thread.Param);
    {$HINTS ON}
    repeat
      can_read := sock.CanRead(0);
      if (sock.LastError <> 0) then exit;
      if (can_read) then
      begin
        S1.Line := sock.RecvString(ReadTimeout);
        if (sock.LastError <> 0) then exit;
        cmd := S1.Fetch(' ');
        url := S1.Fetch(' ');
        http_ver := S1.Fetch;
        if ((cmd = 'GET') or (cmd = 'POST')) then
        begin
          close_socket := (http_ver <> 'HTTP/0.9') and (http_ver <> 'HTTP/1.0');
          S1.Line := sock.RecvString(ReadTimeout);
          if (sock.LastError <> 0) then exit;
          while (s <> '') do
          begin
            nm := S1.Fetch(': ');
            vl := S1.Fetch;
            headers.Values[nm] := vl;
          end;
          //
          // .. do some processing here
          //
          respond_data :=
            '<head><title>Welcome</title></head><body><h1>Welcom to my Web Server</h1><p>You are in '+HttpEncode(url)+'</body>';
          respond_headers.Values['Content-Type'] := 'html/text';
          respond_headers.Values['Content-Length'] := IntToStr(Length(respond_data));
          result_code := 200;
          error_msg := 'OK';
        end
        else begin
          result_code := 400;
          error_msg := 'Bad Request';
        end;
        respond_text :=
          http_ver + ' ' + IntToStr(result_code) + ' ' + error_msg+#13#10;
        for I := 0 to respond_headers.Count do
          respond_text := respond_text + respond_headers.Names[I]+': '+
            respond_headers.ValueFromIndex[I];
        respond_text := respond_text+#13#10+respond_data;
        sock.SendBuffer(@respond_text[1], Length(respond_text));
        headers.Clear;
        respond_headers.Clear;
        respond_text := '';
      end
      else
        Sleep(1);
    until (Thread.Terminated or close_socket);
  finally
    respond_headers.Free;
    headers.Free;
    S1.Free;
    sock.Free;
  end;
end;

You can send and receive data using easy to parse data like JSON, XML, BSON or binary data for data service, it is very flexible.

That's all for this article and I hope it's useful.  

Tidak ada komentar:

Creating Expert System for Database Application Development

  In the year 2016, my friend show me he create a desktop application using Delphi that he create very fast. I think I cannot compete with h...