-3

I try send a .jpg file from ClientSocket to ServerSocket but have a trouble apparently around of SendText and SendStream functions because the results obtained after execution of SendText for example is always 0. But exists other strange thing that is when i put a ShowMessage() before send the size of file, SendText works (and size is received) but SendStream fails with -1 of result.

How solve?

This is my last attempt >

Sender:

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.

Receiver:

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.
FLASHCODER
  • 1
  • 7
  • 24
  • 2
    Suggestion: Use components that haven't been deprecated for more than a decade. – Jerry Dodge Aug 23 '19 at 14:42
  • 1
    Also note that you are risking memory leaks the way your try..finally is set up. Object creation should be *before* the `try`, not *after* it. What happens if `P.Bitmap.LoadFromFile('sent.bmp');` fails? `J` would never be created, but you'd still attempt to free it. You can also do all this without any image components at all. You just need to use a `TFileStream` instead of `TMemoryStream`. You also leak `jpg` in the receiver. – Jerry Dodge Aug 23 '19 at 14:43
  • @JerryDodge, *"`you are risking memory leaks the way your try..finally is set up. Object creation should be before the try, not after it.`"* thank you. – FLASHCODER Aug 23 '19 at 14:46
  • 1
    @JerryDodge Not really... Object variable should be initialized before the `try`, yes. Not necessarily created. Creating them all before the try has its own memory leak risks, unless you do the `try`cascade. – Ken Bourassa Aug 23 '19 at 14:49
  • @KenBourassa, you is right, i tested the suggestion of Jerry and also had memory leaks. – FLASHCODER Aug 23 '19 at 14:56
  • It's certainly wrong as it is regarding memory management. Why don't you do what Remy says? – David Heffernan Aug 23 '19 at 14:59
  • @BowJr What Jerry suggested has no leak unless one of your constructor raise an exception, which normally should be rare (His advice is pretty good, just not "perfect"). I'd suspect it's something else in your code leaking. – Ken Bourassa Aug 23 '19 at 14:59
  • You cold replace the outdated `TClientSocket` with `TIdTCPClient`. – Schneider Infosystems Ltd Aug 23 '19 at 16:27
  • Yes, but on next large project, no in this. I already have a large project (that this example above is part) that is using these components. And belive, to me is better fix this trouble than pratically make the same project from zero. – FLASHCODER Aug 23 '19 at 16:54
  • @BrowJr your code doesn't handle the possibility of `SendText()` and `SendStream()` sending partial data, especially in non-blocking mode, which you are keen on using even though you don't understand how it works or what nuances it has. `SendStream()` MAY OR MAY NOT free the `TStream` before exiting and you have no way of knowing one way or the other. `SendText()` doesn't handle Unicode strings correctly in D2009+. So just don't use `SendText()` and `SendStream()` at all! Use `SendBuf()` instead, and pay attention to its return value to know when you need to call it again with more data. – Remy Lebeau Aug 23 '19 at 17:19
  • Programming takes work, a lot of it. Don't expect to get things accomplished by just modifying a few lines of code. You'll end up spending more time than just switching to something more solid and reliable. – Jerry Dodge Aug 23 '19 at 18:47

1 Answers1

3

Your sender code is not handling the possibility of SendText() and SendStream() sending partial data, especially in non-blocking mode. SendStream() MAY OR MAY NOT free the TStream before exiting and you have no way of knowing one way or the other. SendText() doesn't handle Unicode strings correctly in D2009+.

Your receiver code is not taking into account that ReceiveText() MAY NOT and likely WILL NOT receive all of the data in a single read. It CAN and likely WILL take multiple OnClientRead events to receive all of the data. Or that ReceiveText() MAY receive portions of your image data and incorrectly try to convert those bytes into string characters. Also, you are not caching unprocessed bytes between OnClientRead events if data is incomplete in a single read.

So, just DON'T use SendText()/SendStream() or ReceiveText() at all! You are not using them correctly, especially in non-blocking mode. Always use SendBuf() and ReceiveBuf() instead, and pay attention to their return values so you know when you need to call them again to handle more data.

Try something more like this:

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.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • "SendText() doesn't handle Unicode strings correctly in D2009+." Exactly one great example why *not* to use those old things. They predate unicode enforcement. – Jerry Dodge Aug 23 '19 at 18:53
  • @JerryDodge, on next project i will start use `IdTcpClient`/`IdTcpServer` :-), – FLASHCODER Aug 23 '19 at 18:58
  • 1
    @BrowJr Depending on your exact use-case, you might even benefit from switching over to HTTP, it becomes a lot easier with more built-in features. – Jerry Dodge Aug 23 '19 at 19:03
  • @Remy Lebeau, thank you, i tested you code, and is working only with presence of `ShowMesssage()`, if remove `ShowMesssage()` not works. I still remeber that the code of my question already worked when i used DXE5, now using D10 Rio not worked. I will check if part of my troubles is the IDE version, re-installing and compiling with DXE5 in other pc. – FLASHCODER Aug 23 '19 at 19:40
  • @BrowJr "*is working only with presence of ShowMesssage()*" - on which side? The code I posted should be working fine without any message pump on the client side, and uses the main thread message pump on the server side. Though, on the client side, when using non-blocking mode, you need to wait for the `OnConnect` event before sending anything. I have updated my answer to show that. – Remy Lebeau Aug 23 '19 at 20:24