【问题标题】:Send file over socket: SendText() and SendStream() not sending data correctly通过套接字发送文件:SendText() 和 SendStream() 未正确发送数据
【发布时间】:2019-12-28 21:32:48
【问题描述】:

我尝试将 .jpg 文件从 ClientSocket 发送到 ServerSocket,但显然在 SendTextSendStream 函数周围遇到了麻烦,因为结果例如,执行 SendText 后获得的始终为 0。但是存在其他奇怪的事情,那就是当我在发送文件大小之前放置 ShowMessage() 时,SendText 有效(并且收到了大小)但 SendStream 失败-1 的结果。

如何解决?

这是我最后一次尝试>

发件人

uses
  System.Win.ScktComp, Vcl.Imaging.jpeg;

type
  TForm1 = class(TForm)
    ClientSocket1: TClientSocket;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  P: TPicture;
  J: TJpegImage;
  MS: TMemoryStream;
  Sent: Boolean;
begin
  ClientSocket1.Host := '192.168.0.10';
  ClientSocket1.Port := 1234;
  ClientSocket1.Active := True;

  try
    MS := TMemoryStream.Create;
    MS.Position := 0;
    P := TPicture.Create;
    P.Bitmap.LoadFromFile('sent.bmp');
    J := TJpegImage.Create;
    J.Assign(P.Bitmap);
    J.CompressionQuality := 100;
    J.SaveToStream(MS);
    ShowMessage(IntToStr(Round(MS.Size / 1024)));
    ClientSocket1.Socket.SendText(IntToStr(MS.Size) + #0);
    Sent := ClientSocket1.Socket.SendStream(MS);
    ShowMessage(BoolToStr(Sent));
  finally
    MS.Free;
    P.Free;
    J.Free;
  end;
end;

end.

接收者:

uses
  System.Win.ScktComp, Vcl.Imaging.jpeg;

type
  TForm1 = class(TForm)
    Button1: TButton;
    ServerSocket1: TServerSocket;
    procedure ServerSocket1ClientRead(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
ServerSocket1.Port := 1234;
ServerSocket1.Active := True;
end;

procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
  Socket: TCustomWinSocket);
var
  s: string;
  Stream: TMemoryStream;
  Receiving: Boolean;
  stSize: Integer;
  jpg: TJpegImage;
begin
    if Socket.ReceiveLength > 0 then
    begin
      s := Socket.ReceiveText;

      if not Receiving then
      begin
        if Pos(#0, s) > 0 then
        begin
          stSize := strToInt(Copy(s, 1, Pos(#0, s) - 1));
          ShowMessage(IntToStr(Round(stSize / 1024)));
        end
        else
          ;
        Stream := TMemoryStream.Create;
        Receiving := true;
        Delete(s, 1, Pos(#0, s));
      end;
      try
        Stream.Write(AnsiString(s)[1], length(s));
        if Stream.Size = stSize then
        begin
          Stream.Position := 0;
          Receiving := false;
          jpg := TJpegImage.Create;
          jpg.LoadFromStream(Stream);
          jpg.SaveToFile('received.jpg');
          Stream.Free;
        end;
      except
        Stream.Free;
      end;
    end;
  end;

end.

【问题讨论】:

  • 建议:使用十多年未弃用的组件。
  • 另请注意,您的 try..finally 设置方式存在内存泄漏风险。对象创建应该在try之前,而不是之后。如果P.Bitmap.LoadFromFile('sent.bmp'); 失败会怎样? J 永远不会被创建,但您仍然会尝试释放它。您也可以在没有任何图像组件的情况下完成所有这些操作。您只需要使用TFileStream 而不是TMemoryStream。您还会在接收器中泄漏jpg
  • @JerryDodge, "you are risking memory leaks the way your try..finally is set up. Object creation should be before the try, not after it." 谢谢。
  • @JerryDodge 不是...对象变量应该在try 之前初始化,是的。不一定创建。在尝试之前创建它们有其自身的内存泄漏风险,除非您执行trycascade。
  • @KenBourassa,你是对的,我测试了 Jerry 的建议,也有内存泄漏。

标签: sockets delphi client-server sendfile delphi-10.3-rio


【解决方案1】:

您的发件人代码无法处理SendText()SendStream() 发送部分数据的可能性,尤其是在非阻塞模式下。 SendStream() 可能会或可能不会在退出之前释放TStream,您无法知道其中一种方式。 SendText() 在 D2009+ 中无法正确处理 Unicode 字符串。

您的接收器代码没有考虑到ReceiveText() 可能不会也很可能不会在一次读取中接收到所有数据。它可以并且可能需要多个OnClientRead 事件来接收所有数据。或者ReceiveText() 可能会收到您的部分图像数据并错误地尝试将这些字节转换为字符串字符。此外,如果单次读取中的数据不完整,则不会在 OnClientRead 事件之间缓存未处理的字节。

所以,根本不要使用SendText()/SendStream()ReceiveText()!您没有正确使用它们,尤其是在非阻塞模式下。始终改用SendBuf()ReceiveBuf(),并注意它们的返回值,以便知道何时需要再次调用它们来处理更多数据。

试试这样的:

unit Unit1;

interface

uses
  ..., System.Win.ScktComp;

type
  TForm1 = class(TForm)
    ClientSocket1: TClientSocket;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Imaging.jpeg, System.Math;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  ClientSocket1.Host := '192.168.0.10';
  ClientSocket1.Port := 1234;
  ClientSocket1.Active := True;
end;

procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);
var
  B: TBitmap;
  J: TJpegImage;
  MS: TMemoryStream;
  Sent: Boolean;

  function htonll(Value: UInt64): UInt64;
  var
    UL: Windows.ULARGE_INTEGER;
    L: UInt32;
  begin
    UL.QuadPart := Value;
    L := htonl(UL.HighPart);
    LParts.HighPart := htonl(UL.LowPart);
    LParts.LowPart := L;
    Result := UL.QuadPart;
  end;

  function DoSend(Buf: Pointer; BufLen: Integer): Boolean;
  var
    P: PByte;
    BytesSent: Integer;
  begin
    Result := False;
    P := PByte(Buf);
    while BufLen > 0 do
    begin
      BytesSent := Socket.SendBuf(P^, BufLen);
      if BytesSent = -1 then
      begin
        if WSAGetLastError = WSAEWOULDBLOCK then
        begin
          // TODO: use Winsock.select() or TClientSocket.OnWrite to detect when
          // the socket can accept more bytes again...
          Continue;
        end;
        Exit;
      end;
      Inc(P, BytesSent);
      Dec(BufLen, BytesSent);
    end;
    Result := True;
  end;

  function DoSendStream(Stream: TStream): Boolean;
  const
    MaxChunkSize: UInt64 = 1024;
  var
    Size, TempSize: UInt64;
    Buf: array[0..1023] of Byte;
    ChunkSize: Integer;
  begin
    Result := False;
    Size := Strm.Size - Strm.Position;
    TempSize := htonll(Size);
    if not DoSend(@TempSize, SizeOf(TempSize)) then Exit;
    while Size > 0 do
    begin
      ChunkSize := Integer(Min(Size, MaxChunkSize));
      Stream.ReadBuffer(buf[0], ChunkSize);
      if not DoSend(@buf[0], ChunkSize) then Exit;
      Dec(Size, ChunkSize);
    end;
    Result := True;
  end;

begin
  // NOTE: the DoSend...() functions above are written to operate in a blocking
  // manner, even if the socket is set to non-blocking mode!  If you truly want
  // to operate in a non-blocking manner, you need to handle the case where
  // SendBuf() reports a WSAEWOULDBLOCK error by stopping the sending immediately,
  // cache any unsent bytes, exit and let code flow return to the main message loop,
  // and wait for the TClientSocket.OnWrite event to fire before sending the cached
  // and subsequent bytes.  Repeat every time WSAEWOULDBLOCK is reported...

  try
    MS := TMemoryStream.Create;
    try
      J := TJpegImage.Create;
      try
        B := TBitmap.Create;
        try
          B.LoadFromFile('sent.bmp');
          J.Assign(B);
        finally
          B.Free;
        end;
        J.CompressionQuality := 100;
        J.SaveToStream(MS);
      finally
        J.Free;
      end;
      MS.Position := 0;
      //ShowMessage(IntToStr(Round(MS.Size / 1024)));
      Sent := DoSendStream(MS);
    finally
      MS.Free;
    end;
  finally
    Socket.Close;
  end;
  ShowMessage(BoolToStr(Sent));
end;

end.
unit Unit1;

interface

uses
  ... System.Win.ScktComp;

type
  TForm1 = class(TForm)
    Button1: TButton;
    ServerSocket1: TServerSocket;
    procedure ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket);
    procedure ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
    procedure ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Imaging.jpeg, System.Math;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  ServerSocket1.Port := 1234;
  ServerSocket1.Active := True;
end;

type
  SocketState = (ReadingSize, ReadingData);
  TSocketHelper = class
  public
    Buffer: array[0..1023] of Byte;
    BufSize: Integer;
    ExpectedSize: UInt64;
    Stream: TMemoryStream;
    State: SocketState;
    constructor Create;
    destructor Destroy; override;
  end;

constructor TSocketHelper.Create;
begin
  BufSize := 0;
  ExpectedSize := 0;
  Stream := TMemoryStream.Create;
  State := ReadingSize;
end;

destructor TSocketHelper.Destroy;
begin
  Stream.Free;
  inherited;
end;

procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket);
begin
  Socket.Data := TSocketHelper.Create;
end;

procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
begin
  TSocketHelper(Socket.Data).Free;
end;

procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket);
var
  SH: TSocketHelper;
  jpg: TJpegImage;

  function ntohll(Value: UInt64): UInt64;
  var
    UL: Windows.ULARGE_INTEGER;
    L: UInt32;
  begin
    UL.QuadPart := Value;
    L := ntohl(UL.HighPart);
    LParts.HighPart := ntohl(UL.LowPart);
    LParts.LowPart := L;
    Result := UL.QuadPart;
  end;

begin
  SH := TSocketHelper(Socket.Data);
  repeat
    case SH.State of
      ReadingSize: begin
        while SH.BufSize < SizeOf(UInt64) do
        begin
          BytesReceived := Socket.ReceiveBuf(SH.Buffer[SH.BufSize], SizeOf(UInt64) - SH.BufSize);
          if BytesReceived <= 0 then Exit;
          Inc(SH.BufSize, BytesReceived);
        end;
        SH.ExpectedSize := ntohll(PUInt64(@SH.Buffer)^);
        SH.Data.Clear;
        SH.State := ReadingData;
        //ShowMessage(IntToStr(Round(SH.ExpectedSize / 1024)));
        Continue;
      end;

      ReadingData: begin
        while SH.ExpectedSize > 0 do
        begin
          BytesReceived := Socket.ReceiveBuf(SH.Buffer[0], Integer(Min(SH.ExpectedSize, SizeOf(SH.Buffer))));
          if BytesReceived <= 0 then Exit;
          Dec(SH.ExpectedSize, BytesReceived);
          SH.Data.WriteBuffer(SH.Buffer[0], BytesReceived);
        end;
        try
          jpg := TJpegImage.Create;
          try
            SH.Data.Position := 0;
            jpg.LoadFromStream(SH.Data);
            jpg.SaveToFile('received.jpg');
          finally
            jpg.Free;
          end;
        finally
          SH.Data.Clear;
          SH.BufSize := 0;
          SH.State := ReadingSize;
        end;
        Continue;
      end;
    end;
  until False;
end;

end.

【讨论】:

  • “SendText() 在 D2009+ 中无法正确处理 Unicode 字符串。”为什么使用那些旧东西正是一个很好的例子。它们早于 Unicode 实施。
  • @JerryDodge,在下一个项目中,我将开始使用IdTcpClient/IdTcpServer :-),
  • @BrowJr 根据您的具体用例,您甚至可以从切换到 HTTP 中受益,使用更多内置功能会变得更容易。
  • @Remy Lebeau,谢谢,我测试了你的代码,并且仅在存在 ShowMesssage() 的情况下工作,如果删除 ShowMesssage() 不起作用。我仍然记得,当我使用 DXE5 时,我的问题代码已经有效,现在使用 D10 Rio 无效。我会检查我的部分麻烦是否是IDE版本,在其他电脑上重新安装和编译DXE5。
  • @BrowJr "仅在存在 ShowMesssage() 的情况下工作-在哪一边?我发布的代码应该可以在客户端没有任何消息泵的情况下正常工作,并在服务器端使用主线程消息泵。但是,在客户端,当使用非阻塞模式时,您需要等待OnConnect 事件才能发送任何内容。我已经更新了我的答案以表明这一点。
猜你喜欢
  • 1970-01-01
  • 2016-12-28
  • 1970-01-01
  • 2015-05-27
  • 2011-02-24
  • 2012-02-12
  • 2012-07-12
相关资源
最近更新 更多