I want to transfer data from the TIdTCPServer to the TIdTCPClient.
On the server side I have:
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var x:Integer;
Received:String;
SendBuff:TBytes;
hFile:THandle;
fSize:Int64;
begin
fSize:=0;
if MOpenFileForRead(hFile,MGetExePath+'\test.jpg') then begin
fSize:=MFileSize(hFile);
SetLength(SendBuff,fSize);
MReadFile(hFile,SendBuff[0],fSize);
MCloseFile(hFile);
end;
// ... here the SendBuff contains valid data, I checked.
repeat
Received:=AContext.Connection.Socket.ReadLn;
if not AContext.Connection.Connected then Exit;
if Received=CMD_TEST_FILE then begin
AContext.Connection.Socket.Write(fSize);
AContext.Connection.Socket.WriteBufferOpen;
AContext.Connection.Socket.Write(SendBuff);
AContext.Connection.Socket.WriteBufferClose;
end;
until False;
end;
And the client side:
procedure TForm1.Button2Click(Sender: TObject);
var fSize:Int64;
RecvBuff:TBytes;
hFile:THandle;
begin
IdTCPClient1.Socket.WriteLn(CMD_TEST_FILE);
fSize:=IdTCPClient1.Socket.ReadInt64;
SetLength(RecvBuff,fSize);
IdTCPClient1.Socket.ReadBytes(RecvBuff,fSize);
if MCreateFile(hFile, MGetExePath+'\new.jpg') then begin
MWriteFile(hFile,RecvBuff[0],fSize);
MCloseFile(hFile);
end;
Memo1.Lines.Add('ok');
end;
... but it's not working. I checked the read and write data functions used and they are ok. At the server the buffer is set ok, the file size arrives at client ok, but the content of the buffer at client is only zeros.
P.S: I want to send the file in this way not with stream or anything else.
If you look at the signature of ReadBytes(), it has an optional AAppend parameter that is True by default:
procedure ReadBytes(var VBuffer: TIdBytes; AByteCount: Integer; AAppend: Boolean = True); virtual;
When true, it reads bytes from the socket and appends them to the end of the existing byte array. Since you are pre-allocating the array, the initial bytes are undefined and the file bytes follow after the undefined bytes.
To fix this, you need to either:
Stop pre-allocating the byte array, let ReadBytes() allocate it for you.
procedure TForm1.Button2Click(Sender: TObject);
var
fSize: Int64;
RecvBuff: TBytes;
hFile: THandle;
begin
IdTCPClient1.Socket.WriteLn(CMD_TEST_FILE);
fSize := IdTCPClient1.Socket.ReadInt64;
// SetLength(RecvBuff,fSize); // <-- remove this line
IdTCPClient1.Socket.ReadBytes(RecvBuffer, fSize);
if MCreateFile(hFile, MGetExePath+'\new.jpg') then
begin
MWriteFile(haile, RecvBuff[0], fSize);
MCloseFile(hFile);
end;
Memo1.Lines.Add('ok');
end;
pre-allocate the array, but set AAppend to False so the bytes fill the existing array instead of append to it.
procedure TForm1.Button2Click(Sender: TObject);
var
fSize: Int64;
RecvBuff: TBytes;
hFile: THandle;
begin
IdTCPClient1.Socket.WriteLn(CMD_TEST_FILE);
fSize := IdTCPClient1.Socket.ReadInt64;
SetLength(RecvBuff, fSize);
IdTCPClient1.Socket.ReadBytes(RecvBuff, fSize, False);
if MCreateFile(hFile, MGetExePath+'\new.jpg') then
begin
MWriteFile(haile, RecvBuff[0], fSize);
MCloseFile(hFile);
end;
Memo1.Lines.Add('ok');
end;
Update: That being said, I strongly suggest you use a TStream instead, despite you saying you do not want to. It will greatly simplify the code and memory management, without breaking the communication protocol you have chosen to use:
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
begin
AContext.Data := TFileStream.Create(MGetExePath+'\test.jpg', fmOpenRead or fmShareDenyWrite);
AContext.Connection.IOHandler.LargeStream := True;
end;
TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Received: String;
begin
Received := AContext.Connection.IOHandler.ReadLn;
if Received = CMD_TEST_FILE then
begin
AContext.Connection.IOHandler.Write(TStream(AContext.Data), 0, True);
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
FileName: string;
Strm: TStream;
begin
FileName := MGetExePath+'\new.jpg';
Strm := TFileStream.Create(FileName, fmCreate);
try
try
IdTCPClient1.IOHandler.WriteLn
(CMD_TEST_FILE);
IdTCPClient1.IOHandler.ReadStream(Strm, -1, False);
finally
Strm.Free;
end;
except
DeleteFile(FileName);
raise;
end;
Memo1.Lines.Add('ok');
end;
Related
I am writing a client / server application. There is one server and several clients.
When connecting a client, the task is to add its IP address to the ListBox, and when disconnecting the client, remove it from the ListBox. Then exchange messages between the client and server.
Three questions arose: when a client connects, its IP address is added to the ListBox, but when disconnected, it is not deleted from there, here is the code:
type
TSimpleClient = class(TObject)
DNS,
Name : String;
ListLink : Integer;
Thread : Pointer;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient.Create;
Client.DNS := AContext.Connection.Socket.Binding.PeerIP;
Client.ListLink := ListBox1.Items.Count;
Client.Thread := AContext;
ListBox1.Items.Add(Client.DNS);
AContext.Data := Client;
Clients.Add(Client);
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
sleep(2000);
Client :=Pointer (AContext.Data);
Clients.Delete(Client.ListLink);
ListBox1.Items.Delete(ListBox1.Items.IndexOf(Client.DNS));
Client.Free;
AContext.Data := nil;
end;
The second question, when exchanging messages, the letters in Cyrillic are given as "???", all Google went through it and it was not possible to find an error.
And the third question, on the client is a timer that listens to messages from the server, when the timer is turned on, the client application hangs tight, putting all this into the stream is the same trouble, the code:
if not IdTCPClient1.Connected then
Exit;
s := IdTCPClient1.Socket.ReadLn;
if s <> '' then
Label1.Text := s;
I see quite a few problems with your code.
On the server side, you need to get rid of the TSimpleClient.ListLink field. You are misusing it, causing bad behaviors in your code since you don't keep it updated as clients are added/removed. Think of what happens when you have 2 clients connected, where ListLink is 0 and 1 respectively, and then the 1st client disconnects. The ListLink for the 2nd client will become invalid since you don't decrement it from 1 to 0.
Also TIdTCPServer is a multi-threaded component, its events are fired in the context of worker threads, but your event handler code is not thread-safe. You MUST synchronize with the main UI thread when accessing UI controls from worker threads, and you MUST protect your Clients list from concurrent access across thread boundaries. In this case, you don't really need your own Clients list to begin with as TIdTCPServer has its own thread-safe Contexts list that you can use to access the connected clients.
You are also not handling Unicode at all. By default, Indy's default byte encoding for Unicode strings is US-ASCII, which is why you are getting ? for non-ASCII characters. You can use the IOHandler's DefStringEncoding property to set a different byte encoding, such as IndyTextEncoding_UTF8 (if you are using Delphi 2007 or earlier, you might need to also use the IOHandler's DefAnsiEncoding property to specify how your ANSI strings are converted to/from Unicode. By default, it is set to IndyTextEncoding_OSDefault).
Try something more like this:
type
TSimpleClient = class(TObject)
DNS,
Name : String;
Thread : Pointer;
OutgoingMsgs : TIdThreadSafeStringList;
HasOutgoingMsgs : Boolean;
constructor Create;
destructor Destroy; override;
procedure Queue(const Msg: string);
procedure FlushMsgs;
end;
constructor TSimpleClient.Create;
begin
inherited;
OutgoingMsgs := TIdThreadSafeStringList.Create;
end;
destructor TSimpleClient.Destroy;
begin
OutgoingMsgs.Free;
inherited;
end;
procedure TSimpleClient.Queue(const Msg: string);
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
List.Add(Msg);
HasOutgoingMsgs := True;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TSimpleClient.FlushMsgs;
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
while List.Count > 0 do
begin
TIdContext(Thread).Connection.IOHandler.WriteLn(List[0]);
List.Delete(0);
end;
HasOutgoingMsgs := False;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
PeerIP: string;
Client: TSimpleClient;
begin
PeerIP := AContext.Binding.PeerIP;
Client := TSimpleClient.Create;
Client.DNS := PeerIP;
Client.Thread := AContext;
AContext.Data := Client;
TThread.Queue(nil,
procedure
begin
ListBox1.Items.AddObject(PeerIP, Client);
end
);
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
try
TThread.Queue(nil,
procedure
var
Index: Integer;
begin
Index := ListBox1.Items.IndexOfObject(Client);
if Index <> -1 then
ListBox1.Items.Delete(Index);
end;
);
finally
{ The anonymous procedure being passed to TThread.Queue() above captures
the Client variable itself, not its value. On ARC platforms, we need to
prevent Free() setting the variable to nil before it can be passed to
IndexOfObject(), and also because IndexOfObject() expects a live object
anyway. ARC will free the object when the anonymous procedure exits. On
non-ARC platforms, it is OK to Free() the object here, the variable will
not change value, and IndexOfObject() does not need a live object... }
{$IFNDEF AUTOREFCOUNT}
Client.Free;
{$ENDIF}
AContext.Data := nil;
end;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
if Client.HasOutgoingMsgs then
Client.FlushMsgs
else
Sleep(100);
end;
procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
List: TIdContextList;
begin
List := IdTCPServer1.Contexts.LockList;
try
if List.IndexOf(TIdContext(Client.Thread)) <> -1 then // still connected?
Client.Queue(Msg);
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Index: Integer;
Msg: string;
Client: TSimpleClient;
begin
Index := ListBox1.ItemIndex;
if Index = -1 then Exit;
Msg := Edit1.Text;
if Msg = '' then Exit;
Client := TSimpleClient(ListBox1.Items.Objects[Index]);
SendMessageToClient(Client, Msg);
end;
Alternatively, you can derive TSimpleClient from TIdServerContext and get rid of the Thread field altogether:
type
TSimpleClient = class(TIdServerContext)
DNS,
Name : String;
OutgoingMsgs : TIdThreadSafeStringList;
HasOutgoingMsgs : Boolean;
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override;
destructor Destroy; override;
procedure Queue(const Msg: string);
procedure FlushMsgs;
end;
constructor TSimpleClient.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil);
begin
inherited Create(AConnection, AYarn, AList);
OutgoingMsgs := TIdThreadSafeStringList.Create;
end;
destructor TSimpleClient.Destroy;
begin
OutgoingMsgs.Free;
inherited;
end;
procedure TSimpleClient.Queue(const Msg: string);
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
List.Add(Msg);
HasOutgoingMsgs := True;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TSimpleClient.FlushMsgs;
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
while List.Count > 0 do
begin
Self.Connection.IOHandler.WriteLn(List[0]);
List.Delete(0);
end;
HasOutgoingMsgs := False;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
IdTCPServer1.ContextClass := TSimpleClient;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
PeerIP: string;
Client: TSimpleClient;
begin
PeerIP := AContext.Binding.PeerIP;
Client := TSimpleClient(AContext);
Client.DNS := PeerIP;
TThread.Queue(nil,
procedure
begin
ListBox1.Items.AddObject(PeerIP, Client);
end
);
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
Client := TSimpleClient(AContext);
TThread.Queue(nil,
procedure
var
Index: Integer;
begin
Index := ListBox1.Items.IndexOfObject(Client);
if Index <> -1 then
ListBox1.Items.Delete(Index);
end;
);
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient(AContext);
if Client.HasOutgoingMsgs then
Client.FlushMsgs
else
Sleep(100);
end;
procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
List: TIdContextList;
begin
List := IdTCPServer1.Contexts.LockList;
try
if List.IndexOf(TIdContext(Client)) <> -1 then // still connected?
Client.Queue(Msg);
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Index: Integer;
Msg: string;
Client: TSimpleClient;
begin
Index := ListBox1.ItemIndex;
if Index = -1 then Exit;
Msg := Edit1.Text;
if Msg = '' then Exit;
Client := TSimpleClient(ListBox1.Items.Objects[Index]);
SendMessageToClient(Client, Msg);
end;
On the client side, you are reading from the socket in the main UI thread, but Indy uses blocking sockets, and so its reading methods will block the calling thread until the requested data arrives. DON'T block the main UI thread! Read only if there is actually something available to read, or else move the reading into a separate worker thread. For example:
IdTCPClient1.Connect;
IdTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
...
IdTCPClient1.Disconnect;
...
procedure TForm1.Timer1Timer(Sender: TObject);
var
s: string;
begin
if IdTCPClient1.Connected and (not IdTCPClient1.IOHandler.InputBufferIsEmpty) then
begin
s := IdTCPClient1.IOHandler.ReadLn;
if s <> '' then
Label1.Text := s;
end;
end;
Alternatively:
type
TReadingThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TReadingThread.Execute;
var
s: String;
begin
while not Terminated do
begin
s := Form1.IdTCPClient1.IOHandler.ReadLn;
if s <> '' then
begin
TThread.Queue(nil,
procedure
begin
Form1.Label1.Text := s;
end
);
end;
end;
end;
...
var
ReadingThread: TReadingThread = nil;
...
IdTCPClient1.Connect;
IdTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
ReadingThread := TReadingThread.Create(False);
...
ReadingThread.Terminate;
try
IdTCPClient1.Disconnect;
finally
ReadingThread.WaitFor;
ReadingThread.Free;
end;
Thank you so much Remy, your answer really helped me sort out my problem. I targeted Windows and Android platforms. I fixed your code a little and it worked for me:
type
TSimpleClient = class(TObject)
DNS,
Name : String;
Thread : Pointer;
OutgoingMsgs : TIdThreadSafeStringList;
HasOutgoingMsgs : Boolean;
constructor Create;
destructor Destroy; override;
procedure Queue(const Msg: string);
procedure FlushMsgs;
end;
constructor TSimpleClient.Create;
begin
inherited;
OutgoingMsgs := TIdThreadSafeStringList.Create;
end;
destructor TSimpleClient.Destroy;
begin
OutgoingMsgs.Free;
inherited;
end;
procedure TSimpleClient.Queue(const Msg: string);
var
List: TStringList;
Client: TSimpleClient;
begin
List := OutgoingMsgs.Lock;
try
List.Add(Msg);
HasOutgoingMsgs := True;
Client.FlushMsgs;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TSimpleClient.FlushMsgs;
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
while List.Count > 0 do
begin
TIdContext(Thread).Connection.IOHandler.WriteLn(List[0]);
List.Delete(0);
end;
HasOutgoingMsgs := False;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
PeerIP: string;
Client: TSimpleClient;
begin
PeerIP := AContext.Binding.PeerIP;
Client := TSimpleClient.Create;
Client.DNS := PeerIP;
Client.Thread := AContext;
AContext.Data := Client;
TThread.Queue(nil,
procedure
begin
ListBox1.Items.AddObject(PeerIP, Client);
end
);
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
try
TThread.Queue(nil,
procedure
var
Index: Integer;
begin
Index := ListBox1.Items.IndexOfObject(Client);
if Index <> -1 then
ListBox1.Items.Delete(Index);
end;
);
finally
{ The anonymous procedure being passed to TThread.Queue() above captures
the Client variable itself, not its value. On ARC platforms, we need to
prevent Free() setting the variable to nil before it can be passed to
IndexOfObject(), and also because IndexOfObject() expects a live object
anyway. ARC will free the object when the anonymous procedure exits. On
non-ARC platforms, it is OK to Free() the object here, the variable will
not change value, and IndexOfObject() does not need a live object... }
{$IFNDEF AUTOREFCOUNT}
Client.Free;
{$ENDIF}
AContext.Data := nil;
end;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
if Client.HasOutgoingMsgs then
Client.FlushMsgs
else
Sleep(100);
end;
procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
List: TIdContextList;
begin
List := IdTCPServer1.Contexts.LockList;
try
if List.IndexOf(TIdContext(Client.Thread)) <> -1 then // still connected?
Client.Queue(Msg);
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Index: Integer;
Msg: string;
Client: TSimpleClient;
begin
Index := ListBox1.ItemIndex;
if Index = -1 then Exit;
Msg := Edit1.Text;
if Msg = '' then Exit;
Client := TSimpleClient(ListBox1.Items.Objects[Index]);
SendMessageToClient(Client, Msg);
end;
I added a call to the FlushMsgs method from the TSimpleClient.Queue procedure and messages started to be sent, the list of clients is updated every time clients are connected and disconnected, and the server stopped hanging. Thanks again Remy, you helped a lot to speed up the development, golden man.
I don't know if it is even possible, but what I need is to access (use and free) a TStream variable by it pointer, passed thought a string parameter to another function.
Here is a "not working" example of what I am trying to do:
procedure TForm1.Button1Click(Sender: TObject);
var
Stm: TMemoryStream;
begin
Stm := TMemoryStream.Create;
try
Memo.Lines.SaveToStream(Stm);
Stm.Position := 0;
Memo.Clear;
Edit.Text := IntToStr(Integer(#Stm));
except
Stm.Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
PStm: ^TMemoryStream;
begin
PStm := Pointer(StrToInt(Edit.Text));
try
Memo.Lines.LoadFromStream(PStm^); // <--- Access Violation
finally
PStm^.Free;
end;
end;
Thanks for any help to solve this!
TStream is a reference type. Your Stm variable holds a pointer to the stream object instance. You want to pass this pointer value, not the address of the local variable. Here's the fixed code:
procedure TForm1.Button1Click(Sender: TObject);
var
Stm: TMemoryStream;
begin
Stm := TMemoryStream.Create;
try
Memo.Lines.SaveToStream(Stm);
Stm.Position := 0;
Memo.Clear;
Edit.Text := IntToStr(Integer(Stm));
except
Stm.Free;
raise;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
Stm: TMemoryStream;
begin
Stm := Pointer(StrToInt(Edit.Text));
try
Memo.Lines.LoadFromStream(Stm);
finally
Stm.Free;
end;
end;
I am new to Delphi and trying to convert vb.net apps to learn. The issue I am having is reading from a TCP/IP host. Currently I can connect via telnet to the device, send a command, and the device will send data non-stop until all data is sent. This could be simply two characters followed by CR/LF, or it could be several rows of varing length data. Each row is end is CR/LF. Prior to writing code, we were able to telnet via Hyperterminal to the device. Send a command, and, with the capture text enabled save to a text file.
Below is the code I have so far. I have not coded for saving to text file (one step at a time). The data is pipe delimited. I have no control on the format or operatation of the device aside from sending commands and receiving data. It works most of the time however there are times when not all of the data (65 records for testing) are received. I will greatly appreciate guidence and feel free to comment on my code, good or bad.
function Parse(Char, S: string; Count: Integer): string;
var
I: Integer;
T: string;
begin
if S[Length(S)] <> Char then
S := S + Char;
for I := 1 to Count do
begin
T := Copy(S, 0, Pos(Char, S) - 1);
S := Copy(S, Pos(Char, S) + 1, Length(S));
end;
Result := T;
end;
procedure TForm2.btnEXITClick(Sender: TObject);
begin
if idTcpClient1.connected then
begin
idTcpClient1.IOHandler.InputBuffer.clear;
idTcpClient1.Disconnect;
end;
Close;
end;
procedure TForm2.btnSendDataClick(Sender: TObject);
var
mTXDataString : String;
RXString : String;
begin
IdTCPClient1.Host := IPAddress.Text;
IdTCPClient1.Port := StrToInt(IPPort.Text);
mTXDataString := mTXData.Text + #13#10;
IdTCPClient1.Connect;
If IdTCPClient1.Connected then
begin
IdTCPClient1.IOHandler.Write(mTXDataString);
mTXDataString := mTXData.Lines.Text;
if MTXDataString.Contains('SCHEMA') then
begin
mRXData.Lines.Add(IdTCPClient1.IOHandler.ReadLn);
while not (IdTCPClient1.IOHandler.InputBufferIsEmpty) do
begin
RXString := IdTCPClient1.IOHandler.ReadLn;
If (RXString <> '') and (RXString <> '??') then
begin
//Add received data to RXmemo
mRXData.Lines.Add(RXString);
//Determine number of records to received based on schema data
lblRecords.Caption := Parse(',', RXString, 2);
end;
end; //while not
end // if
else
if mTXDataString.Contains('DATA') then
begin
mRXData.Lines.Add(IdTCPClient1.IOHandler.ReadLn);
while not (IdTCPClient1.IOHandler.InputBufferIsEmpty) do
begin
RXString := IdTCPClient1.IOHandler.ReadLn;
If (RXString <> '') and (RXString <> '??') then
begin
mRXData.Lines.Add(RXString);
end; // if
end; //while not
end; // if Schema or not
end; // if Connected
IdTCPClient1.Disconnect;
end; //Procedure
HyperTerminal and Telnet apps display whatever data they receive, in real-time. TIdTCPClient is not a real-time component. You control when and how it reads. If you are expecting data to arrive asynchronously, especially if you don't know how many rows are going to be received, then you need to perform the reading in a timer or worker thread, eg:
procedure TForm2.TimerElapsed(Sender: TObject);
var
S: String;
begin
if IdTCPClient1.IOHandler = nil then Exit;
if IdTCPClient1.IOHandler.InputBufferIsEmpty then
begin
IdTCPClient1.IOHandler.CheckForDataOnSource(50);
if IdTCPClient1.IOHandler.InputBufferIsEmpty then Exit;
end;
S := IdTCPClient1.IOHandler.ReadLn;
// use S as needed ...
end;
Or:
type
TMyThread = class(TThread)
protected
fClient: TIdTCPClient;
procedure Execute; override;
public
constructor Create(aClient: TIdTCPClient);
end;
constructor TMyThread.Create(aClient: TIdTCPClient);
begin
inherited Create(False);
fClient := aClient;
end;
procedure TMyThread.Execute;
var
S: String;
begin
while not Terminated do
begin
S := fClient.IOHandler.ReadLn;
// use S as needed ...
end;
end;
Or, if the server supports the actual Telnet protocol, have a look at using Indy's TIdTelnet component instead.
I'm sorry I know that I ask too many questions, but tomorrow (actually today cause in my coutry it's 2:00 am right now) I need to show to my teacher what I started to make e.t.c. So as I asked in previous questions I need to send from server to client some data.
But it nothing appers in server's memo field after thatt memo1.Lines.Add(IntToStr(arrOf[1]));
I was trying to send it like that on client
procedure TForm1.btnTestClick(Sender: TObject);
var
msRecInfo: TMemoryStream;
arrOf: array of integer; i:integer;
begin
setLength(arrOf, 11);
for i := 0 to 10 do
arrOf[i]:=random(100);
msRecInfo:= TMemoryStream.Create;
try
msRecInfo.Write(arrOf, SizeOf(arrOf));
idTCPClient1.IOHandler.Write(msRecInfo);
finally
msRecInfo.Free;
end;
end;
on server
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
msRecInfo: TMemoryStream;
arrOf: array of Integer; i:integer;
begin
msRecInfo:= TMemoryStream.Create;
try
AContext.Connection.IOHandler.ReadStream(msRecInfo, -1, False);
SetLength(arrOf,11);
msRecInfo.Position := 0;
msRecInfo.Read(arrOf, SizeOf(arrof));
finally
memo1.Lines.Add(IntToStr(arrOf[1]));
msRecInfo.Free;
end;
end;
Please could you help me to solve this problem and to find some examples of how to send arrays of different types/classes?
As Rufo already explained, you are not writing the array into, and reading the array out of, the TMemoryStream correctly.
Worse, you are not sending the TMemoryStream over the socket correctly, either. The default parameters of TIdIOHandler.Write(TStream) and TIdIOHandler.ReadStream() are not compatible with each other. By default, Write(TStream) does not send the TStream.Size value. However, the default parameters of ReadStream() (which are the same values that you are passing in explicitally) tell it to read the first few bytes and interpret them as the Size, which would be very wrong in this example.
Try this instead:
procedure TForm1.btnTestClick(Sender: TObject);
var
msRecInfo: TMemoryStream;
arrOf: Array of Integer;
i: Integer;
begin
SetLength(arrOf, 11);
for i := Low(arrOf) to High(arrOf) do
arrOf[i] := random(100);
msRecInfo := TMemoryStream.Create;
try
msRecInfo.WriteBuffer(arrOf[0], Length(arrOf) * SizeOf(Integer));
IdTCPClient1.IOHandler.Write(msRecInfo, 0, True);
finally
msRecInfo.Free;
end;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
msRecInfo: TMemoryStream;
arrOf: Array of Integer;
i: Integer;
begin
msRecInfo := TMemoryStream.Create;
try
AContext.Connection.IOHandler.ReadStream(msRecInfo, -1, False);
SetLength(arrOf, msRecInfo.Size div SizeOf(Integer));
if Lenth(arrOf) > 0 then
begin
msRecInfo.Position := 0;
msRecInfo.ReadBuffer(arrOf[0], Length(arrOf) * SizeOf(Integer));
end;
finally
msRecInfo.Free;
end;
...
end;
Alternatively, get rid of the TMemoryStream and send the individual Integer values by themselves:
procedure TForm1.btnTestClick(Sender: TObject);
var
arrOf: Array of Integer;
i: Integer;
begin
SetLength(arrOf, 11);
for i := Low(arrOf) to High(arrOf) do
arrOf[i] := random(100);
IdTCPClient1.IOHandler.Write(Length(arrOf));
for I := Low(arrOf) to High(arrOf) do
IdTCPClient1.IOHandler.Write(arrOf[i]);
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
arrOf: Array of Integer;
i: Integer;
begin
i := AContext.Connection.IOHandler.ReadLongInt;
SetLength(arrOf, i);
for i := Low(arrOf) to High(arrOf) do
arrOf[i] := AContext.Connection.IOHandler.ReadLongInt;
...
end;
Now, with that said, accessing the TMemo directly in the OnExecute event handler is not thread-safe. TIdTCPServer is a multi-threaded component. The OnExecute event is triggered in the context of a worker thread, not the main thread. UI components, like TMemo, cannot be safely accessed from outside of the main thread. You can use Indy's TIdNotify or TIdSync class to synchronize with the main thread, eg:
type
TMemoSync = class(TIdSync)
protected
FLine: String;
procedure DoSynchronize; override;
end;
procedure TMemoSync.DoSynchronize;
begin
Form1.Memo1.Lines.Add(FLine);
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
...
begin
...
with TMemoSync.Create do try
FLine := IntToStr(arrOf[1]);
Synchronize;
finally
Free;
end;
...
end;
If you do not synchronize with the main thread, bad things can happen.
This is your Main Problem here
msRecInfo.Write( arrOf, SizeOf( arrOf ) );
You write the pointer-address of the array into the stream ... i dont't think thats your goal.
If you want to put the content of the array into the stream you should use
msRecInfo.Write( arrOf[ Low( arrOf ) ], SizeOf( Integer ) * Length( arrOf ) );
Why? You have to point to the first position of the data (first element of the array), and you have to calculate the length of the data.
On the receiving part it is just the same
msRecInfo.Read( arrOf[ Low( arrOf ) ], SizeOf( Integer ) * Length( arrOf ) );
PS: This may work well in this special case, but to be safe, you should send at first, the length of all data, so the receiver knows, when message is complete
I need help in understanding how to transfer a record through Indy TCP Server/Client. I have 2 programs, in I put client and in another server.
On client on a button I put connect : Client is TIdTCPClient
Client.Connect();
And at server side I am adding a line to memo that client is connected , on ServerConnect event
Protocol.Lines.Add(TimeToStr(Time)+' connected ');
To send data from client I have a record, which I want to send :
Tmyrecord = record
IPStr: string[15];
end;
And I have a send button there :
procedure Tform1.ButtonSendClick(Sender: TObject);
var
MIRec: Tmyrecord;
msRecInfo: TMemoryStream;
begin
MIRec.IPStr := '172.0.0.1';
msRecInfo := TMemoryStream.Create;
msRecInfo.Write(MIRec, SizeOf(MIRec));
msRecInfo.Position := 0;
Client.IOHandler.Write(msRecInfo);
end;
At server side onexecute I have the following code , I have same tmyrecord declared at server side too :
procedure TServerFrmMain.ServerExecute(AContext: TIdContext);
var
MIRec: Tmyrecord;
msRecInfo: TMemoryStream;
begin
if AContext.Connection.Connected then
begin
AContext.Connection.IOHandler.CheckForDataOnSource(10);
if not AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
msRecInfo:= TMemoryStream.Create;
AContext.Connection.IOHandler.ReadStream(msRecInfo);
msRecInfo.Read(MIRec, sizeOf(msRecInfo));
ShowMessage(MIRec.IPStr);
end;
end;
end
I dont know why it is not working, why I cant show IP adress which I wrote from client side.
I want to read a record (msRecInfo) on server side which I am sending from client side. I want to access my record elements, in this case I want to read IPSTR element of my record. When I press send button from a client side, application hangs, server part.
Thanks a lot in advance
You are making a classic newbie mistake - you are expecting the default behaviors of the TIdIOHandler.Write(TStream) and TIdIOHandler.ReadStream() methods to match each other, but they actually do not.
The default parameter values of TIdIOHandler.ReadStream() tell it to expect an Integer or Int64 (depending on the value of the TIdIOHandler.LargeStream property) to preceed the stream data to specify the length of the data.
However, the default parameter values of TIdIOHandler.Write(TStream) do not tell it to send any such Integer/Int64 value. Thus, your use of TIdIOHandler.ReadStream() reads the first few bytes of the record and interprets them as an Integer/Int64 (which is 926036233 given the string value you are sending), and then waits for that many bytes to arrive, which never will so TIdIOHandler.ReadStream() does not exit (unless you set the TIdIOHandler.ReadTimeout property to a non-infinite value).
There are also some other minor bugs/typos in your code that uses the TMemoryStream objects outside of Indy.
Try this instead:
procedure Tform1.ButtonSendClick(Sender: TObject);
var
MIRec: Tmyrecord;
msRecInfo: TMemoryStream;
begin
MIRec.IPStr := '172.0.0.1';
msRecInfo := TMemoryStream.Create;
try
msRecInfo.Write(MIRec, SizeOf(MIRec));
// writes the stream size then writes the stream data
Client.IOHandler.Write(msRecInfo, 0, True);
finally
msRecInfo.Free;
end;
end;
procedure TServerFrmMain.ServerExecute(AContext: TIdContext);
var
MIRec: Tmyrecord;
msRecInfo: TMemoryStream;
begin
msRecInfo := TMemoryStream.Create;
try
// reads the stream size then reads the stream data
AContext.Connection.IOHandler.ReadStream(msRecInfo, -1, False);
msRecInfo.Position := 0;
msRecInfo.Read(MIRec, SizeOf(MIRec));
...
finally
msRecInfo.Free;
end;
end;
Or this:
procedure Tform1.ButtonSendClick(Sender: TObject);
var
MIRec: Tmyrecord;
msRecInfo: TMemoryStream;
begin
MIRec.IPStr := '172.0.0.1';
msRecInfo := TMemoryStream.Create;
try
msRecInfo.Write(MIRec, SizeOf(MIRec));
// does not write the stream size, just the stream data
Client.IOHandler.Write(msRecInfo, 0, False);
finally
msRecInfo.Free;
end;
end;
procedure TServerFrmMain.ServerExecute(AContext: TIdContext);
var
MIRec: Tmyrecord;
msRecInfo: TMemoryStream;
begin
msRecInfo := TMemoryStream.Create;
try
// does not read the stream size, just the stream data
AContext.Connection.IOHandler.ReadStream(msRecInfo, SizeOf(MIRec), False);
msRecInfo.Position := 0;
msRecInfo.Read(MIRec, SizeOf(MIRec));
...
finally
msRecInfo.Free;
end;
end;
Alternatively, you can send the record using TIdBytes instead of TStream:
procedure Tform1.ButtonSendClick(Sender: TObject);
var
MIRec: Tmyrecord;
Buffer: TIdBytes;
begin
MIRec.IPStr := '172.0.0.1';
Buffer := RawToBytes(MIRec, SizeOf(MIRec));
Client.IOHandler.Write(Buffer);
end;
procedure TServerFrmMain.ServerExecute(AContext: TIdContext);
var
MIRec: Tmyrecord;
Buffer: TIdBytes;
begin
AContext.Connection.IOHandler.ReadBytes(Buffer, SizeOf(MIRec));
BytesToRaw(Buffer, MIRec, SizeOf(MIRec));
...
end;