Search code examples
delphiindyindy10

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


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.


Solution

  • 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;