Asio 注意事項

本文列舉 Asio 各類值得注意的細節。git

另見:基於 Asio 的 C++ 網絡編程編程

No Deprecated

在包含 Asio 頭文件以前,定義宏 BOOST_ASIO_NO_DEPRECATED,這樣在編譯時,Asio 就會剔除那些已通過時的接口。segmentfault

好比在最新的 Boost 1.66 中,io_service 已經更名爲 io_context,若是沒有 BOOST_ASIO_NO_DEPRECATED,仍是能夠用 io_service 的,雖然那只是 io_context 的一個 typedef網絡

BOOST_ASIO_NO_DEPRECATED 能夠保證你用的是最新修訂的 API。長期來看,有便於代碼的維護。況且,這些修訂正是 Asio 進入標準庫的前奏。socket

#define BOOST_ASIO_NO_DEPRECATED
#include "boost/asio/io_context.hpp"
#include "boost/asio/deadline_timer.hpp"
...

_WIN32_WINNT Warning

在 Windows 平臺,編譯時會遇到關於 _WIN32_WINNT 的警告。
能夠說,這是 Asio 自身的問題。
它應該在某個地方包含 SDKDDKVer.h
不該該讓用戶本身去定義平臺的版本。async

若是你用 CMake,能夠藉助下面這個宏自動檢測 _WIN32_WINNT
(詳見:https://stackoverflow.com/a/4...tcp

if (WIN32)
    macro(get_WIN32_WINNT version)
        if (CMAKE_SYSTEM_VERSION)
            set(ver ${CMAKE_SYSTEM_VERSION})
            string(REGEX MATCH "^([0-9]+).([0-9])" ver ${ver})
            string(REGEX MATCH "^([0-9]+)" verMajor ${ver})
            # Check for Windows 10, b/c we'll need to convert to hex 'A'.
            if ("${verMajor}" MATCHES "10")
                set(verMajor "A")
                string(REGEX REPLACE "^([0-9]+)" ${verMajor} ver ${ver})
            endif ("${verMajor}" MATCHES "10")
            # Remove all remaining '.' characters.
            string(REPLACE "." "" ver ${ver})
            # Prepend each digit with a zero.
            string(REGEX REPLACE "([0-9A-Z])" "0\\1" ver ${ver})
            set(${version} "0x${ver}")
        endif(CMAKE_SYSTEM_VERSION)
    endmacro(get_WIN32_WINNT)

    get_WIN32_WINNT(ver)
    add_definitions(-D_WIN32_WINNT=${ver})
endif(WIN32)

儘可能少包含頭文件

儘可能不要直接包含大而全的 boost/asio.hpp
這樣作,是爲了幫助本身記憶哪一個類源於哪一個具體的頭文件,以及避免包含那些沒必要要的頭文件。函數

在實際項目中,在你本身的某個「頭文件」裏簡單粗暴的包含 boost/asio.hpp 是很不妥的;固然,在你的「源文件」裏包含 boost/asio.hpp 是能夠接受的,畢竟實際項目依賴的東西比較多,很難搞清楚每個定義源自哪裏。this

Handler 簽名問題

雖然關於 Handler 的簽名,文檔裏都有說明,可是直接定位到源碼,更方便,也更精確。spa

deadline_timer.async_wait() 爲例,在 IDE 裏定位到 async_wait() 的定義,代碼(片斷)以下:

template <typename WaitHandler>
  BOOST_ASIO_INITFN_RESULT_TYPE(WaitHandler,
      void (boost::system::error_code))
  async_wait(BOOST_ASIO_MOVE_ARG(WaitHandler) handler)
  {
    ...

經過宏 BOOST_ASIO_INITFN_RESULT_TYPEWaitHandler 的簽名一目瞭然。

Handler 的 error_code 參數究竟是不是引用?

其實,早期的版本應該是 const boost::system::error_code&,如今文檔和代碼註釋裏還有這麼寫的,估計是沒來得及更新。
前面在說 Handler 簽名時,已經看到 BOOST_ASIO_INITFN_RESULT_TYPE 這個宏的提示做用,翻一翻 Asio 源碼,error_code 其實都已經傳值了。

奇怪的是,即便你的 Handler 傳 error_code 爲引用,編譯運行也都沒有問題。

void Print(const boost::system::error_code& ec) {
  std::cout << "Hello, world!" << std::endl;
}

int main() {
  boost::asio::io_context ioc;
  boost::asio::deadline_timer timer(ioc, boost::posix_time::seconds(3));

  timer.async_wait(&Print);

  ioc.run();
  return 0;
}

而我發現,當 Handler 是成員函數時,就不行了。下面這個 timer 的例子,若是把 Printerror_code 改爲引用,就不能編譯了。

class Printer {
public:
  ...

  void Start() {
    timer_.async_wait(std::bind(&Printer::Print, this, std::placeholders::_1));
  }

private:
  // 不能用 const boost::system::error_code&
  void Print(boost::system::error_code ec) {
    ...
  }

private:
  boost::asio::deadline_timer timer_;
  int count_;
};

這個問題在習慣了引用的狀況下,害苦了我,真是百思不得其解!也算是 Boost 比較坑的一個地方吧。

Bind 佔位符

調用 bind 時,使用了佔位符(placeholder),其實下面四種寫法均可以:

boost::bind(Print, boost::asio::placeholders::error, &timer, &count)
boost::bind(Print, boost::placeholders::_1, &timer, &count);
boost::bind(Print, _1, &timer, &count);
std::bind(Print, std::placeholders::_1, &timer, &count);

第一種,佔位符是 Boost Asio 定義的。
第二種,佔位符是 Boost Bind 定義的。
第三種,同第二種,之因此可行,是由於 boost/bind.hpp 裏有一句 using namespace boost::placeholders;

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

#ifndef BOOST_BIND_NO_PLACEHOLDERS

using namespace boost::placeholders;
...

第四種,STL Bind,相似於 Boost Bind,只是沒有聲明 using namespace std::placeholders;

四種寫法,推薦使用二或四。至因而用 Boost Bind 仍是 STL Bind,沒那麼重要。
此外,數字佔位符共有 9 個,_1 - _9

Endpoint 是一個單詞

不要寫成 "end point"。

Server 也能夠用 Resolver

TCP Server 的 acceptor 通常是這樣構造的:

tcp::acceptor(io_context, tcp::endpoint(tcp::v4(), port))

也就是說,指定 protocol (tcp::v4()) 和 port 就好了。

可是,Asio 的 http 這個例子,確實用了 resolver,根據 IP 地址 resolve 出 endpoint:

tcp::resolver resolver(io_context_);

  tcp::resolver::results_type endpoints = resolver.resolve(address, port);

  tcp::endpoint endpoint = *endpoints.begin();

  acceptor_.open(endpoint.protocol());
  acceptor_.set_option(tcp::acceptor::reuse_address(true));
  acceptor_.bind(endpoint);
  acceptor_.listen();

  acceptor_.async_accept(...);

http 這個例子之因此這麼寫,主要是初始化 acceptor_ 時,還拿不到 endpoint,不然能夠直接用下面這個構造函數:

basic_socket_acceptor(boost::asio::io_context& io_context,
      const endpoint_type& endpoint, bool reuse_addr = true)

這個構造函數註釋說它等價於下面這段代碼:

basic_socket_acceptor<Protocol> acceptor(io_context);
acceptor.open(endpoint.protocol());
if (reuse_addr)
  acceptor.set_option(socket_base::reuse_address(true));
acceptor.bind(endpoint);
acceptor.listen(listen_backlog);

下面是不一樣的 address 對應的 endpoints 結果(假定 port 都是 8080):

  • "localhost": [::1]:8080, v6; [127.0.0.1]:8080, v4
  • "0.0.0.0": 0.0.0.0:8080, v4
  • "0::0": [::]:8080, v6
  • 本機實際 IP 地址 (e.g., IPv4 "10.123.164.142"): 10.123.164.142:8080, v4。這時候,本機 client 沒法經過 "localhost" 鏈接到這個 server,經過具體的 IP 地址則能夠。
  • 一個具體的非本機地址 (e.g., IPv4 "10.123.164.145"): exception: bind: The requested address is not valid in its context

Move Acceptable Handler

使用 acceptor.async_accept 時,發現了 Move Acceptable Handler。

簡單來講,async_accept 接受兩種 AcceptHandler,直接看源碼:

template <typename MoveAcceptHandler>
  BOOST_ASIO_INITFN_RESULT_TYPE(MoveAcceptHandler,
      void (boost::system::error_code, typename Protocol::socket))
  async_accept(BOOST_ASIO_MOVE_ARG(MoveAcceptHandler) handler)
template <typename Protocol1, typename AcceptHandler>
  BOOST_ASIO_INITFN_RESULT_TYPE(AcceptHandler,
      void (boost::system::error_code))
  async_accept(basic_socket<Protocol1>& peer,
      BOOST_ASIO_MOVE_ARG(AcceptHandler) handler,
      typename enable_if<is_convertible<Protocol, Protocol1>::value>::type* = 0)

第一種是 Move Acceptable Handler,它的第二個參數是新 accept 的 socket。
第二種是普通的 Handler,它的第一個參數是預先構造的 socket。

對於 Move Acceptable Handler,用 bind 行不通。好比給定:

void Server::HandleAccept(boost::system::error_code ec,
                          boost::asio::ip::tcp::socket socket) {
}

在 VS 2015 下(支持 C++14),std::bind 能夠編譯,boost::bind 則不行。

// std::bind 能夠,boost::bind 不能夠。
  acceptor_.async_accept(std::bind(&Server::HandleAccept,
                                   this,
                                   std::placeholders::_1,
                                   std::placeholders::_2));

在 VS 2013 下,std::bindboost::bind 都不行。

結論是,對於 Move Acceptable Handler,不要用 bind,直接用 lambda 表達式:

void DoAccept() {
  acceptor_.async_accept(
    [this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) {
    // Check whether the server was stopped by a signal before this
    // completion handler had a chance to run.
    if (!acceptor_.is_open()) {
      return;
    }

    if (!ec) {
      connection_manager_.Start(
        std::make_shared<Connection>(std::move(socket),
        connection_manager_,
        request_handler_));
    }

    DoAccept();
  });
}
相關文章
相關標籤/搜索