IndyTCP delphi array of objects exchanging - delphi

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

Related

How to transfer data using Indy TCP Server/Client?

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;

TStream as an object inside StringList

I am using Delphi 7 and playing with a StringList, with TStream as object.
My test project has a ListBox, a Memo and 2 buttons (Add and Remove).
Here is what I got so far:
var
List: TStringList;
procedure TForm1.FormCreate(Sender: TObject);
begin
List := TStringList.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
I: Integer;
begin
if (List.Count > 0) then
for I := 0 to Pred(List.Count) do
begin
List.Objects[I].Free;
List.Objects[I] := nil;
end;
FreeAndNil(List);
end;
procedure TForm1.btnAddClick(Sender: TObject);
var
Strm: TStream;
begin
Strm := TMemoryStream.Create;
try
Memo.Lines.SaveToStream(Strm);
List.AddObject(IntToStr(List.Count), TObject(Strm));
Memo.Clear;
ListBox.Items.Assign(List);
finally
// Strm.Free; (line removed)
end;
end;
procedure TForm1.btnDelFirstClick(Sender: TObject);
begin
if (List.Count > 0) then
begin
List.Objects[0].Free;
List.Objects[0] := nil;
List.Delete(0);
ListBox.Items.Assign(List);
end;
end;
When I double-click the ListBox I would like to load the selected item Stream object to Memo. Here is what I tried to do:
procedure TForm1.ListBoxDblClick(Sender: TObject);
var
Idx: Integer;
begin
Memo.Clear;
Idx := ListBox.ItemIndex;
if (Idx >= 0) and (TStream(List.Objects[Idx]).Size > 0) then
Memo.Lines.LoadFromStream(TStream(List.Objects[Idx]));
end;
My questions are:
Is correct the way I am adding and removing (freeing) the TStream object inside the StringList? Maybe I need to first free the Stream and then the Object??
Is correct the way I am freeing all objects on FormDestroy event?
When I try to load the stream back to Memo (Memo.Lines.LoadFromStream(TStream(List.Objects[Idx]))), it doesn't load, despite Stream.Size is higher than zero. What I am doing wrong?
1.Is correct the way I am adding and removing (freeing) the TStream object inside the StringList?
Yes, because the TStrings.Objects[] property returns a TObject pointer and TStream derives from TObject, so you can call Free() on the object pointers.
Maybe I need to first free the Stream and then the Object??
You need to free the TStream objects before freeing the TStringList object. Just as you are already doing.
2.Is correct the way I am freeing all objects on FormDestroy event?
Yes. Though technically, you do not need to check the TStringList.Count property for > 0 before entering the loop, as the loop will handle that condition for you. And you do not need to nil the pointers before freeing the TStringList:
procedure TForm1.FormDestroy(Sender: TObject);
var
I: Integer;
begin
for I := 0 to Pred(List.Count) do
List.Objects[I].Free;
List.Free;
end;
One thing you are doing that is overkill, though, is Assign()ing the entire TStringList to the TListBox whenever you add/delete a single item from the TStringList. You should instead simply add/delete the associated item from the ListBox and preserve the remaining items as-is.
And add some extra error checking to btnAddClick() as well to avoid any memory leaks if something goes wrong.
Try this:
procedure TForm1.btnAddClick(Sender: TObject);
var
Strm: TStream;
Idx: Integer;
begin
Strm := TMemoryStream.Create;
try
Memo.Lines.SaveToStream(Strm);
Strm.Position := 0;
Idx := List.AddObject(IntToStr(List.Count), Strm);
except
Strm.Free;
raise;
end;
try
ListBox.Items.Add(List.Strings[Idx]);
except
List.Objects[Idx].Free;
List.Delete(Idx);
raise;
end;
Memo.Clear;
end;
procedure TForm1.btnDelFirstClick(Sender: TObject);
begin
if List.Count > 0 then
begin
List.Objects[0].Free;
List.Delete(0);
ListBox.Items.Delete(0);
end;
end;
3.When I try to load the stream back to Memo (Memo.Lines.LoadFromStream(TStream(List.Objects[Idx]))), it doesn't load, despite Stream.Size is higher than zero. What I am doing wrong?
You are not seeking the stream back to Position 0 before loading it into the Memo. SaveToStream() always leaves the stream positioned at the end of the stream, and LoadFromStream() leave the stream positioned wherever the load stopped reading from (if not at the end, in case of failure).
Now, with all of this said, I personally would not use TListBox in this manner. I would instead set its Style property to lbVirtual and then use its OnData event to display the strings from the TStringList. No need to copy them into the TListBox directly, or try to keep the two lists in sync at all times. It would be safer, and use less memory, to let the TListBox ask you for what it needs, and then you can provide it from the TStringList (which I would then change to a TList since you are not really storing meaningful names that can't be produced dynamically in the OnData event handler):
var
List: TList;
procedure TForm1.FormCreate(Sender: TObject);
begin
List := TList.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
I: Integer;
begin
ListBox.Count := 0;
for I := 0 to Pred(List.Count) do
TStream(List[I]).Free;
List.Free;
end;
procedure TForm1.btnAddClick(Sender: TObject);
var
Strm: TStream;
Idx: Integer;
begin
Strm := TMemoryStream.Create;
try
Memo.Lines.SaveToStream(Strm);
Strm.Position := 0;
Idx := List.Add(Strm);
except
Strm.Free;
raise;
end;
try
ListBox.Count := List.Count;
except
TStream(List[Idx]).Free;
List.Delete(Idx);
raise;
end;
Memo.Clear;
end;
procedure TForm1.btnDelFirstClick(Sender: TObject);
begin
if List.Count > 0 then
begin
TStream(List[0]).Free;
List.Delete(0);
ListBox.Count := List.Count;
end;
end;
procedure TForm1.ListBoxDblClick(Sender: TObject);
var
Strm: TStream;
Idx: Integer;
begin
Memo.Clear;
Idx := ListBox.ItemIndex;
if Idx >= 0 then
begin
Strm := TStream(List[Idx]);
if Strm.Size > 0 then
begin
Strm.Position := 0;
Memo.Lines.LoadFromStream(Strm);
end;
end;
end;
procedure TForm1.ListBoxData(Control: TWinControl; Index: Integer; var Data: string);
begin
Data := IntToStr(Index);
end;
I don't understand what you suggest about freeing the stream and then the object. As I understand it, the object you're talking about freeing is the stream. You can't destroy one before the other because there's only one object, which is a stream.
Your methods of adding and removing stream objects in the string list are fine. They're not ideal, but I'll limit my comments here because Stack Overflow isn't Code Review.
After you call SaveToStream, the stream's position is at the end of the stream. If you want to read from the stream, then you'll have to set the position back to the start again. Set Position := 0 for the stream prior to calling LoadFromStream.

Delphi TCPClient Issue

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.

Delphi TidTCPServer and TidTCPClient transferring a record

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;

Why doesn't TCusomWinSocket.ReceiveBuf ever return 0?

When it comes to sockets, TClientSocket and TServerSockets are my favourite because of their simple usage.
My task is very simple. I need to send a file (RAW) through these 2 components, so I have 2 routines like the ones below:
procedure csRead(Sender: TObject; Socket: TCustomWinSocket);
var
MSCli : TMemoryStream;
cnt : Integer;
buf : array [0..1023] of byte;
begin
MSCli := TMemoryStream.Create;
try
repeat
cnt := Socket.ReceiveBuf(buf[0], 1024); //This loop repeats endlesly
MSCli.Write(buf[0], cnt)
until cnt = 0;
finally
MSCli.SaveToFile('somefile.dmp');
MSCli.Free;
end;
end;
And of course the sender :
//...some code
MSSErv.LoadFromFile('some file');
MSServ.Position := 0;
Socket.SendStream(MSServ);
end;
The loop in the reader is repeating endelessly and I don't know why. What could be the source of the problem?
SendStream() is not a particularly good choice to use - EVER. It is intended to send the entire TStream and then free it when finished. However, if the socket is set to non-blocking mode and the socket blocks during sending, SendStream() exits immediately and DOES NOT free the TStream. You have to call SendStream() again to continue sending the TStream from where SendStream() left off. But there are other conditions that can cause SendStream() to exit AND free the TStream, and you don't really know when it did or did not free the TStream, so it becomes very dangerous to try to call SendStream() again with the same TStream object. A much safer approach is to avoid SendStream() at all costs and call SendBuf() directly in your own loop instead.
With that said, SendStream() does not inform the receiver how many bytes will be sent, so the receiver does not know when to stop reading (unless you close the connection after sending the TStream). A better choice is to send the intended byte count before then sending the TStream data. That way, the receiver can read the byte count first, then stop reading when the specified number of bytes have been received. For example:
procedure ReadRawFromSocket(Socket: TCustomWinSocket; Buffer: Pointer; BufSize: Integer);
var
buf: PByte;
cnt: Integer;
begin
buf := PByte(Buffer);
while BufSize > 0 do
begin
cnt := Socket.ReceiveBuf(buf^, BufSize);
if cnt < 1 then begin
if (cnt = -1) and (WSAGetLastError() = WSAEWOULDBLOCK) then
begin
Application.ProcessMessages;
Continue;
end;
Abort;
end;
Inc(buf, cnt);
Dec(BufSize, cnt);
end;
end;
function ReadInt64FromSocket(Socket: TCustomWinSocket): Int64;
begin
ReadRawFromSocket(Socket, #Result, SizeOf(Int64));
end;
procedure ReadMemStreamFromSocket(Socket: TCustomWinSocket: Stream: TMemoryStream);
var
cnt: Int64;
begin
cnt := ReadInt64FromSocket(Socket);
if cnt > 0 then
begin
Stream.Size := cnt;
ReadRawFromSocket(Socket, Stream.Memory, cnt);
end;
end;
procedure csRead(Sender: TObject; Socket: TCustomWinSocket);
var
MSCli : TMemoryStream;
begin
MSCli := TMemoryStream.Create;
try
ReadMemStreamFromSocket(Socket, MSCli);
MSCli.SaveToFile('somefile.dmp');
finally
MSCli.Free;
end;
end;
procedure SendRawToSocket(Socket: TCustomWinSocket; Buffer: Pointer; BufSize: Integer);
var
buf: PByte;
cnt: Integer;
begin
buf := PByte(Buffer);
while BufSize > 0 do
begin
cnt := Socket.SendBuf(buf^, BufSize);
if cnt < 1 then begin
if (cnt = -1) and (WSAGetLastError() = WSAEWOULDBLOCK) then
begin
Application.ProcessMessages;
Continue;
end;
Abort;
end;
Inc(buf, cnt);
Dec(BufSize, cnt);
end;
end;
procedure SendInt64ToSocket(Socket: TCustomWinSocket; Value: Int64);
begin
SendRawToSocket(Socket, #Value, SizeOf(Int64));
end;
procedure SendMemStreamToSocket(Socket: TCustomWinSocket: Stream: TMemoryStream);
begin
SendInt64FromSocket(Socket, Stream.Size);
SendRawToSocket(Socket, Stream.Memory, Stream.Size);
end;
begin
...
MSSErv.LoadFromFile('some file');
MSServ.Position := 0;
SendMemStreamToSocket(Socket, MSServ);
...
end;

Resources