Well this is going to be long!
I made a retransmission scheme for managing dropped UDP packets, for a LAN simulation protocol I am working on.
Packet Storage
TDataBuffer = record
PacketID : WORD;
Packet : TMemoryStream;
end;
PDataBuffer = ^TDataBuffer;
Related DataModule Class Members
fRxDataPacketList : TThreadList20;
fTxDataPacketList : TThreadList20;
procedure CreateDataBuffer
(PacketID : WORD; Packet : TMemoryStream;
var DataBuffer : PDataBuffer);
procedure DestroyDataBuffer
(var DataBuffer : PDataBuffer);
procedure AddPacketToPacketList
(PacketID : WORD; Packet : TMemoryStream;
RecievedPacket : Boolean);
function GetPacketFromTxDataPacketList
(PacketID : WORD; var Packet : TMemoryStream): Boolean;
TThreadList20: It is my own thread friendly, encryption and compression supporting wrapper class for TList.
There is another procedure for processing the Rx side which doesn't concern my question so I am skipping it.
Creation
procedure TDataModuleClient.CreateDataBuffer
(PacketID : WORD; Packet : TMemoryStream;
var DataBuffer : PDataBuffer);
begin
New (DataBuffer);
DataBuffer.PacketID := PacketID;
DataBuffer.Packet := TMemoryStream.Create;
if Assigned (Packet) then
begin
DataBuffer.Packet.CopyFrom (Packet,Packet.Size); // NO AV HERE
DataBuffer.Packet.Position := 0;
end;
end;
Destruction
procedure TDataModuleClient.DestroyDataBuffer
(var DataBuffer : PDataBuffer);
begin
DataBuffer.Packet.Free;
Dispose (DataBuffer);
end;
Addition To List
procedure TDataModuleClient.AddPacketToDataPacketList
(PacketID : WORD; Packet : TMemoryStream; RecievedPacket : Boolean);
var
DataBuffer : PDataBuffer;
begin
CreateDataBuffer (PacketID,Packet,DataBuffer);
if RecievedPacket then
fRxDataPacketList.Add (TObject (DataBuffer))
else
begin
fTxDataPacketList.Lock;
try
fTxDataPacketList.Add (TObject (DataBuffer));
if fRxDataPacketList.Count = 21 then
begin
DataBuffer := PDataBuffer (fTxDataPacketList [0]);
DestroyDataBuffer (DataBuffer);
fTxDataPacketList.Delete (0);
end;
finally fTxDataPacketList.Unlock;
end;
end;
end;
Extraction From List
function TDataModuleClient.GetPacketFromTxDataPacketList
(PacketID : WORD; var Packet : TMemoryStream): Boolean;
var
DataBuffer : PDataBuffer;
I : Integer;
begin
Result := False;
fTxDataPacketList.Lock;
try
for I := fTxDataPacketList.Count - 1 downto 0 do
begin
DataBuffer := PDataBuffer (fTxDataPacketList [I]);
if DataBuffer.PacketID < PacketID then
begin
DestroyDataBuffer (DataBuffer);
fTxDataPacketList.Delete (I);
end
else if DataBuffer.PacketID = PacketID then
begin
Result := True;
Packet := TMemoryStream.Create;
Packet.CopyFrom
(DataBuffer.Packet,DataBuffer.Packet.Size); // AV HERE
Packet.Position := 0;
DestroyDataBuffer (DataBuffer);
fTxDataPacketList.Delete (I);
break;
end;
end;
finally fTxDataPacketList.Unlock;
end;
end;
Packet: Output Variable.
Please help me out, I know it looks like a tall order to deduce something thanks to the amount of code.
Based on this limited code, I am making some guesses:
1) the input Packet that is being passed to GetPacketFromTxDataPacketList() has probably not actually been instantiated yet before you call CopyFrom(). That would account for the AV.
2) since the Packet parameter is declared as a var, that suggests to me that it is an output parameter that GetPacketFromTxDataPacketList() is supposed to create and return, instead of having the caller create the TMemoryStream and pass it into GetPacketFromTxDataPacketList() to be filled with data. That would also play into #1, since GetPacketFromTxDataPacketList() is not creating a new TMemoryStream object, it is assuming the object has already been created beforehand.
Related
I'm trying send a record over clientsocket and receive on serversocket, everything works well but only on first time, after send one time i need disconnect clientsocket, connect again to send it again.
If somebody can help me.
here is the server side code how i receive the informations:
procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket);
var
LBuffer: TBytes;
LMessageBuffer: TBytes;
LDataSize: Integer;
LProtocol: TProtocol;
begin
LDataSize := Socket.ReceiveLength;
if LDataSize >= szProtocol then begin
try
Socket.ReceiveBuf(LBuffer, SizeOf(LBuffer));
LProtocol := BytesToProtocol(LBuffer);
// check client command and act accordingly
case LProtocol.Command of
cmdConnect: begin
Memo1.Lines.Add(Format('[%s][%s][%s]', ['Connect', LProtocol.Sender.UserName, TimeToStr(LProtocol.Sender.ID)]));
end; // cmdConnect: begin
cmdDisconnect: begin
Memo1.Lines.Add(Format('[%s][%s][%s]', ['Disconnect', LProtocol.Sender.UserName, TimeToStr(LProtocol.Sender.ID)]));
end; // cmdDisconnect: begin
end;
finally
ClearBuffer(LBuffer);
end;
end;
end;
and here the client side:
var
LBuffer: TBytes;
LProtocol: TProtocol;
x : Integer;
begin
InitProtocol(LProtocol);
LProtocol.Command := cmdConnect;
ClientData.UserName := Edit1.Text;
ClientData.ID := Now;
LProtocol.Sender := ClientData;
LBuffer := ProtocolToBytes(LProtocol);
try
ClientSocket1.Socket.SendBuf(LBuffer, Length(LBuffer));
finally
ClearBuffer(LBuffer);
end;
record declaration:
type
TCommand = (
cmdConnect,
cmdDisconnect,
cmdMessageBroadcast,
cmdMessagePrivate,
cmdScreenShotGet,
cmdScreenShotData);
// client information structure, you can extend this based on your needs
type
TClient = record
UserName: string[50];
ID: TDateTime;
end; // TClient = record
// size of the client information structure
const
szClient = SizeOf(TClient);
Thanks :)
TBytes is a dynamic array, but you are treating it as if it were a static array.
In your client code, you are not sending LBuffer correctly. Being a dynamic array, LBuffer is just a pointer to data that is stored elsewhere in memory. So, you need to dereference LBuffer to pass the correct memory address to SendBuf().
In your server code, you are not even allocating any memory for LBuffer at all before reading bytes into it. And, like in the client, you need to dereference LBuffer when passing it to ReceiveBuf(). You also need to use the correct byte size when telling ReceiveBuf() how many bytes to read (SizeOf(TBytes) is the wrong value to use).
Lastly, you need to pay attention to the return values of SendBuf() and ReceiveBuf(), as they CAN return that fewer bytes than requested were processed! So, you should be calling SendBuf() and ReceiveBuf() in a loop.
Try this:
var
LBuffer: TBytes;
LProtocol: TProtocol;
LBufferPtr: PByte;
LBufferLen: Integer;
LNumSent: Integer;
begin
InitProtocol(LProtocol);
LProtocol.Command := cmdConnect;
ClientData.UserName := Edit1.Text;
ClientData.ID := Now;
LProtocol.Sender := ClientData;
LBuffer := ProtocolToBytes(LProtocol);
LBufferPtr := PByte(LBuffer);
LBufferLen := Length(LBuffer);
repeat
LNumSent := ClientSocket1.Socket.SendBuf(LBufferPtr^, LBufferLen);
if LNumSent = -1 then
begin
// if ClientSocket1.ClientType is set to ctNonBlocking,
// uncomment this check ...
{
if WSAGetLastError() = WSAEWOULDBLOCK then
begin
// optionally call the Winsock select() function to wait
// until the socket can accept more data before calling
// SendBuf() again...
Continue;
end;
}
// ERROR!
ClientSocket1.Close;
Break;
end;
Inc(LBufferPtr, LNumSent);
Dec(LBufferLen, LNumSent);
until LBufferLen = 0;
end;
procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket);
var
LBuffer: TBytes;
LDataSize: Integer;
LProtocol: TProtocol;
LBufferPtr: PByte;
LBufferLen: Integer;
LNumRecvd: Integer;
begin
LDataSize := Socket.ReceiveLength;
if LDataSize < szProtocol then Exit;
SetLength(LBuffer, szProtocol);
repeat
// since you are validating ReceiveLength beforehand, ReceiveBuf()
// *should not* return fewer bytes than requested, but it doesn't
// hurt to be careful...
LBufferPtr := PByte(LBuffer);
LBufferLen := szProtocol;
repeat
LNumRecvd := Socket.ReceiveBuf(LBufferPtr^, LBufferLen);
if LNumRecvd <= 0 then Exit;
Inc(LBufferPtr, LNumRecvd);
Dec(LBufferLen, LNumRecvd);
Dec(LDataSize, LNumRecvd);
until LBufferLen = 0;
LProtocol := BytesToProtocol(LBuffer);
// check client command and act accordingly
case LProtocol.Command of
cmdConnect: begin
Memo1.Lines.Add(Format('[%s][%s][%s]', ['Connect', LProtocol.Sender.UserName, TimeToStr(LProtocol.Sender.ID)]));
end;
cmdDisconnect: begin
Memo1.Lines.Add(Format('[%s][%s][%s]', ['Disconnect', LProtocol.Sender.UserName, TimeToStr(LProtocol.Sender.ID)]));
end;
end;
until LDataSize < szProtocol;
end;
That being said, TClientSocket and TServerSocket have been deprecated for a LONG time. They are not even installed by default anymore (but are still available if you need to install them). You should really consider switching to another socket library that handles these kind of details for you, such as Indy's TIdTCPClient and TIdTCPServer (Indy is preinstalled in Delphi), eg:
type
PTIdBytes = ^TIdBytes;
var
LBuffer: TBytes;
LProtocol: TProtocol;
begin
InitProtocol(LProtocol);
LProtocol.Command := cmdConnect;
ClientData.UserName := Edit1.Text;
ClientData.ID := Now;
LProtocol.Sender := ClientData;
LBuffer := ProtocolToBytes(LProtocol);
// TBytes and TIdBytes are technically the same thing under the hood,
// but they are still distinct types and not assignment-compatible,
// so using a dirty hack to pass a TBytes as a TIdBytes without having
// to make a copy of the bytes...
IdTCPClient1.IOHandler.Write(PTIdBytes(#LBuffer)^);
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
type
PTBytes = ^TBytes;
var
LBuffer: TIdBytes;
LProtocol: TProtocol;
// note: TIdTCPServer is a multi-threaded component, so you must
// sync with the main thread when accessing the UI...
procedure AddToMemo(const AStr: string);
begin
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add(AStr);
end
);
end;
begin
// ReadBytes() can allocate the buffer for you...
AContext.Connection.IOHandler.ReadBytes(LBuffer, szProtocol);
// using a similar dirty hack to pass a TIdBytes as a TBytes
// without making a copy of the bytes ...
LProtocol := BytesToProtocol(PTBytes(#LBuffer)^);
// check client command and act accordingly
case LProtocol.Command of
cmdConnect: begin
AddToMemo(Format('[%s][%s][%s]', ['Connect', LProtocol.Sender.UserName, TimeToStr(LProtocol.Sender.ID)]));
end;
cmdDisconnect: begin
AddToMemo(Format('[%s][%s][%s]', ['Disconnect', LProtocol.Sender.UserName, TimeToStr(LProtocol.Sender.ID)]));
end;
end;
end;
I want to send a record, that right now have only a string on it, but I will add more variables. Is the first time I work with records, so this maybe is a silly question. But, why this works:
type
TDataPipe = record
WindowTitle: String[255];
end;
var
Data: TDataPipe;
copyDataStruct : TCopyDataStruct;
begin
Data.WindowTitle:= String(PChar(HookedMessage.lParam));
copyDataStruct.dwData := 0;
copyDataStruct.cbData := SizeOf(Data);
copyDataStruct.lpData := #Data;
SendMessage(FindWindow('TForm1', nil), WM_COPYDATA, Integer(hInstance), Integer(#copyDataStruct));
end;
Receiving side:
type
TDataPipe = record
WindowTitle: String[255];
end;
procedure TForm1.WMCopyData(var Msg: TWMCopyData);
var
sampleRecord : TDataPipe;
begin
sampleRecord.WindowTitle:= TDataPipe(Msg.CopyDataStruct.lpData^).WindowTitle;
Memo1.Lines.Add(sampleRecord.WindowTitle);
end;
Why if on the record, I use:
WindowTitle: String; //removed the fixed size
and on the sending side I use:
Data.WindowTitle:= PChar(HookedMessage.lParam); //removed String()
it simply doesn't go?
I get access violations / app freeze...
The scenario is: sending side is a DLL hooked using SetWindowsHookEx, receiving side a simple exe that loaded / called SetWindowsHookEx...
A String[255] is a fixed 256-byte block of memory, where the character data is stored directly in that memory. As such, it is safe to pass as-is across process boundaries without serialization.
A String, on the other hand, is a dynamic type. It just contains a pointer to character data that is stored elsewhere in memory. As such, you can't pass a String as-is across process boundaries, all you would be passing is the pointer value, which has no meaning to the receiving process. You have to serialize String data into a flat format that can safely by passed to, and deserialized by, the receiving process. For example:
Sending side:
type
PDataPipe = ^TDataPipe;
TDataPipe = record
WindowTitleLen: Integer;
WindowTitleData: array[0..0] of Char;
//WindowTitleData: array[0..WindowTitleLen-1] of Char;
end;
var
Wnd: HWND;
s: String;
Data: PDataPipe;
DataLen: Integer;
copyDataStruct : TCopyDataStruct;
begin
Wnd := FindWindow('TForm1', nil);
if Wnd = 0 then Exit;
s := PChar(HookedMessage.lParam);
DataLen := SizeOf(Integer) + (SizeOf(Char) * Length(s));
GetMem(Data, DataLen);
try
Data.WindowTitleLen := Length(s);
StrMove(Data.WindowTitleData, PChar(s), Length(s));
copyDataStruct.dwData := ...; // see notes further below
copyDataStruct.cbData := DataLen;
copyDataStruct.lpData := Data;
SendMessage(Wnd, WM_COPYDATA, 0, LPARAM(#copyDataStruct));
finally
FreeMem(Data);
end;
end;
Receiving side:
type
PDataPipe = ^TDataPipe;
TDataPipe = record
WindowTitleLen: Integer;
WindowTitleData: array[0..0] of Char;
//WindowTitleData: array[0..WindowTitleLen-1] of Char;
end;
procedure TForm1.WMCopyData(var Msg: TWMCopyData);
var
Data: PDataPipe;
s: string;
begin
Data := PDataPipe(Msg.CopyDataStruct.lpData);
SetString(s, Data.WindowTitleData, Data.WindowTitleLen);
Memo1.Lines.Add(s);
end;
That being said, in either situation, you really should be assigning your own custom ID number to the copyDataStruct.dwData field. The VCL itself uses WM_COPYDATA internally, so you don't want to get those messages confused with yours, and vice versa. You can use RegisterWindowMessage() to create a unique ID to avoid conflicts with IDs used by other WM_COPYDATA users:
var
dwMyCopyDataID: DWORD;
...
var
...
copyDataStruct : TCopyDataStruct;
begin
...
copyDataStruct.dwData := dwMyCopyDataID;
...
end;
...
initialization
dwMyCopyDataID := RegisterWindowMessage('MyCopyDataID');
var
dwMyCopyDataID: DWORD;
...
procedure TForm1.WMCopyData(var Msg: TWMCopyData);
var
...
begin
if Msg.CopyDataStruct.dwData = dwMyCopyDataID then
begin
...
end else
inherited;
end;
...
initialization
dwMyCopyDataID := RegisterWindowMessage('MyCopyDataID');
Lastly, the WPARAM parameter of WM_COPYDATA is an HWND, not an HINSTANCE. If the sender does not have its own HWND, just pass 0. Do not pass your sender's HInstance variable.
Preparation:
procedure TMainForm.CreateParams(var Params: TCreateParams);
begin
inherited;
StrCopy(Params.WinClassName, PChar(SingleInstClassName)); // Copies a null-terminated string. StrCopy is designed to copy up to 255 characters from the source buffer into the destination buffer. If the source buffer contains more than 255 characters, the procedure will copy only the first 255 characters.
end;
Sender:
procedure TAppData.ResurectInstance(Arg: string);
VAR
Window: HWND;
DataToSend: TCopyDataStruct;
begin
Arg:= Trim(Arg);
{ Prepare the data you want to send }
DataToSend.dwData := CopyDataID; // CopyDataID = Unique ID for my apps
DataToSend.cbData := Length(Arg) * SizeOf(Char);
DataToSend.lpData := PChar(Arg);
{ We should never use PostMessage() with the WM_COPYDATA message because the data that is passed to the receiving application is only valid during the call. Finally, be aware that the call to SendMessage will not return until the message is processed.}
Window:= WinApi.Windows.FindWindow(PWideChar(SingleInstClassName), NIL); // This is a copy of cmWindow.FindTopWindowByClass
SendMessage(Window, WM_COPYDATA, 0, LPARAM(#DataToSend));
end;
Receiver:
procedure TMainForm.WMCopyData(var Msg: TWMCopyData);
VAR
FileName: string;
begin
{ Receives filename from another instance of this program }
if (Msg.CopyDataStruct.dwData = AppData.CopyDataID) { Only react on this specific message }
AND (Msg.CopyDataStruct.cbData > 0) { Do I receive an empty string? }
then
begin
SetString(FileName, PChar(Msg.CopyDataStruct.lpData), Msg.CopyDataStruct.cbData div SizeOf(Char));
msg.Result:= 2006; { Send something back as positive answer }
AppData.Restore;
...
end
else
inherited;
end;
I'm trying to store a pointer to a record in a Tqueue, then dequeue the pointer later and extract the data but am getting in a muddle with the pointers and keep getting a 'Abstract Error'
Can anyone please see what I am doing wrong and advise me on the correct solution?
(BTW, Initially I had it all without the ^ but then realised my mistake but was surprised that it still gave an error)
The record holds email data that gets sent to a smtp server, It uses a TstringList to hold each line of the body and another one to hold each attachment filename
This is the record structure used to store the email data
TPtrEmailData = ^TEmailDataRec;
TEmailDataRec = record
ToAddr : string; //one email address
CcAddr : string; //addresses delimitated by semicolons
BccAddr : string; //addresses delimitated by semicolons
Subject : String;
Body : TStrings; //each string contains one line of the body
attachments: TStrings;//each string contains a filename
end;
To create the records I use
function TFrmSendEmail.CreateNewEmailRec: TPtrEmailData;
var
EmailRecPtr : TPtrEmailData;
begin
new(EmailRecPtr); //make a new record
EmailRecPtr^.Body := Tstrings.Create ;
EmailRecPtr^.attachments := Tstrings.create;
result := EmailRecPtr ;
end;
and to free them after dequeing I use
procedure TFrmSendSllSmtptEmail.DestroyEmailRec(EmailRecPtr : TPtrEmailData);
//frees memory for the Tstrings and then frees the record
begin
freeandnil(EmailRecPtr^.Body); //free one Tstringlist
FreeAndNil(EmailRecPtr^.attachments); //and the other
FreeAndNil(EmailRecPtr ); //now free the precord pointer
end;
CreateNewEmailRec is called when I enqueue a new record pointer in the queue using the following, passing in the memo and list box containig th ebody and attachments. This is where I get the error.
procedure TFrmSendEmail.AddToEmailQueue(ToAddr, CCAddr,
BccAddr,Subject:String;
Body: Tmemo; Attachments: TListBox);
var
i : integer;
s : string;
EmailRecPtr : TPtrEmailData;
begin
EmailRecPtr := CreateNewEmailRec; //allocate memory
//deallocated in RemoveFromEmailQueue
EmailRecPtr^.ToAddr := ToAddr;
EmailRecPtr^.CCAddr := CCAddr;
EmailRecPtr^.BccAddr := BccAddr;
for I := 0 to Attachments.Count - 1 do
begin
s := Attachments.Items[i];
EmailRecPtr^.attachments.add(s ); <---- !!! get abstract error here
end;
for I := 0 to Body.lines.Count - 1 do
begin
s := Body.lines[i];
EmailRecPtr^.Body.Add(s) ;
end;
EmailQueue.Enqueue(EmailRecPtr );
end;
and DestroyEmailRec is called when I dequeue a pointer to use the data in
procedure TFrmSendEmail.RemoveFromEmailQueue(var ToAddr,
CCAddr,
BccAddr,
Subject: String;
var Body,
Attachments: TStringlist);
var
EmailRecPtr :TPtrEmailData;
i : integer;
s : string;
begin
if EmailQueue.Count > 0 then
begin
Body.Clear;
Attachments.Clear;
EmailRecPtr := EmailQueue.Dequeue; //get pointer to next record
ToAddr := EmailRecPtr^.ToAddr; //populate procedure parameters
CCAddr := EmailRecPtr^.CCAddr;
BccAddr := EmailRecPtr^.BccAddr;
for EmailRecPtr^.attachments.Count - 1 do
begin
s := EmailRec^.attachments[i];
Attachments.Add(s) ;
end;
for I := 0 to EmailRecPtr ^.Body.Count - 1 do
begin
s := EmailRecPtr ^.Body[i];
Body.Add(s);
end;
DestroyEmailRec(EmailRecPtr); //release memory
end;
The call to RemoveFromEmailQueue passes in a couple of created TStringLists
TheBody := Tstringlist.Create ;
TheAttachments := Tstringlist.create;
try
RemoveFromEmailQueue(ToAddr, CCAddr, BccAddr, Subject,TheBody,TheAttachments);
// do stuff with the data;
finally
TheBody.Free;
TheAttachments.Free;
end;
Oh, and the queue is declared as
var
EmailQueue : Tqueue<TPtrEmailData>;
You get the "Abstract Error" because you use an astract object (TStrings)! In the TFrmSendEmail.CreateNewEmailRec method replace TStrings with TStringList:
function TFrmSendEmail.CreateNewEmailRec: TPtrEmailData;
begin
new(result); //make a new record
Result^.Body := TStringList.Create ;
Result^.attachments := TStringList.create;
end;
Also, you can't free records using FreeAndNil! So your method to free the record should be like
procedure TFrmSendSllSmtptEmail.DestroyEmailRec(EmailRecPtr : TPtrEmailData);
//frees memory for the Tstrings and then frees the record
begin
EmailRecPtr^.Body.Free; //free one Tstringlist
EmailRecPtr^.attachments.Free; //and the other
Dispose(EmailRecPtr); //now free the precord pointer
end;
i use to send a data on two separate process but it fails. it works only under same process... this is concept.
//-----------------------------------------------------------------------------------
MainApps
//-----------------------------------------------------------------------------------
Type
PMyrec = ^TMyrec;
TMyrec = Record
name : string;
add : string;
age : integer;
end;
:OnButtonSend
var aData : PMyrec;
begin
new(aData);
aData.Name := 'MyName';
aData.Add := 'My Address';
aData.Age : 18;
SendMessage(FindWindow('SubApps'),WM_MyMessage,0,Integer(#aData));
end;
//-----------------------------------------------------------------------------------
SubApps
//-----------------------------------------------------------------------------------
Type
PMyrec = ^TMyrec;
TMyrec = Record
name : string;
add : string;
age : integer;
end;
:OnCaptureMessage
var
aData : PMyrec;
begin
aData := PMyrec(Msg.LParam);
showmessage(aData^.Name);
end;
You're right. Addresses only have meaning within a single process. The PMyRec value you create in the first process is just a garbage address in the target process.
To send an arbitrary block of memory to another process via a window message, you should use the wm_CopyData message. You give that message the address of the data and the size, and the OS takes care of copying it into the target process's address space.
Since your data includes a string, which is represented internally as a another pointer, it won't be enough to just copy the 12 bytes of your record. You'll need to allocate additional memory to hold the record and the string data in a single block of memory so wm_CopyData can copy it and the target process can read it.
Here's one way to do it, using a stream to collect the data into a single block of memory.
procedure SendRecord(Source, Target: HWnd; const Rec: TMyRec);
var
Buffer: TMemoryStream;
Len: Integer;
CopyData: TCopyDataStruct;
begin
Buffer := TMemoryStream.Create;
try
Len := Length(Rec.name);
Buffer.Write(Len, SizeOf(Len));
if Len > 0 then
Buffer.Write(Rec.name[1], Len * SizeOf(Char));
Len := Length(Rec.add);
Buffer.Write(Len, SizeOf(Len));
if Len > 0 then
Buffer.Write(Rec.add[1], Len * SizeOf(Char));
Buffer.Write(Rec.age, SizeOf(Rec.age));
CopyData.dwData := 0;
CopyData.cbData := Buffer.Size;
CopyData.lpData := Buffer.Memory;
SendMessage(Target, wm_CopyData, Source, LParam(#CopyData));
finally
Buffer.free;
end;
end;
We write the lengths of the strings in addition to the strings' characters so that the recipient knows how many characters belong to each one. The recipient's code will look like this:
procedure TBasicForm.WMCopyData(var Message: TWMCopyData);
var
Rec: TMyRec;
Len: Integer;
Buffer: TStream;
begin
Buffer := TReadOnlyMemoryStream.Create(
Message.CopyDataStruct.lpData, Message.CopyDataStruct.cbData);
try
if Message.CopyDataStruct.dwData = 0 then begin
Buffer.Read(Len, SizeOf(Len));
SetLength(Rec.name, Len);
if Len > 0 then
Buffer.Read(Rec.name[1], Len * SizeOf(Char));
Buffer.Read(Len, SizeOf(Len));
SetLength(Rec.add, Len);
if Len > 0 then
Buffer.Read(Rec.add[1], Len * SizeOf(Len));
Buffer.Read(Rec.age, SizeOf(Rec.age));
// TODO: Do stuff with Rec here.
Message.Result := 1;
end else
inherited;
finally
Buffer.Free;
end;
end;
I've used the non-standard TReadOnlyMemoryStream since it makes everything easier. Here's a simple implementation for it:
type
TReadOnlyMemoryStream = class(TCustomMemoryStream)
public
constructor Create(Mem: Pointer; Size: LongInt);
function Write(const Buffer; Count: LongInt): LongInt; override;
end;
constructor TReadOnlyMemoryStream.Create;
begin
inherited Create;
SetPointer(Mem, Size);
end;
function TReadOnlyMemoryStream.Write;
begin
Result := 0;
end;
I want to upgrade my application from Indy 9 to 10 with Delphi 2007.
In this thread there is a call to Indy9 TIdUDPBase.SendBuffer but this won't compile in Indy10 as the method parameter don't exists. The third parameter aBuffer is a var parameter and I didn't find any such method signature in Indy10.
Any alternative method to call ?
procedure TSenderThread.Execute;
var
vTimeData: TTimeDataRecord;
I: Integer;
FElapsed: Int64;
FTimerElappsed,
vLastTimerElappsed: Int64;
begin
vTimeData.Size := SizeOf(TTimeDataRecord);
vTimeData.ClientCount := 1;
Priority := tpHighest;
FIdUDPClient := TIdUDPClient.Create(nil);
FIdUDPClient.BroadcastEnabled := True;
try
while not (Terminated or Application.Terminated) do
begin
Sleep(1000);
//Measure Time frame
vLastTimerElappsed := FTimerElappsed;
QueryPerformanceCounter(FTimerElappsed);
FElapsed := ((FTimerElappsed-vLastTimerElappsed)*1000000) div FFrequency;
vTimeData.TotalTimeFrame := FElapsed;
if FRunning then
begin
FElapsed := ((FTimerElappsed-FStart)*1000000) div FFrequency;
vTimeData.CurrentMessageTime := FElapsed;
end
else
vTimeData.CurrentMessageTime := 0;
//Copy Values
vTimeData.AccumulatedTime := InterlockedExchange(TimeData.AccumulatedTime,0);
vTimeData.MessageCount := InterlockedExchange(TimeData.MessageCount,0);
for I := 0 to TimeClassMax do
vTimeData.TimeClasses[I] := InterlockedExchange(TimeData.TimeClasses[I],0);
// Calls procedure TIdUDPBase.SendBuffer(AHost: string; const APort: Integer; var ABuffer; const AByteCount: integer);
// This is changed in Indy10, unable to compile
FIdUDPClient.SendBuffer('255.255.255.255', UIPerfPort, vTimeData, TimeData.Size);
end;
finally
FreeAndNil(FIdUDPClient);
end;
end;
EDIT:
vTimeData is basically an array of integers.
TTimeDataRecord = record
Size: Integer; //Size of record structure is transfered and compared for safty reasons.
ClientCount: Integer;
AccumulatedTime: Integer; //This is the accumulated time busy in microseconds
CurrentMessageTime: Integer; //This is the time the current message has been processed. If several computers report a high value at the same time it indicates a freeze!
TotalTimeFrame: Integer; //This is the total time measured in microseconds
MessageCount: Integer;
TimeClasses: array [0..TimeClassMax] of Integer;
end;
you have a method with same name
procedure TIdUDPClient.SendBuffer(const AHost: string; const APort: TIdPort;
const ABuffer: TIdBytes);
Instead of an untyped buffer it expects an array of bytes. What is your data like? You just need to write your data as an array of bytes. Something like:
var
Buffer: TIdBytes;
begin
SetLength(Buffer, YourSizeOfData);
Move(YourData, Buffer[0], YourSizeOfData);
FIdUDPClient.SendBuffer('255.255.255.255', UIPerfPort, Buffer);
end;
But as I said it depends on the type of the data. The approach is ok however.
EDIT:
Now that I can see that you have a record you have two options:
Just move the whole record to array of bytes.
Move(#aRecord, Buffer[0], (6 + TimeClassMax) * SizeOf(Integer));
Have a CopyToBytes method in your record that does the actual copy. More general I guess.
TTimeDataRecord = record
Size: Integer; //Size of record structure is transfered and compared for safty reasons.
ClientCount: Integer;
AccumulatedTime: Integer; //This is the accumulated time busy in microseconds
CurrentMessageTime: Integer; //This is the time the current message has been processed. If several computers report a high value at the same time it indicates a freeze!
TotalTimeFrame: Integer; //This is the total time measured in microseconds
MessageCount: Integer;
TimeClasses: array [0..TimeClassMax] of Integer;
procedure CopyToBytes(var Buffer: TIdBytes);
end
Implementation of the CopyToBytes
procedure TTimeDataRecord.CopyToBytes(var Buffer: TIdBytes);
begin
// copy the data however you see fit
end;