本文列舉 Asio 各類值得注意的細節。git
另見:基於 Asio 的 C++ 網絡編程編程
在包含 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" ...
在 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 的簽名,文檔裏都有說明,可是直接定位到源碼,更方便,也更精確。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_TYPE
,WaitHandler
的簽名一目瞭然。
其實,早期的版本應該是 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 的例子,若是把 Print
的 error_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
時,使用了佔位符(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
。
不要寫成 "end point"。
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
):
使用 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::bind
和 boost::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(); }); }