Delphi Indy TIdHttp 和 multipart/x-mixed-replace 带有文本和 jpeg 图像

Delphi Indy TIdHttp and multipart/x-mixed-replace with Text and jpeg image

我正在使用大华面部终端,它有一个 API 类(CGI 风格)和一个 SDK。我问了一些关于 dll 转换的问题,但现在我也在尝试使用 de API。

监视 Facial 处理的事件的 API 是

http://192.168.1.201/cgi-bin/snapManager.cgi?action=attachFileProc&Flags[0]=Event&Events=[AccessControl]

是一个 multipart/x-mixed-replace 响应如何 return 第一个绑定作为 text/plain 与事件数据和一个带有 image/jpeg 与事件快照的绑定。

使用关于 Indy 的在线信息和 Lebeau 发表的一些有用的帖子,我使用 idHttp.IoHanlder.ReadLn(IndyTextEncoding_UTF8)

读取文本数据

我尝试使用 idHttp.IOHandler.ReadByte、ReadBytes、ReadStream 读取下一个 bondary(图像),但没有成功。

这是使用 idHttp.IoHandler.ReadLn

的响应

--myboundary
Content-Type: text/plain
Content-Length: 1035

Events[0].Alive=100
Events[0].CardName=Joao Test
Events[0].CardNo=0B8748EB
Events[0].CardType=0
Events[0].CreateTime=1616297700
Events[0].Door=0
Events[0].ErrorCode=0
Events[0].Method=15
Events[0].ReaderID=1
Events[0].Similarity=99
Events[0].SnapPath=/var/tmp/partsnap96.jpg
Events[0].Status=1
Events[0].Type=Entry
Events[0].UTC=1616297700
Events[0].UserID=1
Events[0].UserType=0

--myboundary
Content-Type: image/jpeg
Content-Length: 54235

����

(1#%(:3=<9387@H\N@DWE78PmQW_bghg>Mqypdx\egc//cB8Bcccccccccccccccccccccccccccccccccccccccccccccccccc//cB8Bcccccccccccccccccccccccccccccccccccccccccccccccccc��

%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz�������������������������������������������������������������������������

�%�&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz��������������������������������������������������������������������������
�XQz �?m�c$[J��5"�Hz�~5���m+��b{�����{��n��*p�p*�8�bx��
`3mjLQ��9CB���T���D���'ޥAY\ų���>�*��c�U���h�R�%��je�W��*]�m�p��m?��c6�?��b��R��1N�.(��.)إ�
u\,8�
)�ny�ڌү
@
#��
L�rɳ���B�Ns�� S�K��
�i�F��Z�af�
������ޭ�T�
/l�i]Q��l������
C�RS�
ʛO^�ҫ���
h�

P)@�
pN��
\S���0�1K�
f2N�⫑S�-�LF-�Y�8?0��.�?[�nE[��t[��s�w�h��_���y�[�R�*��=je ������4�M��,?�x��Ƣ�Sל�t8�F�
ZJZ
(��R�E
)h�bR�K@����
p�S����)تb���x1�O��(��i�1����5��r=i��i�B�A��J�{���ںT5s��_��
Q@-�
Z1J

x�i�i�S�NU��
�Kf=9=Pƺ�s�ٿ��N����uS�Tj��#��d��-N-����gw�W5�0*z0��=&m��=T��S�����
x�`JpJxZpZ"��i
p���@
<S7
P���R�a�wP"���� %!�Q�
��`�M�6��wⵍfjJb�I�������{�h/���ڞ�r�Q��Ta�ϫ�7E��o�����5%���X��C�
�v�G�rO���
��徸�(�O�T�4�4��ц��j8�
��3sM�X�'�G��ڪξ\�/L|��i����I?+|�Z�
�?��K[M��L7�^
K�@d�Hb��q߯�V�'|a��C�E�Z�+�
f�&����`'�ϑ��E�r>�Ƶ'Q�7~��<r}��XT�+������pHQۭ

我正在创建一个 class 来实现 API 和 SDK,在这种情况下 (API) 来管理我写了一个线程 class 的事件使用事件 API 并在事件发生时同步数据,idHttp 由线程 class 拥有并且它在 TDispositivo class 中,所以我 运行 一个线程 class 对于主要 class.

中包含的每个 TDispositivo

这是线程中的执行代码。 (忽略图像数据,因为我现在不知道如何正确处理它。

procedure TMonitor.Execute;
var
  Ret, URL, Chave, Valor : String;
  I, isInteger : Integer;
  Concluido : boolean;
  Tentativas: Integer;
begin
  Tentativas := 3;
  fAtivo := True;
  FContador := 0; //zerando contador
  //executa enquanto fAtivo for True e terminated for False
  while fAtivo and not self.Terminated do
    begin
      if fOwner.alive then
        begin
          //preparando URL
          With fOwner.Config do
            begin
              Url := 'http://'+ IP + ifthen(PortaWeb>=0,':'+PortaWeb.ToString,'')+'/cgi-bin/snapManager.cgi?action=attachFileProc&Flags[0]=Event&Events=[AccessControl]';
            end;
          //Incrementando contado
          Inc(fContador);
          //Efetuando o GET dentro de um try except para evitar interrupcao na thread
          Try
            ret := fHttp.Get(Url);
          except

          End;
          fOwner.doGet(fhttp.ResponseCode, fhttp.ResponseText);
          //tratando erros http
          if fHttp.ResponseCode <> 200 then
            begin
              case fHttp.ResponseCode of
                400: Erro(740,'Chamada ao terminal mal formada');
                401: Erro(741,'Chamada ao terminal não autorizada');
                403: Erro(743,'Recurso do servidor proibido');
                404: Erro(744,'Recurso do terminal não encontrado');
                500: Erro(750,'Erro interno no processamento de chamadas do terminal');
                501: Erro(751,'Recurso do terminal não implementado');
              end;
              if (fContador >= Tentativas) then
                begin
                  Erro(700,'Falha ao conectar o dispositivo. Número de tentativas excedido');
                  fContador := 0;
                  //verificando se vai continuar tentando se reconectar
                  if fOwner.Config.Reconectar then
                    begin
                      Log('Aguardando para tentar reconectar o monitor ...');
                      Sleep(fOwner.Config.TempoEsperaReconectar);
                    end
                  else
                    begin
                      Log('Finalizando monitoramento por falha de comunicação');
                      Self.Stop;
                    end;
                end;
            end
          else
            begin
              //zerando contador após uma conexão bem sucedida;
              FContador := 0;
              //para evitar final do chunk ou close gracefully
              try
                if IsHeaderMediaType(fHttp.Response.ContentType, 'multipart') then
                  begin
                    i := 0;
                    Concluido := False;
                    repeat
                      if (i >= 35) Or Concluido then
                        begin
                          //tirando snap antes de processar o evento para ver se diminui o delay
                          if FOwner.Config.SnapshotEvento then
                            fOwner.getSnapshot;
                          Synchronize(Evento);
                          Concluido := False;
                          i := 0;
                        end;
                      fLinha := fHttp.IOHandler.ReadLn(IndyTextEncoding_UTF8);
                      if Pos('Content-Length', fLinha) > 0 then
                        begin
                          i := 0;
                          Concluido := False;
                          fEvento.Limpar;
                        end
                      Else if Pos('Events[', fLinha) > 0 then
                        begin
                          if fOwner.Config.Depurar then
                            Log(fLinha);
                          Inc(I);
                          Chave := Trim(retPar(1,'=',retPar(2,'.',fLinha)));
                          Valor := Trim(retPar(2,'=',fLinha));
                          if Valor = '' then
                            Valor := '0';
                          With fEvento do
                            begin
                              if Chave = 'Alive' then
                                Alive := ifthen(Valor = '',0,Valor.ToInteger)
                              else if Chave = 'CardName' then
                                CardName := ifthen(Trim(Valor) = '0','Não identificado',trim(Valor))
                              else if Chave = 'CardNo' then
                                begin
                                  if TryStrToInt(Valor, isInteger) then
                                    CardNo :=  ifthen(Valor = '', '-1', Valor)
                                  else
                                    CardNo :=  WiegandHexToInt(Valor).ToString;
                                end
                              else if Chave = 'CardType' then
                                CardType := ifthen(Valor = '', -1, Valor.ToInteger)
                              else if Chave = 'CreateTime' then
                                CreateTime := UnixToDateTime(Valor.ToInt64, False)
                              else if Chave = 'Door' then
                                Door := ifThen(Valor = '',-1, Valor.ToInteger)
                              else if Chave = 'ErrorCode' then
                                ErrorCode := ifThen(Valor = '',-1, Valor.ToInteger)
                              else if Chave = 'EventBaseInfo.Action' then
                                EventBaseAction := Valor
                              else if Chave = 'EventBaseInfo.Code' then
                                EventBaseCode := Valor
                              else if Chave = 'EventBaseInfo.Index' then
                                EventBaseIndex := ifThen(Valor = '',-1, Valor.ToInteger)
                              else if Chave = 'Method' then
                                Metodo := ifThen(Valor = '',-1, Valor.ToInteger)
                              else if Chave = 'ReaderID' then
                                LeitorID := ifThen(Valor = '',-1, Valor.ToInteger)
                              else if Chave = 'Similarity' then
                                Similaridade := ifthen(Valor = '', 0, Valor.ToInteger)
                              else if Chave = 'SnapPath' then
                                snappath := Valor
                              else if Chave = 'Status' then
                                Status := ifthen(Valor = '', -1, Valor.ToInteger)
                              else if Chave = '.Type' then
                                TipoAcesso := Valor
                              else if Chave = 'UTC' then
                                DataHora := UnixToDateTime(Valor.ToInt64, False)
                              else if Chave = 'UserID' then
                                UserID := ifthen(Valor = '', -1, Valor.ToInteger)
                              else if Chave = 'UserType' then
                                begin
                                  i := 34;
                                  Concluido := True;
                                  UserType := ifthen(Valor = '', -1, Valor.ToInteger);
                                end;
                            end;
                        end;
                    until fhttp.IOHandler.ClosedGracefully;
                  end;
              except

              end;
            end;
          inherited;
        end
      else
        begin
          //dispositivo não respondeu ao ping
          //verificando se vai continuar tentando se reconectar
          if fOwner.Config.Reconectar then
            begin
              Log('Aguardando para tentar reconectar o monitor ...');
              Sleep(fOwner.Config.TempoEsperaReconectar);
            end
          else
            begin
              Log('Finalizando monitoramento por falha de comunicação');
              Self.Stop;
            end;
          inherited;
        end;
    end;
  //rotina de finalização da thread
  if FHttp.Connected then
    begin
      try
        Fhttp.Disconnect;
      except

      end;
    end;
end;

第一个问题,我可以做吗? Indy 能做到吗?有什么建议吗?

第二个问题,当我在数据结束后尝试 readLn 时,当 GET 为 运行ning 时,我观察到循环以检索数据。在 Firefox 中,GET 在图像之后仍然 运行ning 并且仍在工作,在我的例子中,GET 完成并且 threand.execute 再次调用 GET。

是否可以保持 GET 运行ning 避免过早中断并在出现时处理新数据?

编辑:当这部分代码 运行 时,NewDecoder := TIdMessageDecoderMIME(Decoder).ReadBody(BodyStream, MsgEnd); 行优雅地结束,深入研究 Indy 代码我发现 TIOHandler.ReadLn 如何在 LTerm = -1 时执行 CheckForDisconnect(True,True) 并且这会在不检索数据的最后字节的情况下中断执行。

考虑到这一点,我怀疑与 Responde.KeepAlive 总是 False 以及 Connection 内容总是 close 有关系,这解释了为什么 Decoder.Readybody 引发 Close gracefully 并中断我的执行。

但是,如果我 运行 调用 api,例如。 Firefox,在 FF 获取图像后连接仍在 运行ning 并且仅在达到超时时中断。

是否有可能 headers 上的内容由终端发送?或者 idHttp 如何解析 headers 数据的方式?

//part  of code after http.get

              mcptAttachment: begin
                BodyStream := TMemoryStream.Create;
                try
                  BodyStream.Position := 0;

                  NewDecoder := TIdMessageDecoderMIME(Decoder).ReadBody(BodyStream, MsgEnd); //<-- here comes a close gracefully               

                  jpg := TJPEGImage.Create;
                  try
                    BodyStream.Position := 0;
                    jpg.LoadFromStream(BodyStream);
                    jpg.SaveToFile('C:\Producao\API_SDK\Facial\teste.jpg');
                  finally
                    jpg.Free;
                    Decoder.Free;
                    Decoder := NewDecoder;
                  end;
                finally
                  BodyStream.Free;
                end;
              end;


//part of the code of tIOHandler.ReadLn

    // ReadFromSource blocks - do not call unless we need to
    else if LTermPos = -1 then begin
      // ReadLn needs to call this as data may exist in the buffer, but no EOL yet disconnected
      CheckForDisconnect(True,True); //The execution stops here whiout return the last bytes and premature finishing the code above with a close gracefully
      // Can only return -1 if timeout
      FReadLnTimedOut := ReadFromSource(True, ATimeout, False) = -1;
      if (not FReadLnTimedOut) and (ATimeout >= 0) then begin
        if GetElapsedTicks(LReadLnStartTime) >= UInt32(ATimeout) then begin
          FReadLnTimedOut := True;
        end;
      end;
      if FReadLnTimedOut then begin
        Result := '';
        Exit;
      end;

编辑:使用New TIdHTTP hoNoReadMultipartMIME flag示例,我编写了一个测试应用程序来尝试从边界获取图像

var
  Boundary, Line: string;
  TCPStream: TIdTCPStream;
  NewDecoder, Decoder: TIdMessageDecoder;
  MsgEnd: Boolean;
  BodyStream: TStream;
  Jpg: TJpegImage;
begin
  {
    The code below comes from Indy Project Blog by Remy Lebeau
    This connection requires Digest Auth, so i set BasicAuth to False, put the flag hoInProcessAuth
    And implement the OnAuthorization and OnSelectAuthorization, the select is needed to do something to the Authorization works,
    do not know why.
   }
  HTTP.HTTPOptions := [hoInProcessAuth, hoForceEncodeParams, hoNoReadMultipartMIME];
  HTTP.Get('http://192.168.1.205/cgi-bin/snapManager.cgi?action=attachFileProc&Flags[0]=Event&Events=[AccessControl]');

  //Here the response always return HTTP.Response.KeepAlive FALSE, so i exclude it from test
  if IsHeaderMediaType(HTTP.Response.ContentType, 'multipart') then //and HTTP.Response.KeepAlive
    begin
      Boundary := ExtractHeaderSubItem(HTTP.Response.ContentType, 'boundary', QuoteHTTP);
      repeat
        Line := HTTP.IOHandler.ReadLn;
      until (Line = ('--' + Boundary)) or (Line = ('--' + Boundary + '--'));

      TCPStream := TIdTCPStream.Create(HTTP);
      try
        Decoder := TIdMessageDecoderMIME.Create(nil);
        try
          TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary;

          MsgEnd := False;
          repeat
            TIdMessageDecoderMIME(Decoder).SourceStream := TCPStream;
            TIdMessageDecoderMIME(Decoder).FreeSourceStream := False;

            Decoder.ReadHeader;
            case Decoder.PartType of
              mcptText: begin
                BodyStream := TMemoryStream.Create;
                try
                  NewDecoder := TIdMessageDecoderMIME(Decoder).ReadBody(BodyStream, MsgEnd);
                  try
                    BodyStream.Position := 0;
                    mLog.Lines.LoadFromStream(BodyStream);      //<-- OK i got the text data from body and showing in a memo
                  finally
                    Decoder.Free;
                    Decoder := NewDecoder;
                  end;
                finally
                  BodyStream.Free;
                end;
              end;

              mcptAttachment: begin
                BodyStream := TMemoryStream.Create;
                try
                  BodyStream.Position := 0;
                  NewDecoder := TIdMessageDecoderMIME(Decoder).ReadBody(BodyStream, MsgEnd); //<-- here comes a close gracefully,
                  //so i debug the ReadBody and found a exception on line 438 of idMessadeCoderMime
                  //          LLine := ReadLnRFC(VMsgEnd, LF, '.', LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF}); {do not localize}
                  //When it try to read a exception occurs and the connection closes gracefully
                  //but it read a lot of data befora except, so i thinking is something about a end of msg missing or misformed
                  jpg := TJPEGImage.Create;
                  try
                    BodyStream.Position := 0;
                    jpg.LoadFromStream(BodyStream);
                    jpg.SaveToFile('C:\Producao\API_SDK\Facial\teste.jpg');
                  finally
                    jpg.Free;
                    Decoder.Free;
                    Decoder := NewDecoder;
                  end;
                finally
                  BodyStream.Free;
                end;
              end;

              mcptIgnore: begin
                FreeAndNil(Decoder);
                Decoder := TIdMessageDecoderMIME.Create(nil);
                TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary;
              end;

              mcptEOF: begin
                FreeAndNil(Decoder);
                MsgEnd := True;
              end;
            end;
          until (Decoder = nil) or MsgEnd;
        finally
          Decoder.Free;
        end;
      finally
        TCPStream.Free;
      end;
    end;
end;

代码mcptAttachment的部分,当NewDecoder := Decoder.ReadBody(BodyStream, MsgEnd);被调用时,readbody函数运行,读取字节直到它在达到'Content-Length'大小之前崩溃。 我试图在它崩溃时隔离它,但可以肯定的是,当它尝试读取下一个数据时会发生错误。但我还无法在 Indy 源上找到异常的触发器

编辑:得到图像但无法继续阅读,直到服务器关闭连接导致响应关闭。

工作代码

var
  Boundary, Line: string;
  TCPStream: TIdTCPStream;
  NewDecoder, Decoder: TIdMessageDecoder;
  MsgEnd: Boolean;
  BodyStream: TStream;
  Jpg: TJpegImage;
begin
  HTTP.HTTPOptions := [hoInProcessAuth, hoForceEncodeParams, hoNoReadMultipartMIME];
  HTTP.Get('http://192.168.1.205/cgi-bin/snapManager.cgi?action=attachFileProc&Flags[0]=Event&Events=[AccessControl]');
  if IsHeaderMediaType(HTTP.Response.ContentType, 'multipart') then
    begin
      Boundary := ExtractHeaderSubItem(HTTP.Response.ContentType, 'boundary', QuoteHTTP);
      repeat
        Line := HTTP.IOHandler.ReadLn;
      until (Line = ('--' + Boundary)) or (Line = ('--' + Boundary + '--'));

      TCPStream := TIdTCPStream.Create(HTTP);
      try
        Decoder := TIdMessageDecoderMIME.Create(nil);
        try
          TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary;

          MsgEnd := False;
          repeat
            TIdMessageDecoderMIME(Decoder).SourceStream := TCPStream;
            TIdMessageDecoderMIME(Decoder).FreeSourceStream := False;
            if not http.IOHandler.InputBufferIsEmpty then
              begin
                Decoder.ReadHeader;
                case Decoder.PartType of
                  mcptText: begin
                    BodyStream := TMemoryStream.Create;
                    try
                      NewDecoder := TIdMessageDecoderMIME(Decoder).ReadBody(BodyStream, MsgEnd);
                      try
                        BodyStream.Position := 0;
                        mEvent.Lines.LoadFromStream(BodyStream);
                      finally
                        Decoder.Free;
                        Decoder := NewDecoder;
                      end;
                    finally
                      BodyStream.Free;
                    end;
                  end;

                  mcptAttachment: begin
                    BodyStream := TMemoryStream.Create;
                    http.IOHandler.InputBuffer.ExtractToStream(BodyStream);
                    try
                      BodyStream.Position := 0;
                      jpg := TJPEGImage.Create;
                      try
                        jpg.LoadFromStream(BodyStream);
                        JvImage1.Picture.Assign(jpg);
                        jpg.SaveToFile('C:\Producao\API-SDK\Facial\teste.jpg');
                      finally
                        jpg.Free;
                        FreeAndNil(Decoder);
                        Decoder := TIdMessageDecoderMIME.Create(nil);
                        TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary;
                      end;
                    finally
                      BodyStream.Free;
                    end;
                  end;

                  mcptIgnore: begin
                    mLog.Lines.Add('Ignore');
                    FreeAndNil(Decoder);
                    Decoder := TIdMessageDecoderMIME.Create(nil);
                    TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary;
                  end;

                  mcptEOF: begin
                    mLog.Lines.Add('Fim');
                    FreeAndNil(Decoder);
                    MsgEnd := True;
                  end;
                end;
              end
            else
              begin
                http.IOHandler.CheckForDisconnect(False,True);
                msgend := True
              end;
          until (Decoder = nil) or MsgEnd;
        finally
          Decoder.Free;
        end;
      finally
        TCPStream.Free;
      end;
    end;
end;

您所要求的可以用 TIdHTTP 完成,但需要一些额外的工作。详情请参阅 Indy 网站上的以下博客文章:

https://www.indyproject.org/2014/03/05/new-tidhttp-honoreadmultipartmime-flag/

简而言之,您需要在 TIdHTTP.HTTPOptions 属性 中启用 hoNoReadMultipartMIME 标志,这样 TIdHTTP.Get() 就不会尝试从中读取 MIME 数据TIdHTTP.IOHandler 收到 HTTP headers 后。这将允许您自己阅读 MIME 数据。您可以使用 Indy 的 TIdMessageDecoderMIME class 来帮助阅读。博客文章中提供了一个代码示例。