【问题标题】:boost::asio::read() blocking in custom web serviceboost::asio::read() 在自定义 Web 服务中阻塞
【发布时间】:2016-08-17 16:08:25
【问题描述】:

我很高兴看到有一个跨平台的 TCP/IP 套接字标准出现在 C++ 的 boost 中。到目前为止,我已经能够为我遇到的所有主题找到帮助。但现在我陷入了一种奇怪的行为。我正在 2013 年末的 iMac 上使用 Xcode 7.3.1 进行开发。

我正在为特殊目的开发一个简单的 Web 服务器。下面的代码是演示不良行为的精简版本:

#include <boost/asio.hpp>
#include <boost/bind.hpp>

using namespace std;
using namespace boost;
using namespace boost::asio;
using namespace boost::asio::ip;

int main(int argc, const char * argv[]) {

    static asio::io_service ioService;
    static tcp::acceptor tcpAcceptor(ioService, tcp::endpoint(tcp::v4(), 2080));

    while (true) {

        // creates a socket
        tcp::socket* socket = new tcp::socket(ioService);

        // wait and listen
        tcpAcceptor.accept(*socket);

        asio::streambuf inBuffer;
        istream headerLineStream(&inBuffer);

        char buffer[1];
        asio::read(*socket, asio::buffer(buffer, 1));  // <--- Yuck!

        asio::write(*socket, asio::buffer((string) "HTTP/1.1 200 OK\r\n\r\nYup!"));

        socket->shutdown(asio::ip::tcp::socket::shutdown_both);
        socket->close();
        delete socket;

    }

    return 0;
}

当我访问此服务时,在特定条件下,浏览器会阻塞 20 秒以上。如果我暂停在调试模式下运行的程序,我可以看到 asio::read() 调用正在阻塞。它实际上是在等待浏览器中出现一个字符。这是为什么呢?

让我澄清一下,因为我必须做些什么才能在我的机器上重现这个很奇怪。一旦我启动程序(用于调试),我会从 Chrome 中打开“页面”(http://localhost:2080/)。我可以多次点击刷新,它工作得很好。但是后来我使用了 Firefox(或 Safari),它可能会挂起 20 秒,然后页面按预期显示。现在得到这个。如果在 Firefox 延迟期间,我在 Chrome 中点击 Refresh,Firefox 页面也会立即显示。在另一个实验中,我在 Chrome 中点击 Refresh(工作正常),然后在 Firefox 和 Safari 中点击 Refresh。两人都挂了。我在 Chrome 中点击了 Refresh,所有 3 个都立即显示出来。

在这个实验的一个改变中,当我启动这个程序时,我在 Firefox 或 Safari 中点击了刷新,它们工作得很好。不管我刷新多少次。并且在他们之间来回穿梭。我实际上是按住 CMD-R 来快速刷新这些浏览器。但只要我在同一页面上刷新 Chrome,然后尝试刷新其他两个浏览器,它们就会再次挂起。

自 1993 年左右开始从事 Web 编程,我非常了解 HTTP 标准。最基本的工作流程是浏览器发起 TCP 连接。一旦 Web 服务器接受连接,客户端就会发送一个 HTTP 标头。根页面(“/”)的“GET /\r\n\r\n”之类的东西。服务器通常会读取所有标题行并停止,直到它到达第一个空白行,这表示标题结束和上传内容(例如,POST 表单内容)的开始,Web 应用程序可以随意使用或忽略这些内容.服务器在准备好时使用自己的 HTTP 标头进行响应,通常以“HTTP/1.1 200 OK\r\n”开头,然后是实际页面内容(或二进制文件内容等)。

在我的应用程序中,我实际上使用 asio::read_until(*socket, inBuffer, "\r\n\r\n") 来读取整个 HTTP 标头。由于那是挂起的,我想也许那些其他浏览器正在发送损坏的标头或其他东西。因此,我将样本缩减为仅读取单个字符(应该是“GET /”中的“G”)。一个字符。没有。

作为旁注,我知道我正在同步执行此操作,但我真的想要一个简单的线性演示来展示这种不良行为。我假设这不是导致此问题的原因,但我知道这是可能的。

这里有什么想法吗?在我的用例中,这是很痛苦的,因为服务器最终会响应,但我真的更愿意理解消除这种不良行为。

【问题讨论】:

  • 无法在我的 Mac 上重现它
  • 进一步研究,我发现 Chrome 发出了两个连接请求,而其他浏览器只发出一个。第一个带有 HTTP 标头,但第二个没有。看来这是 Chrome 中的一个错误。以下帖子相关:stackoverflow.com/questions/4761913/…
  • 我确实注意到来自 Chrome 的两个请求,一个针对“/”,另一个针对“favicon.ico”,两个请求似乎都很好。也许您使用的是旧版本的 chrome ?
  • 是的,Chrome 会单独尝试获取 /favicon.ico,但这与此无关。

标签: c++ http boost-asio


【解决方案1】:

这似乎是 Chrome 的设计怪癖所致。看这篇文章:

server socket receives 2 http requests when I send from chrome and receives one when I send from firefox

我知道现在发生了什么。 Chrome 发出 2 个连接请求。第一个用于所需页面,并包含正确的请求 HTTP 标头。第二个连接一旦被接受,甚至不包含一个字节的输入数据。所以我尝试读取第一个字节没有得到回报。幸运的是,读取尝试超时。使用 try/catch 很容易恢复。

这似乎是一种贪婪的优化,以加快 Chrome 的性能。也就是说,它保持下一个连接打开,直到浏览器需要来自站点的某些内容,然后它在该打开的套接字上发送 HTTP 请求。然后它立即打开一个新连接,再次期待未来的请求。虽然我知道这如何加快 Chrome 的体验,但这似乎是一个可疑的设计,因为它给服务器带来了额外的负担。

这是一个很好的论据,可以打开一个单独的线程来处理每个接受的套接字。当其他线程处理其他请求时,一个线程可以耐心等待永远不会出现的请求。为此,我在 tcpAcceptor.accept(*socket); 之后结束了所有内容。在一个新线程中,以便循环可以继续等待下一个请求。

【讨论】:

  • ASIO 的整个想法是避免创建新线程的必要性。你只是不应该无条件地阅读。您也不应该每次都创建一个套接字。无论如何都不使用 new ,它应该只是一个局部变量。
  • 这里也不需要静态。
  • 这里的“静态”是一个糟糕的事情。谢谢。
  • 据我所知,即使是 ASIO 的异步方法也需要线程内调用您的回调,这意味着如果您只有一个线程,一次将处理一个请求。我还没有测试证实这一点,但我会的。因此,如果一个请求需要很长时间,而另一个请求在第一个请求之后出现,它将坐等第一个请求完成。因此,我不认为 ASIO 消除了对多线程的需求。 boost.org/doc/libs/1_41_0/doc/html/boost_asio/overview/core/…
  • @IlyaPopov "ASIO 的整个想法是避免创建新线程的必要性"。当然不是。它确实为您提供了线程之上的抽象(几乎没有)。但是你必须在必要时使用线程(根据设计),当你必须处理不同类型的异步任务(如定时器事件、I/O 事件、阻塞事件等)时肯定会使用线程。
猜你喜欢
  • 1970-01-01
  • 2012-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多