Encountering 'Socket Error # 10053' while transferring files using TIdTCPClient and TIdTCPServer

159 views Asked by At

When attempting to send a file from a client to a server using TIdTCPClient and TIdTCPServer components in Delphi, I encounter a persistent issue. The server code is set to receive a file, but during the transfer process, an error reading 'Connection abort error by software' occurs, disrupting the file transmission.

Here's a snippet of the server-side code:

procedure TForm2.IdTCPServer1Execute(AContext: TIdContext);
var
  ReceivedText, FileName: string;
  FileStream: TFileStream;
  TotalSize, BytesRead: Int64;
  Buffer: TIdBytes;
  SaveFileName: string;
begin
  ReceivedText := AContext.Connection.IOHandler.ReadLn; // Read incoming data

  if ReceivedText = '$File$' then
  begin
    AContext.Connection.IOHandler.LargeStream:=true;
    UpdateProgress(0);
    FileName := AContext.Connection.IOHandler.ReadLn; // Receive the file name from the client
    TotalSize := AContext.Connection.IOHandler.ReadInt64; // Read the total size of the incoming file
    SaveFile(FileName,TotalSize,AContext);
  end;
End;


Procedure TForm2.SaveFile(FileName:String;TotalSize:Int64;AContext:TIdContext);
var
SaveFileName:String;
FileStream:TFileStream;
BytesRead:Int64;
Buffer: TIdBytes;
begin

  TThread.Synchronize(nil,
      procedure
      var
        SaveDialog: TSaveDialog;
      begin
        SaveDialog := TSaveDialog.Create(nil);
        try
          SaveDialog.InitialDir := 'D:\TestFolder\';
          SaveDialog.FileName := FileName; // Set default file name to the received file name
          BytesRead:=0;
          if SaveDialog.Execute then
          begin
            SaveFileName := SaveDialog.FileName;
            ProgressBar1.Max := TotalSize;
            FileStream := TFileStream.Create(SaveFileName, fmCreate);
            try
              while BytesRead < TotalSize do
              begin
                AContext.Connection.IOHandler.ReadBytes(Buffer, Min(1024, TotalSize - BytesRead), False);
                FileStream.Write(Buffer[0], Length(Buffer));
                UpdateProgress(BytesRead);
                Inc(BytesRead, Length(Buffer));
              end;

              // File transfer completed
              Memo1.Lines.Add('File received and saved: ' + SaveFileName);
              UpdateProgress(0);
            finally
              FileStream.Free;
            end;
          end;
        finally
          SaveDialog.Free;
        end;
      end
    );
  end;

And the client-side code:

procedure TForm2.PngSpeedButton1Click(Sender: TObject);
var
  FileStream: TFileStream;
  FileToSend: string;
  Buffer: TIdBytes;
  TotalSize, BytesRead: Int64;
begin

  if OpenDialog1.Execute then
  begin
    TotalSize:=0;
    FileToSend := OpenDialog1.FileName;
    IdTCPClient1.IOHandler.LargeStream:=true;
    IdTCPClient1.IOHandler.WriteLn('$File$'); // Indicate to server to expect a file
    IdTCPClient1.IOHandler.WriteLn( ExtractFileName(OpenDialog1.FileName));
    IdTCPClient1.IOHandler.Write(Int64(TotalSize)); // Send the total size of the file

    FileStream := TFileStream.Create(FileToSend, fmOpenRead or fmShareDenyWrite);
    try
      TotalSize := FileStream.Size;


      BytesRead := 0;
      SetLength(Buffer, 1024); // Set buffer size
      Try
      while BytesRead < TotalSize do
      begin
        SetLength(Buffer, Min(1024, TotalSize - BytesRead));
        BytesRead := BytesRead + FileStream.Read(Buffer[0], Length(Buffer));
        IdTCPClient1.IOHandler.Write(Buffer, Length(Buffer)); // Send file content in chunks
      end;
      Except On E:Exception do    Clipboard.AsText:=E.Message;

      End;

      Memo1.lines.add('File : ['+ExtractFileName(OpenDialog1.FileName)+'] sent successfully.');
    finally
      FileStream.Free;
    end;
  end;


end;

Despite setting the stream to be large and attempting to transfer the file in chunks, this error persists. Any insights or suggestions.

1

There are 1 answers

1
Remy Lebeau On BEST ANSWER

I see a number of issues with your code:

  • You are forcing the client and server to perform the transfer entirely within their main UI threads. Don't do that.

  • In the server:

    • Synchronize only the portions of code that directly interact with the UI, don't sync the transfer itself.

    • if the user decides not to save the file, the client still sends the file anyway.

    • you are not taking into account that the final ReadBytes() at the end of the file may be smaller than your Buffer's allocated size. ReadBytes() can grow the Buffer, but will never shrink it. You need to use the calculated chunk size, not the Buffer's allocated size, when calling FileStream.Write() and incrementing BytesRead.

  • In the client:

    • consider using a worker thread instead (or at least use TIdAntiFreeze) so you are not blocking the UI thread.

    • you are sending TotalSize to the server before you have even opened the file to be transferred. You should open the file first to make sure you can access it before then sending the '$File$' command to the server.

    • you are not adequately taking into account that FileStream.Read() can return fewer bytes than requested, or that Read() can return 0 on failure. Use Read()'s return value to tell you how many bytes to Write() to the server, do not use the Buffer's allocated size.

  • You are manually sending/reading the stream on both ends. Indy can automate that for you. In the client, you can use the IOHandler.Write(TStream) method. In the server, you can use the IOHandler.ReadStream() method, and the Connection.OnWork... events to update your UI progress, syncing the updates with the UI thread.

With all of that said, try something more like this:

Server:

procedure TForm2.IdTCPServer1Execute(AContext: TIdContext);
var
  ReceivedText: string;
begin
  ReceivedText := AContext.Connection.IOHandler.ReadLn; // Read incoming data
  if ReceivedText = '$File$' then
    ReceiveFile(AContext);
End;   

Procedure TForm2.ReceiveFile(AContext: TIdContext);
var
  FileName, SaveFileName: String;
  FileStream: TFileStream;
begin
  FileName := AContext.Connection.IOHandler.ReadLn; // Receive the file name from the client

  TThread.Synchronize(nil,
    procedure
    var
      SaveDialog: TSaveDialog;
    begin
      SaveDialog := TSaveDialog.Create(nil);
      try
        SaveDialog.InitialDir := 'D:\TestFolder\';
        SaveDialog.FileName := FileName; // Set default file name to the received file name
        if SaveDialog.Execute then
          SaveFileName := SaveDialog.FileName;
      finally
        SaveDialog.Free;
      end;
    end
  );            

  if SaveFileName = '' then
  begin
    AContext.Connection.IOHandler.WriteLn('REFUSED');
    Exit;
  end;

  try
    FileStream := TFileStream.Create(SaveFileName, fmCreate);
  except
    AContext.Connection.IOHandler.WriteLn('ERROR');
    Exit;
  end;

  try
    AContext.Connection.OnWorkBegin := TransferWorkBegin;
    AContext.Connection.OnWorkEnd := TransferWorkEnd;
    AContext.Connection.OnWork := TransferWork;
    try
      AContext.Connection.IOHandler.WriteLn('SEND');
      try
        AContext.Connection.IOHandler.LargeStream := True;
        AContext.Connection.IOHandler.ReadStream(FileStream, -1, False);
      except
        AContext.Connection.IOHandler.WriteLn('ERROR');
        raise;
      end;
    finally
      AContext.Connection.OnWorkBegin := nil;
      AContext.Connection.OnWorkEnd := nil;
      AContext.Connection.OnWork := nil;
    end;
  finally
    FileStream.Free;
  end;

  AContext.Connection.IOHandler.WriteLn('OK');

  TThread.Queue(nil,
    procedure
    begin
      Memo1.Lines.Add('File received and saved: ' + SaveFileName);
    end
  );
end;

var
  TransferProgress: Int64 = 0;

procedure TForm2.TransferTimerElapsed(Sender: TObject);
begin
  ProgressBar1.Position := TInterlocked.Read(TransferProgress);
end;

procedure TForm2.TransferWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
begin
  TInterlocked.Exchange(TransferProgress, 0);
  TThread.Queue(nil,
    procedure
    begin
      ProgressBar1.Position := 0;
      ProgressBar1.Max := AWorkCountMax;
      TransferTimer.Enabled := True;
    end
  );
end;

procedure TForm2.TransferWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
begin
  TInterlocked.Exchange(TransferProgress, 0);
  TThread.Queue(nil,
    procedure
    begin
      TransferTimer.Enabled := False;
      ProgressBar1.Position := 0;
    end
  );
end;

procedure TForm2.TransferWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
begin
  TInterlocked.Exchange(TransferProgress, AWorkCount);
end;

Client:

procedure TForm2.PngSpeedButton1Click(Sender: TObject);
var
  FileStream: TFileStream;
  FileToSend, FileName: string;
begin
  if not OpenDialog1.Execute then Exit;

  FileToSend := OpenDialog1.FileName;
  FileName := ExtractFileName(FileToSend);

  try
    FileStream := TFileStream.Create(FileToSend, fmOpenRead or fmShareDenyWrite);
    try
      IdTCPClient1.IOHandler.WriteLn('$File$'); // Indicate to server to expect a file
      IdTCPClient1.IOHandler.WriteLn(FileName);

      if IdTCPClient1.IOHandler.ReadLn <> 'SEND' then
        raise Exception.Create('Server cannot receive file');

      IdTCPClient1.IOHandler.LargeStream := True;
      IdTCPClient1.IOHandler.Write(FileStream, 0, True);
    finally
      FileStream.Free;
    end;

    if IdTCPClient1.IOHandler.ReadLn <> 'OK' then
      raise Exception.Create('Server failed to receive file');

  except
    on E: Exception do
    begin
      Memo1.Lines.Add('File : [' + FileName + '] failed.');
      Clipboard.AsText := E.Message;
      Exit;
    end;
  end;

  Memo1.Lines.Add('File : [' + FileName + '] sent successfully.');
end;