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
我的微信公眾號平台,謝謝關注: