在上一篇博文中提到asio的異步發送稍微復雜一點,有必要單獨拿出來說說。asio異步發送復雜的地方在於: 不能連續調用異步發送接口async_write,因為async_write內部是不斷調用async_write_some,直到所有的數據發送完成為止。由於async_write調用之后就直接返回了,如果第一次調用async_write發送一個較大的包時,馬上又再調用async_write發送一個很小的包時,有可能這時第一次的async_write還在循環調用async_write_some發送,而第二次的async_write要發送的數據很小,一下子就發出去了,這使得第一次發送的數據和第二次發送的數據交織在一起了,導致發送亂序的問題。解決這個問題的方法就是在第一次發送完成之后再發送第二次的數據。具體的做法是用一個發送緩沖區,在異步發送完成之后從緩沖區再取下一個數據包發送。下面看看異步發送的代碼是如何實現的。
list<MyMessage> m_sendQueue; //發送隊列 void HandleAsyncWrite(char* data, int len) { bool write_in_progress = !m_sendQueue.empty(); m_sendQueue.emplace_back(data, len); if (!write_in_progress) { AsyncWrite(); } } void AsyncWrite() { auto msg = m_sendQueue.front(); async_write(m_sock, buffer(msg.pData, msg.len), [this](const boost::system::error_code& ec, std::size_t size) { if (!ec) { m_sendQueue.pop_front(); if (!m_sendQueue.empty()) { AsyncWrite(); } } else { HandleError(ec); if (!m_sendQueue.empty()) m_sendQueue.clear(); } }); }
代碼的邏輯是這樣的:當用戶發送數據時,不直接調用異步發送接口,而是將數據放到一個發送隊列中,異步發送接口會循環從隊列中取數據發送。循環發送過程的一個細節需要注意,用戶發送數據時,如果發送隊列為空時,說明異步發送已經將隊列中所有的數據都發送完了,也意味着循環發送結束了,這時,需要在數據入隊列之后再調用一下async_write重新發起異步循環發送。
可以看到,異步發送比異步接收等其他異步操作更復雜,需要一個發送隊列來保證發送不會亂序。但是,還有一個問題需要注意就是這個發送隊列是沒有加限制的,如果接收端收到數據之后阻塞處理,而發送又很快的話,就會導致發送隊列的內存快速增長甚至內存爆掉。解決辦法有兩個:
- 發慢一點,並且保證接收端不會長時間阻塞socket;
- 控制發送隊列的上限。
第一種方法對實際應用的約束性較強,實際可操作性不高。第二種方法需要控制隊列上限,不可避免的要加鎖,這樣就喪失了單線程異步發送的性能優勢。所以建議用同步發送接口來發送數據,一來不用發送隊列,自然也不會有內存暴漲的問題,二來也不會有復雜的循環發送過程,而且還可以通過線程池來提高發送效率。
總結:
- 不要連續發起異步發送,要等上次發送完成之后再發起下一個異步發送;
- 要考慮異步發送的發送隊列內存可能會暴漲的問題;
- 相比復雜的異步發送,同步發送簡單可靠,推薦優先使用同步發送接口。
如果你覺得這篇文章對你有用,可以點一下推薦,謝謝。
c++11 boost技術交流群:296561497,歡迎大家來交流技術。