4

我有一个用 Delphi 6 编写的应用程序,它使用 Indy 9.0.18 HTTP 客户端组件来驱动机器人。如果我在安装 Indy 时没记错的话,版本 10 和更新版本不适用于 Delphi 6,所以我使用的是 9.0.18。在较新的桌面上,该程序运行完全正常。但是我在旧笔记本电脑上遇到了一些问题。请注意,在所有情况下,我都绝对没有错误或异常。

机器人是一个 HTTP 服务器,它响应 HTTP 请求来驱动它。要获得连续运动,您必须在连续循环中发送驱动命令(例如 - 向前移动)。驱动器命令是对机器人响应的数字 IP 地址的 HTTP 请求,请求中使用的 URL 不涉及域名,因此排除了域名解析的问题(我相信)。一旦你从上一个 HTTP 请求中得到响应,你立即转身发送下一个请求。您可以判断系统何时无法跟上循环,因为机器人会做出一些不平稳的小动作,永远无法达到平稳运动所需的连续动量,因为电机有时间稳定下来并停止。

有问题的两个系统是笔记本电脑,具有以下 CPU 和内存,并且运行的是 Windows XP SP3:

  • AMD Turion 64 X2 移动技术 TL-50(双核),1 GB 主内存,1.6 GHz,双核。
  • AMD Sempron(tm) 140 处理器,1 GB 主内存,2.7 GHZ。请注意,此 CPU 是双核,但仅启用了一个核。

这两个系统都不能获得平滑运动,除非是瞬态,如下所示。

我说这是笔记本电脑问题的原因是因为上面的两个系统都是笔记本电脑。相比之下,我有一个旧的 Pentium 4 单核超线程(2.8 GHz,2.5 GB 内存)。它可以获得平滑的运动。但是,尽管机器人连续移动,但移动速度明显变慢,表明 HTTP 请求之间仍然存在轻微延迟,但不足以完全停止电机,因此运动仍然是连续的,尽管明显慢于我的四核或双核桌面.

我知道数据点的另一个区别是旧的 Pentium 4 台式机的内存是笔记本电脑的 2.5 倍,尽管它是一台几乎过时的 PC。也许真正的罪魁祸首是一些内存抖动?机器人时不时地运行平稳,但很快又恢复到口吃状态,这表明在没有任何东西破坏插座上的交互的情况下,偶尔可以实现平稳的运动。请注意,机器人还双向传输音频到 PC 和从 PC 传输视频,并将视频传输到 PC(但不是另一个方向),因此在驱动机器人的同时进行了大量的处理。

Indy HTTP 客户端是在后台线程上创建和运行的,而不是在主 Delphi 线程上,在没有睡眠状态的紧密循环中。它确实在循环中进行了 PeekMessage() 调用,以查看是否有任何新命令进入了应该循环而不是当前循环的命令。在循环中调用 GetMessage() 的原因是当机器人应该空闲时线程阻塞,也就是说,在用户决定再次驱动它之前,不应向它发送 HTTP 请求。在这种情况下,向线程发布新命令会解除对 GetMessage() 调用的阻塞,并且新命令会被循环。

我尝试将线程优先级提高到 THREAD_PRIORITY_TIME_CRITICAL 但效果绝对为零。请注意,我确实使用 GetThreadPriority() 来确保确实提高了优先级,并且在 SetThreadPriority() 调用之前最初返回 0 之后,它返回了 15 的值。

1) 那么我能做些什么来提高这些旧的低功耗系统的性能,因为我的几个最好的用户都有它们呢?

2)我的另一个问题是,有没有人知道 Indy 是否必须在每个 HTTP 请求中重建连接,或者它是否智能地缓存套接字连接,这样就不会有问题了?如果我使用较低级别的 Indy 客户端套接字并自己制作 HTTP 请求会有所不同吗?我想避免这种可能性,因为这将是一次重大的重写,但如果这是一个高度确定的解决方案,请告诉我。

我在下面包含了后台线程的循环,以防您发现任何低效的情况。要执行的命令通过来自主线程的异步 PostThreadMessage() 操作发布到线程。

// Does the actual post to the robot.
function doPost(
            commandName,    // The robot command (used for reporting purposes)
            // commandString,  // The robot command string (used for reporting purposes)
            URL,            // The URL to POST to.
            userName,       // The user name to use in authenticating.
            password,       // The password to use.
            strPostData     // The string containing the POST data.
                : string): string;
var
    RBody: TStringStream;
    bRaiseException: boolean;
    theSubstituteAuthLine: string;
begin
    try
        RBody := TStringStream.Create(strPostData);

        // Custom HTTP request headers.
        FIdHTTPClient.Request.CustomHeaders := TIdHeaderList.Create;

        try
            FIdHTTPClient.Request.Accept := 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
            FIdHTTPClient.Request.ContentType := 'application/xml';
            FIdHTTPClient.Request.ContentEncoding := 'utf-8';
            FIdHTTPClient.Request.CacheControl := 'no-cache';
            FIdHTTPClient.Request.UserAgent := 'RobotCommand';

            FIdHTTPClient.Request.CustomHeaders.Add('Connection: keep-alive');
            FIdHTTPClient.Request.CustomHeaders.Add('Keep-Alive: timeout=30, max=3 header');

            // Create the correct authorization line for the commands stream server.
            theSubstituteAuthLine :=
                basicAuthenticationHeaderLine(userName, password);

            FIdHTTPClient.Request.CustomHeaders.Add(theSubstituteAuthLine);

            Result := FIdHTTPClient.Post(URL, RBody);

            // Let the owner component know the HTTP operation
            //  completed, whether the response code was
            //  successful or not.  Return the response code in the long
            //  parameter.
            PostMessageWithUserDataIntf(
                                FOwner.winHandleStable,
                                WM_HTTP_OPERATION_FINISHED,
                                POSTMESSAGEUSERDATA_LPARAM_IS_INTF,
                                TRovioCommandIsFinished.Create(
                                        FIdHttpClient.responseCode,
                                        commandName,
                                        strPostData,
                                        FIdHttpClient.ResponseText)
                                );
        finally
            FreeAndNil(RBody);
        end; // try/finally
    except
        {
            Exceptions that occur during an HTTP operation should not
            break the Execute() loop.  That would render this thread
            inactive.  Instead, call the background Exception handler
            and only raise an Exception if requested.
        }
        On E: Exception do
        begin
            // Default is to raise an Exception.  The background
            //  Exception event handler, if one exists, can
            //  override this by setting bRaiseException to
            //  FALSE.
            bRaiseException := true;

            FOwner.doBgException(E, bRaiseException);

            if bRaiseException then
                // Ok, raise it as requested.
                raise;
        end; // On E: Exception do
    end; // try/except
end;


// The background thread's Excecute() method and loop (excerpted).
procedure TClientThread_roviosendcmd_.Execute;
var
    errMsg: string;
    MsgRec : TMsg;
    theHttpCliName: string;
    intfCommandTodo, intfNewCommandTodo: IRovioSendCommandsTodo_indy;
    bSendResultNotification: boolean;
    responseBody, S: string;
    dwPriority: DWORD;
begin
    // Clear the current command todo and the busy flag.
    intfCommandTodo := nil;
    FOwner.isBusy := false;

    intfNewCommandTodo := nil;

    // -------- BEGIN: THREAD PRIORITY SETTING ------------

    dwPriority := GetThreadPriority(GetCurrentThread);

    {$IFDEF THREADDEBUG}
    OutputDebugString(PChar(
        Format('Current thread priority for the the send-commands background thread: %d', [dwPriority])
    ));
    {$ENDIF}

    // On single CPU systems like our Dell laptop, the system appears
    //  to have trouble executing smooth motion.  Guessing that
    //  the thread keeps getting interrupted.  Raising the thread priority
    //  to time critical to see if that helps.
    if not SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_TIME_CRITICAL) then
        RaiseLastOSError;

    dwPriority := GetThreadPriority(GetCurrentThread);

    {$IFDEF THREADDEBUG}
    OutputDebugString(PChar(
        Format('New thread priority for the the send-commands background thread after SetThreadPriority() call: %d', [dwPriority])
    ));
    {$ENDIF}

    // -------- END  : THREAD PRIORITY SETTING ------------

    // try

    // Create the client Indy HTTP component.
    theHttpCliName := '(unassigned)';

    theHttpCliName := FOwner.Name + '_idhttpcli';

    // 1-24-2012: Added empty component name check.
    if theHttpCliName = '' then
        raise Exception.Create('(TClientThread_roviosendcmd_.Execute) The client HTTP object is nameless.');

    FIdHTTPClient := TIdHTTP.Create(nil);

    { If GetMessage retrieves the WM_QUIT, the return value is FALSE and    }
    { the message loop is broken.                                           }
    while not Application.Terminated do
    begin
        try
            bSendResultNotification := false;

            // Reset the variable that detects new commands to do.
            intfNewCommandTodo := nil;

            {
                If we are repeating a command, use PeekMessage so that if
                there is nothing in the queue, we do not block and go
                on repeating the command.  Note, intfCommandTodo 
                becomes NIL after we execute a single-shot command.

                If we are not repeating a command, use GetMessage so
                it will block until there is something to do or we
                quit.
            }
            if Assigned(intfCommandTodo) then
            begin
                // Set the busy flag to let others know we have a command
                //  to execute (single-shot or looping).
                // FOwner.isBusy := true;

                {
                    Note: Might have to start draining the queue to
                    properly handle WM_QUIT if we have problems with this
                    code.
                }

                // See if we have a new command todo.
                if Integer(PeekMessage(MsgRec, 0, 0, 0, PM_REMOVE)) > 0 then
                begin
                    // WM_QUIT?
                    if MsgRec.message = WM_QUIT then
                        break // We're done.
                    else
                        // Recover the command todo if any.
                        intfNewCommandTodo := getCommandToDo(MsgRec);
                end; // if Integer(PeekMessage(MsgRec, FWndProcHandle, 0, 0, PM_REMOVE)) > 0 then
            end
            else
            begin
                // Not repeating a command.  Block until something new shows
                //  up or we quit.
                if GetMessage(MsgRec, 0, 0, 0) then
                    // Recover the command todo if any.
                    intfNewCommandTodo := getCommandToDo(MsgRec)
                else
                    // GetMessage() returned FALSE. We're done.
                    break;
            end; // else - if Assigned(intfCommandTodo) then

            // Did we get a new command todo?
            if Assigned(intfNewCommandTodo) then
            begin
                //  ----- COMMAND TODO REPLACED!

                // Update/Replace the command todo variable.  Set the
                //  busy flag too.
                intfCommandTodo := intfNewCommandTodo;
                FOwner.isBusy := true;

                // Clear the recently received new command todo.
                intfNewCommandTodo := nil;

                // Need to send a result notification after this command
                //  executes because it is the first iteration for it.
                //  (repeating commands only report the first iteration).
                bSendResultNotification := true;
            end; // if Assigned(intfNewCommandTodo) then

            // If we have a command to do, make the request.
            if Assigned(intfCommandTodo) then
            begin
                // Check for the clear command.
                if intfCommandTodo.commandName = 'CLEAR' then
                begin
                    // Clear the current command todo and the busy flag.
                    intfCommandTodo := nil;
                    FOwner.isBusy := false;

                    // Return the response as a simple result.
                    // FOwner.sendSimpleResult(newSimpleResult_basic('CLEAR command was successful'), intfCommandToDo);
                end
                else
                begin
                    // ------------- SEND THE COMMAND TO ROVIO --------------
                    // This method makes the actual HTTP request via the TIdHTTP
                    //  Post() method.
                    responseBody := doPost(
                        intfCommandTodo.commandName,
                        intfCommandTodo.cgiScriptName,
                        intfCommandTodo.userName_auth,
                        intfCommandTodo.password_auth,
                        intfCommandTodo.commandString);

                    // If this is the first or only execution of a command,
                    //  send a result notification back to the owner.
                    if bSendResultNotification then
                    begin
                        // Send back the fully reconstructed response since
                        //  that is what is expected.
                        S := FIdHTTPClient.Response.ResponseText + CRLF + FIdHTTPClient.Response.RawHeaders.Text + CRLF + responseBody;

                        // Return the response as a simple result.
                        FOwner.sendSimpleResult(newSimpleResult_basic(S), intfCommandToDo);
                    end; // if bSendResultNotification then

                    // If it is not a repeating command, then clear the
                    //  reference.  We don't need it anymore and this lets
                    //  us know we already executed it.
                    if not intfCommandTodo.isRepeating then
                    begin
                        // Clear the current command todo and the busy flag.
                        intfCommandTodo := nil;
                        FOwner.isBusy := false;
                    end; // if not intfCommandTodo.isRepeating then
                end; // if intfCommandTodo.commandName = 'CLEAR' then
            end
            else
                // Didn't do anything this iteration.  Yield
                //  control of the thread for a moment.
                Sleep(0);

        except
            // Do not let Exceptions break the loop.  That would render the
            //  component inactive.
            On E: Exception do
            begin
                // Post a message to the component log.
                postComponentLogMessage_error('ERROR in client thread for socket(' + theHttpCliName +').  Details: ' + E.Message, Self.ClassName);

                // Return the Exception to the current EZTSI if any.
                if Assigned(intfCommandTodo) then
                begin
                    if Assigned(intfCommandTodo.intfTinySocket_direct) then
                        intfCommandTodo.intfTinySocket_direct.sendErrorToRemoteClient(exceptionToErrorObjIntf(E, PERRTYPE_GENERAL_ERROR));
                end; // if Assigned(intfCommandTodo) then

                // Clear the command todo interfaces to avoid looping an error.
                intfNewCommandTodo      := nil;

                // Clear the current command todo and the busy flag.
                intfCommandTodo := nil;
                FOwner.isBusy := false;
            end; // On E: Exception do
        end; // try
    end; // while not Application.Terminated do
4

1 回答 1

4

要正确使用 HTTP keep-alives,请使用FIdHTTPClient.Request.Connection := 'keep-alive'instead ofFIdHTTPClient.Request.CustomHeaders.Add('Connection: keep-alive')或 set FIdHTTPClient.ProtocolVersion := pv1_1。至少它在 Indy 10 中是这样工作的。当我有机会时,我会仔细检查 Indy 9。

无论您使用哪个版本,机器人都必须首先支持keep-alives,否则TIdHTTP别无选择,只能为每个请求建立一个新的套接字连接。如果机器人发送不包含Connection: keep-alive标头的 HTTP 1.0 响应或包含标头的 HTTP 1.1 响应,Connection: close则不支持 keep-alives。

于 2012-04-14T03:59:46.707 回答