muduo網路庫學習筆記(14):chargen服務示例
chargen簡介
chargen(Character Generator Protocol)是指在TCP連線建立後,伺服器不斷傳送任意的字元到客戶端,直到客戶端關閉連線。
它生成資料的邏輯如下:
for (int i = 33; i < 127; ++i)
{
line.push_back(char(i));
}
line += line;
for (size_t i = 0; i < 127-33; ++i)
{
message_ += line.substr(i, 72) + '\n';
}
其輸出資料格式類似下圖,每行有72個字元,完整的一組輸出資料有95行:
在介紹muduo chargen服務示例前,我們需要了解muduo::TcpConnection是如何傳送資料的。
TcpConnection傳送資料
一、程式碼的改動
之前幾篇部落格介紹了TcpConnection關於連線建立和連線斷開的處理,在此基礎上,TcpConnection的介面中新增了兩個函式send()和shutdown(),這兩個函式都可以跨執行緒呼叫。其內部實現增加了sendInLoop()和shutdownInLoop()兩個成員函式,並使用Buffer(muduo對應用層緩衝區的封裝)作為輸出緩衝區。
TcpConnection的狀態也增加到了四個:
// 連線斷開,連線中,已連線,斷開連線中
enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
TcpConnection介面的變化:
// send()有多種過載
void send(const void* message, size_t len);
void send(const StringPiece& message);
void shutdown();
......
void sendInLoop(const StringPiece& message);
void sendInLoop(const void* message, size_t len);
void shutdownInLoop();
Buffer outputBuffer_; // 應用層傳送緩衝區
傳送資料會用到Channel的WriteCallback,且由於muduo採用Level Trigger(水平觸發),因此我們只在需要的時候才關注可寫事件,否則會造成busy loop。
Channel.h的改動如下:
void enableReading() { events_ |= kReadEvent; update(); }
void enableWriting() { events_ |= kWriteEvent; update(); }
void disableWriting() { events_ &= ~kWriteEvent; update(); }
void disableAll() { events_ = kNoneEvent; update(); }
bool isWriting() const { return events_ & kWriteEvent; }
二、原始碼分析
我們首先來看傳送資料的TcpConnection::send()函式,它是執行緒安全的,可以跨執行緒呼叫。如果在非IO執行緒中呼叫,它會把message複製一份,傳給IO執行緒中的sendInLoop()來傳送。
程式碼片段1:TcpConnection::send()
檔名:TcpConnection.cc
void TcpConnection::send(const std::string& message)
{
if (state_ == kConnected)
{
if (loop_->isInLoopThread())
{
sendInLoop(message);
}
else
{
loop_->runInLoop(
boost::bind(&TcpConnection::sendInLoop, this, message));
}
}
}
sendInLoop()會先嚐試直接傳送資料,如果一次傳送完畢就不會啟用WriteCallback;如果只傳送了部分資料,則把剩餘的資料放入outputBuffer_,並開始關注writable事件,以後在handleWrite()中傳送剩餘的資料。如果當前outputBuffer_已經有待傳送的資料,那麼就不能先嚐試傳送了,因為這會造成資料亂序。
程式碼如下:
程式碼片段2:TcpConnection::sendInLoop()
檔名:TcpConnection.cc
void TcpConnection::sendInLoop(const std::string& message)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
// if no thing in output queue, try writing directly
if(!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
nwrote = ::write(channel_->fd(), message.data(), message.size());
if(nwrote >= 0) {
if(implicit_cast<size_t>(nwrote) < message.size()) {
LOG_TRACE << "I am going to write more data.";
}
} else {
nwrote = 0;
if(errno != EWOULDBLOCK) {
LOG_SYSERR << "TcpConnection::sendInLoop";
}
}
}
assert(nwrote >= 0);
if(implicit_cast<size_t>(nwrote) < message.size()) {
outputBuffer_.append(message.data()+nwrote, message.size()-nwrote);
if(!channel_->isWriting()) {
channel_->enableWriting();
}
}
}
當socket變得可寫時,Channel會呼叫TcpConnection::handleWrite(),這裡會繼續傳送outputBuffer_中的資料。一旦傳送完畢,立即停止關注可寫事件,避免busy loop。另外如果此時連線正在關閉,則呼叫shutdownInLoop(),繼續執行關閉過程。
程式碼片段3:TcpConnection::handleWrite()
檔名:TcpConnection.cc
// 核心傳送緩衝區有空間了,回撥該函式
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting())
{
ssize_t n = ::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0) // 傳送緩衝區已清空
{
channel_->disableWriting(); // 停止關注POLLOUT事件,以免出現busy loop
if (state_ == kDisconnecting) // 傳送緩衝區已清空並且連線狀態是kDisconnecting, 要關閉連線
{
shutdownInLoop(); // 關閉連線
}
}
else
{
LOG_TRACE << "I am going to write more data";
}
}
else
{
LOG_SYSERR << "TcpConnection::handleWrite";
}
}
else
{
LOG_TRACE << "Connection is down, no more writing";
}
}
傳送資料時,我們還需考慮如果傳送資料的速度高於對方接收資料的速度該怎麼辦,這會造成資料在本地記憶體中堆積。muduo使用“高水位回撥”HighWaterMarkCallback和“低水位回撥”WriteCompleteCallback來處理這個問題。
WriteCompleteCallback
問題:在大流量的場景下,不斷生成資料然後傳送,如果對等方接收不及時,受到通告視窗的控制,核心傳送緩衝區空間不足,這時就會將使用者資料新增到應用層傳送緩衝區,一直這樣下去,可能會撐爆應用層傳送緩衝區。
一種解決方法就是調整傳送頻率,通過設定WriteCompleteCallback,等傳送緩衝區傳送完畢為空後,呼叫WriteCompleteCallback,然後繼續傳送。
TcpConnection有兩處可能觸發此回撥,如下:
程式碼片段4:TcpConnection::sendInLoop()
檔名:TcpConnection.cc
void TcpConnection::sendInLoop(const void* data, size_t len)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0; // 已傳送的大小
size_t remaining = len; // 還未傳送的大小
bool error = false; // 是否有錯誤發生
if (state_ == kDisconnected)
{
LOG_WARN << "disconnected, give up writing";
return;
}
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
nwrote = sockets::write(channel_->fd(), data, len);
if (nwrote >= 0)
{
remaining = len - nwrote;
// 寫完了,回撥writeCompleteCallback_
if (remaining == 0 && writeCompleteCallback_)
{
loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
}
}
else // nwrote < 0
{
nwrote = 0;
if (errno != EWOULDBLOCK)
{
LOG_SYSERR << "TcpConnection::sendInLoop";
if (errno == EPIPE) // FIXME: any others?
{
error = true;
}
}
}
}
assert(remaining <= len);
// 沒有錯誤,並且還有未寫完的資料(說明核心傳送緩衝區滿,要將未寫完的資料新增到output buffer中)
if (!error && remaining > 0)
{
LOG_TRACE << "I am going to write more data";
size_t oldLen = outputBuffer_.readableBytes();
// 如果超過highWaterMark_(高水位標),回撥highWaterMarkCallback_
if (oldLen + remaining >= highWaterMark_
&& oldLen < highWaterMark_
&& highWaterMarkCallback_)
{
loop_->queueInLoop(boost::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
}
outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
if (!channel_->isWriting())
{
channel_->enableWriting(); // 關注POLLOUT事件
}
}
}
程式碼片段5:TcpConnection::handleWrite()
檔名:TcpConnection.cc
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting())
{
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0)
{
channel_->disableWriting();
if (writeCompleteCallback_) // 回撥writeCompleteCallback_
{
// 應用層傳送緩衝區被清空,就回撥用writeCompleteCallback_
loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
}
if (state_ == kDisconnecting)
{
shutdownInLoop();
}
}
else
{
LOG_TRACE << "I am going to write more data";
}
}
else
{
LOG_SYSERR << "TcpConnection::handleWrite";
}
}
else
{
LOG_TRACE << "Connection fd = " << channel_->fd()
<< " is down, no more writing";
}
}
muduo chargen示例
chargen服務只傳送資料,不接收資料,而且它傳送資料的速度不能快過客戶端接收的速度,因此需要關注TCP“三個半事件”中的半個“訊息/資料傳送完畢”事件。
示例程式碼:
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>
#include <boost/bind.hpp>
#include <stdio.h>
using namespace muduo;
using namespace muduo::net;
// TestServer中包含一個TcpServer
class TestServer
{
public:
TestServer(EventLoop* loop,
const InetAddress& listenAddr)
: loop_(loop),
server_(loop, listenAddr, "TestServer")
{
// 設定連線建立/斷開、訊息到來、訊息傳送完畢回撥函式
server_.setConnectionCallback(
boost::bind(&TestServer::onConnection, this, _1));
server_.setMessageCallback(
boost::bind(&TestServer::onMessage, this, _1, _2, _3));
server_.setWriteCompleteCallback(
boost::bind(&TestServer::onWriteComplete, this, _1));
// 生成資料
string line;
for (int i = 33; i < 127; ++i)
{
line.push_back(char(i));
}
line += line;
for (size_t i = 0; i < 127-33; ++i)
{
message_ += line.substr(i, 72) + '\n';
}
}
void start()
{
server_.start();
}
private:
void onConnection(const TcpConnectionPtr& conn)
{
if (conn->connected())
{
printf("onConnection(): new connection [%s] from %s\n",
conn->name().c_str(),
conn->peerAddress().toIpPort().c_str());
conn->setTcpNoDelay(true);
conn->send(message_);
}
else
{
printf("onConnection(): connection [%s] is down\n",
conn->name().c_str());
}
}
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
Timestamp receiveTime)
{
muduo::string msg(buf->retrieveAllAsString());
printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
msg.size(),
conn->name().c_str(),
receiveTime.toFormattedString().c_str());
conn->send(msg);
}
// 訊息傳送完畢回撥函式,繼續傳送資料
void onWriteComplete(const TcpConnectionPtr& conn)
{
conn->send(message_);
}
EventLoop* loop_;
TcpServer server_;
muduo::string message_;
};
int main()
{
printf("main(): pid = %d\n", getpid());
InetAddress listenAddr(8888);
EventLoop loop;
TestServer server(&loop, listenAddr);
server.start();
loop.loop();
}
執行結果:
啟動服務端
啟動一個客戶端來連線,連線建立後會收到服務端源源不斷髮送過來的訊息
再次啟動客戶端,將接收到的資料重定向到一個檔案中,可以看到短短几秒就接收到了如此多的資料
相關文章
- muduo網路庫學習筆記(1):Timestamp類筆記
- muduo網路庫學習筆記(2):原子性操作筆記
- muduo網路庫學習筆記(3):Thread類筆記thread
- muduo網路庫學習筆記(11):有用的runInLoop()函式筆記OOP函式
- muduo網路庫學習筆記(12):TcpServer和TcpConnection類筆記TCPServer
- muduo網路庫學習筆記(13):TcpConnection生命期的管理筆記TCP
- muduo網路庫學習筆記(10):定時器的實現筆記定時器
- muduo網路庫學習筆記(7):執行緒特定資料筆記執行緒
- muduo網路庫學習筆記(9):Reactor模式的關鍵結構筆記React模式
- muduo網路庫學習筆記(8):高效日誌類的封裝筆記封裝
- muduo網路庫學習筆記(4):互斥量和條件變數筆記變數
- muduo網路庫學習筆記(5):執行緒池的實現筆記執行緒
- muduo網路庫學習筆記(6):單例類(執行緒安全的)筆記單例執行緒
- muduo網路庫學習之muduo_http 庫涉及到的類HTTP
- muduo網路庫學習之muduo_inspect 庫涉及到的類
- iOS學習筆記14 網路(三)WebViewiOS筆記WebView
- muduo網路庫學習筆記(15):關於使用stdio和iostream的討論筆記iOS
- muduo網路庫學習之EventLoop(七):TcpClient、ConnectorOOPTCPclient
- 【學習筆記】網路流筆記
- [網路]NIO學習筆記筆記
- 網路流學習筆記筆記
- muduo網路庫學習之EventLoop(四):EventLoopThread 類、EventLoopThreadPool 類OOPthread
- muduo網路庫Timestamp類
- muduo網路庫使用心得
- Kubernetes學習筆記(四):服務筆記
- Consul 學習筆記-服務註冊筆記
- Laravel 學習筆記 —— 神奇的服務容器Laravel筆記
- muduo網路庫學習之EventLoop(六):TcpConnection::send()、shutdown()、handleRead()、handleWrite()OOPTCP
- muduo網路庫學習之EventLoop(一):事件迴圈類圖簡介和muduo 定時器TimeQueueOOP事件定時器
- muduo網路庫Exception異常類Exception
- muduo網路庫編譯安裝編譯
- springCloud學習筆記2(服務發現)SpringGCCloud筆記
- angular學習筆記(十五)-module裡的'服務'Angular筆記
- angular學習筆記(二十九)-$q服務Angular筆記
- Symfony2 學習筆記之服務容器筆記
- nacos學習筆記之服務發現中心筆記
- Laravel底層學習筆記02 - 服務容器,服務提供者Laravel筆記
- 學習筆記16:殘差網路筆記