我正在设置一个TCP客户端/服务器系统,其中客户端发送一个标头块,可选择一个字符串流或一个文件流,服务器使用带有任何错误代码的标头块以及可选的字符串流进行响应或文件流。通信工作很好,只有标头块传输和带字符串流的标头块传输;尝试传输大量文件时出现问题。来自服务器的文件传输涉及:发送FileGet头,接收FileGet响应头,接收文件流(FilePut发送FilePut头,然后是文件流,然后接收带有任何错误指示的FilePut响应头)。文件传输大部分时间都完美无缺,每次传输279个文件,但在6次尝试的某个地方,我会遇到某种类型的错误;接收随机文件流时,典型错误会冻结,断言失败(... \ IdGlobal.pas,第4876行)(表示流量大于我认为的指定大小),同时接收文件流,有时是MD5校验和发送者指示的内容与接收者计算的内容之间不匹配。以下是相关的代码部分:
TCommandTypeHeader = Packed Record
CommandType: TCommandType;
ErrorType: TErrorType;
Location: Integer; //used as site id for ctAssignID/ctConnectUnit/ctNewSite/ctRemoveSite
UnitId: Integer; //used as unit id for ctAssignID/ctConnectUnit/ctNewUnit/ctRemoveUnit
DataType: TDataType; //type of data sent after header packet
DataSize: Int64; //0 if no data or length of file stream/string data following header (in bytes)
DataMD5: String[32]; //MD5 checksum of additional data
FileLoc: Integer; //folder location within current site
FileName: String[12]; //file name for put/get file commands
End;
TUsers = Record
InActive: Boolean;
InContext: TUserContext;
InSecurity: TSecurity; //used to compress/decompress TIdBytes for security
InIP: String;
InAuthTimer: Integer;
InAuth: Boolean;
InSATimer: Integer;
InConnected: Boolean;
InXFer: Boolean;
InGotHeader: Boolean;
InTries: Integer;
End;
客户端
{ TReadingThread }
Constructor TReadingThread.Create(AClient: TOutTCPClient);
Begin
Inherited Create(True);
FClient := AClient;
FClient.IOHandler.ReadTimeout := cRTime;
FWaiting := False;
FFileName := DataDir + PathDelim + 'TEMP.DAT';
FTries := cCTries;
End;
Procedure TReadingThread.GetResponse(CTH: TCommandTypeHeader);
Begin
If FWaiting Then
Exit;
FCTH := CTH;
FTries := cCTries;
FDone := False;
FGetHeader := True;
FWaiting := True;
End;
Procedure TReadingThread.Execute;
Var
BufRaw, BufDec: TIdBytes;
FS: TFileStream;
Begin
While Not Terminated Do
If FWaiting Then
Begin
If FGetHeader Then
Try //try to get the header
SetLength(BufRaw, 0);
FClient.IOHandler.ReadBytes(BufRaw, SizeOf(TCommandTypeHeader) + 1); //get header bytes
SetLength(BufDec, 0);
BufDec := FClient.Security.DecIdBytes(BufRaw); //decode bytes
SetLength(BufRaw, 0);
BytesToRaw(BufDec, FCTH, SizeOf(TCommandTypeHeader)); //move bytes to header
SetLength(BufDec, 0);
FStrData := '';
FTries := cCTries;
FGetHeader := False;
If FCTH.DataType = dtNone Then
Begin //no extra data
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
FDone := True;
End;
Except
On E: EIdConnClosedGracefully Do
Begin //server disconnected
SetLength(BufRaw, 0);
SetLength(BufDec, 0);
FDone := True;
FCTH.ErrorType := etServerDropped;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
On E: EIdReadTimeout Do
Begin //no data received within 1 sec
SetLength(BufRaw, 0);
SetLength(BufDec, 0);
FDone := True;
Dec(FTries, 1);
If FTries = 0 Then
Begin //no data received within 5 sec, bad packet
FCTH.ErrorType := etPacketFailure;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
End;
End;
If Not FDone Then
Case FCTH.DataType Of
dtString:
Try //get the string data
SetLength(BufDec, 0);
FClient.IOHandler.ReadBytes(BufDec, FCTH.DataSize + 1);
FStrData := FClient.Security.DecStr(BufDec); //decode username/password
SetLength(BufDec, 0);
If FCTH.CommandType = ctFileGet Then
Begin //create empty file
If ForcePath(ExtractFileDir(FFileName)) Then
Try
If FileExists(FFileName) Then
DeleteFile(FFileName);
FS := TFileStream.Create(FFileName, fmOpenWrite Or fmCreate);
Finally
FreeAndNil(FS);
End
Else
FCTH.ErrorType := etCannotCreatePath;
End;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
Except
On E: EIdConnClosedGracefully Do
Begin //server disconnected
FCTH.ErrorType := etServerDropped;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
On E: EIdReadTimeout Do
Begin //no data received within 1 sec
SetLength(BufDec, 0);
Dec(FTries, 1);
If FTries = 0 Then
Begin //no data received within 5 sec, bad packet
FCTH.ErrorType := etPacketFailure;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
End;
End;
dtStream:
If ForcePath(ExtractFileDir(FFileName)) Then
Begin //get the stream data
If FileExists(FFileName) Then
DeleteFile(FFileName);
FS := TFileStream.Create(FFileName, fmOpenWrite Or fmCreate);
Try
Try
If FCTH.DataSize > 0 Then
Begin
FS.Position := 0;
FClient.IOHandler.LargeStream := True;
FClient.IOHandler.ReadStream(FS, FCTH.DataSize, False);
End;
Finally
FreeAndNil(FS);
End;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
Except
On E: EIdConnClosedGracefully Do
Begin //server disconnected
FCTH.ErrorType := etServerDropped;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
On E: EIdReadTimeout Do
Begin //no data received within 1 sec
Dec(FTries, 1);
If FTries = 0 Then
Begin //no data received within 5 sec, bad packet
FCTH.ErrorType := etPacketFailure;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
End;
End;
End
Else
Begin
FCTH.ErrorType := etCannotCreatePath;
FClient.IOHandler.InputBuffer.clear;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
End;
End;
End;
Procedure TReadingThread.DataReceived;
Begin
If Assigned(FOnData) Then
FOnData(FCTH, FStrData);
End;
服务器端
Procedure TFK30MasForm.ServerExecute(AContext: TIdContext);
Var
BufRaw, BufDec: TIdBytes;
CTH: TCommandTypeHeader;
I: Integer;
S: String;
FS: TFileStream;
Begin
I := TUserContext(AContext).UserID;
If Not FUsers[I].InGotHeader Then //False after every transfer
Try
If FUsers[I].InContext.Connection.IOHandler.InputBuffer.Size = 0 Then// IsEmpty Then
Begin //no data yet
FUsers[I].InTries := cSTries;
FUsers[I].InContext.Connection.IOHandler.CheckForDataOnSource(0);
End;
If FUsers[I].InContext.Connection.IOHandler.InputBuffer.Size >=
(SizeOf(TCommandTypeHeader) + 1) Then
Begin //get header
SetLength(BufRaw, 0);
FUsers[I].InContext.Connection.IOHandler.ReadBytes(BufRaw, SizeOf(TCommandTypeHeader) + 1); //get header bytes
SetLength(BufDec, 0);
BufDec := FUsers[I].InSecurity.DecIdBytes(BufRaw); //decode bytes
BytesToRaw(BufDec, CTH, SizeOf(TCommandTypeHeader)); //move bytes to header
SetLength(BufRaw, 0);
SetLength(BufDec, 0);
If CTH.DataType = dtNone Then
Begin //single packet commands
FUsers[I].InSATimer := cSATime; //reset the stay alive timer
FUsers[I].InTries := cSTries;
Case CTH.CommandType Of
... //code irrelevant to issue
ctFileGet:
Begin //User downloading a file
PCommon.TMyNotify.NotifyCTH(SFileGetNotify, IntToStr(I) + cCSep, CTH);
End;
... //code irrelevant to issue
End;
End
Else
Begin //2 packet commands
FUsers[I].InTries := cSTries;
FUsers[I].InGotHeader := True; //now process 2nd packet
End;
End;
End
Else
Begin //signal a packet failure
SetLength(BufRaw, 0);
Dec(FUsers[I].InTries, 1);
If FUsers[I].InTries = 0 Then
Begin //invalid packet length
FUsers[I].InTries := cSTries;
CTH.CommandType := ctFailure;
CTH.ErrorType := etInvalidHeader;
CTH.Location := 0;
CTH.UnitId := 0;
CTH.FileLoc := Ord(flRoot);
CTH.FileName := ' ';
CTH.DataType := dtNone;
CTH.DataMD5 := ' ';
CTH.DataSize := 0;
FUsers[I].InContext.Connection.IOHandler.InputBuffer.clear;
PCommon.TMyNotify.NotifyCTH(SPacketErrorNotify, IntToStr(I) + cCSep +
'invalid header received!' + cCSep, CTH);
End;
End;
Except
On E: Exception Do
Begin
FUsers[I].InTries := cSTries;
Raise;
End;
End;
If FUsers[I].InGotHeader Then //process 2 packet commands, string or file stream
Case CTH.DataType Of
dtString:
Case CTH.CommandType Of
ctAuthenticate:
...//irrelevant code
End;
dtStream:
Case CTH.CommandType Of
ctFilePut: //get a file from the client
Try //stream commands here
FUsers[I].InSATimer := cSATime; //reset the stay alive timer
S := PCommon.GetFullName(PCommon.DataDir + PathDelim + IntToStr(CTH.Location) +
PathDelim + IntToStr(CTH.UnitId), CTH); //make the file name
If S[Length(S)] = PathDelim Then
Begin //error, no file name
FUsers[I].InTries := cSTries;
FUsers[I].InContext.Connection.IOHandler.InputBuffer.Clear;
FUsers[I].InGotHeader := False;
CTH.ErrorType := etNoFileName;
PCommon.TMyNotify.NotifyCTH(SPacketErrorNotify, IntToStr(I) + cCSep +
'no file name!' + cCSep, CTH);
End
Else
Begin //download the file
If FileExists(S + '.TMP') Then
DeleteFile(S + '.TMP');
If PCommon.ForcePath(ExtractFileDir(S)) Then
Begin //make the path
FS := TFileStream.Create(S + '.TMP', fmCreate Or fmOpenWrite); //create the file
Try
If CTH.DataSize > 0 Then
Begin //fill the file if not empty
FUsers[I].InXFer := True;
FUsers[I].InContext.Connection.IOHandler.LargeStream := True;
FUsers[I].InContext.Connection.IOHandler.ReadStream(FS, CTH.DataSize, False);
End;
Finally
FUsers[I].InTries := cSTries;
FreeAndNil(FS); //close the file
FUsers[I].InGotHeader := False;
FUsers[I].InXFer := False; //reenable stay alive timer
End;
PCommon.TMyNotify.NotifyCTH(SFilePutNotify, IntToStr(I) + cCSep, CTH);
FUsers[I].InTries := cSTries;
End
Else
Begin //cannot create destination path
FUsers[I].InTries := cSTries;
FUsers[I].InContext.Connection.IOHandler.InputBuffer.Clear;
FUsers[I].InGotHeader := False;
CTH.ErrorType := etCannotCreatePath;
PCommon.TMyNotify.NotifyCTH(SPacketErrorNotify, IntToStr(I) + cCSep +
'cannot create path!' + cCSep, CTH);
End;
End;
Except
On E: EIdReadTimeout Do
Begin //no data received within 1 sec
Dec(FUsers[I].InTries, 1);
If FUsers[I].InTries = 0 Then
Begin //no data received within 5 sec, bad packet
FUsers[I].InTries := cSTries;
FUsers[I].InGotHeader := False;
CTH.ErrorType := etPacketFailure;
PCommon.TMyNotify.NotifyCTH(SPacketErrorNotify, IntToStr(I) + cCSep +
'second packet not received!' + cCSep, CTH);
End;
Raise;
End;
On E: Exception Do
Begin
FUsers[I].InTries := cSTries;
Raise;
End;
End;
Else
FUsers[I].InTries := cSTries;
FUsers[I].InContext.Connection.IOHandler.InputBuffer.Clear;
FUsers[I].InGotHeader := False;
CTH.ErrorType := etInvalidCommand;
PCommon.TMyNotify.NotifyCTH(SPacketErrorNotify, IntToStr(I) + cCSep +
'invalid command received!' + cCSep, CTH);
End;
End;
End;
Procedure TFK30MasForm.SFileGetNotify(Const AMessage: String; CTH: TCommandTypeHeader);
Var
I: Integer;
S, FLoc, FUnit, FName: String;
ErrType: TErrorType;
Begin
S := AMessage;
I := StrToIntDef(Fetch(S, cCSep), -1); //get the user ID
If (I = -1) Or (Not Server.Active) Then
Exit;
FUsers[I].InXFer := True;
FLoc := IntToStr(CTH.Location);
FUnit := IntToStr(CTH.UnitId);
FName := PCommon.GetFullName(DataDir + PathDelim + FLoc + PathDelim + FUnit, CTH);
AddMessage('File> ' + FName, mcGreen, I);
If FileExists(FName) Then
Begin //source file exists, send it
ErrType := PCommon.SendSCommand(FUsers[I], CTH.CommandType, CTH.Location, CTH.UnitId,
TFileLoc(CTH.FileLoc), FName, etNone); //signal received okay
If ErrType = etNone Then
AddMessage('FileGet-File sent', mcWhite, I) //show good send
Else
AddMessage('FileGet-' + PCommon.GetErrType(ErrType), mcRed, I); //show error
End
Else
Begin //send file doesn't exist message
PCommon.SendSString(FUsers[I], 'FileGet-Error, file does not exist!', CTH.CommandType,
CTH.Location, CTH.UnitId, TFileLoc(CTH.FileLoc), FName, etNotFileExist);
AddMessage('FileGet-file does not exist!', mcRed, I); //show error
End;
End;
Procedure TFK30MasForm.SFilePutNotify(Const AMessage: String; CTH: TCommandTypeHeader);
Var
I: Integer;
S, FName, FLoc, FUnit: String;
ErrType: TErrorType;
Begin
S := AMessage;
I := StrToIntDef(Fetch(S, cCSep), -1); //get the user ID
If (I = -1) Or (Not Server.Active) Then
Exit;
FUsers[I].InXFer := True;
FLoc := IntToStr(CTH.Location);
FUnit := IntToStr(CTH.UnitId);
FName := PCommon.GetFullName(DataDir + PathDelim + FLoc + PathDelim + FUnit, CTH);
If ExtractFileName(FName) = '' Then
Begin //signal no file name supplied error
PCommon.SendSString(FUsers[I], 'FilePut-Error, no file name specified!',
CTH.CommandType, CTH.Location, CTH.UnitId, TFileLoc(CTH.FileLoc),
FName, etNoFileName);
AddMessage('FilePut-No file name specified!', mcRed, I); //show error
Exit;
End;
If CTH.DataMD5 = PCommon.GetFileMD5(FName + '.TMP') Then
Begin //good checksum, save the file
If FileExists(FName + '.TMP') Then
Begin
If FileExists(FName) Then
DeleteFile(FName); //delete the true destination file
RenameFile(FName + '.TMP', FName); //rename the temp file
ErrType := PCommon.SendSCommand(FUsers[I], CTH.CommandType, CTH.Location,
CTH.UnitId, TFileLoc(CTH.FileLoc),
FName, CTH.ErrorType); //signal received okay
If ErrType = etNone Then
AddMessage('FilePut-File received!', mcWhite, I) //show good receive
Else
AddMessage('FilePut-' + PCommon.GetErrType(ErrType), mcRed, I); //show error
End
Else
Begin //signal couldn't create file
PCommon.SendSCommand(FUsers[I], CTH.CommandType, CTH.Location, CTH.UnitId,
TFileLoc(CTH.FileLoc), FName, etNotFileExist);
AddMessage('FilePut-Could not create file!', mcRed, I); //show error
End;
End
Else
Begin //checksum mismatch
If FileExists(FName + '.TMP') Then
DeleteFile(FName + '.TMP'); //erase the temp file
PCommon.SendSString(FUsers[I], 'FilePut-Error, file check sum mismatch!',
CTH.CommandType, CTH.Location, CTH.UnitId, TFileLoc(CTH.FileLoc),
FName, etCheckSumMismatch); //send checksum mismatch error
AddMessage('FilePut-File Check Sum mismatch!', mcRed, I); //show error
End;
End;
正如您所看到的,我已尝试进行读取超时处理,客户端将允许10次尝试1秒读取超时,并且服务器将允许5次尝试1秒超时,然后才会触发错误响应/重试。
修改
所以我更改了Stream部分的客户端阅读线程,如下所示:
dtStream:
If ForcePath(ExtractFileDir(FFileName)) Then
Begin //get the stream data
If FileExists(FFileName) Then
DeleteFile(FFileName);
FS := TFileStream.Create(FFileName, fmOpenWrite Or fmCreate);
FClient.IOHandler.LargeStream := True;
Try
While FTries > 0 Do
Try
If FCTH.DataSize > 0 Then
While FTries > 0 Do
Begin
FClient.IOHandler.CheckForDataOnSource(1000);
If Not FClient.IOHandler.InputBufferIsEmpty Then
Begin
FS.Position := 0;
FClient.IOHandler.ReadStream(FS, FCTH.DataSize, False);
FTries := 0;
End
Else
Begin //no data received within 1 sec
Dec(FTries, 1);
If FTries = 0 Then
Begin //no data received within 5 sec, bad packet
FClient.IOHandler.InputBuffer.Clear;
FCTH.ErrorType := etPacketFailure;
End;
End;
End;
FTries := 0;
Except
On E: EIdConnClosedGracefully Do
Begin //server disconnected
FTries := 0;
FClient.IOHandler.InputBuffer.Clear;
FCTH.ErrorType := etServerDropped;
End;
On E: EIdReadTimeout Do
Begin //no data received within 1 sec
Dec(FTries, 1);
If FTries = 0 Then
Begin //no data received within 5 sec, bad packet
FClient.IOHandler.InputBuffer.Clear;
FCTH.ErrorType := etPacketFailure;
End;
End;
End;
Finally
FreeAndNil(FS);
End;
FWaiting := False;
If Assigned(FOnData) Then
Synchronize(DataReceived);
End;
我试图在尝试检索它之前验证流并且只是在读取超时时重新启动检索,但我仍然遇到间歇性问题,其中读取线程在尝试读取流时挂起;我仍然可以关闭连接并正常退出程序,在某些情况下它似乎不会抛出读取异常。