網絡協議收發數據問題

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 模式:註冊監聽可寫事件後,可寫事件觸發後,嘗試發送數據,如果數據此時還不能全部發送完,需要再次註冊可寫事件以便讓可寫事件下次再次觸發(給予再次發數據的機會);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章