1. 程式人生 > >Windows下基於TCP協議的大檔案傳輸(流形式)

Windows下基於TCP協議的大檔案傳輸(流形式)

在TCP下進行大檔案傳輸,不像小檔案那樣直接打包個BUFFER傳送出去,因為檔案比較大可能是1G,2G或更大,第一效率問題,第二TCP粘包問題。針對服務端的設計來說就更需要嚴緊些。下面介紹簡單地實現大檔案在TCP的傳應用。

粘包出現原因:在流傳輸中出現,UDP不會出現粘包,因為它有訊息邊界(參考Windows 網路程式設計)
1 傳送端需要等緩衝區滿才傳送出去,造成粘包
2 接收方不及時接收緩衝區的包,造成多個包接收

解決辦法:
為了避免粘包現象,可採取以下幾種措施:

一是對於傳送方引起的粘包現象,使用者可通過程式設計設定來避免,TCP提供了強制資料立即傳送的操作指令push,TCP軟體收到該操作指令後,就立即將本段資料傳送出去,而不必等待發送緩衝區滿;

二是對於接收方引起的粘包,則可通過優化程式設計、精簡接收程序工作量、提高接收程序優先順序等措施,使其及時接收資料,從而儘量避免出現粘包現象;

三是由接收方控制,將一包資料按結構欄位,人為控制分多次接收,然後合併,通過這種手段來避免粘包。

對於基於TCP開發的通訊程式,有個很重要的問題需要解決,就是封包和拆包.

為什麼基於TCP的通訊程式需要進行封包和拆包?

TCP是個"流"協議,所謂流,就是沒有界限的一串資料.大家可以想想河裡的流水,是連成一片的,其間是沒有分界線的.但一般通訊程式開發是需要定義一個個相互獨立的資料包的,比如用於登陸的資料包,用於登出的資料包.由於TCP"流"的特性以及網路狀況,在進行資料傳輸時會出現以下幾種情況.
假設我們連續呼叫兩次send分別傳送兩段資料data1和data2,在接收端有以下幾種接收情況(當然不止這幾種情況,這裡只列出了有代表性的情況).
A.先接收到data1,然後接收到data2.
B.先接收到data1的部分資料,然後接收到data1餘下的部分以及data2的全部.
C.先接收到了data1的全部資料和data2的部分資料,然後接收到了data2的餘下的資料.
D.一次性接收到了data1和data2的全部資料.

對於A這種情況正是我們需要的,不再做討論.對於B,C,D的情況就是大家經常說的"粘包",就需要我們把接收到的資料進行拆包,拆成一個個獨立的資料包.為了拆包就必須在傳送端進行封包.

另:對於UDP來說就不存在拆包的問題,因為UDP是個"資料包"協議,也就是兩段資料間是有界限的,在接收端要麼接收不到資料要麼就是接收一個完整的一段資料,不會少接收也不會多接收.

二.為什麼會出現B.C.D的情況.
"粘包"可發生在傳送端也可發生在接收端.
1.由Nagle演算法造成的傳送端的粘包:Nagle演算法是一種改善網路傳輸效率的演算法.簡單的說,當我們提交一段資料給TCP傳送時,TCP並不立刻傳送此段資料,而是等待一小段時間,看看在等待期間是否還有要傳送的資料,若有則會一次把這兩段資料傳送出去.這是對Nagle演算法一個簡單的解釋,詳細的請看相關書籍.象C和D的情況就有可能是Nagle演算法造成的.
2.接收端接收不及時造成的接收端粘包:

TCP會把接收到的資料存在自己的緩衝區中,然後通知應用層取資料.當應用層由於某些原因不能及時的把TCP的資料取出來,就會造成TCP緩衝區中存放了幾段資料.

三.怎樣封包和拆包.
   最初遇到"粘包"的問題時,我是通過在兩次send之間呼叫sleep來休眠一小段時間來解決.這個解決方法的缺點是顯而易見的,使傳輸效率大大降低,而且也並不可靠.後來就是通過應答的方式來解決,儘管在大多數時候是可行的,但是不能解決象B的那種情況,而且採用應答方式增加了通訊量,加重了網路負荷. 再後來就是對資料包進行封包和拆包的操作.
    封包:
封包就是給一段資料加上包頭,這樣一來資料包就分為包頭和包體兩部分內容了(以後講過濾非法包時封包會加入"包尾"內容).包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,這是個很重要的變數,其他的結構體成員可根據需要自己定義.根據包頭長度固定以及包頭中含有包體長度的變數就能正確的拆分出一個完整的資料包.
    對於拆包目前我最常用的是以下兩種方式.
    1.動態緩衝區暫存方式.之所以說緩衝區是動態的是因為當需要緩衝的資料長度超出緩衝區的長度時會增大緩衝區長度.
    大概過程描述如下:
    A,為每一個連線動態分配一個緩衝區,同時把此緩衝區和SOCKET關聯,常用的是通過結構體關聯.
    B,當接收到資料時首先把此段資料存放在緩衝區中.
    C,判斷快取區中的資料長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
    D,根據包頭資料解析出裡面代表包體長度的變數.
    E,判斷快取區中除包頭外的資料長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
    F,取出整個資料包.這裡的"取"的意思是不光從緩衝區中拷貝出資料包,而且要把此資料包從快取區中刪除掉.刪除的辦法就是把此包後面的資料移動到緩衝區的起始地址.

這種方法有兩個缺點.1.為每個連線動態分配一個緩衝區增大了記憶體的使用.2.有三個地方需要拷貝資料,一個地方是把資料存放在緩衝區,一個地方是把完整的資料包從緩衝區取出來,一個地方是把資料包從緩衝區中刪除.第二種拆包的方法會解決和完善這些缺點.

前面提到過這種方法的缺點.下面給出一個改進辦法, 即採用環形緩衝.但是這種改進方法還是不能解決第一個缺點以及第一個資料拷貝,只能解決第三個地方的資料拷貝(這個地方是拷貝資料最多的地方).第2種拆包方式會解決這兩個問題.
環形緩衝實現方案是定義兩個指標,分別指向有效資料的頭和尾.在存放資料和刪除資料時只是進行頭尾指標的移動.

2.利用底層的緩衝區來進行拆包
由於TCP也維護了一個緩衝區,所以我們完全可以利用TCP的緩衝區來快取我們的資料,這樣一來就不需要為每一個連線分配一個緩衝區了.另一方面我們知道recv或者wsarecv都有一個引數,用來表示我們要接收多長長度的資料.利用這兩個條件我們就可以對第一種方法進行優化.
     對於阻塞SOCKET來說,我們可以利用一個迴圈來接收包頭長度的資料,然後解析出代表包體長度的那個變數,再用一個迴圈來接收包體長度的資料.

    tcp是流,沒有界限.也就無所謂包;tcp是協議,而socket是一種介面。本文以流的形式發單個大檔案,也就無所謂封包和拆包問題,見下面程式碼;但是要連續傳送多個大檔案,封包和拆包就是要考慮的問題了!