Indy10, 400ms for a simple TCP/IP request and response - performance

I can't figure out why a simple request and response is taking 400 ms to complete. It only needs under 1 ms to complete on localhost (loopback). When I make a request from my virtual machine to my main development machine it takes 400 ms to complete. It should take max 40 ms. This is how much it takes max for a HTTP request, so TCP should be faster. Here is the code for client and server. I just can't see where I loose time. I can profile if you need more info.
The code is Indy 9 and 10 compatible that is why the IFDEF-s. Also the connection is already established, it takes 400 ms without the connect part, only data send and response.
function TIMCClient.ExecuteConnectedRequest(const Request: IMessageData): IMessageData;
var
DataLength: Int64;
FullDataSize: Int64;
IDAsBytes: TIdBytes;
IDAsString: ustring;
begin
Result := AcquireIMCData;
FAnswerValid := False;
with FTCPClient{$IFNDEF Indy9}.IOHandler{$ENDIF} do
begin
Request.Data.Storage.Seek(0, soFromBeginning);
DataLength := Length(Request.ID) * SizeOf(uchar);
FullDataSize := DataLength + Request.Data.Storage.Size + 2 * SizeOf(Int64);
SetLength(IDAsBytes, DataLength);
Move(Request.ID[1], IDAsBytes[0], DataLength);
// write data
{$IFDEF Indy9}WriteInteger{$ELSE}Write{$ENDIF}(FullDataSize);
{$IFDEF Indy9}WriteInteger{$ELSE}Write{$ENDIF}(DataLength);
{$IFDEF Indy9}WriteBuffer{$ELSE}Write{$ENDIF}(IDAsBytes{$IFDEF Indy9}[0]{$ENDIF}, DataLength);
{$IFDEF Indy9}WriteInteger{$ELSE}Write{$ENDIF}(Request.Data.Storage.Size);
{$IFDEF Indy9}WriteStream{$ELSE}Write{$ENDIF}(Request.Data.Storage);
// set the read timeout
ReadTimeout := FExecuteTimeout;
FullDataSize := ReadInt(FTCPClient);
// read the message ID
SetLength(IDAsBytes, 0);
DataLength := ReadInt(FTCPClient);
ReadBuff(FTCPClient, DataLength, IDAsBytes);
if DataLength > 0 then
begin
SetLength(IDAsString, DataLength div SizeOf(uchar));
Move(IDAsBytes[0], IDAsString[1], DataLength);
Result.ID := IDAsString;
end;
// read the message data
DataLength := ReadInt(FTCPClient);
ReadStream(Result.Data.Storage, DataLength, False);
Result.Data.Storage.Seek(0, soFromBeginning);
// we were succesfull
FAnswerValid := True;
end;
end;
The server side:
procedure TIMCServer.OnServerExecute(AContext: TIMCContext);
var
Request: IMessageData;
Response: IMessageData;
DataLength: Int64;
FullDataSize: Int64;
IDAsBytes: TIdBytes;
IDAsString: ustring;
begin
with AContext.Connection{$IFNDEF Indy9}.IOHandler{$ENDIF} do
begin
ReadTimeout := FExecuteTimeout;
//read the data length of the comming response
FullDataSize := ReadInt(AContext.Connection);
// Acquire the data objects
Request := AcquireIMCData;
Response := AcquireIMCData;
// read the message ID
DataLength := ReadInt(AContext.Connection);
ReadBuff(AContext.Connection, DataLength, IDAsBytes);
if DataLength > 0 then
begin
SetLength(IDAsString, DataLength div SizeOf(uchar));
Move(IDAsBytes[0], IDAsString[1], DataLength);
Request.ID := IDAsString;
end;
// read the message data
DataLength := ReadInt(AContext.Connection);
ReadStream(Request.Data.Storage, DataLength, False);
Request.Data.Storage.Seek(0, soFromBeginning);
try
// execute the actual request handler
FOnExecuteRequest(Request, Response);
finally
// write the data stream to TCP
Response.Data.Storage.Seek(0, soFromBeginning);
DataLength := Length(Response.ID) * SizeOf(uchar);
FullDataSize := DataLength + Response.Data.Storage.Size + 2 * SizeOf(Int64);
// write ID as binary data
SetLength(IDAsBytes, DataLength);
Move(Response.ID[1], IDAsBytes[0], DataLength);
// write data
{$IFDEF Indy9}WriteInteger{$ELSE}Write{$ENDIF}(FullDataSize);
{$IFDEF Indy9}WriteInteger{$ELSE}Write{$ENDIF}(DataLength);
{$IFDEF Indy9}WriteBuffer{$ELSE}Write{$ENDIF}(IDAsBytes{$IFDEF Indy9}[0]{$ENDIF}, DataLength);
{$IFDEF Indy9}WriteInteger{$ELSE}Write{$ENDIF}(Response.Data.Storage.Size);
{$IFDEF Indy9}WriteStream{$ELSE}Write{$ENDIF}(Response.Data.Storage);
end;
end;
The same slow communication was reported by one of the users of my code. He also tested from a virtual machine to a physical machine.
UPDATE:
The following code executes in 2-3 ms between same two machines. Its Indy10, smallest possible case.
procedure TForm2.Button1Click(Sender: TObject);
var
MyVar: Int64;
begin
TCPClient.Host := Edit1.Text;
TCPClient.Port := StrToInt(Edit2.Text);
TCPClient.Connect;
try
stopwatch := TStopWatch.StartNew;
MyVar := 10;
TCPClient.IOHandler.Write(MyVar);
TCPClient.IOHandler.ReadInt64;
stopwatch.Stop;
Caption := IntToStr(stopwatch.ElapsedMilliseconds) + ' ms';
finally
TCPClient.Disconnect;
end;
end;
procedure TForm2.TCPServerExecute(AContext: TIdContext);
var
MyVar: Int64;
begin
if AContext.Connection.IOHandler.InputBuffer.Size > 0 then
begin
MyVar := 10;
AContext.Connection.IOHandler.ReadInt64;
AContext.Connection.IOHandler.Write(MyVar);
end;
end;

Solved the problem. It was easy when you find out what to do and where the problem is. Indy simply did not sent my data straight away. I had to add
OpenWriteBuffer
CloseWriteBuffer
calls to make Indy send the data when I want it. A lot of trouble over a simple misunderstanding of internal workings. Maybe this will spare someone some time.
When the buffer is closed, the data is sent immediately!

Related

compressing TIdMemoryBufferStream with TIdCompressorZLib

i am asking about the possibility of compressing TIdMemoryBufferStream Using TIdCompressorZLib how can it be done properly using the following code
procedure TClientThread.SendBuffer(Buffer: TIdBytes; BufferSize: Cardinal);
Var
Strm: TIdMemoryBufferStream;
IdCompressorZLib : TIdCompressorZLib;
begin
Strm := TIdMemoryBufferStream.Create(PByte(Buffer), BufferSize);
IdCompressorZLib := TIdCompressorZLib.Create(nil);
try
// then can't figure what the right process to do
FTCP.Socket.WriteLn('Stream');
FTCP.Socket.LargeStream := True;
FTCP.Socket.Write(Strm, 0, True);
finally
FreeAndNil(Strm);
FreeAndNil(IdCompressorZLib);
end;
end;
i am not sure about the right process that needs to be done
as example should i create another variable as StrmB then calling the compress to it ?
The TIdCompressorZLib component is intended to be used only with the Compressor property of the TIdHTTP and TIdFTP components.
For general purpose compression over a TCP connection, you can assign a TIdCompressionIntercept component to the TCP connection's IOHandler.Intercept property, eg:
var
Compressor : TIdCompressionIntercept;
begin
Compressor := TIdCompressionIntercept.Create(nil);
try
Compressor.CompressionLevel := 9;
FTCP.Socket.Intercept := Compressor;
try
// any data written here will be compressed....
finally
FTCP.Socket.Intercept := nil;
end;
finally
Compressor.Free;
end;
end;
However, since you are sending the byte size of the compressed data (by setting the AWriteByteCount parameter to True in TIdIOHandler.Write(TStream)), that will not work with the compression intercept.
Indy's IdZLib unit has a TCompressionStream class, and various ...CompressStream/Ex() functions, that you can use instead, eg:
procedure TClientThread.SendBuffer(Buffer: TIdBytes; BufferSize: Cardinal);
var
Strm: TMemoryStream;
Compressor : TCompressionStream;
begin
Strm := TMemoryStream.Create;
try
Compressor := TCompressionStream.Create(clMax, Strm, False);
try
WriteTIdBytesToStream(Compressor, Buffer, BufferSize);
finally
Compressor.Free;
end;
FTCP.Socket.WriteLn('Stream');
FTCP.Socket.LargeStream := True;
FTCP.Socket.Write(Strm, 0, True);
finally
Strm.Free;
end;
end;
Or:
procedure TClientThread.SendBuffer(Buffer: TIdBytes; BufferSize: Cardinal);
var
InStrm: TIdMemoryBufferStream;
OutStrm: TMemoryStream;
begin
OutStrm := TMemoryStream.Create;
try
InStrm := TIdMemoryBufferStream.Create(PByte(Buffer), BufferSize);
try
CompressStreamEx(InStrm, OutStrm, clMax, zsZLib);
finally
InStrm.Free;
end;
FTCP.Socket.WriteLn('Stream');
FTCP.Socket.LargeStream := True;
FTCP.Socket.Write(OutStrm, 0, True);
finally
OutStrm.Free;
end;
end;

Delphi :Transferring Image

Delphi 2010
I'm Transferring Image via custom TCP Socket control uses UTF-8
Client Side
var
TSS: TStringStream;
STR :String;
JPG:TJPEGImage;
BMP:TBitmap;
begin
Try
BMP.LoadFromFile('C:\1.bmp');
JPG.Assign(BMP);
JPG.CompressionQuality:=80;
JPG.Compress;
TSS:=TStringStream.Create;
JPG.SaveToStream(TSS);
STR:=TSS.DataString;
MyTCPSocket.SendString(STR);
finally
BMP.free;
JPG.free;
TSS.free;
end;
end;
Server Side
Var
TSS: TStringStream;
TSS:=TStringStream.Create;
TSS.WriteString(STR);
TSS.SaveToFile('C:\2.jpg');
This code working on the same pc great.
The problem when I send the image to other pc that uses different encoding
it receive the image but I see many wrong characters in the data "?????"
I think when TStringStream writes the bytes to the file it fails to convert the unicode characters to bytes so it appears like "?"
Any idea is much appreciated
You are trying to send binary data as if it were UTF-8 encoded text. It is not, so do not try to do that! Send the binary data in its original binary form, eg:
var
MS: TMemoryStream;
JPG: TJPEGImage;
BMP: TBitmap;
begin
MS := TMemoryStream.Create;
try
JPG := TJPEGImage.Create;
try
BMP := TBitmap.Create;
try
BMP.LoadFromFile('C:\1.bmp');
JPG.Assign(BMP);
finally
BMP.Free;
end;
JPG.CompressionQuality := 80;
JPG.Compress;
JPG.SaveToStream(MS);
finally
JPG.Free;
end;
MS.Position := 0;
MyTCPSocket.SendStream(MS);
finally
MS.free;
end;
end;
var
MS: TMemoryStream;
begin
MS := TMemoryStream.Create;
try
MyTCPSocket.ReadStream(MS);
MS.Position := 0;
MS.SaveToFile('C:\2.jpg');
finally
MS.Free;
end;
end;
If you must send binary data as text, you need to encode the data using a real binary-to-text encoding algorithm, such as base64 or yEnc, not UTF-8 (which is designed for only encoding Unicode text, not binary data), eg:
// TIdEncoderMIME and TIdDecoderMIME are part of Indy,
// which ships with Delphi, but you can use whatever
// you want...
uses
..., IdCoderMIME;
var
S: String;
MS: TMemoryStream;
JPG: TJPEGImage;
BMP: TBitmap;
begin
MS := TMemoryStream.Create;
try
JPG := TJPEGImage.Create;
try
BMP := TBitmap.Create;
try
BMP.LoadFromFile('C:\1.bmp');
JPG.Assign(BMP);
finally
BMP.Free;
end;
JPG.CompressionQuality := 80;
JPG.Compress;
JPG.SaveToStream(MS);
finally
JPG.Free;
end;
MS.Position := 0;
S := TIdEncoderMIME.EncodeStream(MS);
finally
MS.free;
end;
MyTCPSocket.SendString(S);
end;
uses
..., IdCoderMIME;
var
S: string;
MS: TMemoryStream;
begin
S := MyTCPSocket.ReadString;
MS := TMemoryStream.Create;
try
TIdDecoderMIME.DecodeStream(S, MS);
MS.Position := 0;
MS.SaveToFile('C:\2.jpg');
finally
MS.Free;
end;
end;

Delphi: Sending multiple strings through sockets?

I am currently using delphi 6 (yes, I know.. but its so far getting the job done.)
I am using Serversocket, and client socket. When I have my client connect to my server, I would like to have it send some info, like computername, lAN IP, OS name, ping.
At the moment I only have the client sending the computer name to the server, I am wondering how can I send multiple information, and set it up accordingly in my grid? here is the source code:
Client:
unit client1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ScktComp;
type
TForm1 = class(TForm)
Client1: TClientSocket;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure Client1Connect(Sender: TObject; Socket: TCustomWinSocket);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function Getusernamefromwindows: string;
var
iLen: Cardinal;
begin
iLen := 256;
Result := StringOfChar(#0, iLen);
GetUserName(PChar(Result), iLen);
SetLength(Result, iLen);
end;
function Getcomputernamefromwindows: string;
var
iLen: Cardinal;
begin
iLen := MAX_COMPUTERNAME_LENGTH + 1;
Result := StringOfChar(#0, iLen);
GetComputerName(PChar(Result), iLen);
SetLength(Result, iLen);
end;
function osver: string;
begin
result := 'Unknown';
case Win32MajorVersion of
4:
case Win32MinorVersion of
0: result := 'windows 95';
10: result := 'Windows 98';
90: result := 'Windows ME';
end;
5:
case Win32MinorVersion of
0: result := 'windows 2000';
1: result := 'Windows XP';
end;
6:
case Win32MinorVersion of
0: result := 'Windows Vista';
1: result := 'Windows 7';
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Client1.Host := '192.168.1.106';
Client1.Active := true;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Client1.Active := false;
end;
procedure TForm1.Client1Connect(Sender: TObject; Socket: TCustomWinSocket);
begin
Client1.Socket.SendText(Getcomputernamefromwindows + '/' + Getusernamefromwindows);
(*Upon connection to server, I would like it send the os name, but as you
can see I already have SendText being used*)
end;
end.
Server:
unit server1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, ScktComp, Grids, DBGrids;
type
TForm1 = class(TForm)
Server1: TServerSocket;
PageControl1: TPageControl;
TabSheet1: TTabSheet;
TabSheet2: TTabSheet;
TabSheet3: TTabSheet;
TabSheet4: TTabSheet;
StringGrid1: TStringGrid;
procedure FormCreate(Sender: TObject);
procedure Server1ClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
With StringGrid1 do begin
Cells[0,0] := 'Username';
Cells[1,0] := 'IP Address';
Cells[2,0] := 'Operating System';
Cells[3,0] := 'Ping';
end;
end;
(* cells [0,0][1,0][2,0][3,0]
are not to be changed for
these are used to put the
titles in
*)
procedure TForm1.Server1ClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
var
begin
with StringGrid1 do begin
Cells[0,1] := Socket.ReceiveText;
Cells[1,1] := Server1.Socket.Connections[0].RemoteAddress;
(*in this area I want it to receive the os version info and place it in
Cells[2,1]*)
end;
end;
end.
You already know the answer, because you are already doing it. Send the various strings with delimiters in between them (in your example, you are using /, but you could also use CRLFs, string length prefixes, etc), and then some final delimiter to signal the end of the data.
The real problem with your code is the use of SendText() and ReceiveText(). SendText() is not guaranteed to send the entire string in one go. It returns how many bytes it actually sent. If fewer than your string length, you have to call SendText() again to send the remaining bytes. As for ReceiveText(), it just returns whatever arbitrary data is on the socket at that moment, which may be an incomplete string, or multiple strings merged together.
You are using lower-level I/O methods without first designing a higher level protocol to describe the data being sent. You need a protocol. Design how you want your data to look, then format your data that way and use the methods to send/receive that data, then break apart the data as needed when received.
So, in this case, you could send a single CRLF-delimited string that contains /-delimited values. The server would then read until it reaches the CRLF, then split the line and use the values according.

Firemonkey OSX update progressbar during download

The progressbar being updated is shown in Windows. In OSX the progressbar is shown but without see the progressbar shifting.
See code below.
procedure TForm1.Button1Click(Sender: TObject);
var
m : TMemoryStream;
begin
IdHTTP1.OnWork:= HttpWork;
m := TMemoryStream.Create;
IdHTTP1.Get('http://www.example.com/pictures.zip', m);
m.SaveToFile('/users/demo/pictures.zip');
m.Free;
end;
procedure TForm1.HttpWork(ASender: TObject; AWorkMode: TWorkMode;
AWorkCount: Int64);
var
Http: TIdHTTP;
ContentLength: Int64;
Percent: Integer;
begin
Http := TIdHTTP(ASender);
ContentLength := Http.Response.ContentLength;
Percent := 100 * AWorkCount div ContentLength;
ProgressBar1.Value := Percent;
end;
How can the progressbar be updated in OSX?
Here's what I have in my code, and it works on both Windows and Mac:
Percent := 100 * AWorkCount div ContentLength;
frmDownloadProgress.ProgressBar1.Value := Percent;
Application.ProcessMessages;
Application.ProcessMessages is the key for updating the progress bar if the TIdHTTP component is on the main thread.

Downloading a file in Delphi

A google search shows a few examples on how to download a file in Delphi but most are buggy and half of the time don't work in my experience.
I'm looking for a simple robust solution which will let me download a single exe (for updating my app) and will hold the execution of the current update thread until the download is done or errors out. The process is already threaded so the download code should hold execution until it's done (hopefully).
Here's two implementations, both seem very complicated
1. http://www.scalabium.com/faq/dct0116.htm
2. http://delphi.about.com/od/internetintranet/a/get_file_net.htm
Why not make use of Indy? If you use the TIdHTTP component, it's simple:
procedure TMyForm.DownloadFile;
var
IdHTTP1: TIdHTTP;
Stream: TMemoryStream;
Url, FileName: String;
begin
Url := 'http://www.rejbrand.se';
Filename := 'download.htm';
IdHTTP1 := TIdHTTP.Create(Self);
Stream := TMemoryStream.Create;
try
IdHTTP1.Get(Url, Stream);
Stream.SaveToFile(FileName);
finally
Stream.Free;
IdHTTP1.Free;
end;
end;
You can even add a progress bar by using the OnWork and OnWorkBegin events:
procedure TMyForm.IdHTTPWorkBegin(ASender: TObject; AWorkMode: TWorkMode;AWorkCountMax: Int64);
begin
ProgressBar.Max := AWorkCountMax;
ProgressBar.Position := 0;
end;
procedure TMyForm.IdHTTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
begin
ProgressBar.Position := AWorkCount;
end;
procedure TMyForm.DownloadFile;
var
IdHTTP1: TIdHTTP;
Stream: TMemoryStream;
Url, FileName: String;
begin
Url := 'http://www.rejbrand.se';
Filename := 'download.htm';
IdHTTP1 := TIdHTTP.Create(Self);
Stream := TMemoryStream.Create;
try
IdHTTP1.OnWorkBegin := IdHTTPWorkBegin;
IdHTTP1.OnWork := IdHTTPWork;
IdHTTP1.Get(Url, Stream);
Stream.SaveToFile(FileName);
finally
Stream.Free;
IdHTTP1.Free;
end;
end;
I'm not sure if these events fire in the context of the main thread, so any updates done to VCL components may have to be done using the TIdNotify component to avoid threading issues. Maybe someone else can check that.
The second approach is the standard way of using Internet resources using WinINet, a part of Windows API. I have used it a lot, and it has always worked well. The first approach I have never tried. (Neither is "very complicated". There will always be a few additional steps when using the Windows API.)
If you want a very simple method, you could simply call UrlMon.URLDownloadToFile. You will not get any fine control (at all!) about the download, but it is very simple.
Example:
URLDownloadToFile(nil,
'http://www.rejbrand.se',
PChar(ExtractFilePath(Application.ExeName) + 'download.htm'),
0,
nil);
For people that has later version of delphi, you can use this:
var
http : TNetHTTPClient;
url : string;
stream: TMemoryStream;
begin
http := TNetHTTPClient.Create(nil);
stream := TMemoryStream.Create;
try
url := YOUR_URL_TO_DOWNLOAD;
http.Get(url, stream);
stream.SaveToFile('D:\Temporary\1.zip');
finally
stream.Free;
http.Free;
end;
end;
Using URLMon.
errcode := URLMon.URLDownloadToFile(nil,
PChar('http://www.vbforums.com/showthread.php?345726-DELPHI-Download-Files'),
PChar( 'a:\download.htm'),
0,
nil);
if errcode > 0 then
showmessage('Error while downloading: ' + inttostr(errcode));

Resources