Jump to content
Clément

Studying TSslHttpAppSrv

Recommended Posts

Hi,

 

I'm using TSslHttpAppSrv in a Delphi Rio project. I'm following the example and I manage to build my AppServer. Very cool!
There are some points that I would like to understand better and maybe improve a little.

 

I create a "TcoreWebServerThread" derived from TThread that creates an instance of TcoreWebServer.
TcoreWebServer is a descendant of TSslHttpAppSrv.
I hooked event_ClientConnect to OnClientConnect.

 

procedure TcoreWebServer.event_ClientConnect(Sender, Client: TObject;
  Error: Word);
var
    lRemoteClient : TcoreWebServerClientConnection;
begin
    lRemoteClient                := TcoreWebServerClientConnection(Client);
    lRemoteClient.WSessionCookie := 'dhsWeb_' + lRemoteClient.HostTag+ '_'+  Port;

    lRemoteClient.OnBgException  := event_ClientBgException;
    fLogRequest[  lRemoteClient.WebLogIdx ].logFmt('Thread ID %d',[ThreadID]);

end;

 

This Connection is created in the context of coreServerThread. That same Thread will handle the Authorization, Permission and the URLHandler. It is too much for the server thread to handle, specially because Authorization (custom) and Permission will be checked against Database. Most of my TURLHandler descendants will also query a Database. Some will be slow ( Reports or Large data transfers). I must use a separate thread. And here's the problem. Where is the best place to create that thread?

I haven't found a way to create the client connection in another thread. In the event_ClientConnect I would like "client" parameter to be in its own thread. This way all Authorization, Permission and TURLHandler execution would happen without blocking the main WebServer thread.

 

Once I create the Client in a it's own thread context, I need to handle my Database Connection. I derived a class from TWebSessionData called ( TcoreWebServerWebSession ) :

  TcoreWebServerWebSession = Class( TWebSessionData )
  private
    FLoginChallenge: String;
    FUserCode: String;
    FLastRequest: TDateTime;
    FRequestCount: Integer;
    FLogonTime: TDateTime;
    FIP: String;
    FSQLConnection: IcoreSQLConnection;
  public
    constructor Create( aOwner : TComponent ); override;
    property UserCode       : String     read FUserCode write FUserCode;
    property LogonTime      : TDateTime  read FLogonTime write FLogonTime;
    property RequestCount   : Integer    read FRequestCount write FRequestCount;
    property LastRequest    : TDateTime  read FLastRequest write FLastRequest;
    property IP             : String     read FIP write FIP;
    property LoginChallenge : String     read FLoginChallenge write FLoginChallenge;
    property SQLConnection  : IcoreSQLConnection read FSQLConnection;
  End;

The property SQLConnection is an interface, and it's life time will be different from the WebSession's. I would like to return the SQLConnection back to its pool as soon as the user request is done. ( All the database connection pooling is already done! :classic_wink: ). All the requests will return a JSON object.
So, basically I need to place the ClientConnection on a separate thread that will Acquired a Database Connection from a pool and assign it to coreWebServerWEbSession.SQLConnection. This connection will be used to Authorize, Check permission and execute TURLHandler. Once Finished I would like to release it back to it's pool. No bad for a Sunday brainstorm :classic_cool:
 

 

TIA,

Clément

 

Share this post


Link to post

You should create a worker thread for the lengthy database operations and keep your TCoreWebServer run in his own thread. Having the database thread will transform blocking database operation into non blocking one which will nicely fit into the ICS stuff. Having specialized database thread will also be easier to develop and debug.

 

Share this post


Link to post

Not sure why you want to run the server in a thread, that just listens on one or more ports and creates clients.  Running the client in a thread can be done, ICS has TWSocketThrdServer that does exactly that, but it  is rarely used, has not been updated for 10 years, lacks newer multi-listen and SSL stuff and there is no web server derived from it, lack of demand from end users.

 

My own TSslHttpAppSrv SQL driven web server runs happily in a single thread,  the SQL stored procedures rarely take very long so are not blocking others users significantly, obviously this depends on volume, I'm only handling couple of thousand SQL requests a day, from the logging, SQL proc took 16ms, 31ms, 141ms, 63ms, 390ms, 16ms.  So I never bothered with threads.

 

As Francois says, using a thread just for the database lookup and returning the page in the main thread is the safest solution, look at class TClientProcessingThread in the ICS FTP server which uses a thread to calculate MD5sums and directory listing,

 

Angus

 

 

Share this post


Link to post
15 hours ago, Angus Robertson said:

Not sure why you want to run the server in a thread, that just listens on one or more ports and creates clients. 

 

This server will run as a Windows Service. It may be a bad habit, but I don't like to share the service thread with my applications.

 

15 hours ago, Angus Robertson said:

Running the client in a thread can be done, ICS has TWSocketThrdServer that does exactly that, but it  is rarely used, has not been updated for 10 years, lacks newer multi-listen and SSL stuff and there is no web server derived from it, lack of demand from end users.

 

I will have a look. I need SocketThrdServer features in TsslHttpAppSrv. It makes no sense going back.

 

15 hours ago, Angus Robertson said:

As Francois says, using a thread just for the database lookup and returning the page in the main thread is the safest solution, look at class TClientProcessingThread in the ICS FTP server which uses a thread to calculate MD5sums and directory listing,

This is the tricky part. Some requests will be synchronous and some asynchronous. Synchronous request will have the database in the same thread as the client connection. This is the fast kind of request. Usually a result is expected from 1ms to less than 2s (in my case). The asynchronous request will release the client connection as soon as possible. It will trigger a process that sill handle longer requests and alert the users once it's completed.

Share this post


Link to post
17 hours ago, FPiette said:

You should create a worker thread for the lengthy database operations and keep your TCoreWebServer run in his own thread. Having the database thread will transform blocking database operation into non blocking one which will nicely fit into the ICS stuff. Having specialized database thread will also be easier to develop and debug.

 

Sure. My concern is the response to the request. I don't want the user to wait longer than (s)he needs to. 
 I will divide my request in Sync and Async calls. Sync calls should be very fast and it might work without a database thread (I will do some test as soon as I manage to create a thread for the client connection). Async calls on the other hand might take several hours. 

Share this post


Link to post

All Windows services run in their own thread already, TWSocketServer often runs in Windows services without needing extra threads.  Using a thread for some client responses and not for others is exactly what the FTP servers does, it is no difficult, just make sure the response is sent in the main thread once SQL is done.  

 

Bringing TWSocketThrdServer up to date and creating TsslHttpThrdSrv is a major project and needs someone to sponsor it.  My company effectively sponsors most ICS development since I create features mostly that are needed for our applications but which are then used by others without any cost.  But so far I've not needed TsslHttpThrdSrv.  It has been disscused in the past, it was going to be designed to handle x clients in a single thread before using another thread for x more, typically x would be 100 or more for a simple web server but could be one so each client gets a thread. 

 

Angus

 

 

Share this post


Link to post
On 10/15/2019 at 4:20 AM, Angus Robertson said:

All Windows services run in their own thread already, TWSocketServer often runs in Windows services without needing extra threads.  Using a thread for some client responses and not for others is exactly what the FTP servers does, it is no difficult, just make sure the response is sent in the main thread once SQL is done.  

 

I found a way to get the result I want. I just hope it is the right way.

I wrote a TUrlHandler descendant that spawn a thread. One of those parameters is the TUrlHandler instance. The code looks like this:

 

procedure TWSWebAPIHandler.Execute;
var
  lJsPostedData : TJsonObject;
  lPath         : String;
begin
  inherited;
  if IsLogged then begin
     lJsPostedData := TJsonObject.ParseJSONValue( Client.PostedData ) as TJSONObject ;
     lPath         := Client.Path;
     TWSWebServerAPIThread.Create( LPath, lJsPostedData, Self );

//     AnswerString('', '', '', '<HTML><BODY>WebAPI called - '+Client.Path+' </BODY></HTML>');
  end;
//  Finish;
end;

As you can see I commented the answer string and the finish. They are in the Thread implementation:

constructor TWSWebServerAPIThread.Create(const aPath: String;
  aPostedData: TJsonObject; aURLHandler: TcoreWebServerHandler);
begin
  FreeOnTerminate := True;
  fPostedData := aPostedData;
  fPath       := aPath;
  fURLHandler := aURLHandler;
  inherited create(False);
end;

procedure TWSWebServerAPIThread.Execute;
begin
  inherited;
  Sleep(9000);
  fURLHandler.AnswerString('','','','<HTML><BODY>In a thread for '+fPath+' with '+fPostedData.ToString+'</BODY></HTML>');
  fURLHandler.Finish;
end;

The Sleep(9000) is there just to simulate a long database operation. I have tested the calling from 3 different REST Client and every response came at 9s as expected. (Without that thread, the requests will return at 9s, 12s and 18s. Exactly what I'm trying to avoid).

 

This codes uses an instance of TUrlHandler because it seems to me that every request will get an instance of the corresponding TURLHandler. Is it thread safe to call it that way?

 

TIA,

Clément
 

Share this post


Link to post

Hi,

 

I have two questions:
1) In my implementation I see that fUrlHandler.Client messages are processed by TCoreWebServer message pump. Would that be a problem? I could instead pass TURLHandler's FWndHandle, but still would have to PostMessage that would be processed in TCoreWebServer thread :classic_huh:


2) Some database operation can take a few hours to complete. From the source code, TURLHandler is released after calling fUrlHandler.Finish. Would maintain a TUrhHandler instance live for a few hours be an issue?

 

TIA,

Clément

Share this post


Link to post

Your example is not thread safe, since AnswerString is being sent from within your own thread rather than the main thread where the client connection was opened. 

 

You should have AnswerDelayed := TRUE; in the handler, and then sent the Answer after the thread terminates. 

 

Web clients are unlikely to wait hours for a request to conclude, the web server has timeouts.

 

Angus

 

Share this post


Link to post
7 hours ago, Angus Robertson said:

Your example is not thread safe, since AnswerString is being sent from within your own thread rather than the main thread where the client connection was opened. 

 

7 hours ago, Angus Robertson said:

You should have AnswerDelayed := TRUE; in the handler, and then sent the Answer after the thread terminates. 

 

7 hours ago, Angus Robertson said:

Web clients are unlikely to wait hours for a request to conclude, the web server has timeouts.

 

Angus

 

Thanks for taking your time to answer!
I'm still studying the best course of action. All client connections will receive a response very fast. For synchronous request, I want to return an answer with a maximum delay of 2s. 
For Asynchronous request the response will be very fast, just the time to spawn a thread ( as you saw in the example).
The example is just a test, the final version will mostly look like:


 

procedure TWSWebAPIHandler.Execute;
var
  lJsPostedData : TJsonObject;
  lPath         : String;
begin
  inherited;
  if IsLogged then begin
     lJsPostedData := TJsonObject.ParseJSONValue( Client.PostedData ) as TJSONObject ;
     lPath         := Client.Path;
     TWSWebServerAPIThread.Create( LPath, lJsPostedData);

     AnswerString('', '', '', '<HTML><BODY>WebAPI called - '+Client.Path+' </BODY></HTML>');
  end
  else
    AnswerString('', '', '', '<HTML><BODY>Not logged </BODY></HTML>');
  Finish;
end;

I thought I might need access to TURLHandler from within the thread. But that's not the case anymore.

 

By the way, The user authentication is done very fast ( I'm already connecting to a database to get the credentials in the context of the TcoreWebServer). The performance is great.
I  need to polish some classes and interfaces to start the project. 
I will decorate my class definitions with attributes like [Operation(Sync)] or [Operation(Async)] to know if I need to spawn a thread or not in the API call layer.

 

Thanks,

Clément

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×