Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I write a file to a socket using the 'chunked' HTTP Transfer-Protocol in boost::asio?

So working off of the boost HTTP Server 3 example, I want to modify connection::handle_read to support sending a body along with the message. However, the method for doing this is not apparent to me. I want to write something like:

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)));
}

where body_stream_ is an asio::windows::stream_handle.

But this approach doesn't handle the http chunking at all (all that means is the size of the chunk is sent before each chunk). What is the best way to approach this problem? Do I write my own wrapper for an ifstream that adheres to the requiresments of a boost const buffer? Or try to simulate the effect of async_write with multiple calls to async_write_some in a loop? I should mention a requirement of the solution is that I never have the entire file in memory at any given time - only one or a few chunks.

Very new to ASIO and sockets, any advice is appreciated!

like image 746
Rollie Avatar asked Apr 16 '13 20:04

Rollie


People also ask

How does transfer encoding chunked work?

In chunked transfer encoding, the data stream is divided into a series of non-overlapping "chunks". The chunks are sent out and received independently of one another. No knowledge of the data stream outside the currently-being-processed chunk is necessary for both the sender and the receiver at any given time.

What is chunked HTTP request?

Chunking is a technique that HTTP servers use to improve responsiveness. Chunking can help you avoid situations where the server needs to obtain dynamic content from an external source and delays sending the response to the client until receiving all of the content so the server can calculate a Content-Length header.

What is API chunking?

A chunked response means that instead of waiting for the entire result, split the result into chunks (partial results) and send one after the other. Sending a response in chunks is useful for a RESTful web API if the resource returned by the API is huge in size.


1 Answers

It may be easier to visualize asynchronous programming as a chain of functions rather than looping. When breaking apart the chains, I find it to be helpful to break operations into two parts (initiation and completion), then illustrate the potential call paths. Here is an example illustration that asynchronously reads some data from body_stream_, then writes it out the socket via 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();  ---------------------------------------'
}

As illustrated above, the chunking is done via an asynchronous chain, where data is read from body_stream_, prepared for writing based on the HTTP Chunked Transfer Encoding specification, then written to the socket. If body_stream_ still has data, then another iteration occurs.


I do not have a Windows environment to test on, but here is a basic complete example on Linux that chunks data 10 bytes at a time.

#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();
}

I have a small html file that will be served over chunked encoding, 10 bytes at a time:

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

Running server:

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

The output of the server:

Opening pipe.
Writing http header.
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...10 bytes read.
Writing chunk...
Reading from body_stream_...7 bytes read.
Writing chunk...

The wireshark output shows no-malformed data:

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                                              ..               
like image 79
Tanner Sansbury Avatar answered Nov 15 '22 10:11

Tanner Sansbury