TCP粘包處理


TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,

因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,

然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。即面向流的通信是無消息保護邊界的。

UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,

由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,

在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。

 

TCP粘包我總結了幾種情況

 

tcp發送端發送三個包過來,tcp接收緩存區收到了這三個包,而用戶的讀寫緩存區比這三個包的總大小還大,

此時數據是接受完全的,用戶緩存區讀到三個包需要分開,這是比較好處理的。

 

第二種情況是因為用戶的接收緩存區比tcp接受緩存區大,或者比tcp目前接收到的總數據大,那么用戶緩存區讀到

的數據就是tcp接收緩存區的數據,這是第一種情況的特例,這種情況需要判斷那些包接受完全,那些包沒接受完全。

 

 

第三種情況是用戶的接受緩存區比tcp接受緩存區要小,導致用戶緩存區讀到的數據是tcp接收緩存區

的一部分,這其中有完整的包,也有殘缺的包。

 

 

第四種情況是第三種情況的一個特例,用戶緩存區的數據是不完全的,只是tcp緩存區的一部分。

對應特別大的那種包。

 

我提倡的解決辦法就是首先實現一套從tcp緩存區中讀取數據的數據結構和算法,因為tcp是面向

字節流的,將tcp緩存區中的數據讀到用戶緩存區里,這里我簡單叫做outstreambuffer和instreambuffer,

這兩個結構一個用於向tcp寫,一個用於從tcp讀。把tcp緩存區的數據盡可能多的讀出來,不要判斷是否是

完整的包,保證tcp緩存區沒數據,這樣會減少tcp粘包幾率。

第二部就是將讀到的數據,也就是instreambuffer中的數據進行分割,我叫做切包,切出一個個完整的包,

剩余不完整的留着下次繼續接收。

第三步服務器應用層接口從instreambuffer中讀取切割好的完整的包進行邏輯處理。

 

所以為了處理粘包和切包,需要我們自己設計包頭,我設計的包頭是八字節的結構體,

包含四字節的包id和四字節的包長度,這個長度既可以表示包頭+消息體的長度,

也可以表示后面消息體的長度。我設計的是表示后面消息體的長度。

而上面所說的instreambuffer和outstreambuffer用戶可以自己設計實現,也可以

利用成熟的網絡庫,我用的是libevent中的bufferevent,bufferevent實現了類似

的instreambuffer和outstreambuffer。

我設計的服務器部分代碼如下,感興趣可以去git下載:

https://github.com/secondtonone1/smartserver

簡單列舉下接收端處理讀數據的過程。

void NetWorkSystem::tcpread_cb(struct bufferevent *bev, void *ctx)
{
    getSingleton().dealReadEvent(bev, ctx);
}

networksystem是單例模式,處理讀事件。因為靜態函數tcpread_cb是libevent

設計格式的回調處理函數,在靜態函數中調用非靜態函數,我采用了單例調用。

 

void NetWorkSystem::dealReadEvent(struct bufferevent *bev, void *ctx)
{

    //
    evutil_socket_t  bufferfd = bufferevent_getfd(bev);
    std::map<evutil_socket_t, TcpHandler *>::iterator tcpHandlerIter = m_mapTcpHandlers.find(bufferfd);
    if(tcpHandlerIter != m_mapTcpHandlers.end())
    {
        tcpHandlerIter->second->dealReadEvent();
    }
}

tcphandler是我設計的切包類,這里通過bufferfd找到對應的instream和outstream,從而處理里面的數據完成切包。

//處理讀事件
void TcpHandler::dealReadEvent()
{
    evbuffer * inputBuf = bufferevent_get_input(m_pBufferevent);
    size_t inputLen = evbuffer_get_length(inputBuf);

    while(inputLen > 0)
    {
        //tcphandler第一次接收消息或者該node接收完消息,需要開辟新的node接受消息
        if(!m_pLastNode || m_pLastNode->m_nMsgLen <= m_pLastNode->m_nOffSet)
        {
            //判斷消息長度是否滿足包頭大小,不滿足跳出
            if(inputLen  < PACKETHEADLEN)
            {
                break;
            }

            char data[PACKETHEADLEN]  = {0};
            bufferevent_read(m_pBufferevent, data, PACKETHEADLEN);
            struct PacketHead  packetHead;

            memcpy(&packetHead, data, PACKETHEADLEN);

            cout << "packetId is : " <<packetHead.packetID << endl;

            cout << "packetLen is :  " << packetHead.packetLen << endl;

            insertNode(packetHead.packetID, packetHead.packetLen);

            inputLen -= PACKETHEADLEN;
        }

        //考慮可能去包頭后剩余的為0
        if(inputLen <= 0)
        {
            break;
        }
        //讀取去除包頭后剩余消息
        tcpRead(inputLen);
    }

}

 

這個函數判斷是否讀完一個消息,讀完就開辟新的節點存儲新來的消息,否則就將新來的消息放入沒讀完的節點里。

void TcpHandler::tcpRead(UInt32 &inputLen)
{
    //node節點中的數據還有多少沒讀完
    UInt32 remainLen = m_pLastNode->m_nMsgLen - m_pLastNode->m_nOffSet;

    UInt32 readLen = bufferevent_read(m_pBufferevent, m_pLastNode->m_pMsg + m_pLastNode->m_nOffSet, remainLen);
    //統計bufferevent 的inputbuffer中剩余的長度
    inputLen -= readLen;
    //更改偏移標記
    m_pLastNode->m_nOffSet += readLen;
    //判斷讀完
    if(m_pLastNode->m_nOffSet >= m_pLastNode->m_nMsgLen)
    {
        m_pLastNode->m_pMsg[m_pLastNode->m_nMsgLen + 1] = '\0'; 
        cout << "receive msg is : " << m_pLastNode->m_pMsg << endl;
        //cout <<"read times is :  " << ++readtimes<< endl;
    }

}

我的服務器還在完善中,目前已經能處理連續收到1萬個包的切包和大並發的問題了,最近在設計應用層的序列化

和應用層消息回調。感興趣可以下載看看,下載地址:https://github.com/secondtonone1/smartserver

我的微信公眾號平台,謝謝關注:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM