1

i'm trying to transfer a record from server to client, directly using .SendBuf().

however, this record has a member which is a dynamic array, and i have read somewhere (here in SOF) that when sending records, the members must be STATIC (fixed-length), but the problem is... i cannot determine how many arguments i would send (in the future).

how can i solve this problem ?

procedure TServerClass.SendBufToSocket(const vmName: TVMNames; const vmArgs: Array of TValue);
var
  // this record is sent to client
  // vmName = method to be called [in]
  // vmArgs = Argument for the method [in, optional]
  BufRec: packed record
    vmName: array[0..49] of char;
    vmArgs: Array of TValue;
  end;

  s: string;
  i: integer;
begin
  // convert enum method name to string
  s:= GetEnumName(TypeInfo(TVMNames), Integer(vmName));

  // copy method name to record
  lstrcpy(BufRec.vmName, pChar(s));

  // copy arg array to record
  SetLength(BufRec.vmArgs, length(vmArgs));

  for i:=0 to high(vmArgs)
    do BufRec.vmArgs[i] := vmArgs[i];

  // send record
  ServerSocket.Socket.Connections[idxSocket].SendBuf(PByte(@BufRec)^, SizeOf(BufRec));
end;

I found out from where i've read it, here: ReceiveBuf from TCustomWinSocket won't work with dynamic arrays for the buffer

Community
  • 1
  • 1

2 Answers2

6

You will not be able to send the record as-is, so in fact you don't even need to use a record at all. You must serialize your data into a flat format that is suitable for transmission over a network. For example, when sending a string, send the string length before sending the string data. Likewise, when sending an array, send the array length before sending the array items. As for the items themselves, since TValue is dynamic, you have to serialize it into a flat format as well.

Try something like this on the sending side:

procedure TServerClass.SendBufToSocket(const vmName: TVMNames; const vmArgs: Array of TValue);
var
  I: integer;

  procedure SendRaw(Data: Pointer; DataLen: Integer);
  var
    DataPtr: PByte;
    Socket: TCustomWinSocket;
    Sent, Err: Integer;
  begin
    DataPtr := PByte(Data);
    Socket := ServerSocket.Socket.Connections[idxSocket];
    while DataLen > 0 do
    begin
      Sent := Socket.SendBuf(DataPtr^, DataLen);
      if Sent > 0 then
      begin
        Inc(DataPtr, Sent);
        Dec(DataLen, Sent)
      end else
      begin
        Err := WSAGetLastError();
        if Err <> WSAEWOULDBLOCK then
          raise Exception.CreateFmt('Unable to sent data. Error: %d', [Err]);
        Sleep(10);
      end;
    end;
  end;

  procedure SendInteger(Value: Integer);
  begin
    Value := htonl(Value);
    SendRaw(@Value, SizeOf(Value));
  end;

  procedure SendString(const Value: String);
  var
    S: UTF8string;
    Len: Integer;
  begin
    S := Value;
    Len := Length(S);
    SendInteger(Len);
    SendRaw(PAnsiChar(S), Len);
  end;

begin
  SendString(GetEnumName(TypeInfo(TVMNames), Integer(vmName)));
  SendInteger(Length(vmArgs));
  for I := Low(vmArgs) to High(vmArgs) do
    SendString(vmArgs[I].ToString);
end;

And then on the receiving side:

type
  TValueArray := array of TValue;

procedure TServerClass.ReadBufFromSocket(var vmName: TVMNames; var vmArgs: TValueArray);
var
  Cnt, I: integer;
  Tmp: String;

  procedure ReadRaw(Data: Pointer; DataLen: Integer);
  var
    DataPtr: PByte;
    Socket: TCustomWinSocket;
    Read, Err: Integer;
  begin
    DataPtr := PByte(Data);
    Socket := ClientSocket.Socket;
    while DataLen > 0 do
    begin
      Read := Socket.ReceiveBuf(DataPtr^, DataLen);
      if Read > 0 then
      begin
        Inc(DataPtr, Read);
        Dec(DataLen, Read);
      end
      else if Read = 0 then
      begin
        raise Exception.Create('Disconnected');
      end else
      begin
        Err := WSAGetLastError();
        if Err <> WSAEWOULDBLOCK then
          raise Exception.CreateFmt('Unable to read data. Error: %d', [Err]);
        Sleep(10);
      end;
    end;
  end;

  function ReadInteger: Integer;
  begin
    ReadRaw(@Result, SizeOf(Result));
    Result := ntohl(Result);
  end;

  function ReadString: String;
  var
    S: UTF8String;
    Len: Integer;
  begin
    Len := ReadInteger;
    SetLength(S, Len);
    ReadRaw(PAnsiChar(S), Len);
    Result := S;
  end;

begin
  vmName := TVMNames(GetEnumValue(TypeInfo(TVMNames), ReadString));
  Cnt := ReadInteger;
  SetLength(vmArgs, Cnt);
  for I := 0 to Cnt-1 do
  begin
    Tmp := ReadString;
    // convert to TValue as needed...
    vmArgs[I] := ...;
  end;
end;

With that said, note that socket programming is more complex than this simple example shows. You have to do proper error handling. You have to account for partial data sends and receives. And if you are using non-blocking sockets, if the socket enters a blocking state then you have to wait for it to enter a readable/writable state again before you can attempt to read/write data that is still pending. You are not doing any of that yet. You need to get yourself a good book on effective socket programming.

Update: if you are trying to utilize the OnRead and OnWrite events of the socket components, you have to take a different approach:

procedure TServerClass.ClientConnect(Sender: TObject; Socket: TCustomWinSocket);
begin
  Socket.Data := TMemoryStream.Create;
end;

procedure TServerClass.ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
begin
  TMemoryStream(Socket.Data).Free;
  Socket.Data := nil;
end;

procedure TServerClass.ClientWrite(Sender: TObject; Socket: TCustomWinSocket);
var
  OutBuffer: TMemoryStream;
  Ptr: PByte;
  Sent, Len: Integer;
begin
  OutBufer := TMemoryStream(Socket.Data);
  if OutBuffer.Size = 0 then Exit;

  OutBuffer.Position := 0;
  Ptr := PByte(OutBuffer.Memory);

  Len := OutBuffer.Size - OutBuffer.Position;
  while Len > 0 do
  begin
    Sent := Socket.SendBuf(Ptr^, Len);
    if Sent <= 0 then Break;
    Inc(Ptr, Sent);
    Dec(Len, Sent)
  end;

  if OutBuffer.Position > 0 then
  begin
    if OutBuffer.Position >= OutBuffer.Size then
      OutBuffer.Clear
    else
    begin
      Move(Ptr^, OutBuffer.Memory^, Len);
      OutBuffer.Size := Len;
    end;
  end;
end;

procedure TServerClass.SendBufToSocket(const vmName: TVMNames; const vmArgs: Array of TValue);
var
  I: integer;
  Socket: TCustomWinSocket;
  OutBuffer: TMemoryStream;

  procedure SendRaw(Data: Pointer; DataLen: Integer);
  var
    DataPtr: PByte;
    Sent: Integer;
  begin
    if DataLen < 1 then Exit;
    DataPtr := PByte(Data);
    if OutBuffer.Size = 0 then
    begin
      repeat
        Sent := Socket.SendBuf(DataPtr^, DataLen);
        if Sent < 1 then Break;
        Inc(DataPtr, Sent);
        Dec(DataLen, Sent)
      until DataLen < 1;
    end;
    if DataLen > 0 then
    begin
      OutBuffer.Seek(0, soEnd);
      OutBuffer.WriteBuffer(DataPtr^, DataLen);
    end;
  end;

  procedure SendInteger(Value: Integer);
  begin
    Value := htonl(Value);
    SendRaw(@Value, SizeOf(Value));
  end;

  procedure SendString(const Value: String);
  var
    S: UTF8string;
    Len: Integer;
  begin
    S := Value;
    Len := Length(S);
    SendInteger(Len);
    SendRaw(PAnsiChar(S), Len);
  end;

begin
  Socket := ServerSocket.Socket.Connections[idxSocket];
  OutBuffer := TMemoryStream(Socket.Data);

  SendString(GetEnumName(TypeInfo(TVMNames), Integer(vmName)));
  SendInteger(Length(vmArgs));
  for I := Low(vmArgs) to High(vmArgs) do
    SendString(vmArgs[I].ToString);
end;

And then on the receiving side:

procedure TServerClass.ClientConnect(Sender: TObject; Socket: TCustomWinSocket);
begin
  Socket.Data := TMemoryStream.Create;
end;

procedure TServerClass.ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
begin
  TMemoryStream(Socket.Data).Free;
  Socket.Data := nil;
end;

procedure TServerClass.ClientRead(Sender: TObject; Socket: TCustomWinSocket);
var
  InBuffer: TMemoryStream;      
  Ptr: PByte;
  OldSize, Pos, Read: Integer;

  function HasAvailable(DataLen: Integer): Boolean;
  being
    Result := (InBuffer.Size - InBuffer.Position) >= DataLen;
  end;

  function ReadInteger(var Value: Integer);
  begin
    Result := False;
    if HasAvailable(SizeOf(Integer)) then
    begin
      InBuffer.ReadBuffer(Value, SizeOf(Integer));
      Value := ntohl(Value);
      Result := True;
    end;
  end;

  function ReadString(var Value: String);
  var
    S: UTF8String;
    Len: Integer;
  begin
    Result := False;
    if not ReadInteger(Len) then Exit;
    if not HasAvailable(Len) then Exit;
    SetLength(S, Len);
    InBuffer.ReadBuffer(PAnsiChar(S)^, Len);
    Value := S;
    Result := True;
  end;

  function ReadNames: Boolean;
  var
    S: String;
    vmName: TVMNames;
    vmArgs: TValueArray;
  begin
    Result := False;
    if not ReadString(S) then Exit;
    vmName := TVMNames(GetEnumValue(TypeInfo(TVMNames), S));
    if not ReadInteger(Cnt) then Exit;
    SetLength(vmArgs, Cnt);
    for I := 0 to Cnt-1 do
    begin
      if not ReadString(S) then Exit;
      // convert to TValue as needed...
      vmArgs[I] := ...;
    end;
    // use vmArgs as needed...
    Result := True;
  end;

begin
  InBuffer := TMemoryStream(Socket.Data);

  Read := Socket.ReceiveLength;
  if Read <= 0 then Exit;

  OldSize := InBuffer.Size;
  InBuffer.Size := OldSize + Read;

  try
    Ptr := PByte(InBuffer.Memory);
    Inc(Ptr, OldSize);
    Read := Socket.ReceiveBuf(Ptr^, Read);
  except
    Read := -1;
  end;

  if Read < 0 then Read := 0;
  InBuffer.Size := OldSize + Read;
  if Read = 0 then Exit;

  InBuffer.Position := 0;

  repeat
    Pos := InBuffer.Position;
  until not ReadNames;

  InBuffer.Position := Pos;
  Read := InBuffer.Size - InBuffer.Position;
  if Read < 1 then
    InBuffer.Clear
  else
  begin
    Ptr := PByte(InBuffer.Memory);
    Inc(Ptr, InBuffer.Position);
    Move(Ptr^, InBuffer.Memory^, Read);
    InBuffer.Size := Read;
  end;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • i'm aware that i'm not sending a record as-is, i have the SAME record on both server/client, and what i did is the FIRST member is a BYTE type which holds the record ID (which tells the client where the manipulation goes) i have the SAME record on both sides, so lets say i send the record to the client and i only filled the 2nd member of it, in the 1st member (which is the ID and is first read) i then know what to do with the record –  Nov 07 '12 at 01:32
  • 6
    You did not show any of that in your question. Next time, please explain what you are actually doing when you ask for help. – Remy Lebeau Nov 07 '12 at 01:55
  • I would also say that TValue itself is not a simple type but sometimes a secondary pointer to real data as well. So, @0x90, please do serialization. For example http://blog.synopse.info/post/2011/03/12/TDynArray-and-Record-compare/load/save-using-fast-RTTI and another exaple is JSON http://www.progdigy.com/?page_id=6 – Arioch 'The Nov 07 '12 at 05:43
  • I appreciate your time and dedication for this answer, i thank you. –  Nov 11 '12 at 22:09
  • I just want to ask one question, in the other side, how do i accept this transmitted data ? –  Nov 11 '12 at 22:12
  • On client (read) side, the OnRead event is triggered like 17 times, why ? the first READ is correct, then the 16 rest are 0 bytes.. –  Nov 12 '12 at 22:23
  • Also, how do i convert the string back to its original TValue ? –  Nov 12 '12 at 22:38
  • Then you are not reading the inbound data correctly. Did you take into account the comments I made about "partial data" and "multiple OnRead" events? As far `TValue`, it does not support conversions from `String`, so you will have to convert the data manually. You will have to send each `TValue`'s actual data type along with its data value so the receiver knows what data type to parse each `String` value into. – Remy Lebeau Nov 12 '12 at 23:07
  • I found out a nice way to send TValue record, it has a methid called "ExtractRawData" which you can see here: http://docwiki.embarcadero.com/Libraries/XE3/en/System.Rtti.TValue.ExtractRawData i'm using this and on the other side rebuilding the record. –  Nov 13 '12 at 01:32
  • i am also sending the TTypeInfo of course... now i have a new problem, and is tkString -_- i'm using TValue.Make to rebuild it, i'm sending a record: `InvokeRec : packed record Method: Array[0..19] of Char; ArgRawSize: Integer; ArgTypeInf: TTypeInfo; ArgRawData: Array[0..255] of Byte; end;` –  Nov 13 '12 at 09:37
  • `TTypeInfo` is variable length and contains pointers to other data, so you can't transmit it as-is and have it be meaningful on the receiving end. You will have to transmit the `TValue.TypeInfo.Name` string and then use `TRttiContext.FindType(Name).Handle` to get the appropriate `TTypeInfo` on the receiving end to pass to `TValue.Make`. – Remy Lebeau Nov 13 '12 at 19:15
  • i'm still having trouble here with reading the inbound data correctly due to multiple OnRead events... i read it correctly when OnRead (in recv side) on first fire, then on 2nd fire i get "out of memory", i debugged and saw that ReadInteger receives a large number (56552452435) and the exception happens inside ReadString SetLength. can you help me here ? –  Nov 28 '12 at 15:24
  • That means you are not reading the inbound data correctly, likely as a result of inadequate error handling. The code I gave you earlier omitted error handling for brevity. I have added it now. – Remy Lebeau Nov 28 '12 at 19:10
  • the earlier code you showed me (before your update) i just tried sending a simple String and when Receiving it fired OnClientRead twice.. 1st catch it read all buffer correctly, 2nd was pointless since there was no valid data in the recv buffer... if i add a Sleep(1000) in the beginning of the OnClientRead, it's like i'm telling it to wait for all the data to get buffered (right?) and then it works PERFECT (fires once) –  Nov 28 '12 at 19:22
  • The `OnRead` event is triggered when the socket enters a readable state, which happens for one of two reasons - new data has arrived in the socket's receive buffer and is available for reading, or the other party has gracefully closed the socket on their end. You have to actually read from the socket and pay attention to the result of the read in order to differentiate which is the case. If the read returns 0 bytes, then the socket has been closed. – Remy Lebeau Nov 28 '12 at 19:38
  • @RemyLebeau, i just tried your new code, the client (recv side) gets stuck, probably due to the loop.. it reads the data correctly, but the loop is the problem.. i debugged and in this line: Read := Socket.ReceiveBuf(DataPtr^, DataLen); Read = -1 (after reading all data), and in your code there's no condition that checks if it's -1 and does something. –  Nov 28 '12 at 19:51
  • i found a semi working solution for this, checking the buf length BEFORE actually READING from the recv buf. BufLen := Socket.ReceiveLength; // OutputDebugString(PChar(IntToStr(BufLen))); if (BufLen = 0) then Exit; // read from recv buffer Buffer := ReadString; –  Nov 28 '12 at 20:48
  • Yes, my reading code does handle the condition where `Read = -1`. It is the last `else` block that calls `WSAGetLastError()` and checks for the `WSAEWOULDBLOCK` error. – Remy Lebeau Nov 28 '12 at 22:19
  • If `ReceiveLength()` returns 0 in an `OnRead` event then the connection has likely been closed. Better to ignore `ReceiveLength()` and let `ReceiveBuf()` tell you for sure. – Remy Lebeau Nov 28 '12 at 22:20
  • but isn't it better to check Read <= 0 and then BREAK on it ? your WHILE loop has a condition, there for it does not need IF conditions in it... if you were to use WHILE TRUE then it would be right to use it like that. –  Nov 29 '12 at 13:36
  • If `Read==0`, the socket was disconnected, and I raise an exception. if `Read==-1`, a socket error occurred. If the error is not `WSAEWOULDBLOCK` then I raise an exception, otherwise it is not a fatal error and I retry the same read after a short delay. The loop is broken only when `DataLen` reaches 0. So where do you think a logic hole exists? – Remy Lebeau Nov 29 '12 at 18:32
  • Anyhow, can you explain me more about this "partial reading" and "multiple OnRead" ? –  Nov 30 '12 at 11:42
  • 1
    TCP is a byte stream. There is no 1:1 relationship between `send()` and `recv()`, like with UDP. `recv()` can return fewer bytes than requested. You have to call it in a loop until you read all of the bytes you are expecting. When using a non-blocking socket, the `OnRead` event occurs when the socket has data to read. If you finish reading what is available and need more bytes, you have to wait until the `OnRead` event occurs again. – Remy Lebeau Nov 30 '12 at 20:04
  • 1
    Each time you do a read, if you have a complete "message" (based on the protocol you are implementing - in this case HTTP, so you have to look for the line breaks between HTTP headers and body, and then parse the headers to know how many additional bytes to read), then process it and save whatever bytes are left over so they can be used with later bytes to handle the next "message". – Remy Lebeau Nov 30 '12 at 20:05
  • Like I said in my original answer: "You need to get yourself a good book on effective socket programming." – Remy Lebeau Nov 30 '12 at 20:06
  • that explanation is very good and is what i wanted to hear, thank you. –  Dec 01 '12 at 19:49
  • @RemyLebeau, your solution (code) is PERFECT !!!! i used DebugView and analyze it, it works PERFECTLY and now I FULLY UNDERSRAND IT :D !!! from my POV analyzation: `Server: - OnRead (1st fire), RecvLen = 4 Byte (SendInteger) - OnRead (2nd fire), RecvLen = X Byte (SendString)` –  Dec 02 '12 at 19:22
  • Both the Integer and String could arrive in the same OnRead firing, the Integer and/or String could be split between multiple OnRead firings, or any combination thereof. Worst case scenario, it is entirely possible (though unlikely) to only get 1 byte per OnRead firing. That is why you have to buffer everything you receive, and only process complete messages from your buffer as needed. You don't know how the messages are split up during transmission. You are only guaranteed that bytes arrive in the same order they were sent. – Remy Lebeau Dec 03 '12 at 03:15
  • You have a tiny bug... in ReadIneger i wouldn't recommend using ReadRaw since it could be called twice (due to Multiple OnRead) and get stuck in an endless loop of result -1... i have debugged this case... that's why my GUI got stuck... it kept waiting for a data of 4 byte in size... in order to avoid this, i have added this line in ReadRaw (after `Sleep(10)`) `if (DataLen = 4) then Abort;` –  Dec 04 '12 at 00:46
  • Under normal conditions, you cannot get multiple `OnRead` events overlapping each other for the same socket. The only way that can happen is if you are manually pumping the calling thread's message queue when you should not be. There is nothing wrong with the implementation as I showed. In fact, the implementation I showed intentionally does not use the `OnRead` event at all. If you are trying to mix the `OnRead` event with the code I showed, it will not work and has to be completely re-written. I will update my answer with an example... – Remy Lebeau Dec 04 '12 at 01:23
  • Awesome EDIT mate !!!, i will check into it... my infrastructure of the program i am working on is so a little complex right now it will take me some time to apply the changes you just showerd me, but i will take it into account... –  Dec 04 '12 at 02:28
0

As mentioned in some comments, serialize your record to a stream and then send the stream contents over the wire. I use kbLib in some of my projects and it works really good. You can use any dynamic type like strings, arrays in your record.

Small example:

type 
  TMyRecord = record
    str : string;
  end;

procedure Test;

var
 FStream : TMemoryStream;
 MYrecord : TMyRecord;
 MYrecord1 : TMyRecord;

begin
 FStream := TMemoryStream.Create;
 try
  MyRecord.Str := 'hello world';
  // save record to stream 
  TKBDynamic.WriteTo(FStream, MyRecord, TypeInfo(TMyRecord)); 
  FStream.Position := 0;
  // read record from stream
  TKBDynamic.ReadFrom(FStream, MyRecord1, TypeInfo(TMyRecord));  
  If MyRecord1.Str <> MyRecord.Str then
   ShowMessage('this should not happen!');
  finally
   FStream.Free; 
  end;
end;
whosrdaddy
  • 11,720
  • 4
  • 50
  • 99
  • that's what i'm trying to avoid, using 3rd party components. –  Nov 07 '12 at 15:01
  • it's not really a component, it's one unit... If you really want to roll your own, I suggest looking at this lib as it exposes all the needed bits to do this... – whosrdaddy Nov 07 '12 at 15:22
  • I found this link, in there he shows how to deserialize JSON back to TValue, amazing... http://www.delphipraxis.net/1186299-post3.html –  Nov 28 '12 at 17:10