不少朋友已經對此做了很多研究,也花費很多心血編寫了實現代碼和blog文檔。固然也充斥着一些各式的評論,本身看了一下,總結一些心得。html
首先咱們學習一下這些朋友的心得,他們是:算法
http://blog.csdn.net/stamhe/article/details/4569530數據庫
http://www.cppblog.com/tx7do/archive/2011/05/04/145699.htmlwindows
//………………服務器
固然還有太多,不少東西粘來粘區也不知道究竟是誰的原做,J網絡
看這些朋友的blog是我建議親自看一下TCP-IP詳解卷1中的相關內容【原理性的內容必定要看】。框架
TCP-IP詳解卷1第17章中17.2節對TCP服務原理做了一個簡明介紹(如下藍色字體摘自《TCP-IP詳解卷1第17章17.2節》):socket
儘管T C P和U D P都使用相同的網絡層( I P),T C P卻嚮應用層提供與U D P徹底不一樣的服務。T C P提供一種面向鏈接的、可靠的字節流服務。tcp
面向鏈接意味着兩個使用T C P的應用(一般是一個客戶和一個服務器)在彼此交換數據以前必須先創建一個T C P鏈接。這一過程與打電話很類似,先撥號振鈴,等待對方摘機說「喂」,而後才說明是誰。在第1 8章咱們將看到一個T C P鏈接是如何創建的,以及當一方通訊結束後如何斷開鏈接。函數
在一個T C P鏈接中,僅有兩方進行彼此通訊。在第1 2章介紹的廣播和多播不能用於T C P。
T C P經過下列方式來提供可靠性:
• 應用數據被分割成T C P認爲最適合發送的數據塊。這和U D P徹底不一樣,應用程序產生的數據報長度將保持不變。由T C P傳遞給I P的信息單位稱爲報文段或段( s e g m e n t)(參見圖1 - 7)。在1 8 . 4節咱們將看到T C P如何肯定報文段的長度。
• 當T C P發出一個段後,它啓動一個定時器,等待目的端確認收到這個報文段。若是不能及時收到一個確認,將重發這個報文段。在第2 1章咱們將瞭解T C P協議中自適應的超時及重傳策略。
• 當T C P收到發自T C P鏈接另外一端的數據,它將發送一個確認。這個確認不是當即發送,一般將推遲幾分之一秒,這將在1 9 . 3節討論。
• T C P將保持它首部和數據的檢驗和。這是一個端到端的檢驗和,目的是檢測數據在傳輸過程當中的任何變化。若是收到段的檢驗和有差錯, T C P將丟棄這個報文段和不確認收到此報文段(但願發端超時並重發)。
• 既然T C P報文段做爲I P數據報來傳輸,而I P數據報的到達可能會失序,所以T C P報文段的到達也可能會失序。若是必要, T C P將對收到的數據進行從新排序,將收到的數據以正確的順序交給應用層。
• 既然I P數據報會發生重複, T C P的接收端必須丟棄重複的數據。
• T C P還能提供流量控制。T C P鏈接的每一方都有固定大小的緩衝空間。T C P的接收端只容許另外一端發送接收端緩衝區所能接納的數據。這將防止較快主機導致較慢主機的緩衝區溢出。兩個應用程序經過T C P鏈接交換8 bit字節構成的字節流。T C P不在字節流中插入記錄標識符。咱們將這稱爲字節流服務( byte stream service)。若是一方的應用程序先傳1 0字節,又傳2 0字節,再傳5 0字節,鏈接的另外一方將沒法瞭解發方每次發送了多少字節。收方能夠分4次接收這8 0個字節,每次接收2 0字節。一端將字節流放到T C P鏈接上,一樣的字節流將出如今T C P鏈接的另外一端。另外,T C P對字節流的內容不做任何解釋。T C P不知道傳輸的數據字節流是二進制數據,仍是A S C I I字符、E B C D I C字符或者其餘類型數據。對字節流的解釋由T C P鏈接雙方的應用層解釋。這種對字節流的處理方式與U n i x操做系統對文件的處理方式很類似。U n i x的內核對一個應用讀或寫的內容不做任何解釋,而是交給應用程序處理。對U n i x的內核來講,它沒法區分一個二進制文件與一個文本文件。
我仍然引用官方解釋《TCP-IP詳解卷1》第18章18.4節:
最大報文段長度( M S S)表示T C P傳往另外一端的最大塊數據的長度。當一個鏈接創建時【三次握手】,鏈接的雙方都要通告各自的M S S。咱們已經見過M S S都是1 0 2 4。這致使I P數據報一般是4 0字節長:2 0字節的T C P首部和2 0字節的I P首部。
在有些書中,將它看做可「協商」選項。它並非任何條件下均可協商。當創建一個連
接時,每一方都有用於通告它指望接收的M S S選項(M S S選項只能出如今S Y N報文段中)。若是一方不接收來自另外一方的M S S值,則M S S就定爲默認值5 3 6字節(這個默認值容許2 0字節的I P首部和2 0字節的T C P首部以適合5 7 6字節I P數據報)。
通常說來,若是沒有分段發生, M S S仍是越大越好(這也並不老是正確,參見圖2 4 - 3和圖2 4 - 4中的例子)。報文段越大容許每一個報文段傳送的數據就越多,相對I P和T C P首部有更高的網絡利用率。當T C P發送一個S Y N時,或者是由於一個本地應用進程想發起一個鏈接,或者是由於另外一端的主機收到了一個鏈接請求,它能將M S S值設置爲外出接口上的M T U長度減去固定的I P首部和T C P首部長度。對於一個以太網, M S S值可達1 4 6 0字節。使用IEEE 802.3的封裝(參見2 . 2節),它的M S S可達1 4 5 2字節。
若是目的I P地址爲「非本地的( n o n l o c a l )」,M S S一般的默認值爲5 3 6。而區分地址是本地仍是非本地是簡單的,若是目的I P地址的網絡號與子網號都和咱們的相同,則是本地的;若是目的I P地址的網絡號與咱們的徹底不一樣,則是非本地的;若是目的I P地址的網絡號與咱們的相同而子網號與咱們的不一樣,則多是本地的,也多是非本地的。大多數T C P實現版都提供了一個配置選項(附錄E和圖E - 1),讓系統管理員說明不一樣的子網是屬於本地仍是非本地。這個選項的設置將肯定M S S能夠選擇儘量的大(達到外出接口的M T U長度)或是默認值5 3 6。
M S S讓主機限制另外一端發送數據報的長度。加上主機也能控制它發送數據報的長度,這將使以較小M T U鏈接到一個網絡上的主機避免分段。
只有當一端的主機以小於5 7 6字節的M T U直接鏈接到一個網絡中,避免這種分段纔會有效。
若是兩端的主機都鏈接到以太網上,都採用5 3 6的M S S,但中間網絡採用2 9 6的M T U,也將會
出現分段。使用路徑上的M T U發現機制(參見2 4 . 2節)是關於這個問題的惟一方法。
以上說明MSS的值能夠經過協商解決,這個協商過程會涉及MTU的值的大小,前面說了:【MSS=外出接口上的MTU-IP首部-TCP首部】,咱們來看看數據進入TCP協議棧的封裝過程:
最後一層以太網幀的大小應該就是咱們的出口MTU大小了。當目的主機收到一個以太網數據幀時,數據就開始從協議棧中由底向上升,同時去掉各層協議加上的報文首部。每層協議盒都要去檢查報文首部中的協議標識,以肯定接收數據的上層協議。這個過程稱做分用( D e m u l t i p l e x i n g),圖1 - 8顯示了該過程是如何發生的。
那麼什麼是MTU呢,這其實是數據鏈路層的一個概念,以太網和802.3這兩種局域網技術標準都對「鏈路層」的數據幀有大小限制:
l 最大傳輸單元MTU
正如在圖2 - 1看到的那樣,以太網和8 0 2 . 3對數據幀的長度都有一個限制,其最大值分別是1 5 0 0和1 4 9 2字節。鏈路層的這個特性稱做M T U,最大傳輸單元。不一樣類型的網絡大多數都有一個上限。
若是I P層有一個數據報要傳,並且數據的長度比鏈路層的M T U還大,那麼I P層就須要進行分片( f r a g m e n t a t i o n),把數據報分紅若干片,這樣每一片都小於M T U。咱們將在11 . 5節討論I P分片的過程。
圖2 - 5列出了一些典型的M T U值,它們摘自RFC 1191[Mogul and Deering 1990]。點到點的鏈路層(如S L I P和P P P)的M T U並不是指的是網絡媒體的物理特性。相反,它是一個邏輯限制,目的是爲交互使用提供足夠快的響應時間。在2 . 1 0節中,咱們將看到這個限制值是如何計算出來的。在3 . 9節中,咱們將用n e t s t a t命令打印出網絡接口的M T U。
l 路徑MTU
當在同一個網絡上的兩臺主機互相進行通訊時,該網絡的M T U是很是重要的。可是若是
兩臺主機之間的通訊要經過多個網絡,那麼每一個網絡的鏈路層就可能有不一樣的M T U。重要的
不是兩臺主機所在網絡的M T U的值,重要的是兩臺通訊主機路徑中的最小M T U。它被稱做路
徑M T U。
兩臺主機之間的路徑M T U不必定是個常數。它取決於當時所選擇的路由。而選路不必定
是對稱的(從A到B的路由可能與從B到A的路由不一樣),所以路徑M T U在兩個方向上不必定是
一致的。
RFC 1191[Mogul and Deering 1990]描述了路徑M T U的發現機制,即在任什麼時候候肯定路徑
M T U的方法。咱們在介紹了I C M P和I P分片方法之後再來看它是如何操做的。在11 . 6節中,我
們將看到I C M P的不可到達錯誤就採用這種發現方法。在11 . 7節中,還會看到, t r a c e r o u t e程序
也是用這個方法來肯定到達目的節點的路徑M T U。在11 . 8節和2 4 . 2節,將介紹當產品支持路
徑M T U的發現方法時,U D P和T C P是如何進行操做的。
前面談到TCP如何保證傳輸可靠性是說到「當T C P發出一個段後,它啓動一個定時器,等待目的端確認收到這個報文段。若是不能及時收到一個確認,將重發這個報文段」,下面我看一下TCP的超時與重傳。
T C P提供可靠的運輸層。它使用的方法之一就是確認從另外一端收到的數據。但數據和確認都有可能會丟失。T C P經過在發送時設置一個定時器來解決這種問題。若是當定時器溢出時尚未收到確認,它就重傳該數據。對任何實現而言,關鍵之處就在於超時和重傳的策略,即怎樣決定超時間隔和如何肯定重傳的頻率。
對每一個鏈接,T C P管理4個不一樣的定時器。
1) 重傳定時器使用於當但願收到另外一端的確認。
2) 堅持( p e r s i s t )定時器使窗口大小信息保持不斷流動,即便另外一端關閉了其接收窗口。
3) 保活( k e e p a l i v e )定時器可檢測到一個空閒鏈接的另外一端什麼時候崩潰或重啓。
4) 2MSL定時器測量一個鏈接處於T I M E _ WA I T狀態的時間。
T C P超時與重傳中最重要的部分就是對一個給定鏈接的往返時間( RT T)的測量。因爲路由器和網絡流量均會變化,所以咱們認爲這個時間可能常常會發生變化, T C P應該跟蹤這些變化並相應地改變其超時時間。
大多數源於伯克利的T C P實如今任什麼時候候對每一個鏈接僅測量一次RT T值。在發送一個報文段時,若是給定鏈接的定時器已經被使用,則該報文段不被計時。
具體RTT值的估算比較麻煩,須要能夠參考《TCP-IP詳解卷1第21章》
交互數據老是以小於最大報文段長度的分組發送。對於這些小的報文段,接收方使用經受時延的確認方法來判斷確認是否可被推遲發送,以便與回送數據一塊兒發送。這樣一般會減小報文段的數目 。
一般T C P在接收到數據時並不當即發送A C K;相反,它推遲發送,以便將A C K與須要沿該方向發送的數據一塊兒發送(有時稱這種現象爲數據捎帶A C K)。絕大多數實現採用的時延爲200 ms,也就是說,T C P將以最大200 ms 的時延等待是否有數據一塊兒發送。
咱們看看另外一位朋友的blog對此的介紹:
摘要:當使用TCP傳輸小型數據包時,程序的設計是至關重要的。若是在設計方案中不對TCP數據包的
延遲應答,Nagle算法,Winsock緩衝做用引發重視,將會嚴重影響程序的性能。這篇文章討論了這些
問題,列舉了兩個案例,給出了一些傳輸小數據包的優化設計方案。
背景:當Microsoft TCP棧接收到一個數據包時,會啓動一個200毫秒的計時器。當ACK確認數據包
發出以後,計時器會復位,接收到下一個數據包時,會再次啓動200毫秒的計時器。爲了提高應用程序
在內部網和Internet上的傳輸性能,Microsoft TCP棧使用了下面的策略來決定在接收到數據包後
何時發送ACK確認數據包:
一、若是在200毫秒的計時器超時以前,接收到下一個數據包,則當即發送ACK確認數據包。
二、若是當前剛好有數據包須要發給ACK確認信息的接收端,則把ACK確認信息附帶在數據包上當即發送。
三、當計時器超時,ACK確認信息當即發送。
爲了不小數據包擁塞網絡,Microsoft TCP棧默認啓用了Nagle算法,這個算法可以將應用程序屢次
調用Send發送的數據拼接起來,當收到前一個數據包的ACK確認信息時,一塊兒發送出去。下面是Nagle
算法的例外狀況:
一、若是Microsoft TCP棧拼接起來的數據包超過了MTU值,這個數據會當即發送,而不等待前一個數據
包的ACK確認信息。在以太網中,TCP的MTU(Maximum Transmission Unit)值是1460字節。
二、若是設置了TCP_NODELAY選項,就會禁用Nagle算法,應用程序調用Send發送的數據包會當即被
投遞到網絡,而沒有延遲。
爲了在應用層優化性能,Winsock把應用程序調用Send發送的數據從應用程序的緩衝區複製到Winsock
內核緩衝區。Microsoft TCP棧利用相似Nagle算法的方法,決定何時才實際地把數據投遞到網絡。
內核緩衝區的默認大小是8K,使用SO_SNDBUF選項,能夠改變Winsock內核緩衝區的大小。若是有必要的話,
Winsock能緩衝大於SO_SNDBUF緩衝區大小的數據。在絕大多數狀況下,應用程序完成Send調用僅僅代表數據
被複制到了Winsock內核緩衝區,並不能說明數據就實際地被投遞到了網絡上。惟一一種例外的狀況是:
經過設置SO_SNDBUT爲0禁用了Winsock內核緩衝區。
Winsock使用下面的規則來嚮應用程序代表一個Send調用的完成:
一、若是socket仍然在SO_SNDBUF限額內,Winsock複製應用程序要發送的數據到內核緩衝區,完成Send調用。
二、若是Socket超過了SO_SNDBUF限額而且先前只有一個被緩衝的發送數據在內核緩衝區,Winsock複製要發送
的數據到內核緩衝區,完成Send調用。
三、若是Socket超過了SO_SNDBUF限額而且內核緩衝區有不僅一個被緩衝的發送數據,Winsock複製要發送的數據
到內核緩衝區,而後投遞數據到網絡,直到Socket降到SO_SNDBUF限額內或者只剩餘一個要發送的數據,才
完成Send調用。
案例1
一個Winsock TCP客戶端須要發送10000個記錄到Winsock TCP服務端,保存到數據庫。記錄大小從20字節到100
字節不等。對於簡單的應用程序邏輯,可能的設計方案以下:
一、客戶端以阻塞方式發送,服務端以阻塞方式接收。
二、客戶端設置SO_SNDBUF爲0,禁用Nagle算法,讓每一個數據包單獨的發送。
三、服務端在一個循環中調用Recv接收數據包。給Recv傳遞200字節的緩衝區以便讓每一個記錄在一次Recv調用中
被獲取到。
性能:
在測試中發現,客戶端每秒只能發送5條數據到服務段,總共10000條記錄,976K字節左右,用了半個多小時
才所有傳到服務器。
分析:
由於客戶端沒有設置TCP_NODELAY選項,Nagle算法強制TCP棧在發送數據包以前等待前一個數據包的ACK確認
信息。然而,客戶端設置SO_SNDBUF爲0,禁用了內核緩衝區。所以,10000個Send調用只能一個數據包一個數據
包的發送和確認,因爲下列緣由,每一個ACK確認信息被延遲200毫秒:
一、當服務器獲取到一個數據包,啓動一個200毫秒的計時器。
二、服務端不須要向客戶端發送任何數據,因此,ACK確認信息不能被髮回的數據包順路攜帶。
三、客戶端在沒有收到前一個數據包的確認信息前,不能發送數據包。
四、服務端的計時器超時後,ACK確認信息被髮送到客戶端。
如何提升性能:
在這個設計中存在兩個問題。第一,存在延時問題。客戶端須要可以在200毫秒內發送兩個數據包到服務端。
由於客戶端默認狀況下使用Nagle算法,應該使用默認的內核緩衝區,不該該設置SO_SNDBUF爲0。一旦TCP
棧拼接起來的數據包超過MTU值,這個數據包會當即被髮送,不用等待前一個ACK確認信息。第二,這個設計
方案對每個如此小的的數據包都調用一次Send。發送這麼小的數據包是不頗有效率的。在這種狀況下,應該
把每一個記錄補充到100字節而且每次調用Send發送80個記錄。爲了讓服務端知道一次總共發送了多少個記錄,
客戶端能夠在記錄前面帶一個頭信息。
案例二:
一個Winsock TCP客戶端程序打開兩個鏈接和一個提供股票報價服務的Winsock TCP服務端通訊。第一個鏈接
做爲命令通道用來傳輸股票編號到服務端。第二個鏈接做爲數據通道用來接收股票報價。兩個鏈接被創建後,
客戶端經過命令通道發送股票編號到服務端,而後在數據通道上等待返回的股票報價信息。客戶端在接收到第一
個股票報價信息後發送下一個股票編號請求到服務端。客戶端和服務端都沒有設置SO_SNDBUF和TCP_NODELAY
選項。
性能:
測試中發現,客戶端每秒只能獲取到5條報價信息。
分析:
這個設計方案一次只容許獲取一條股票信息。第一個股票編號信息經過命令通道發送到服務端,當即接收到
服務端經過數據通道返回的股票報價信息。而後,客戶端當即發送第二條請求信息,send調用當即返回,
發送的數據被複制到內核緩衝區。然而,TCP棧不能當即投遞這個數據包到網絡,由於沒有收到前一個數據包的
ACK確認信息。200毫秒後,服務端的計時器超時,第一個請求數據包的ACK確認信息被髮送回客戶端,客戶端
的第二個請求包才被投遞到網絡。第二個請求的報價信息當即從數據通道返回到客戶端,由於此時,客戶端的
計時器已經超時,第一個報價信息的ACK確認信息已經被髮送到服務端。這個過程循環發生。
如何提升性能:
在這裏,兩個鏈接的設計是沒有必要的。若是使用一個鏈接來請求和接收報價信息,股票請求的ACK確認信息會
被返回的報價信息當即順路攜帶回來。要進一步的提升性能,客戶端應該一次調用Send發送多個股票請求,服務端
一次返回多個報價信息。若是因爲某些特殊緣由必需要使用兩個單向的鏈接,客戶端和服務端都應該設置TCP_NODELAY
選項,讓小數據包當即發送而不用等待前一個數據包的ACK確認信息。
提升性能的建議:
上面兩個案例說明了一些最壞的狀況。當設計一個方案解決大量的小數據包發送和接收時,應該遵循如下的建議:
一、若是數據片斷不須要緊急傳輸的話,應用程序應該將他們拼接成更大的數據塊,再調用Send。由於發送緩衝區
極可能被複制到內核緩衝區,因此緩衝區不該該太大,一般比8K小一點點是頗有效率的。只要Winsock內核緩衝區
獲得一個大於MTU值的數據塊,就會發送若干個數據包,剩下最後一個數據包。發送方除了最後一個數據包,都不會
被200毫秒的計時器觸發。
二、若是可能的話,避免單向的Socket數據流接連。
三、不要設置SO_SNDBUF爲0,除非想確保數據包在調用Send完成以後當即被投遞到網絡。事實上,8K的緩衝區適合大多數
狀況,不須要從新改變,除非新設置的緩衝區通過測試的確比默認大小更高效。
四、若是數據傳輸不用保證可靠性,使用UDP。
1. TCP提供了面向「連續字節流」的可靠的傳輸服務,TCP並不理解流所攜帶的數據內容,這個內容須要應用層本身解析。
2. 「字節流」是連續的、非結構化的,而咱們的應用須要的是有序的、結構化的數據信息,所以咱們須要定義本身的「規則」去解讀這個「連續的字節流「,那解決途徑就是定義本身的封包類型,而後用這個類型去映射「連續字節流」。
如何定義封包,咱們回顧一下前面這個數據進入協議棧的封裝過程圖:
封包其實就是將上圖中進入協議棧的用戶數據[即用戶要發送的數據]定義爲一種方便識別和交流的類型,這有點相似信封的概念,信封就是一種人們之間通訊的格式,信封格式以下:
信封格式:
收信人郵編
收信人地址
收信人姓名
信件內容
那麼在程序裏面咱們也須要定義這種格式:在C++裏面只有結構和類這種兩種類型適合表達這個概念了。網絡上不少朋友對此表述了本身的見解並貼出了代碼:好比
/************************************************************************/
/* 數據封包信息定義開始 */
/************************************************************************/
#pragma pack(push,1) //將原對齊方式壓棧,採用新的1字節對齊方式
/* 封包類型枚舉[此處根據需求列舉] */
typedef enum{
NLOGIN=1,
NREG=2,
NBACKUP=3,
NRESTORE=3,
NFILE_TRANSFER=4,
NHELLO=5
} PACKETTYPE;
/* 包頭 */
typedef struct tagNetPacketHead{
byte version;//版本
PACKETTYPE ePType;//包類型
WORD nLen;//包體長度
} NetPacketHead;
/* 封包對象[包頭&包體] */
typedef struct tagNetPacket{
NetPacketHead netPacketHead;//包頭
char * packetBody;//包體
} NetPacket;
#pragma pack(pop)
/**************數據封包信息定義結束**************************/
3. 發包順序與收包問題
a) 因爲TCP要經過協商解決發送出去的報文段的長度,所以咱們發送的數據頗有可能被分割甚至被分割後再重組交給網絡層發送,而網絡層又是採用分組傳送,即網絡層數據報到達目標的順序徹底沒法預測,那麼收包會出現半包、粘包問題。舉個例子,發送端連續發送兩端數據msg1和msg2,那麼發送端[傳輸層]可能會出現如下狀況:
i. Msg1和msg2小於TCP的MSS,兩個包按照前後順序被髮出,沒有被分割和重組
ii. Msg1過大被分割成兩段TCP報文msg1-一、msg2-2進行傳送,msg2較小直接被封裝成一個報文傳送
iii. Msg1過大被分割成兩段TCP報文msg1-一、msg2-2,msg1-1先被傳送,剩下的msg1-2和msg2[較小]被組合成一個報文傳送
iv. Msg1過大被分割成兩段TCP報文msg1-一、msg2-2,msg1-1先被傳送,剩下的msg1-2和msg2[較小]組合起來仍是過小,組合的內容在和後面再發送的msg3的前部分數據組合起來發送
v. ……………………….太多……………………..
b) 接收端[傳輸層]可能出現的狀況
i. 先收到msg1,再收到msg2,這種方式太順利了。
ii. 先收到msg1-1,再收到msg1-2,再收到msg2
iii. 先收到msg1,再收到msg2-1,再收到msg2-2
iv. 先收到msg1和msg2-1,再收到msg2-2
v. //…………還有不少………………
c) 其實「接收端網絡層」接收到的分組數據報順序和發送端比較可能徹底是亂的,好比發「送端網絡層」發送一、二、三、四、5,而接收端網絡層接收到的數據報順序卻多是二、一、五、四、3,可是「接收端的傳輸層」會保證連接的有序性和可靠性,「接收端的傳輸層」會對「接收端網絡層」收到的順序紊亂的數據報重組成有序的報文[即發送方傳輸層發出的順序],而後交給「接收端應用層」使用,因此「接收端傳輸層」老是可以保證數據包的有序性,「接收端應用層」[咱們編寫的socket程序]不用擔憂接收到的數據的順序問題。
d) 可是如上所述,粘包問題和半包問題不可避免。咱們在接收端應用層須要本身編碼處理粘包和半包問題。通常作法是定義一個緩衝區或者是使用標準庫/框架提供的容器循環存放接收到數據,邊接收變判斷緩衝區數據是否知足包頭大小,若是知足包頭大小再判斷緩衝區剩下數據是否知足包體大小,若是知足則提取。詳細步驟以下:
1. 接收數據存入緩衝區尾部
2. 緩衝區數據知足包頭大小否
3. 緩衝區數據不知足包頭大小,回到第1步;緩衝區數據知足包頭大小則取出包頭,接着判斷緩衝區剩餘數據知足包頭中定義的包體大小否,不知足則回到第1步。
4. 緩衝區數據知足一個包頭大小和一個包體大小之和,則取出包頭和包體進行使用,此處使用能夠採用拷貝方式轉移緩衝區數據到另一個地方,也能夠爲了節省內存直接採起調用回調函數的方式完成數據使用。
5. 清除緩衝區的第一個包頭和包體信息,作法通常是將緩衝區剩下的數據拷貝到緩衝區首部覆蓋「第一個包頭和包體信息」部分便可。
粘包、半包處理具體實現不少朋友都有本身的作法,好比最前面貼出的連接,這裏我也貼出一段參考:
緩衝區實現頭文件:
#include <windows.h>
#ifndef _CNetDataBuffer_H_
#define _CNetDataBuffer_H_
#ifndef TCPLAB_DECLSPEC
#define TCPLAB_DECLSPEC _declspec(dllimport)
#endif
/************************************************************************/
/* 數據封包信息定義開始 */
/************************************************************************/
#pragma pack(push,1) //將原對齊方式壓棧,採用新的1字節對齊方式
/* 封包類型枚舉[此處根據需求列舉] */
typedef enum{
NLOGIN=1,
NREG=2,
NBACKUP=3,
NRESTORE=3,
NFILE_TRANSFER=4,
NHELLO=5
} PACKETTYPE;
/* 包頭 */
typedef struct tagNetPacketHead{
byte version;//版本
PACKETTYPE ePType;//包類型
WORD nLen;//包體長度
} NetPacketHead;
/* 封包對象[包頭&包體] */
typedef struct tagNetPacket{
NetPacketHead netPacketHead;//包頭
char * packetBody;//包體
} NetPacket;
#pragma pack(pop)
/**************數據封包信息定義結束**************************/
//緩衝區初始大小
#define BUFFER_INIT_SIZE 2048
//緩衝區膨脹係數[緩衝區膨脹後的大小=原大小+係數*新增數據長度]
#define BUFFER_EXPAND_SIZE 2
//計算緩衝區除第一個包頭外剩下的數據的長度的宏[緩衝區數據總長度-包頭大小]
#define BUFFER_BODY_LEN (m_nOffset-sizeof(NetPacketHead))
//計算緩衝區數據當前是否知足一個完整包數據量[包頭&包體]
#define HAS_FULL_PACKET ( \
(sizeof(NetPacketHead)<=m_nOffset) && \
((((NetPacketHead*)m_pMsgBuffer)->nLen) <= BUFFER_BODY_LEN) \
)
//檢查包是否合法[包體長度大於零且包體不等於空]
#define IS_VALID_PACKET(netPacket) \
((netPacket.netPacketHead.nLen>0) && (netPacket.packetBody!=NULL))
//緩衝區第一個包的長度
#define FIRST_PACKET_LEN (sizeof(NetPacketHead)+((NetPacketHead*)m_pMsgBuffer)->nLen)
/* 數據緩衝 */
class /*TCPLAB_DECLSPEC*/ CNetDataBuffer
{
/* 緩衝區操做相關成員 */
private:
char *m_pMsgBuffer;//數據緩衝區
int m_nBufferSize;//緩衝區總大小
int m_nOffset;//緩衝區數據大小
public:
int GetBufferSize() const;//得到緩衝區的大小
BOOL ReBufferSize(int);//調整緩衝區的大小
BOOL IsFitPacketHeadSize() const;//緩衝數據是否適合包頭大小
BOOL IsHasFullPacket() const;//緩衝區是否擁有完整的包數據[包含包頭和包體]
BOOL AddMsg(char *pBuf,int nLen);//添加消息到緩衝區
const char *GetBufferContents() const;//獲得緩衝區內容
void Reset();//緩衝區復位[清空緩衝區數據,但並未釋放緩衝區]
void Poll();//移除緩衝區首部的第一個數據包
public:
CNetDataBuffer();
~CNetDataBuffer();
};
#endif
緩衝區實現文件:
#define TCPLAB_DECLSPEC _declspec(dllexport)
#include "CNetDataBuffer.h"
/* 構造 */
CNetDataBuffer::CNetDataBuffer()
{
m_nBufferSize = BUFFER_INIT_SIZE;//設置緩衝區大小
m_nOffset = 0;//設置數據偏移值[數據大小]爲0
m_pMsgBuffer = NULL;
m_pMsgBuffer = new char[BUFFER_INIT_SIZE];//分配緩衝區爲初始大小
ZeroMemory(m_pMsgBuffer,BUFFER_INIT_SIZE);//緩衝區清空
}
/* 析構 */
CNetDataBuffer::~CNetDataBuffer()
{
if (m_nOffset!=0)
{
delete [] m_pMsgBuffer;//釋放緩衝區
m_pMsgBuffer = NULL;
m_nBufferSize=0;
m_nOffset=0;
}
}
/************************************************************************/
/* Description: 得到緩衝區中數據的大小 */
/* Return: 緩衝區中數據的大小 */
/************************************************************************/
INT CNetDataBuffer::GetBufferSize() const
{
return this->m_nOffset;
}
/************************************************************************/
/* Description: 緩衝區中的數據大小是否足夠一個包頭大小 */
/* Return: 若是知足則返回True,不然返回False
/************************************************************************/
BOOL CNetDataBuffer::IsFitPacketHeadSize() const
{
return sizeof(NetPacketHead)<=m_nOffset;
}
/************************************************************************/
/* Description: 判斷緩衝區是否擁有完整的數據包(包頭和包體) */
/* Return: 若是緩衝區包含一個完整封包則返回True,不然False */
/************************************************************************/
BOOL CNetDataBuffer::IsHasFullPacket() const
{
//若是連包頭大小都不知足則返回
//if (!IsFitPacketHeadSize())
// return FALSE;
return HAS_FULL_PACKET;//此處採用宏簡化代碼
}
/************************************************************************/
/* Description: 重置緩衝區大小 */
/* nLen: 新增長的數據長度 */
/* Return: 調整結果 */
/************************************************************************/
BOOL CNetDataBuffer::ReBufferSize(int nLen)
{
char *oBuffer = m_pMsgBuffer;//保存原緩衝區地址
try
{
nLen=(nLen<64?64:nLen);//保證最小增量大小
//新緩衝區的大小=增長的大小+原緩衝區大小
m_nBufferSize = BUFFER_EXPAND_SIZE*nLen+m_nBufferSize;
m_pMsgBuffer = new char[m_nBufferSize];//分配新的緩衝區,m_pMsgBuff指向新緩衝區地址
ZeroMemory(m_pMsgBuffer,m_nBufferSize);//新緩衝區清零
CopyMemory(m_pMsgBuffer,oBuffer,m_nOffset);//將原緩衝區的內容所有拷貝到新緩衝區
}
catch(...)
{
throw;
}
delete []oBuffer;//釋放原緩衝區
return TRUE;
}
/************************************************************************/
/* Description: 向緩衝區添加消息 */
/* pBuf: 要添加的數據 */
/* nLen: 添加的消息長度
/* return: 添加成功返回True,不然False */
/************************************************************************/
BOOL CNetDataBuffer::AddMsg(char *pBuf,int nLen)
{
try
{
//檢查緩衝區長度是否知足,不知足則從新調整緩衝區大小
if (m_nOffset+nLen>m_nBufferSize)
ReBufferSize(nLen);
//拷貝新數據到緩衝區末尾
CopyMemory(m_pMsgBuffer+sizeof(char)*m_nOffset,pBuf,nLen);
m_nOffset+=nLen;//修改數據偏移
}
catch(...)
{
return FALSE;
}
return TRUE;
}
/* 獲得緩衝區內容 */
const char * CNetDataBuffer::GetBufferContents() const
{
return m_pMsgBuffer;
}
/************************************************************************/
/* 緩衝區復位 */
/************************************************************************/
void CNetDataBuffer::Reset()
{
if (m_nOffset>0)
{
m_nOffset = 0;
ZeroMemory(m_pMsgBuffer,m_nBufferSize);
}
}
/************************************************************************/
/* 移除緩衝區首部的第一個數據包 */
/************************************************************************/
void CNetDataBuffer::Poll()
{
if(m_nOffset==0 || m_pMsgBuffer==NULL)
return;
if (IsFitPacketHeadSize() && HAS_FULL_PACKET)
{
CopyMemory(m_pMsgBuffer,m_pMsgBuffer+FIRST_PACKET_LEN*sizeof(char),m_nOffset-FIRST_PACKET_LEN);
}
}
對TCP發包和收包進行簡單封裝:
頭文件:
#include <windows.h>
#include "CNetDataBuffer.h"
// #ifndef TCPLAB_DECLSPEC
// #define TCPLAB_DECLSPEC _declspec(dllimport)
// #endif
#ifndef _CNETCOMTEMPLATE_H_
#define _CNETCOMTEMPLATE_H_
//通訊端口
#define TCP_PORT 6000
/* 通訊終端[包含一個Socket和一個緩衝對象] */
typedef struct {
SOCKET m_socket;//通訊套接字
CNetDataBuffer m_netDataBuffer;//該套接字關聯的數據緩衝區
} ComEndPoint;
/* 收包回調函數參數 */
typedef struct{
NetPacket *pPacket;
LPVOID processor;
SOCKET comSocket;
} PacketHandlerParam;
class CNetComTemplate{
/* Socket操做相關成員 */
private:
public:
void SendPacket(SOCKET m_connectedSocket,NetPacket &netPacket);//發包函數
BOOL RecvPacket(ComEndPoint &comEndPoint,void (*recvPacketHandler)(LPVOID)=NULL,LPVOID=NULL);//收包函數
public:
CNetComTemplate();
~CNetComTemplate();
};
#endif
實現文件:
#include "CNetComTemplate.h"
CNetComTemplate::CNetComTemplate()
{
}
CNetComTemplate::~CNetComTemplate()
{
}
/************************************************************************/
/* Description:發包 */
/* m_connectedSocket:創建好鏈接的套接字 */
/* netPacket:要發送的數據包 */
/************************************************************************/
void CNetComTemplate::SendPacket(SOCKET m_connectedSocket,NetPacket &netPacket)
{
if (m_connectedSocket==NULL || !IS_VALID_PACKET(netPacket))//若是還沒有創建鏈接則退出
{
return;
}
::send(m_connectedSocket,(char*)&netPacket.netPacketHead,sizeof(NetPacketHead),0);//先發送包頭
::send(m_connectedSocket,netPacket.packetBody,netPacket.netPacketHead.nLen,0);//在發送包體
}
/**************************************************************************/
/* Description:收包 */
/* comEndPoint:通訊終端[包含套接字和關聯的緩衝區] */
/* recvPacketHandler:收包回調函數,當收到一個包後調用該函數進行包的分發處理*/
/**************************************************************************/
BOOL CNetComTemplate::RecvPacket(ComEndPoint &comEndPoint,void (*recvPacketHandler)(LPVOID),LPVOID pCallParam)
{
if (comEndPoint.m_socket==NULL)
return FALSE;
int nRecvedLen = 0;
char pBuf[1024];
//若是緩衝區數據不夠包大小則繼續從套接字讀取tcp報文段
while (!(comEndPoint.m_netDataBuffer.IsHasFullPacket()))
{
nRecvedLen = recv(comEndPoint.m_socket,pBuf,1024,0);
if (nRecvedLen==SOCKET_ERROR || nRecvedLen==0)//若果Socket錯誤或者對方鏈接已經正常關閉則結束讀取
break;
comEndPoint.m_netDataBuffer.AddMsg(pBuf,nRecvedLen);//將新接收的數據存入緩衝區
}
//執行到此處多是三種狀況:
//1.已經讀取到的數據知足一個完整的tcp報文段
//2.讀取發生socket_error錯誤
//3.在還未正常讀取完畢的過程當中對方鏈接已經關閉
//若是沒有讀取到數據或者沒有讀取到完整報文段則返回返回
if (nRecvedLen==0 || (!(comEndPoint.m_netDataBuffer.IsHasFullPacket())))
{
return FALSE;
}
if (recvPacketHandler!=NULL)
{
//構造準備傳遞給回調函數的數據包
NetPacket netPacket;
netPacket.netPacketHead = *(NetPacketHead*)comEndPoint.m_netDataBuffer.GetBufferContents();
netPacket.packetBody = new char[netPacket.netPacketHead.nLen];//動態分配包體空間
//構造回調函數參數
PacketHandlerParam packetParam;
packetParam.pPacket = &netPacket;
packetParam.processor = pCallParam;
//呼叫回調函數
recvPacketHandler(&packetParam);
delete []netPacket.packetBody;
}
//移除緩衝區的第一個包
comEndPoint.m_netDataBuffer.Poll();
return TRUE;
}
from:http://www.cnblogs.com/jiangtong/archive/2012/03/22/2411985.html