使用delphi indy 10.6 tcpclient,我得到间歇性的流读取错误

时间:2017-05-08 18:12:08

标签: delphi tcpclient indy

我正在设置一个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;

我试图在尝试检索它之前验证流并且只是在读取超时时重新启动检索,但我仍然遇到间歇性问题,其中读取线程在尝试读取流时挂起;我仍然可以关闭连接并正常退出程序,在某些情况下它似乎不会抛出读取异常。

0 个答案:

没有答案