【问题标题】:How can I write a file to a socket using the 'chunked' HTTP Transfer-Protocol in boost::asio?如何使用 boost::asio 中的“分块”HTTP 传输协议将文件写入套接字?
【发布时间】:2013-04-16 20:00:23
【问题描述】:

所以使用boost HTTP Server 3 example,我想修改connection::handle_read 以支持将正文与消息一起发送。但是,这样做的方法对我来说并不明显。我想写这样的东西:

void connection::handle_read(const boost::system::error_code& e,
    std::size_t bytes_transferred)
{
    ...
 if (result)
    {     
      boost::asio::async_write(socket_, reply.to_buffers(),
          strand_.wrap(
            boost::bind(&connection::write_body, shared_from_this(),
              boost::asio::placeholders::error)));
    }
}

void connection::write_body(const boost::system::error_code& e)
{
    boost::asio::async_write(socket_, body_stream_,
      strand_.wrap(
      boost::bind(&connection::handle_write, shared_from_this(),
      boost::asio::placeholders::error)));
}

其中body_stream_asio::windows::stream_handle

但是这种方法根本不处理 http 分块(这意味着在每个块之前发送块的大小)。解决这个问题的最佳方法是什么?我是否为符合boost const buffer 要求的 ifstream 编写自己的包装器?或者尝试通过循环多次调用async_write_some来模拟async_write的效果?我应该提到解决方案的一个要求是,我永远不会在任何给定时间将整个文件放在内存中 - 只有一个或几个块。

对 ASIO 和套接字非常陌生,感谢任何建议!

【问题讨论】:

    标签: c++ boost boost-asio


    【解决方案1】:

    将异步编程可视化为函数链而不是循环可能更容易。在分解链时,我发现将操作分成两部分(启动和完成)很有帮助,然后说明潜在的调用路径。这是一个示例说明,它从body_stream_ 异步读取一些数据,然后通过HTTP Chunked Transfer Encoding 将其写出套接字:

    void connection::start()
    {
      socket.async_receive_from(..., &handle_read);  --.
    }                                                  |
        .----------------------------------------------'
        |      .-----------------------------------------.
        V      V                                         |
    void connection::handle_read(...)                    |
    {                                                    |
      if (result)                                        |
      {                                                  |
        body_stream_.assign(open(...))                   |
                                                         |
        write_header();  --------------------------------|-----.
      }                                                  |     |
      else if (!result)                                  |     |
        boost::asio::async_write(..., &handle_write);  --|--.  |
      else                                               |  |  |
        socket_.async_read_some(..., &handle_read);  ----'  |  |
    }                                                       |  |
        .---------------------------------------------------'  |
        |                                                      |
        V                                                      |
    void connection::handle_write()                            |
    {}                                                         |
        .------------------------------------------------------'
        |
        V
    void connection::write_header() 
    {
      // Start chunked transfer coding.  Write http headers:
      //   HTTP/1.1. 200 OK\r\n
      //   Transfer-Encoding: chunked\r\n
      //   Content-Type: text/plain\r\n
      //   \r\n
      boost::asio::async_write(socket_, ...,
        &handle_write_header);  --.
    }   .-------------------------'
        |
        V
    void connection::handle_write_header(...)
    {
      if (error) return;
    
      read_chunk(); --.
    }   .-------------'
        |      .--------------------------------------------.
        V      V                                            |
    void connection::read_chunk()                           |
    {                                                       |
      boost::asio::async_read(body_stream_, ...,            |
        &handle_read_chunk);  --.                           |
    }   .-----------------------'                           |
        |                                                   |
        V                                                   |
    void connection::handle_read_chunk(...)                 |
    {                                                       |
      bool eof = error == boost::asio::error::eof;          |
                                                            |
      // On non-eof error, return early.                    |
      if (error && !eof) return;                            |
                                                            |
      write_chunk(bytes_transferred, eof);  --.             |
    }   .-------------------------------------'             |
        |                                                   |
        V                                                   |
    void connection::write_chunk(...)                       |
    {                                                       |
      // Construct chunk based on rfc2616 section 3.6.1     |
      // If eof has been reached, then append last-chunk.   |
      boost::asio::async_write(socket_, ...,                |
        &handle_write_chunk);  --.                          |
    }   .------------------------'                          |
        |                                                   |
        V                                                   |
    void connection::handle_write_chunk(...)                |
    {                                                       |
      // If an error occured or no more data is available,  |
      // then return early.                                 |
      if (error || eof) return;                             |
                                                            |
      // Read more data from body_stream_.                  |
      read_chunk();  ---------------------------------------'
    }
    

    如上所示,分块是通过异步链完成的,其中数据从body_stream_ 读取,根据 HTTP 分块传输编码规范准备写入,然后写入套接字。如果body_stream_ 仍有数据,则进行另一次迭代。


    我没有要测试的 Windows 环境,但这里有一个 Linux 上的基本完整示例,它一次将数据分块 10 个字节。

    #include <iostream>
    #include <sstream>
    #include <string>
    #include <vector>
    
    #include <boost/array.hpp>
    #include <boost/asio.hpp>
    #include <boost/bind.hpp>
    
    using boost::asio::ip::tcp;
    namespace posix = boost::asio::posix;
    
    // Constant strings.
    const std::string http_chunk_header = 
      "HTTP/1.1 200 OK\r\n"
      "Transfer-Encoding: chunked\r\n"
      "Content-Type: text/html\r\n"
      "\r\n";
    const char crlf[]       = { '\r', '\n' };
    const char last_chunk[] = { '0', '\r', '\n' };
    
    std::string to_hex_string(std::size_t value)
    {
      std::ostringstream stream;
      stream << std::hex << value;
      return stream.str();
    }
    
    class chunk_connection
    {
    public:
    
      chunk_connection(
          boost::asio::io_service& io_service,
          const std::string& pipe_name)
        : socket_(io_service),
          body_stream_(io_service),
          pipe_name_(pipe_name)
      {}
    
      /// Get the socket associated with the connection
      tcp::socket& socket() { return socket_; }
    
      /// Start asynchronous http chunk coding.
      void start(const boost::system::error_code& error)
      {
        // On error, return early.
        if (error)
        {
          close();
          return;
        }
    
        std::cout << "Opening pipe." << std::endl;
        int pipe = open(pipe_name_.c_str(), O_RDONLY);
        if (-1 == pipe)
        {
          std::cout << "Failed to open pipe." << std::endl;
          close();
          return;
        }
       
        // Assign native descriptor to Asio's stream_descriptor.
        body_stream_.assign(pipe); 
    
        // Start writing the header.
        write_header();
      }
    
    private:
    
      // Write http header.
      void write_header()
      {    
        std::cout << "Writing http header." << std::endl;
    
        // Start chunked transfer coding.  Write http headers:
        //   HTTP/1.1. 200 OK\r\n
        //   Transfer-Encoding: chunked\r\n
        //   Content-Type: text/plain\r\n
        //   \r\n 
        boost::asio::async_write(socket_,
          boost::asio::buffer(http_chunk_header),
          boost::bind(&chunk_connection::handle_write_header, this,
            boost::asio::placeholders::error));
      }
    
      /// Handle writing of http header.
      void handle_write_header(const boost::system::error_code& error)
      {
        // On error, return early.
        if (error)
        {
          close();
          return;
        }
    
        read_chunk();
      }
    
      // Read a file chunk.
      void read_chunk()
      {
        std::cout << "Reading from body_stream_...";
        std::cout.flush();
    
        // Read body_stream_ into chunk_data_ buffer.
        boost::asio::async_read(body_stream_,
          boost::asio::buffer(chunk_data_),
          boost::bind(&chunk_connection::handle_read_chunk, this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));
      }
    
      // Handle reading a file chunk.
      void handle_read_chunk(const boost::system::error_code& error, 
                             std::size_t bytes_transferred)
      {
        bool eof = error == boost::asio::error::eof;
    
        // On non-eof error, return early.
        if (error && !eof)
        {
          close();
          return;
        }
    
        std::cout << bytes_transferred << " bytes read." << std::endl;
        write_chunk(bytes_transferred, eof);
      }
    
      // Prepare chunk and write to socket.
      void write_chunk(std::size_t bytes_transferred, bool eof)
      {
        std::vector<boost::asio::const_buffer> buffers;
    
        // If data was read, create a chunk-body.
        if (bytes_transferred)
        {
          // Convert bytes transferred count to a hex string.
          chunk_size_ = to_hex_string(bytes_transferred);
    
          // Construct chunk based on rfc2616 section 3.6.1
          buffers.push_back(boost::asio::buffer(chunk_size_));
          buffers.push_back(boost::asio::buffer(crlf));
          buffers.push_back(boost::asio::buffer(chunk_data_, bytes_transferred));
          buffers.push_back(boost::asio::buffer(crlf));
        }
    
        // If eof, append last-chunk to outbound data.
        if (eof)
        {
          buffers.push_back(boost::asio::buffer(last_chunk));
          buffers.push_back(boost::asio::buffer(crlf));
        }
    
        std::cout << "Writing chunk..." << std::endl;
    
        // Write to chunk to socket.
        boost::asio::async_write(socket_, buffers,
          boost::bind(&chunk_connection::handle_write_chunk, this,
            boost::asio::placeholders::error, 
            eof));
      }
    
      // Handle writing a chunk.
      void handle_write_chunk(const boost::system::error_code& error,
                              bool eof)
      {
        // If eof or error, then shutdown socket and return.
        if (eof || error)
        {
          // Initiate graceful connection closure.
          boost::system::error_code ignored_ec;
          socket_.shutdown(tcp::socket::shutdown_both, ignored_ec);
          close();
          return;
        }
    
        // Otherwise, body_stream_ still has data.
        read_chunk();
      }
    
      // Close the socket and body_stream.
      void close()
      {
        boost::system::error_code ignored_ec;
        socket_.close(ignored_ec);
        body_stream_.close(ignored_ec);
      }
    
    private:
    
      // Socket for the connection.
      tcp::socket socket_;
    
      // Stream file being chunked.
      posix::stream_descriptor body_stream_;
    
      // Buffer to read part of the file into.
      boost::array<char, 10> chunk_data_;
    
      // Buffer holds hex encoded value of chunk_data_'s valid size.
      std::string chunk_size_;
    
      // Name of pipe.
      std::string pipe_name_;
    };
      
    int main()
    {
      boost::asio::io_service io_service;
    
      // Listen to port 80.
      tcp::acceptor acceptor_(io_service, tcp::endpoint(tcp::v4(), 80));
    
      // Asynchronous accept connection.
      chunk_connection connection(io_service, "example_pipe");
      acceptor_.async_accept(connection.socket(),
        boost::bind(&chunk_connection::start, &connection,
          boost::asio::placeholders::error));
    
      // Run the service.
      io_service.run();
    }
    

    我有一个小 html 文件,它将通过分块编码提供,一次 10 个字节:

    <html>
    <body>
      Test transfering html over chunked encoding.
    </body>
    </html>
    

    运行服务器:

    $ mkfifo example_pipe
    $ sudo ./a.out &
    [1] 28963
    <open browser and connected to port 80>
    $ cat html > example_pipe
    

    服务器的输出:

    打开管道。
    编写 http 标头。
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 10 个字节。
    写块...
    从 body_stream_ 读取...读取 7 个字节。
    写块...

    wireshark 输出显示没有格式错误的数据:

    0000  48 54 54 50 2f 31 2e 31  20 32 30 30 20 4f 4b 0d   HTTP/1.1  200 OK.
    0010  0a 54 72 61 6e 73 66 65  72 2d 45 6e 63 6f 64 69   .Transfe r-Encodi
    0020  6e 67 3a 20 63 68 75 6e  6b 65 64 0d 0a 43 6f 6e   ng: chun ked..Con
    0030  74 65 6e 74 2d 54 79 70  65 3a 20 74 65 78 74 2f   tent-Typ e: text/
    0040  68 74 6d 6c 0d 0a 0d 0a  61 0d 0a 3c 68 74 6d 6c   html.... a..<html
    0050  3e 0a 3c 62 6f 0d 0a 61  0d 0a 64 79 3e 0a 20 20   >.<bo..a ..dy>.  
    0060  54 65 73 74 0d 0a 61 0d  0a 20 74 72 61 6e 73 66   Test..a. . transf
    0070  65 72 69 0d 0a 61 0d 0a  6e 67 20 68 74 6d 6c 20   eri..a.. ng html 
    0080  6f 76 0d 0a 61 0d 0a 65  72 20 63 68 75 6e 6b 65   ov..a..e r chunke
    0090  64 0d 0a 61 0d 0a 20 65  6e 63 6f 64 69 6e 67 2e   d..a.. e ncoding.
    00a0  0d 0a 61 0d 0a 0a 3c 2f  62 6f 64 79 3e 0a 3c 0d   ..a...</ body>.<.
    00b0  0a 37 0d 0a 2f 68 74 6d  6c 3e 0a 0d 0a 30 0d 0a   .7../htm l>...0..
    00c0  0d 0a                                              ..               
    

    【讨论】:

    • 很好的描述!这与我最终想出的非常相似,但在我的情况下,我需要完整的 http 消息才能开始回复,因为回复取决于消息。
    【解决方案2】:

    这个例子很简单,只是为了告诉你如何简单地处理HTTP请求。这个例子不支持分块传输编码。 给你一些建议:

    • 要了解什么是块传输编码,您可以在 RFC2616,第 3.6 节中找到它。
    • 在发送前做一些事情: 设置 HTTP headers 以指示 reapons 消息使用 Chunked Transfer Encoding; 使用分块传输编码对您的数据进行编码。

    逻辑是这样的:

    std::string http_head;
    std::string http_body;
    
    char buff[10000];
    read_file_to_buff(buff);
    set_http_head_values(http_head);
    encode_chunk_format(buff,http_body);
    
    
    boost::asio::async_write(socket_,
        boost::asio::buffer(http_head.c_str(), http_head.length()),
        boost::bind(&connection::handle_write, shared_from_this(),
        boost::asio::placeholders::error);
    
    boost::asio::async_write(socket_,
        boost::asio::buffer(http_body.c_str(), http_body.length()),
        boost::bind(&connection::handle_write, shared_from_this(),
        boost::asio::placeholders::error);
    

    当你测试你的程序时,你可以使用 Fiddler2 来监控 http 消息。

    【讨论】:

    • 我确实了解块传输编码并实现了一个工作版本 - 我遇到的问题是使用 'get_headers()' 逻辑,我需要在转发之前将整个文件加载到内存中到异步发送。我想做的是在发送时读取,所以我不必将整个大文件加载到内存中。另外,上面的行不通,因为如果我没看错,你不能同步调用 2 async_writes;相反,您可以从 handle_write 函数中调用它。我正在从更熟悉 asio 的人那里寻找一个简单/优雅的内存问题解决方案
    【解决方案3】:

    所以我想出的解决方案是拥有 2 个写入函数 - write_body()write_complete()。读取完成后,假设我们要发送一个正文,我们调用

    body_fp_.open(bodyFile);
    async_write(get_headers(), write_body)
    

    在 write_body 内部,我们执行类似的操作

    vector<boost::asio::const_buffer> buffers;
    body_fp_.read(buffer_, buffer_.size())
    buffers.push_back(...size, newlines, etc...);
    buffers.push_back(buffer_, body_fp_.gcount());
    

    一旦我们完成文件内容的写入,最后一次写入:

    boost::asio::async_write(socket_, boost::asio::buffer(misc_strings::eob),
                strand_.wrap(
                boost::bind(&connection::write_complete, shared_from_this(),
                boost::asio::placeholders::error)));
    

    这似乎工作得很好,并且减轻了对内存使用的担忧。对此解决方案的任何 cmet 表示赞赏;希望这将有助于将来遇到类似问题的其他人。

    【讨论】:

      猜你喜欢
      • 2016-08-03
      • 1970-01-01
      • 2015-06-09
      • 2021-10-14
      • 1970-01-01
      • 2015-06-11
      • 1970-01-01
      • 2023-03-13
      • 1970-01-01
      相关资源
      最近更新 更多