I have a webserver made in Delphi which is responsible to fetch data from MySQL server and retrieve formatted in JSON. Here is a simple example of how it fetches the list of loteamentos from the DB.
type
TWM = class(TWebModule)
...
procedure WMactLoteamentosAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
...
procedure TWM.WMactLoteamentosAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
qryLoteamentos: TFDQuery;
JsonArray: TJSONArray;
JsonObject: TJSONObject;
begin
Response.ContentType := APPLICATION_JSON + '; ' + CHARSET_UTF8;
// Search for loteamentos
qryLoteamentos := TFDQuery.Create(nil);
with qryLoteamentos do
begin
Connection := FDConnection;
Active := False;
SQL.Clear;
Open('SELECT * FROM ' + T_LOTEAMENTO);
if qryLoteamentos.RecordCount > 0 then
begin
JsonArray := TJSONArray.Create;
try
First;
while not Eof do
begin
JsonObject := TJSONObject.Create;
CapturarCamposLoteamento(JsonObject, qryLoteamentos);
JsonArray.AddElement(JsonObject);
Next;
end;
finally
Response.Content := JsonArray.ToString;
JsonArray.DisposeOf;
end;
end
else
handleEmptyResponse(Response);
end;
end;
The logic of the method doesn't matter too much, it just matters that it fetch a table from the database and retrieve it in JSON.
The application will be running in a machine, the MySQL will be from this machine's localhost and the user will access the webserver by an external IP and the port.
Therefore, if the server is running on port 9070 in a machine which external IP is for example 45.65.89.187
The method will be called in the following way:
GET -> http://45.65.89.187/loteamentos
It will retrieve for me something like this:
[{"id":1,"nome":"RESIDENCIAL ...","metros":"348516,57"},
{"id":2,"nome":"RESIDENCIAL ...","metros":"215465,65"}]
Questions
My question is, suppose 100 people are using my API on their phone. Imagine 100 people calling this same endpoint /loteamentos multiple times. Wouldn't it crash the server?
I wonder that people calling the same endpoint at the same time doesn't create a line in the same Thread and disturb the server? Shouldn't I put the webserver to run in MultiThreading?
What I've done
I tested calling the endpoints from the webserver multiple times in 4 phones. The webserver start running at 2MB, after multiple calls it goes up to 40MB in a couple of minutes. Then, I stop calling it, but it keeps on 40MB and does not get lower.
A WebBroker application will create the first instance of TWebModule when the first request from a client comes in.
Whenever a 2nd HTTP request of a client arrives at the WebBroker application, the WebBroker framework will search if the previously created WebModule instance is idle (idle = it is not executing a request action handler).
If there are no idle instances of WebModule then a new TWebModule will be instantiated.
The code for that is in Web.WebReq.pas, function TWebRequestHandler.ActivateWebModules: TComponent;
By default a WebBroker application will create up to 32 TWebModule instances when the load is high. That number 32 is defined by the property Application.MaxConnections.
Be aware that simultanious requests are already multithreaded, and all the code in your request handlers must be thread safe.
A single TWebModule instance will only serve 1 request at a time, any other concurrent requests will be served by other instances of your TWebModule.
With the possibility of having multiple TWebModule instances serving requests in parallel, the queries in a single TWebModule instance should use their own dedicated DB connection instance.
While testing the load handling you could add a long Sleep(10000) to have many busy web request handlers, and check how your application responds. It's then easy to hit the limit of Application.MaxConnections, resulting in an exception.
Your webserver might consume 40MB because the WebBroker framework has created for instance 10 TWebModule instances when the load was at it's peak (Application.InactiveConnections + Application.ActiveConnections = 10). If your TWebModule allocates objects in it's constructor, or if it has many components in it's DFM then they will all persist.
Be also aware that any client-specific data should not reside in the TWebModule itself when the request is finished. Client A might be served by TWebModule instance 1 during the first request, and by instance 2 serves Client B simultaniously.
In the next simultanious request, Client A might be served by instance 2 and Client B by instance 1.
Am using plain webBroker under Delphi 10.1
I have a server that accepts a Json String. I parse it and send the same data back to the client.
My assumption is Webbroker by default has 32 threads that will be created progressively as concurrent requests from client start hitting the server and stop at 32 (refer web.webreq.pas)
To test this scenario I have created a simple Client program that has a For-loop that will keep firing at the the server with a request that will carry a JSon String.
For 10,000 requests it takes about 14 seconds. Only ONE instance of the webModule gets created...which is fine because the for loop sends the request in a serial and SYNCHRONOUS mode.
When I run another parallel instance of the client program, the 2nd instance of the WebModule gets created....fair enough. When I run one more parallel instance of the client the, 3rd instance of the WebModule gets created....so on and so forth
Now is the interesting part....
Remember the time taken for 10K request which is 14 seconds when only my first Client program was running? I am observing the following - As I increase the number of concurrent clients, the time taken to process also increases It actually takes 42 seconds to complete (30K requests in total) when 3 Client programs fire requests concurrently.
If the Server was truly Multi-threaded as claimed, then at least up to 32 concurrent client requests the time taken should be the SAME to process individual 10K records from each client, right?
Can you pls clarify if Web Broker is truly Multi-threaded? And if so what is it I am missing?
I have attached both the Client and the Server source here.
**Client Code follows
enter code here
unit URestclient;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, REST.Types, REST.Client,
Data.Bind.Components, Data.Bind.ObjectScope, Vcl.StdCtrls, JsonTools, system.DateUtils;
type
TForm21 = class(TForm)
RESTClient1: TRESTClient;
RESTRequest1: TRESTRequest;
RESTResponse1: TRESTResponse;
Button1: TButton;
Memo1: TMemo;
Label2: TLabel;
Edit1: TEdit;
Edit2: TEdit;
Label1: TLabel;
Label3: TLabel;
Label4: TLabel;
Edit3: TEdit;
procedure Button1Click(Sender: TObject);
private
procedure AfterRun;
function SendJSonDataToIVR(MachineID, JS: String): boolean;
{ Private declarations }
public
{ Public declarations }
end;
var
Form21: TForm21;
RESTClient: TRESTClient;
RESTRequest: TRESTRequest;
RESTResponse: TRESTResponse;
N: TJSONnode;
JS : String;
i : integer;
implementation
{$R *.dfm}
procedure TForm21.AfterRun;
begin
Memo1.Lines.Add(RESTResponse.content);
end;
procedure TForm21.Button1Click(Sender: TObject);
var
st, et : TDatetime;
S : String;
begin
Label2.caption := 'Started!';
Label2.Repaint;
memo1.Clear;
JS := '{"CallStatus":"t","CallType":"N","BookingType":"I","CallerType":"C","RoomNo":"1788882",'
+'"Channel":"1191","OriginalChannel":"1191","MobileNumber":"09123456789","DialTry":"1","MaxTry":"1"'
+',"ServerID":"3","StartTime":"2020-10-13 10:41:40","EndTime":"2020-10-13 10:41:40",'
+'"TransferNumber":"","Dstatus":"","userid":"1"}';
S := 'http://' + trim(edit2.text) + ':' + trim(edit1.text) + '/tconnected';
st := now;
for i := 0 to strtoint(trim(Edit3.Text)) do
SendJSonDataToIVR(S, JS);
et := now;
Label2.Caption := 'Time Taken (in ms) ' + MilliSecondsBetween(et, st).ToString;
end;
Function TForm21.SendJSonDataToIVR(MachineID : String; JS : String) : boolean;
begin
SendJSonDataToIVR := false;
RESTClient := TRESTClient.Create('nil');
RESTRequest := TRESTRequest.Create(nil);
RESTRequest.Client := RESTClient;
RESTClient.BaseURL := MachineID;
RESTRequest.ClearBody;
RESTRequest.AddBody(JS, ctAPPLICATION_JSON);
RESTRequest.Method := TRESTRequestMethod.rmPost;
RESTResponse := TRESTResponse.Create(nil);
RESTRequest.Response := RESTResponse;
RESTRequest.Execute();
//Memo1.Lines.Add(RESTResponse.content);
// RESTRequest.ExecuteAsync(AfterRun, true, True);
end;
end.
**Server Code follows
unit wmTConnected;
interface
uses
System.SysUtils, System.Classes, Web.HTTPApp, JSontools,
Data.DB,
FireDAC.Stan.Def,
FireDAC.Phys.PG,
FireDAC.Phys.PGDef,
FireDAC.DApt,
FireDAC.Stan.Async,
FireDAC.Stan.Option,
FireDAC.Comp.Client, FireDAC.Stan.Intf,
FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Pool,
FireDAC.Phys, FireDAC.ConsoleUI.Wait;
type
TWebModule2 = class(TWebModule)
procedure WebModule2TConnectedAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
private
{ Private declarations }
procedure ParseJson(var Request: TWebRequest);
public
{ Public declarations }
end;
var
WebModuleClass: TComponentClass = TWebModule2;
N : TJsonNode;
implementation
{%CLASSGROUP 'System.Classes.TPersistent'}
{$R *.dfm}
procedure TWebModule2.ParseJson(var Request: TWebRequest);
begin
N := TJsonNode.Create;
try
N.Parse(Request.Content);
except
begin
Response.Content := 'Something went wrong during parsing of Incoming Json from Client machine';
end;
end;//try
end;
procedure TWebModule2.WebModule2TConnectedAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
ParseJson(Request);
writeln(Timetostr(now));
Response.Content := Request.Content;
end;
end.
Unit JsonTools follows
type
TJsonNode = class
public
{ A parent node owns all children. Only destroy a node if it has no parent.
To destroy a child node use Delete or Clear methods instead. }
destructor Destroy; override;
{ GetEnumerator adds 'for ... in' statement support }
function GetEnumerator: TJsonNodeEnumerator;
{ Loading and saving methods }
procedure LoadFromStream(Stream: TStream);
procedure SaveToStream(Stream: TStream);
procedure LoadFromFile(const FileName: string);
procedure SaveToFile(const FileName: string);
{ Convert a json string into a value or a collection of nodes. If the
current node is root then the json must be an array or object. }
procedure Parse(const Json: string);
{ The same as Parse, but returns true if no exception is caught }
function TryParse(const Json: string): Boolean;
{ Add a child node by node kind. If the current node is an array then the
name parameter will be discarded. If the current node is not an array or
object the Add methods will convert the node to an object and discard
its current value.
Note: If the current node is an object then adding an existing name will
overwrite the matching child node instead of adding. }
function Add(const Name: string; K: TJsonNodeKind = nkObject): TJsonNode; overload;
function Add(const Name: string; B: Boolean): TJsonNode; overload;
function Add(const Name: string; const N: Double): TJsonNode; overload;
function Add(const Name: string; const S: string): TJsonNode; overload;
{ Delete a child node by index or name }
procedure Delete(Index: Integer); overload;
procedure Delete(const Name: string); overload;
{ Remove all child nodes }
procedure Clear;
{ Get a child node by index. EJsonException is raised if node is not an
array or object or if the index is out of bounds.
See also: Count }
function Child(Index: Integer): TJsonNode; overload;
{ Get a child node by name. If no node is found nil will be returned. }
function Child(const Name: string): TJsonNode; overload;
{ Search for a node using a path string }
function Find(const Path: string): TJsonNode;
{ Format the node and all its children as json }
function ToString: string; override;
{ Root node is read only. A node the root when it has no parent. }
property Root: TJsonNode read GetRoot;
{ Parent node is read only }
property Parent: TJsonNode read FParent;
{ Kind can also be changed using the As methods:
Note: Changes to Kind cause Value to be reset to a default value. }
property Kind: TJsonNodeKind read FKind write SetKind;
{ Name is unique within the scope }
property Name: string read GetName write SetName;
{ Value of the node in json e.g. '[]', '"hello\nworld!"', 'true', or '1.23e2' }
property Value: string read GetValue write Parse;
{ The number of child nodes. If node is not an object or array this
property will return 0. }
property Count: Integer read GetCount;
{ AsJson is the more efficient version of Value. Text returned from AsJson
is the most compact representation of the node in json form.
Note: If you are writing a services to transmit or receive json data then
use AsJson. If you want friendly human readable text use Value. }
property AsJson: string read GetAsJson write Parse;
{ Convert the node to an array }
property AsArray: TJsonNode read GetAsArray;
{ Convert the node to an object }
property AsObject: TJsonNode read GetAsObject;
{ Convert the node to null }
property AsNull: TJsonNode read GetAsNull;
{ Convert the node to a bool }
property AsBoolean: Boolean read GetAsBoolean write SetAsBoolean;
{ Convert the node to a string }
property AsString: string read GetAsString write SetAsString;
{ Convert the node to a number }
property AsNumber: Double read GetAsNumber write SetAsNumber;
end;
Related
I have a Delphi (Windows) application created using Delphi 10 that has some blocking calls that I would like to convert to using threads. Unfortunately for me, these are not procedures, but functions. (And information on how to return function results from threads appears to be much more limited.) I am trying to familiarize myself with the OmniThreadLibrary, since it seems to be the most flexible and best supported threading library for Delphi, but I'm having trouble with understanding how to do this.
I have been able to get the various OmniThreadLibrary routines to work well with procedures, but when I try to set up a function, I get an error about capturing the result. When I use OmniThreadLibrary's Future example as a starting point, I can get the function to work, but I can't figure out how to connect to the event monitor, how to send messages from the task, etc. So, it seems as if I'm overlooking something no matter which way I try to solve this problem.
Currently, my program does something like this:
If myPing(IPAddress) then
begin
//Do other things hereā¦
end;
Because myPing is blocking, and I actually need it to wait until myPing returns true before processing further, the application gets sluggish during this process. I'd like to put the myPing call in a thread, which would solve the sluggishness problem, but I can't figure out how to do that in the form of a function using OmniThreadLibrary. (Unless I use a future, in which case I can't figure out how to connect to the Event Monitor.)
Edit 1: Since my original post, I have made a little progress. I was able to connect the Event Monitor to the Future by adding Parallel.TaskConfig.MonitorWith(Form1.OmniEventMonitor1) to my code, right after the function. However, I still can't figure out how to send messages to that event monitor from within the Future function.
Edit 2: I now have some sample code. My first attempt was similar to this:
function myPing(HostName: string): IOmniFuture<boolean>;
begin
Result := Parallel.Future<boolean>(function: boolean
begin
Result := False;
//Do actual ping here... Set Result := True if successful.
end
);
end;
The basic function worked, but did not allow me to send any messages to the TOmniEventMonitor. I was able to get that part working by changing the code to this:
function myPing(HostName: string): IOmniFuture<boolean>;
begin
Result := Parallel.Future<boolean>(function: boolean
begin
Result := False;
//Do actual ping here... Set Result := True if successful.
end,
Parallel.TaskConfig.MonitorWith(Form1.OmniEventMonitor1)
);
end;
Now, I can successfully monitor the OnTaskTerminated event, but I still can't send messages to the Event Monitor from the task. By changing my code once again, I can access the task itself and send messages using task.Comm.Send(), but the messages don't reach the EventMonitor:
function myPing(HostName: string): IOmniFuture<boolean>;
begin
Result := Parallel.Future<boolean>(function(const task: IOmniTask): boolean
begin
Result := False;
//Do actual ping here... Set Result := True if successful.
task.Comm.Send(0,'Test 1');
end,
Parallel.TaskConfig.MonitorWith(Form1.OmniEventMonitor1)
);
end;
Here's a simple example on how to retrieve the function result from the async call. It does not use an "OmniEventMonitor" but instead calls a function once the async call returns ("Ping" is defined in PingU.pas, but not of importance here):
unit MainFormU;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Menus;
type
TPingResultEvent = procedure (const bResult: Boolean) of object;
TOnTerminateTestForm = class(TForm)
LogMemo: TMemo;
MainMenu: TMainMenu;
PingMenu: TMenuItem;
procedure PingMenuClick(Sender: TObject);
private
procedure BackgroundPing (const sServer: String;
const OnResult: TPingResultEvent);
procedure PingResult (const bResult: Boolean);
public
{ Public declarations }
end;
var
OnTerminateTestForm: TOnTerminateTestForm;
implementation
{$R *.dfm}
uses PingU, OtlParallel, OtlTaskControl;
procedure TOnTerminateTestForm.PingMenuClick (Sender: TObject);
var
sServer : String;
begin
if (InputQuery ('Ping computer', 'Computer name:', sServer)) then
if (sServer <> '') then
begin
PingMenu.Enabled := false;
LogMemo.Lines.Add (Format ('Pinging %s',[sServer]));
BackgroundPing (sServer, PingResult);
end; { if }
end; { TOnTerminateTestForm.PingMenuClick }
procedure TOnTerminateTestForm.BackgroundPing (const sServer: String;
const OnResult: TPingResultEvent);
var
bResult : Boolean;
begin
Parallel.Async (
procedure
begin
bResult := Ping (sServer);
end,
Parallel.TaskConfig.OnTerminated(
procedure (const task: IOmniTaskControl)
begin
// executed in main thread after the async has finished
if Assigned (OnResult) then
OnResult (bResult);
end
)
);
end; { TOnTerminateTestForm.BackgroundPing }
procedure TOnTerminateTestForm.PingResult (const bResult: Boolean);
begin
PingMenu.Enabled := true;
LogMemo.Lines.Add ('Ping result = ' + BoolToStr (bResult, true));
end; { TOnTerminateTestForm.PingResult }
end.
Code source: Get a function result asynchronously in Delphi using Omni Thread Library
I'm fairly new to IPC(interprocess communication). Doing my research, I decided on the Named pipes.
My application consists of 2 parts: a monitoring app, and a UI dashboard. The dashboard receives updates from the monitor constantly and shows stats, the user should be able to change certain parameters of the monitor through the dashboard (refresh rate, restart process,scheduled task...) so it has to be a bidirectional communication. The Monitor would later become a service application, but that's a plan for later.
Getting to the point, I used the Pipes unit by Overbyte(Francois Piette) v1.01
and followed the example made on their website. When connecting the pipes on the main thread, it functions normally. But due to my monitor needing a separate thread to (monitor-send message-sleep-loop...), when I try to connect on the other thread, I get an error "The notify window and the component window do not exist in the same thread!"
What I need to know is, can named pipes communicate using a separate thread? (Judging by the error msg, I feel it may be fetching the window handle on the main UI and would not work on a different thread)
Is there a better way to implement my application? or named pipes better than Overbyte?
Sample of my code:
TThreadMonitor = Class(TThread)
private
PipeClient1: TPipeClient;
listOfProcesses: Array of String;
protected
procedure Execute; override;
public
constructor Create(CreateSuspended: Boolean);
procedure ConnectPipe;
procedure SendMessage(const Msg: String);
End;
var
t: TThreadMonitor;
procedure TClientFormMain.BtnDifferentThreadClick(Sender: TObject);
begin
t := TThreadMonitor.Create(TRUE);
t.FreeOnTerminate := TRUE;
t.Start;
end;
procedure TThreadMonitor.ConnectPipe;
begin
if not PipeClient1.Connect(2000, TRUE) then
LogThis('Pipe connection failed', LogFilePath, TRUE)
else
LogThis('Pipe connected', LogFilePath, TRUE);
end;
procedure TThreadMonitor.Execute;
begin
inherited;
ConnectPipe; //<---throws exception here although getting "Pipe Connected"
while not Terminated do
begin
for i := 0 to Length(listOfProcesses) - 1 do
begin
...
MonitorProcess(listOfProcesses[i]);
...
end;
sleep(2000);
end;
end;
I am trying to call a function from another unit/class which would take some time in performing the task and would return a string value. I couldn't find a good reference something similar to C# async-await like simple approach in Delphi. Using Omni Thread library seems a good idea for me.
A simple example will be a great start for me.
Sample approach:
procedure TForm1.button1Click(Sender: TObject);
begin
// notify before starting the task
memo1.Lines.Add('calling a asynchronous function..');
// call to the function that takes some time and returns a string value
memo1.Lines.Add(GetMagicString);
// notify that the task has been completed
memo1.Lines.Add('Results fetched successfully.');
end;
Here, the function GetMagicString should process the result asynchronously. Once the result is obtained, only then the program should notify that the task has been completed. By the way, I'm using Delphi-XE.
Edit1:
Here is what I tried. But I am still unable to figure out the proper way to make the job done. The problem is how to return the value.
.....
private
ResultValue: IOmniFuture<string>;
.........
.....
function TForm1.FutureGet: string;
begin
Sleep(3000);
Result := 'my sample magic string response ' + IntToStr(Random(9999));
end;
procedure TForm1.FutureGetTerminated;
begin
// This code fired when the task is completed
memo1.Lines.Add(ResultValue.Value);
end;
function TForm1.GetMagicString: string;
begin
ResultValue := Parallel.Future<string>(FutureGet,
Parallel.TaskConfig.OnTerminated(FutureGetTerminated));
end;
Here, using Result := ResultValue.Value feezes the UI.
Edit2
I made changes as per the answer provided.
MainForm Code:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Unit2;
type
TForm1 = class(TForm)
memo1: TMemo;
button1: TButton;
procedure button1Click(Sender: TObject);
private
FOnStringReceived: TMyEvent;
procedure StringReceived(const AValue: string);
property OnStringReceived: TMyEvent read FOnStringReceived write FOnStringReceived;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.button1Click(Sender: TObject);
var
MyObject: TMyClass;
begin
// notify before starting the task
memo1.Lines.Add('calling a asynchronous function..');
// call to the function that takes some time and returns a string value
MyObject := TMyClass.Create;
OnStringReceived := StringReceived;
try
MyObject.GetMagicStringInBackground(OnStringReceived);
finally
MyObject.Free;
end;
end;
procedure TForm1.StringReceived(const AValue: string);
begin
memo1.Lines.Add(AValue);
// notify that the task has been completed
memo1.Lines.Add('Results fetched successfully.');
end;
end.
Other Unit Code:
unit Unit2;
interface
uses SysUtils, OtlTask, OtlParallel, OtlTaskControl;
type
TMyEvent = procedure(const aValue: string) of object;
type
TMyClass = class
private
FOnStringReceived: TMyEvent;
function GetMagicString: string;
public
procedure GetMagicStringInBackground(AEvent: TMyEvent);
end;
implementation
{ TMyClass }
function TMyClass.GetMagicString: string;
begin
Sleep(3000);
Result := 'my sample magic string response ' + IntToStr(Random(9999));
end;
procedure TMyClass.GetMagicStringInBackground(AEvent: TMyEvent);
var
theFunctionResult: string;
begin
Parallel.Async(
procedure
begin
theFunctionResult := GetMagicString;
end,
Parallel.TaskConfig.OnTerminated(
procedure (const task: IOmniTaskControl)
begin
if Assigned(AEvent) then
AEvent(theFunctionResult);
end)
);
end;
end.
Yes, the code works as expected. I just want to know if this is the best way of doing what I really want to perform.
You would normally use a future in a case where you want something executed in the background but still need the result in the same execution path. It basically lets you do something in the background while doing another thing in the main thread and you can then use the result of the background thread.
What you need to use is the Async abstraction that TLama linked to:
In your case it would be:
procedure TForm1.DoSomething;
var
theFunctionResult: string;
begin
memo1.Lines.Add('calling a asynchronous function..');
Parallel.Async(
procedure
begin
// executed in background thread
theFunctionResult := GetMagicString;
end,
procedure
begin
// executed in main thread after the async has finished
memo1.Lines.Add(theFunctionResult);
// notify that the task has been completed
memo1.Lines.Add('Results fetched successfully.');
end
);
end;
This is a bit messy but you should get the idea. You need to make sure that your async code is completed before you destroy the form that owns this code (TForm1).
If you want to try and setup a system that will call an event when the code completes then you can do something like this:
type
TMyEvent = procedure(const aValue: string) of object;
procedure GetMagicStringInBackground(AEvent: TMyEvent);
var
theFunctionResult: string;
begin
Parallel.Async(
procedure
begin
// executed in background thread
theFunctionResult := GetMagicString;
end,
Parallel.TaskConfig.OnTerminated(
procedure (const task: IOmniTaskControl)
begin
// executed in main thread after the async has finished
if Assigned(AEvent) then
AEvent(theFunctionResult );
end
)
);
end;
You can then put the threaded code in the GetMagicString unit and just call the method above from your form passing in an event that will get called when it completes.
The code:
var
WinHttpReq: OleVariant;
procedure TForm1.Button1Click(Sender: TObject);
begin
WinHttpReq := CreateOleObject('WinHttp.WinHttpRequest.5.1');
WinHttpReq.Open('GET', 'http://stackoverflow.com', TRUE); // asynchronously
WinHttpReq.setRequestHeader('User-Agent', 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0');
WinHttpReq.Send();
// HOW to set a callback procedure here and get the response?
end;
Note: I do not want to import mshttp.dll and use TLB. I want to use it via late binding. I also would like to handle exceptions if any.
EDIT:
I'm accepting TLama's answer becouse it gives me a good alternative to what I initially was asking. plus it has a good example source.
Here is a very nice implementation of WinHTTPRequest Wrapper with IConnectionPoint for Events (source code is attached).
As Stijn said in his answer, to prevent your program to lag, use the threads. IWinHttpRequest.Open has the asynchronous configuration capability too but it would be very difficult to catch the events and IWinHttpRequest.WaitForResponse would stuck your program even so.
Here is the simple example of how to get the response text into the form's memo box.
Please note that the following example uses the synchronous mode and that you can additionally modify the timeout values using IWinHttpRequest.SetTimeouts. If you want to use the asynchronous mode as you have in your question then you'll have to wait for the result with IWinHttpRequest.WaitForResponse method.
///////////////////////////////////////////////////////////////////////////////
///// WinHttpRequest threading demo unit //////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
unit WinHttpRequestUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ActiveX, ComObj, StdCtrls;
type
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
///////////////////////////////////////////////////////////////////////////////
///// THTTPRequest - TThread descendant for single request ////////////////
///////////////////////////////////////////////////////////////////////////////
type
THTTPRequest = class(TThread)
private
FRequestURL: string;
FResponseText: string;
procedure Execute; override;
procedure SynchronizeResult;
public
constructor Create(const RequestURL: string);
destructor Destroy; override;
end;
///////////////////////////////////////////////////////////////////////////////
///// THTTPRequest.Create - thread constructor ////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// RequestURL - the requested URL
constructor THTTPRequest.Create(const RequestURL: string);
begin
// create and start the thread after create
inherited Create(False);
// free the thread after THTTPRequest.Execute returns
FreeOnTerminate := True;
// store the passed parameter into the field for future use
FRequestURL := RequestURL;
end;
///////////////////////////////////////////////////////////////////////////////
///// THTTPRequest.Destroy - thread destructor ////////////////////////////
///////////////////////////////////////////////////////////////////////////////
destructor THTTPRequest.Destroy;
begin
inherited;
end;
///////////////////////////////////////////////////////////////////////////////
///// THTTPRequest.Execute - thread body //////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
procedure THTTPRequest.Execute;
var
Request: OleVariant;
begin
// COM library initialization for the current thread
CoInitialize(nil);
try
// create the WinHttpRequest object instance
Request := CreateOleObject('WinHttp.WinHttpRequest.5.1');
// open HTTP connection with GET method in synchronous mode
Request.Open('GET', FRequestURL, False);
// set the User-Agent header value
Request.SetRequestHeader('User-Agent', 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0');
// sends the HTTP request to the server, the Send method does not return
// until WinHTTP completely receives the response (synchronous mode)
Request.Send;
// store the response into the field for synchronization
FResponseText := Request.ResponseText;
// execute the SynchronizeResult method within the main thread context
Synchronize(SynchronizeResult);
finally
// release the WinHttpRequest object instance
Request := Unassigned;
// uninitialize COM library with all resources
CoUninitialize;
end;
end;
///////////////////////////////////////////////////////////////////////////////
///// THTTPRequest.SynchronizeResult - synchronization method /////////////
///////////////////////////////////////////////////////////////////////////////
procedure THTTPRequest.SynchronizeResult;
begin
// because of calling this method through Synchronize it is safe to access
// the VCL controls from the main thread here, so let's fill the memo text
// with the HTTP response stored before
Form1.Memo1.Lines.Text := FResponseText;
end;
///////////////////////////////////////////////////////////////////////////////
///// TForm1.Button1Click - button click event ////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// Sender - object which invoked the event
procedure TForm1.Button1Click(Sender: TObject);
begin
// because the thread will be destroyed immediately after the Execute method
// finishes (it's because FreeOnTerminate is set to True) and because we are
// not reading any values from the thread (it fills the memo box with the
// response for us in SynchronizeResult method) we don't need to store its
// object instance anywhere as well as we don't need to care about freeing it
THTTPRequest.Create('http://stackoverflow.com');
end;
end.
IWinHttpRequest is quite primitive.
Caveat with the Async mode specified in Open()!
If you think that you can download a big file using an IStream that is returned by get_ResponseStream() and write the data back to a file in small chunks as it arrives, you are wrong.
No matter if you use Sync or Async mode: IWinHttpRequest always loads the entire server response into memory and get_ResponseStream() returns E_PENDING until the ENTIRE download has been stored in memory.
This interface has been designed only for small files.
I would suggest you learn about the TThread object. Create a new class that inherits from TThread, override the Execute method, call CoInitialize (to enable COM) and perform the WinHTTPRequest code. When the request is done, use Synchronize to pass the result back to the foreground thread. Also you should be able to catch exceptions in a try/except clause in the Execute method.
Another option is switching to the IXMLHTTPRequest object, which has an async boolean property. Catching events with late-binding may be pretty difficult, but you could check the state property at regular intervals.
I am using Delphi 2007 and threads.
My problem (sorry, i'll try to explain better):
1) I created a file "utilities.pas" where i have the function i use more.
2) I created a new program, in this program i have one thread
3) in the execute method of the thread i call one function in my file "utilities.pas".
this function connects to an ftp using clever components (tclftp). This components logs the server responce in a dedicated event. What i would like to do is to save the log in a stringlist and then send the stringlist back to the calling thread.
This is part of the file "utilities.pas":
// I created TEventHandlers because it's the only way to assign the event runtime
// without having a class
type
TEventHandlers = class
procedure clFtp1SendCommand(Sender: TObject; const AText: string);
end;
var EvHandler: TEventHandlers;
// this is the porcedure called from the thread. i want to send the stringlist
// back to it containing the ftp log
procedure Test(VAR slMain: tStringlist);
var cFTP: TclFtp;
begin
cFTP := TclFtp.Create(nil);
cFTP.Server := 'XXX';
cFTP.UserName := 'XXX';
cFTP.Password := 'XXX';
cFTP.OnSendCommand := EvHandler.clFtp1SendCommand;
// i connect to the ftp
cFTP.Open;
FreeAndNil(cFTP);
end;
procedure TEventHandlers.clFtp1SendCommand(Sender: TObject; const AText: string);
begin
// here the component (cftp) sends me back the answer from the server.
// i am logging it
// HERE IT'S THE PROBLEM:
// I can't reach slMain from here.....
slmain.add(Atext);
end;
this is the calling thread:
procedure TCalcThread.Execute;
var slMain: tstringlist;
begin
inherited;
slmain := tstringlist.create(nil);
Test(slmain);
if slMain.count > 0 then
slMain.savetofile('c:\a.txt');
// i won't free the list box now, but in the thread terminated.
end;
this is the main program:
procedure TfMain.ThreadTerminated(Sender: TObject);
Var ExThread: TCalcThread;
begin
ExThread := (Sender as TCalcThread);
if ExThread.slMain.Count > 0 then
ExThread.slMain.SaveToFile('LOG\Errori.log');
freeandnil(slMain);
end;
Please can anybody help me in solving this? I really don't know what to do.
I hope now it more clear.
p.s. thanks for all the answer..
Another approach would be to have your thread object have its own instance of the stringlist and its own cFTP. If you need to have one "master thread" that everything writes to (perhaps for a summary of what each thread accomplished), use this class:
TThreadStringList by Tilo Eckert
http://www.swissdelphicenter.ch/torry/showcode.php?id=2167
I think one (BAD) approach would be to create a pool of components in the main thread or at design time, and assign one to each thread. i.e. 5 instances of cFTP, 5 stringlists, 5 threads.
Update: Martin James points out why this is a terrible idea, and I agree. So don't do this. Post stays as a deterrent.
Intercept the event within the thread class, and fire an own typed event from within that handler. Synchronize this call! And try to prevent the global variable. All this as follows:
type
TFtpSendCommandEvent = procedure(Mail: TStrings; const AText: String) of object;
TMyThread = class(TThread)
private
FclFtp: TclFtp;
FslMail: TStrings;
FOnFtpSendCommand: TFtpSendCommandEvent;
FText: String;
procedure clFtpSendCommand(Sender: TObject; const AText: String);
procedure DoFtpSendCommand;
protected
procedure Execute; override;
public
// You could add this property as parameter to the constructor to prevent the
// need to assign it separately
property OnFtpSendCommand: TFtpSendCommandEvent read FOnFtpSendCommand
write FOnFtpSendCommand;
end;
// If you dont want to make this a property or private field of the thread class:
var
EvHandler: TFtpSendCommandEvent;
{ TMyThread }
procedure TMyThread.clFtpSendCommand(Sender: TObject; const AText: string);
begin
// Store the AText parameter temporarily in a private field: Synchronize only
// takes a parameterless method
FText := AText;
Synchronize(DoFtpSendCommand);
end;
procedure TMyThread.DoFtpSendCommand;
begin
if Assigned(FOnFtpSendCommand) then
FOnFtpSendCommand(FslMail, FText);
// Or, if you really like to use that global variable:
if Assigned(EvHandler) then
EvHandler(FslMail, FText);
end;
procedure TMyThread.Execute;
begin
...
FclFtp := TclFtp.Create(nil);
FslMail := TStringList.Create(nil);
try
FclFtp.Server := 'XXX';
FclFtp.UserName := 'XXX';
FclFtp.Password := 'XXX';
FclFtp.OnSendCommand := clFtpSendCommand;
FclFtp.Open;
finally
FreeAndNil(FclFtp);
FreeAndNil(FslMail);
end;
...
end;