【问题标题】:Error to receive file on socket inside a thread在线程内的套接字上接收文件时出错
【发布时间】:2017-12-30 03:53:58
【问题描述】:

我无法接收包含 PNG 文件的字节数组。 当代码在OnClientRead 事件中执行时,它工作正常,当为线程传输时,发生MemoryStream 的错误,它说:

扩展内存流时内存不足。

此时:

if SD.State = ReadingSize then

我想知道如何解决这个特定的问题,以及如何检查我收到的数据是否包含文件或简单的String

代码:

type
  TSock_Thread = class(TThread)
  private
    Socket: TCustomWinSocket;
  public
    constructor Create(aSocket: TCustomWinSocket);
    procedure Execute; override;
  end;
  
type
  TInt32Bytes = record
    case Integer of
      0: (Bytes: array[0..SizeOf(Int32)-1] of Byte);
      1: (Value: Int32);
  end;

  TSocketState = (ReadingSize, ReadingStream);

  TSocketData = class
  public
    Stream: TMemoryStream;
    Png: TPngImage;
    State: TSocketState;
    Size: TInt32Bytes;
    Offset: Integer;
    constructor Create;
    destructor Destroy; override;
  end;
  
 { ... }

constructor TSock_Thread.Create(aSocket: TCustomWinSocket);
begin
  inherited Create(true);
  Socket := aSocket;
  FreeOnTerminate := true;
end;

procedure TSock_Thread.Execute;
var
  s: String;
  BytesReceived: Integer;
  BufferPtr: PByte;
  SD: TSocketData;
  Item: TListItem;
begin
  inherited;

  while Socket.Connected do
  begin
    if Socket.ReceiveLength > 0 then
    begin
      s := Socket.ReceiveText;

     { SD := TSocketData(Socket.Data);

      if SD.State = ReadingSize then
      begin
        while SD.Offset < SizeOf(Int32) do
        begin
          BytesReceived := Socket.ReceiveBuf(SD.Size.Bytes[SD.Offset],
            SizeOf(Int32) - SD.Offset);
          if BytesReceived <= 0 then
            Exit;
          Inc(SD.Offset, BytesReceived);
        end;
        SD.Size.Value := ntohl(SD.Size.Value);
        SD.State := ReadingStream;
        SD.Offset := 0;
        SD.Stream.Size := SD.Size.Value;
      end;

      if SD.State = ReadingStream then
      begin
        if SD.Offset < SD.Size.Value then
        begin
          BufferPtr := PByte(SD.Stream.Memory);
          Inc(BufferPtr, SD.Offset);
          repeat
            BytesReceived := Socket.ReceiveBuf(BufferPtr^,
              SD.Size.Value - SD.Offset);
            if BytesReceived <= 0 then
              Exit;
            Inc(BufferPtr, BytesReceived);
            Inc(SD.Offset, BytesReceived);
          until SD.Offset = SD.Size.Value;
        end;
        try
          SD.Stream.Position := 0;
          SD.Png.LoadFromStream(SD.Stream);
          SD.Stream.Clear;
        except
          SD.Png.Assign(nil);
        end;
        Item := Form1.ListView1.Selected;
        if (Item <> nil) and (Item.Data = Socket) then
          Form1.img1.Picture.Graphic := SD.Png;
        SD.State := ReadingSize;
        SD.Offset := 0;
      end;  }

    end;
    Sleep(100);
  end;

end;

procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket);
var
  TST: TSock_Thread;
begin
  TST := TSock_Thread.Create(Socket);
  TST.Resume;
end;

更新:

答案中的代码对我不起作用,因为ServerType=stThreadBlocking 阻止了所有客户端与服务器的连接。正因为如此,我正在寻找这样的东西(ServerType=stNonBlockingTThreadOnAccept 事件):

type
  TSock_Thread = class(TThread)
  private
    Png: TPngImage;
    Socket: TCustomWinSocket;
  public
    constructor Create(aSocket: TCustomWinSocket);
    procedure Execute; override;
     procedure PngReceived;
  end;
  
// ...

// ===============================================================================

constructor TSock_Thread.Create(aSocket: TCustomWinSocket);
begin
  inherited Create(true);
  Socket := aSocket;
  FreeOnTerminate := true;
end;

// ===============================================================================

procedure TSock_Thread.PngReceived;
var
  Item: TListItem;
begin
  Item := Form1.ListView1.Selected;
  if (Item <> nil) and (Item.Data = Socket) then
    Form1.img1.Picture.Graphic := Png;
end;

procedure TSock_Thread.Execute;
var
  Reciving: Boolean;
  DataSize: Integer;
  Data: TMemoryStream;
  s, sl: String;
begin
  inherited;
  while Socket.Connected do
  begin
    if Socket.ReceiveLength > 0 then
    begin
      s := Socket.ReceiveText;
      if not Reciving then
      begin

        SetLength(sl, StrLen(PChar(s)) + 1);
        StrLCopy(@sl[1], PChar(s), Length(sl) - 1);
        DataSize := StrToInt(sl);
        Data := TMemoryStream.Create;
        Png := TPngImage.Create;
        Delete(s, 1, Length(sl));
        Reciving := true;
      end;
      try
        Data.Write(s[1], Length(s));
        if Data.Size = DataSize then
        begin
          Data.Position := 0;
          Png.LoadFromStream(Data);
          Synchronize(PngReceived);
          Data.Free;
          Reciving := false;
        end;
      except
        Png.Assign(nil);
        Png.Free;
        Data.Free;
      end;
    end;
    Sleep(100);
  end;

end;

procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket);
var
  TST: TSock_Thread;
begin
  TST := TSock_Thread.Create(Socket);
  TST.Resume;
end;

此代码在此行有数据转换错误: DataSize := StrToInt(sl);

我该如何解决这个问题?

【问题讨论】:

    标签: multithreading sockets delphi


    【解决方案1】:

    如何解决这个具体的问题

    您没有按照应有的方式使用TServerSocket 线程。

    如果你想在stThreadBlocking模式下使用TServerSocket(在stNonBlocking模式下如何使用TServerSocket,请参阅my other answer),正确的方法是:

    如果您不这样做,TServerSocket 将创建自己的默认线程(以在主线程中触发 OnClient(Read|Write) 事件),这会干扰您的手动线程。

    另外,您不需要我在my answeryour other question 中向您展示的状态机。那是针对事件驱动的代码。线程 I/O 代码可以改为线性编写。

    试试这样的:

    type
      TSock_Thread = class(TServerClientThread)
      private
        Png: TPngImage;
        procedure PngReceived;
      protected
        procedure ClientExecute; override;
      end;
    
    type
      TInt32Bytes = record
        case Integer of
          0: (Bytes: array[0..SizeOf(Int32)-1] of Byte);
          1: (Value: Int32);
      end;
    
    procedure TSock_Thread.ClientExecute;
    var
      SocketStrm: TWinSocketStream;
      Buffer: TMemoryStream;
      Size: TInt32Bytes;
      Offset: Integer;
      BytesReceived: Integer;
      BufferPtr: PByte;
    begin
      SocketStrm := TWinSocketStream.Create(ClientSocket, 5000);
      try
        Buffer := TMemoryStream.Create;
        try
          Png := TPngImage.Create;
          try
            while ClientSocket.Connected do
            begin
              if not SocketStrm.WaitForData(100) then Continue;
    
              Offset := 0;
              while Offset < SizeOf(Int32) do
              begin
                BytesReceived := SocketStrm.Read(Size.Bytes[Offset], SizeOf(Int32) - Offset);
                if BytesReceived <= 0 then Exit;
                Inc(Offset, BytesReceived);
              end;
              Size.Value := ntohl(Size.Value);
              Buffer.Size := Size.Value;
              BufferPtr := PByte(Buffer.Memory);
    
              Offset := 0;
              while Offset < Size.Value do
              begin
                BytesReceived := SocketStrm.Read(BufferPtr^, Size.Value - Offset);
                if BytesReceived <= 0 then Exit;
                Inc(BufferPtr, BytesReceived);
                Inc(Offset, BytesReceived);
              end;
    
              Buffer.Position := 0;
              try
                Png.LoadFromStream(Buffer);
              except
                Png.Assign(nil);
              end;
    
              Synchronize(PngReceived);
            end;
          finally
            Png.Free;
          end;
        finally
          Buffer.Free;
        end;
      finally
        SocketStrm.Free;
      end;
    end;
    
    procedure TSock_Thread.PngReceived;
    var
      Item: TListItem;
    begin
      Item := Form1.ListView1.Selected;
      if (Item <> nil) and (Item.Data = ClientSocket) then
        Form1.img1.Picture.Graphic := Png;
    end;
    
    procedure TForm1.ServerSocket1GetThread(Sender: TObject; ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread);
    begin
      SocketThread := TSock_Thread.Create(False, ClientSocket);
    end;
    

    如何检查我收到的数据是否包含文件或简单字符串?

    客户端需要将该信息发送到您的服务器。在发送实际数据之前,您已经发送了一个值来指定数据大小。您还应该在数据前面加上一个值来指定数据的类型。然后就可以根据需要对数据进行类型处理了。

    【讨论】:

    • Then you can handle the data according to its type as needed. 然后基本上我已经将String 命令发送到服务器并检查例如:if Pos('{file}', Socket.ReceiveText) &gt; 0 then begin // handle data end; if Pos('{string}', Socket.ReceiveText) &gt; 0 then begin // handle data end;
    • 我认为Socket 有一些属性可以检查是否正在接收文件或字符串:-)。
    • @JeffersonFarias 不要使用ReceiveText()。它没有消息边界的概念。由于所使用的协议本质上是二进制的,因此只需发送一个包含类型的额外字节,例如:&lt;1 byte type&gt;&lt;4 byte size&gt;&lt;N byte data&gt;。如果type 是字符串,data 是字节编码字符,如 UTF-8。否则data 是一个文件/PNG。等等。当ServerType=stThreadBlocking 时,OnGetThread 对我来说效果很好
    • @JeffersonFarias 近 20 年来我一直在研究 VCL 的内部工作原理。我在许多项目中都使用了TServerSocketTClientSocket,在这两种模式下。你认为你比我更了解,然后自己去想办法玩得开心。我在这两种模式下都给了你两个完整的解决方案。你明明不知道自己在做什么,竟然选择不理我。所以我完成了。
    • @JeffersonFarias 更仔细地阅读了我的答案和以前的 cmets。我已经解决了这个问题。你不能使用你自己的TThread类!时期!停止要求它。在非阻塞模式下,TServerSocket 不使用它自己的任何线程,你也不打算使用自己的线程,所以不要尝试。在阻塞模式下,TServerSocket 使用它自己的线程来接受连接,然后一旦连接了客户端,您就必须将TServerClientThread 用于您的 I/O 逻辑。其他任何事情都超出了TServerSockets 的设计范围。现在请注意告诉你的内容。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-31
    • 2021-06-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多