【问题标题】:Sending a Dynamic array (Inside a record) through Socket?通过 Socket 发送动态数组(在记录内)?
【发布时间】:2012-12-04 10:24:14
【问题描述】:

我正在尝试将记录从服务器传输到客户端,直接使用 .SendBuf()。

然而,这条记录有一个动态数组的成员,我在某处(在 SOF 中)读到,在发送记录时,成员必须是 STATIC(固定长度),但问题是......我无法确定我将发送多少个参数(将来)。

我该如何解决这个问题?

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;

我是从我读过它的地方发现的,这里: ReceiveBuf from TCustomWinSocket won't work with dynamic arrays for the buffer

【问题讨论】:

  • 滚动您自己的 RPC 是一项非常艰巨的任务。最好使用众所周知的 RPC。
  • @DavidHeffernan 实际上到目前为止我取得了不错的成绩;],它运作良好..

标签: delphi sockets winsock


【解决方案1】:

您将无法按原样发送记录,因此实际上您甚至根本不需要使用记录。您必须将数据序列化为适合通过网络传输的平面格式。例如发送字符串时,先发送字符串长度,再发送字符串数据。同样,发送数组时,请在发送数组项之前发送数组长度。至于项目本身,由于TValue 是动态的,因此您也必须将其序列化为平面格式。

在发送端尝试这样的操作:

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;

然后在接收端:

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;

话虽如此,请注意套接字编程比这个简单示例显示的更复杂。您必须进行适当的错误处理。您必须考虑部分数据的发送和接收。如果您使用的是非阻塞套接字,如果套接字进入阻塞状态,那么您必须等待它再次进入可读/可​​写状态,然后才能尝试读取/写入仍然挂起的数据。你还没有这样做。你需要给自己找一本关于有效套接字编程的好书。

更新:如果您尝试利用套接字组件的OnReadOnWrite 事件,则必须采取不同的方法:

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;

然后在接收端:

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;

【讨论】:

  • 我知道我没有按原样发送记录,我在服务器/客户端上都有相同的记录,我所做的是 FIRST 成员是一个 BYTE 类型,它包含记录 ID(告诉客户端操作的去向)我在两边都有相同的记录,所以假设我将记录发送给客户端,我只在第一个成员中填充了它的第二个成员(这是 ID并且是第一次阅读)然后我知道如何处理记录
  • 您的问题中没有显示任何内容。下一次,当你寻求帮助时,请说明你实际在做什么。
  • 我还要说 TValue 本身不是一个简单的类型,但有时也是指向真实数据的辅助指针。所以,@0x90,请进行序列化。例如blog.synopse.info/post/2011/03/12/TDynArray-and-Record-compare/… 另一个例子是 JSON progdigy.com/?page_id=6
  • TCP 是一个字节流。 send()recv() 之间没有 1:1 的关系,就像 UDP 一样。 recv() 可以返回比请求更少的字节。您必须循环调用它,直到您读取所有预期的字节。使用非阻塞套接字时,当套接字有数据要读取时会发生OnRead 事件。如果您读完可用的内容并需要更多字节,则必须等到OnRead 事件再次发生。
  • 每次读取时,如果您有完整的“消息”(基于您正在实施的协议 - 在这种情况下为 HTTP,因此您必须查找 HTTP 标头和正文,然后解析标头以了解要读取多少额外字节),然后对其进行处理并保存剩余的任何字节,以便它们可以与以后的字节一起用于处理下一个“消息”。
【解决方案2】:

正如某些 cmets 中提到的,将您的记录序列化为流,然后通过网络发送流内容。我在一些项目中使用kbLib,效果非常好。 您可以在记录中使用任何动态类型,例如字符串、数组。

小例子:

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;

【讨论】:

  • 这是我试图避免的,使用 3rd 方组件。
  • 它不是一个真正的组件,它是一个单元...如果您真的想自己动手,我建议您查看这个库,因为它公开了执行此操作所需的所有位...跨度>
  • 我找到了这个链接,在那里他展示了如何将 JSON 反序列化回 TValue,太棒了...delphipraxis.net/1186299-post3.html
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-04
  • 1970-01-01
  • 2012-08-27
  • 2012-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多