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