中止XMLHttpRequest的内部(客户端和服务器)

pos*_*spi 10 javascript ajax xmlhttprequest internals

所以我很好奇在中止异步javascript请求时发生的实际底层行为.这个问题有一些相关的信息,但我还没有找到任何全面的信息.

我的假设一直是,中止请求会导致浏览器关闭连接并完全停止处理,从而导致服务器执行相同操作(如果已设置).但我想,在这里我可能没有想到特定于浏览器的怪癖或边缘情况.

我的理解如下,我希望有人可以在必要时纠正它,这对其他人来说是一个很好的参考.

  • 中止XHR请求客户端会导致浏览器在内部关闭套接字并停止处理它.我希望这种行为而不是简单地忽略进来的数据并浪费内存.我不打算在IE上打赌.
  • 服务器上的中止请求将取决于在那里运行的内容:
    • 我知道使用PHP时,默认行为是在客户端套接字关闭时停止处理,除非ignore_user_abort()已被调用.因此,关闭XHR连接也可以节省服务器功耗.
    • 我真的很想知道如何在node.js中处理这个问题,我假设在那里需要一些手工工作.
    • 我真的不知道其他服务器语言/框架以及它们的行为方式,但如果有人想提供具体内容,我很乐意在这里添加它们.

Tra*_*man 18

对于客户来说,最好看的地方是在源头,所以让我们这样做!:)

让我们来看看眨眼的执行XMLHttpRequest的abort方法(线1083至1119年在XMLHttpRequest.cpp):

void XMLHttpRequest::abort()
{
    WTF_LOG(Network, "XMLHttpRequest %p abort()", this);
    // internalAbort() clears |m_loader|. Compute |sendFlag| now.
    //
    // |sendFlag| corresponds to "the send() flag" defined in the XHR spec.
    //
    // |sendFlag| is only set when we have an active, asynchronous loader.
    // Don't use it as "the send() flag" when the XHR is in sync mode.
    bool sendFlag = m_loader;
    // internalAbort() clears the response. Save the data needed for
    // dispatching ProgressEvents.
    long long expectedLength = m_response.expectedContentLength();
    long long receivedLength = m_receivedLength;
    if (!internalAbort())
        return;
    // The script never gets any chance to call abort() on a sync XHR between
    // send() call and transition to the DONE state. It's because a sync XHR
    // doesn't dispatch any event between them. So, if |m_async| is false, we
    // can skip the "request error steps" (defined in the XHR spec) without any
    // state check.
    //
    // FIXME: It's possible open() is invoked in internalAbort() and |m_async|
    // becomes true by that. We should implement more reliable treatment for
    // nested method invocations at some point.
    if (m_async) {
        if ((m_state == OPENED && sendFlag) || m_state == HEADERS_RECEIVED || m_state == LOADING) {
            ASSERT(!m_loader);
            handleRequestError(0, EventTypeNames::abort, receivedLength, expectedLength);
        }
    }
    m_state = UNSENT;
} 
Run Code Online (Sandbox Code Playgroud)

因此,看起来大部分的grunt工作都是在内部完成的internalAbort,看起来像这样:

bool XMLHttpRequest::internalAbort()
{
    m_error = true;
    if (m_responseDocumentParser && !m_responseDocumentParser->isStopped())
        m_responseDocumentParser->stopParsing();
    clearVariablesForLoading();
    InspectorInstrumentation::didFailXHRLoading(executionContext(), this, this);
    if (m_responseLegacyStream && m_state != DONE)
        m_responseLegacyStream->abort();
    if (m_responseStream) {
        // When the stream is already closed (including canceled from the
        // user), |error| does nothing.
        // FIXME: Create a more specific error.
        m_responseStream->error(DOMException::create(!m_async && m_exceptionCode ? m_exceptionCode : AbortError, "XMLHttpRequest::abort"));
    }
    clearResponse();
    clearRequest();
    if (!m_loader)
        return true;
    // Cancelling the ThreadableLoader m_loader may result in calling
    // window.onload synchronously. If such an onload handler contains open()
    // call on the same XMLHttpRequest object, reentry happens.
    //
    // If, window.onload contains open() and send(), m_loader will be set to
    // non 0 value. So, we cannot continue the outer open(). In such case,
    // just abort the outer open() by returning false.
    RefPtr<ThreadableLoader> loader = m_loader.release();
    loader->cancel();
    // If abort() called internalAbort() and a nested open() ended up
    // clearing the error flag, but didn't send(), make sure the error
    // flag is still set.
    bool newLoadStarted = m_loader;
    if (!newLoadStarted)
        m_error = true;
    return !newLoadStarted;
}
Run Code Online (Sandbox Code Playgroud)

我不是C++专家,但从它的外观来看,internalAbort做了一些事情:

  • 停止当前对给定传入响应执行的任何处理
  • 清除与请求/响应关联的任何内部XHR状态
  • 告诉检查员报告XHR失败了(这真的很有意思!我打赌这是那些好的控制台消息来源的地方)
  • 关闭响应流的"遗留"版本,或响应流的现代版本(这可能是与您的问题有关的最有趣的部分)
  • 处理一些线程问题以确保错误传播正确(谢谢,评论).

经过大量的挖掘后,我在HttpResponseBodyDrainer(第110-124行)中遇到了一个有趣的函数,调用Finish它看起来像是在取消请求时最终会被调用的东西:

void HttpResponseBodyDrainer::Finish(int result) {
  DCHECK_NE(ERR_IO_PENDING, result);
  if (session_)
    session_->RemoveResponseDrainer(this);
  if (result < 0) {
    stream_->Close(true /* no keep-alive */);
  } else {
    DCHECK_EQ(OK, result);
    stream_->Close(false /* keep-alive */);
  }
  delete this;
}
Run Code Online (Sandbox Code Playgroud)

事实证明stream_->Close,至少在BasicHttpStream中,委托给HttpStreamParser::Close,当给出一个non-reusable标志(当请求被中止时似乎确实发生了,如图所示HttpResponseDrainer)时,会关闭套接字:

void HttpStreamParser::Close(bool not_reusable) {
  if (not_reusable && connection_->socket())
    connection_->socket()->Disconnect();
  connection_->Reset();
}
Run Code Online (Sandbox Code Playgroud)

所以,就客户端发生的情况而言,至少在Chrome的情况下,看起来你的初始直觉是正确的,据我所知:)似乎大多数怪癖和边缘情况都与调度/事件通知/线程问题,以及特定于浏览器的处理,例如将中止的XHR报告给devtools控制台.

就服务器而言,在NodeJS的情况下,您需要监听http响应对象上的"close"事件.这是一个简单的例子:

'use strict';

var http = require('http');

var server = http.createServer(function(req, res) {
  res.on('close', console.error.bind(console, 'Connection terminated before response could be sent!'));
  setTimeout(res.end.bind(res, 'yo'), 2000);
});

server.listen(8080);
Run Code Online (Sandbox Code Playgroud)

尝试运行它并在完成之前取消请求.您将在控制台上看到错误.

希望你发现这很有用.挖掘Chromium/Blink源非常有趣:)