1 如何收取數據?
對於收數據,當接受連接成功得到 clientfd 後,我們會將該 clientfd 綁定到相應的 IO 複用函數上並監聽其可讀事件。當可讀事件觸發後,調用 recv 函數從 clientfd 上收取數據(這裏不考慮出錯的情況),根據不同的網絡模式我們可能會收取部分或一次性收完。收取到的數據我們會放入接收緩衝區內,然後做解包操作。對於使用 epoll 的 LT 模式(水平觸發模式),我們每次可以只收取部分數據;但是對於 ET 模式(邊緣觸發模式),我們必須將本次收到的數據全部收完。
- ET 模式收完的標誌是 recv 或者 read 函數的返回值是 -1,錯誤碼是 EAGAIN。
linux進行非阻塞的socket接收數據時會出現Resource temporarily unavailable,errno代碼爲11(EAGAIN),表明你在非阻塞模式下調用了阻塞操作,在該操作沒有完成就返回這個錯誤,這個錯誤不會破壞socket的同步,不用管它,下次循環接着recv就可以。
2 如何發送數據?
對於發數據,除了 epoll 模型的 ET 模式外,epoll 的 LT 模式或者其他 IO 複用函數,我們通常都不會去註冊監聽該 clientfd 的可寫事件。這是因爲,只要對端正常收數據,一般不會出現 TCP 窗口太小導致 send 或 write 函數無法寫的問題。因此大多數情況下,clientfd 都是可寫的,如果註冊了可寫事件,會導致一直觸發可寫事件,而此時不一定有數據需要發送。故而,如果有數據要發送一般都是調用 send 或者 write 函數直接發送,如果發送過程中, send 函數返回 -1,並且錯誤碼是EAGAIN 表明由於 TCP 窗口太小數據已經無法寫入時,而仍然還剩下部分數據未發送,此時我們才註冊監聽可寫事件,並將剩餘的服務存入自定義的發送緩衝區中,等可寫事件觸發後再接着將發送緩衝區中剩餘的數據發送出去,如果仍然有部分數據不能發出去,繼續註冊可寫事件,當已經無數據需要發送時應該立即移除對可寫事件的監聽。這是目前主流網絡庫的做法。
直接嘗試發送消息處理邏輯:
/**
*@param data 待發送的數據
*@param len 待發送數據長度
*/
void TcpConnection::sendMessage(const void* data, size_t len)
{
int32_t nwrote = 0;
size_t remaining = len;
bool faultError = false;
if (state_ == kDisconnected)
{
LOGW("disconnected, give up writing");
return;
}
// 當前未監聽可寫事件,且發送緩衝區中沒有遺留數據
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
//直接發送數據
nwrote = sockets::write(channel_->fd(), data, len);
if (nwrote >= 0)
{
remaining = len - nwrote;
}
else // nwrote < 0
{
nwrote = 0;
//錯誤碼不等於EWOULDBLOCK說明發送出錯了
if (errno != EWOULDBLOCK)
{
LOGSYSE("TcpConnection::sendInLoop");
if (errno == EPIPE || errno == ECONNRESET)
{
faultError = true;
}
}
}
}
//發送未出錯且還有剩餘字節未發出去
if (!faultError && remaining > 0)
{
//將剩餘部分加入發送緩衝區
outputBuffer_.append(static_cast<const char*>(data) + nwrote, remaining);
if (!channel_->isWriting())
{
//註冊可寫事件
channel_->enableWriting();
}
}
}
不能全部發出去監聽可寫事件後,可寫事件觸發後處理邏輯:
//可寫事件觸發後會調用handleWrite()函數
void TcpConnection::handleWrite()
{
//將發送緩衝區中的數據發送出去
int32_t n = sockets::write(channel_->fd(), outputBuffer_.peek(), outputBuffer_.readableBytes());
if (n > 0)
{
//發送多少從發送緩衝區移除多少
outputBuffer_.retrieve(n);
//如果發送緩衝區中已經沒有剩餘,則移除監聽可寫事件
if (outputBuffer_.readableBytes() == 0)
{
//移除監聽可寫事件
channel_->disableWriting();
if (state_ == kDisconnecting)
{
shutdown();
}
}
}
else
{
//發數據出錯處理
LOGSYSE("TcpConnection::handleWrite");
handleClose();
}
}
LT和ET模式需要注意如下問題:
- epoll LT 模式:註冊監聽一次可寫事件後,可寫事件觸發後,嘗試發送數據,如果數據此時還不能全部發送完,不用再次註冊可寫事件;
- epoll ET 模式:註冊監聽可寫事件後,可寫事件觸發後,嘗試發送數據,如果數據此時還不能全部發送完,需要再次註冊可寫事件以便讓可寫事件下次再次觸發(給予再次發數據的機會);