boost::asio的http client应用笔记

简介: 1 踩过的坑1.1 io_serviceboost::asio::io_service::run()会一直运行到没有任务为止,如果中途调用stop(),则所有等待中的任务会立刻执行。

1 踩过的坑

1.1 io_service

boost::asio::io_service::run()会一直运行到没有任务为止,如果中途调用stop(),则所有等待中的任务会立刻执行。要在停止的时候抛弃所有的任务,解决方案是用run_one(),即

while (keep_running)
    io_service_.run_one();

keep_running是个bool值,要stop io_service的时候直接置false即可。

1.2 deadline_timer

在调用async_wait()后,无论调用deadline_timer::cancel()还是这个deadline_timer都析构掉,handler都会被触发。当然,这个在文档是有写的。规避野指针的办法有两个,一是传入的handler是shared_ptr,二是再封装一层。后者适用于handler的生命周期无法由自身控制的情况,示例代码请看http client一节的TimerHolder类。

1.3 async*

这个其实和deadline_timer::asyn::wait()差不多,async_readasync_read_until等带async_前缀的函数,只要中途被停止(例如调用ip::tcp::socket::close()),Handler都会被执行并传入一个代表aborted的boost::system::error_code。

1.4 ip::tcp::socket

  1. 官方的example过于简单,反而迷惑人了。HTTP协议说来也算复杂,例如chunked encoding还得自己解析。
  2. 从ip::tcp::resolver得到的可能是多个IP,如果把返回的迭代器交给async_connect,那么很可能出错,应为IP里可能有不合理的地址。比如可能返回的是全0的地址。解决办法参考http client代码的DoResolveAndConnect()函数。
  3. socket的read可能会读到额外的数据,这个文档里有写。

2. http client的应用

封装成了C++类。这是单线程的实现(io_service是同一线程下run的),同步地调用socket函数并用deadline_timer来异步返回数据会更容易控制。
不细说了,请看代码。
注:URL、HttpRequest、HttpResponse等类未列出源码,请自行实现对应函数。

// header
#include <assert.h>
#include <string>
#include "boost/asio.hpp"
#include "boost/make_shared.hpp"
#include "boost/thread.hpp"

class HttpTransaction {
 public:
  // The Delegate implementation MAY delete HttpTransaction during Delegate
  // method is invoked.
  // Possible flow: ("*" means zero or more times.)
  //   1. OnResponseReceived() -> OnDataReceived()* -> OnFinished().
  //   2. OnError().
  //   3. OnResponseReceived() -> OnError().
  //   4. OnResponseReceived() -> OnDataReceived()* -> OnError().
  class Delegate {
   public:
    virtual ~Delegate() {}

    virtual void OnResponseReceived(HttpTransaction* transaction,
                                    const HttpResponse& response) = 0;

    virtual void OnDataReceived(HttpTransaction* transaction,
                                const char* data,
                                size_t length) = 0;

    virtual void OnFinished(HttpTransaction* transaction) = 0;

    virtual void OnError(HttpTransaction* transaction, int error_code) = 0;
  };

  explicit HttpTransaction(boost::asio::io_service& io);
  ~HttpTransaction();

  void Start();

  void Cancel();

  const HttpRequest* request() const {
    return http_request_;
  }
  void set_request(const HttpRequest* request) {
    http_request_ = request;
  }

  Delegate* delegate() const { return delegate_; }
  void set_delegate(Delegate* delegate) {
    delegate_ = delegate;
  }

 private:
  enum State {
    STATE_NONE,
    STATE_CONNECT,
    STATE_SEND_REQUEST,
    STATE_READ_HEADER,
    STATE_READ_BODY,
    STATE_READ_CHUNK_SIZE,
    STATE_READ_CHUNK_DATA,
    STATE_READ_UNTIL_EOF,
    STATE_CALL_ON_FINISHED,
  };

  void DoLoop();

  bool DoResolveAndConnect();
  bool DoSendRequest();
  bool DoReadHeader();
  bool DoReadChunkSize();
  bool DoReadChunkData();
  bool DoReadBody();
  bool DoReadUntilEof();
  bool DoCallOnFinished();

  void CallOnDataReceived(size_t size, size_t additional_consume_size = 0);

  const HttpRequest* http_request_;
  Delegate* delegate_;
  HttpResponse* http_response_;

  boost::asio::ip::tcp::resolver resolver_;
  boost::asio::ip::tcp::socket socket_;
  boost::shared_ptr<boost::asio::streambuf> stream_buf_;

  size_t pending_read_size_;

  State next_state_;

  bool started_;

  class TimerHolder;
  boost::shared_ptr<TimerHolder> timer_holder_;
};

#####################################################################

// implementation
#include <string.h>
#include <assert.h>
#include <algorithm>
#include "boost/algorithm/hex.hpp"
#include "boost/bind.hpp"
#include "http_transaction.h"
#include "http_request.h"
#include "http_response.h"
#include "util/url.h"
#include "util/logging.h"

// TimerHolder is needed because the callback is invoked even the timer
// cancelled or deleted.
class HttpTransaction::TimerHolder {
 public:
  TimerHolder(HttpTransaction* trans,
              boost::asio::io_service& io)  // NOLINT
      : trans_(trans),
        timer_(io) {}

  void Schedule() {
    timer_.expires_from_now(boost::posix_time::microseconds(0));
    timer_.async_wait(boost::bind(&TimerHolder::OnTimer, trans_->timer_holder_,
                                  boost::asio::placeholders::error));
  }

  void OnTransactionCancelled() {
    trans_ = NULL;
    timer_.cancel();
  }

 private:
  void OnTimer(const boost::system::error_code& err) {
    if (!err && trans_) {
      trans_->DoLoop();
    }
  }

  HttpTransaction* trans_;
  boost::asio::deadline_timer timer_;
};

HttpTransaction::HttpTransaction(boost::asio::io_service& io)
    : http_request_(NULL),
      delegate_(NULL),
      http_response_(NULL),
      resolver_(io),
      socket_(io),
      stream_buf_(new boost::asio::streambuf),
      pending_read_size_(0),
      next_state_(STATE_NONE),
      started_(false),
      timer_holder_(new TimerHolder(this, io)) {
}

HttpTransaction::~HttpTransaction() {
  Cancel();
}

void HttpTransaction::Start() {
  assert(!started_);
  started_ = true;
  next_state_ = STATE_CONNECT;
  timer_holder_->Schedule();
}

void HttpTransaction::Cancel() {
  next_state_ = STATE_NONE;
  timer_holder_->OnTransactionCancelled();
  socket_.close();
  if (http_response_) {
    delete http_response_;
    http_response_ = NULL;
  }
}

void HttpTransaction::DoLoop() {
  bool rv = false;
  do {
    State state = next_state_;
    next_state_ = STATE_NONE;
    switch (state) {
      case STATE_CONNECT:
        rv = DoResolveAndConnect();
        break;
      case STATE_SEND_REQUEST:
        rv = DoSendRequest();
        break;
      case STATE_READ_HEADER:
        rv = DoReadHeader();
        break;
      case STATE_READ_BODY:
        rv = DoReadBody();
        break;
      case STATE_READ_UNTIL_EOF:
        rv = DoReadUntilEof();
        break;
      case STATE_READ_CHUNK_SIZE:
        rv = DoReadChunkSize();
        break;
      case STATE_READ_CHUNK_DATA:
        rv = DoReadChunkData();
        break;
      case STATE_CALL_ON_FINISHED:
        rv = DoCallOnFinished();
        break;
      default:
        assert(0);
        break;
    }
  } while (rv);
}

bool HttpTransaction::DoResolveAndConnect() {
  URL url(http_request_->url());
  // TODO(liuhx): if url is ip address.
  boost::asio::ip::tcp::resolver::query query(
      url.host(), url.port() == 0 ? url.protocol() : url.port_string());
  boost::system::error_code err;
  boost::asio::ip::tcp::resolver::iterator it = resolver_.resolve(query, err);
  boost::asio::ip::tcp::resolver::iterator end;  // Default is end.
  if (err || it == end) {
    LOG_DEBUG(kLogTagHttpTrans, "resolve error:%s",
                      err.message().c_str());
    delegate_->OnError(this, err.value());
    return false;
  }

  do {
    LOG_INFO(kLogTagHttpTrans, "dns result:%s",
                     it->endpoint().address().to_string().c_str());
    // "unspecified" means address is "0.0.0.0". It may appear on some machines
    // running Apache. Please google it for more detail.
    if (!it->endpoint().address().is_unspecified()) {
      socket_.close();
      LOG_INFO(kLogTagHttpTrans, "connecting:%s",
                       it->endpoint().address().to_string().c_str());
      socket_.connect(*it, err);
      if (!err)
        break;
    }
    ++it;
  } while (it != end);

  if (err) {
    LOG_DEBUG(kLogTagHttpTrans, "connect error:%s",
                      err.message().c_str());
    delegate_->OnError(this, err.value());
    return false;
  }

  next_state_ = STATE_SEND_REQUEST;
  return true;
}

bool HttpTransaction::DoSendRequest() {
  URL url(http_request_->url());
  std::ostream request_stream(stream_buf_.get());
  request_stream << http_request_->method() << " " << url.path()
                 << " HTTP/1.1\r\n";
  if (!http_request_->HasHeader("Host")) {
    request_stream << "Host: " << url.host() << "\r\n";
  }
  if (!http_request_->HasHeader("Connection")) {
    request_stream << "Connection: keep-alive\r\n";
  }
  const char* name;
  const char* value;
  void* iter = NULL;
  while (http_request_->EnumerateHeaderLines(&iter, &name, &value))
    request_stream << name << ": " << value << "\r\n";
  size_t size = 0;
  if (http_request_->body(&value, &size)) {
    if (!http_request_->HasHeader("Content-Length")) {
      request_stream << "Content-Length: " << size << "\r\n";
    }
    request_stream << "\r\n";
    request_stream.write(value, size);
  } else {
    request_stream << "\r\n";
  }

  boost::system::error_code err;
  // boost::asio::write() consumes |stream_buf_|, no need to do it ourselves.
  boost::asio::write(socket_, *stream_buf_, err);
  if (err) {
    LOG_DEBUG(kLogTagHttpTrans, "send request error:%s",
                      err.message().c_str());
    delegate_->OnError(this, err.value());
  } else {
    next_state_ = STATE_READ_HEADER;
    timer_holder_->Schedule();
  }
  return false;
}

bool HttpTransaction::DoReadHeader() {
  boost::system::error_code err;
  boost::asio::read_until(socket_, *stream_buf_, "\r\n\r\n", err);
  if (err) {
    LOG_DEBUG(kLogTagHttpTrans, "read header error:%s",
        err.message().c_str());
    delegate_->OnError(this, err.value());
    return false;
  }

  size_t size = stream_buf_->size();
  const char* data = boost::asio::buffer_cast<const char*>(stream_buf_->data());
  size_t pos = std::string(data, size).find("\r\n\r\n");
  if (pos == std::string::npos) {
    LOG_DEBUG(kLogTagHttpTrans,
                      "Can not find header end. Maybe TCP data Out-of-Order");
    delegate_->OnError(this, 1234);
    return false;
  }
  http_response_ = new HttpResponse(http_request_->url(), data, pos);
  stream_buf_->consume(pos + 4);  // Skip 4 = "\r\n\r\n".
  if (http_response_->status_code() < 100) {
    LOG_DEBUG(kLogTagHttpTrans, "Header invalid");
    delegate_->OnError(this, 2345);
    return false;
  }

  // TODO(liuhx): Body may be available even status code is not 200.For
  // example, some website may display a web page while replying with 404.
  if (http_response_->status_code() != 200) {
    next_state_ = STATE_CALL_ON_FINISHED;
  } else {
    const char* content_length = http_response_->GetHeader("content-length");
    if (content_length) {
      pending_read_size_ = atoi(content_length);
      next_state_ = STATE_READ_BODY;
    } else {
      const char* encoding = http_response_->GetHeader("Transfer-Encoding");
      bool is_chunk = encoding && (std::string(encoding).find("chunked") !=
                                  std::string::npos);
      if (is_chunk) {
        next_state_ = STATE_READ_CHUNK_SIZE;
      } else {
        next_state_ = STATE_READ_UNTIL_EOF;
      }
    }
  }
  timer_holder_->Schedule();

  delegate_->OnResponseReceived(this, *http_response_);
  return false;
}

bool HttpTransaction::DoReadBody() {
  // If content-length exists, the connection may be keep-alive. We MUST keep
  // counting |pending_read_size_| instead of reading until EOF.
  if (pending_read_size_ == 0) {
    delegate_->OnFinished(this);
    return false;
  }

  while (true) {
    // boost may read addtional data beyond the condition in STATE_READ_HEADER,
    // pass left data first if exists.
    size_t size = stream_buf_->size();
    if (size) {
      next_state_ = STATE_READ_BODY;
      timer_holder_->Schedule();
      // TODO(liuhx): assert -> OnError
      assert(pending_read_size_ >= size);
      pending_read_size_ -= size;
      CallOnDataReceived(size);
      break;
    } else {
      boost::system::error_code err;
      boost::asio::read(socket_, *stream_buf_,
                        boost::asio::transfer_at_least(1), err);
      if (err) {
        LOG_DEBUG(kLogTagHttpTrans, "read body error:%s",
            err.message().c_str());
        delegate_->OnError(this, err.value());
        break;
      }
    }
  }

  return false;
}

// About chunked encoding, refer to http://tools.ietf.org/html/rfc2616#page-25
bool HttpTransaction::DoReadChunkSize() {
  while (true) {
    // boost may read addtional data beyond the condition, find "\r\n" first.
    size_t size = stream_buf_->size();
    if (size) {
      const char* data =
          boost::asio::buffer_cast<const char*>(stream_buf_->data());
      size_t index = std::string(data, size).find("\r\n");
      if (index != std::string::npos) {
        pending_read_size_ = static_cast<size_t>(strtol(data, NULL, 16));
        stream_buf_->consume(index + 2);  // Skip +2 = "\r\n"
        if (pending_read_size_ == 0) {
          delegate_->OnFinished(this);
          return false;
        }
        break;
      }
    }
    boost::system::error_code err;
    boost::asio::read_until(socket_, *stream_buf_, "\r\n", err);
    if (err) {
      LOG_DEBUG(kLogTagHttpTrans, "read chunk size error:%s",
                        err.message().c_str());
      delegate_->OnError(this, err.value());
      return false;
    }
  }

  next_state_ = STATE_READ_CHUNK_DATA;
  return true;
}

bool HttpTransaction::DoReadChunkData() {
  while (true) {
    size_t size = stream_buf_->size();
    if (size) {
      bool reach_end = size >= pending_read_size_;
      size_t data_size = reach_end ? pending_read_size_ : size;
      pending_read_size_ -= data_size;
      next_state_ = reach_end ? STATE_READ_CHUNK_SIZE : STATE_READ_CHUNK_DATA;
      timer_holder_->Schedule();
      CallOnDataReceived(data_size, reach_end ? 2 : 0);  // Skip 2 = "\r\n".
      break;
    } else {
      boost::system::error_code err;
      boost::asio::read_until(socket_, *stream_buf_, "\r\n", err);
      if (err) {
        LOG_DEBUG(kLogTagHttpTrans, "read chunk data error:%s",
                          err.message().c_str());
        delegate_->OnError(this, err.value());
        break;
      }
    }
  }

  return false;
}

bool HttpTransaction::DoReadUntilEof() {
  while (true) {
    size_t size = stream_buf_->size();
    if (size) {
      next_state_ = STATE_READ_UNTIL_EOF;
      timer_holder_->Schedule();

      const char* data =
          boost::asio::buffer_cast<const char*>(stream_buf_->data());
      boost::shared_ptr<boost::asio::streambuf> buf = stream_buf_;
      delegate_->OnDataReceived(this, data, size);
      buf->consume(size);
      break;
    } else {
      boost::system::error_code err;
      boost::asio::read(socket_, *stream_buf_,
                        boost::asio::transfer_at_least(1), err);
      if (err) {
        if (err == boost::asio::error::eof) {
          delegate_->OnFinished(this);
        } else {
          LOG_DEBUG(kLogTagHttpTrans, "%s", err.message().c_str());
          delegate_->OnError(this, err.value());
        }
        break;
      }
    }
  }

  return false;
}

bool HttpTransaction::DoCallOnFinished() {
  delegate_->OnFinished(this);
  return false;
}

void HttpTransaction::CallOnDataReceived(size_t size,
                                             size_t additional_consume_size) {
  const char* data = boost::asio::buffer_cast<const char*>(stream_buf_->data());
  // Because Delegate may delete HttpTransaction during callback, we MUST NOT
  // access member variable after callback method, instead, use the |buf|.
  boost::shared_ptr<boost::asio::streambuf> buf = stream_buf_;
  delegate_->OnDataReceived(this, data, size);
  buf->consume(size + additional_consume_size);
}
目录
相关文章
|
1月前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
131 3
|
1月前
|
前端开发 JavaScript 安全
前端性能调优:HTTP/2与HTTPS在Web加速中的应用
【10月更文挑战第27天】本文介绍了HTTP/2和HTTPS在前端性能调优中的应用。通过多路复用、服务器推送和头部压缩等特性,HTTP/2显著提升了Web性能。同时,HTTPS确保了数据传输的安全性。文章提供了示例代码,展示了如何使用Node.js创建一个HTTP/2服务器。
58 3
|
3月前
|
安全 网络协议 应用服务中间件
AJP Connector:深入解析及在Apache HTTP Server中的应用
【9月更文挑战第6天】在Java Web应用开发中,Tomcat作为广泛使用的Servlet容器,经常与Apache HTTP Server结合使用,以提供高效、稳定的Web服务。而AJP Connector(Apache JServ Protocol Connector)作为连接Tomcat和Apache HTTP Server的重要桥梁,扮演着至关重要的角色
93 2
|
23天前
|
前端开发 UED 开发者
CSS Sprites和图标字体在网页图标加载优化中的应用。CSS Sprites通过合并多图标减少HTTP请求,提升加载速度
本文探讨了CSS Sprites和图标字体在网页图标加载优化中的应用。CSS Sprites通过合并多图标减少HTTP请求,提升加载速度;图标字体则以字体形式呈现图标,便于调整样式。文章分析了两者的优缺点及应用场景,并提供了应用技巧和注意事项,旨在帮助开发者提升页面性能,改善用户体验。
23 5
|
1月前
|
缓存 安全 网络安全
HTTP/2与HTTPS在Web加速中的应用
HTTP/2与HTTPS在Web加速中的应用
|
1月前
|
缓存 安全 前端开发
HTTP 协议的请求方法在实际应用中有哪些注意事项?
【10月更文挑战第29天】HTTP协议的请求方法在实际应用中需要根据具体的业务场景和需求,合理选择和使用,并注意各种方法的特点和限制,以确保网络通信的安全、高效和数据的一致性。
|
1月前
|
前端开发 安全 应用服务中间件
前端性能调优:HTTP/2与HTTPS在Web加速中的应用
【10月更文挑战第26天】随着互联网的快速发展,前端性能调优成为开发者的重要任务。本文探讨了HTTP/2与HTTPS在前端性能优化中的应用,介绍了二进制分帧、多路复用和服务器推送等特性,并通过Nginx配置示例展示了如何启用HTTP/2和HTTPS,以提升Web应用的性能和安全性。
37 3
|
1月前
|
网络协议 前端开发 API
HTTP 和 TCP 协议的应用场景有哪些不同
【10月更文挑战第25天】HTTP(超文本传输协议)和 TCP(传输控制协议)处于网络协议栈的不同层次,各自具有独特的功能和特点,因此它们的应用场景也存在明显的差异。
|
2月前
|
JavaScript 安全 Java
谈谈UDP、HTTP、SSL、TLS协议在java中的实际应用
下面我将详细介绍UDP、HTTP、SSL、TLS协议及其工作原理,并提供Java代码示例(由于Deno是一个基于Node.js的运行时,Java代码无法直接在Deno中运行,但可以通过理解Java示例来类比Deno中的实现)。
81 1
|
3月前
|
安全 网络安全 数据安全/隐私保护
HTTP与HTTPS协议区别及应用场景
在互联网高速发展的今天,HTTP与HTTPS作为数据传输的基石,作用至关重要。HTTP允许客户端与服务器间传输超文本文档,但其数据传输过程未加密,存在安全隐患;HTTPS则在此基础上加入了SSL/TLS协议,实现了数据加密传输,增强了安全性,广泛应用于电子商务、网上银行、政府网站及社交媒体平台等涉及敏感信息传输的领域,有效保护了用户隐私和数据安全。随着网络安全意识提升,HTTPS正逐渐成为主流。
下一篇
DataWorks