http://blog.csdn.net/solstice/article/details/6527585
TCP是“面向連接的、可靠的、字節流傳輸協議”,這里的“可靠”究竟是什么意思?《Effective TCP/IP Programming》第9條說:Realize That TCP Is a Reliable Protocol, Not an Infallible Protocol,那么TCP在哪種情況下會出錯?這里說的“出錯”指的是收到的數據與發送的數據不一致,而不是數據不可達。
我在《一種自動反射消息類型的 Google Protobuf 網絡傳輸方案》中設計了帶check sum的消息格式,很多人表示不理解,認為是多余的。IP header里邊有check sum,TCP header也有check sum,鏈路層以太網還有CRC32校驗,那么為什么還需要在應用層做校驗?什么情況下TCP傳送的數據會出錯?
IP header和TCP header的check sum是一種非常弱的16-bit check sum算法,把數據當成反碼表示的16-bit integers,再加到一起。這種checksum算法能檢出一些簡單的錯誤,而對某些錯誤無能為力,由於是簡單的加法,遇到“和”不變的情況就無法檢查出錯誤(比如交換兩個16-bit整數,加法滿足交換律,結果不變)。以太網的CRC32比較強,但它只能保證同一個網段上的通信不會出錯(兩台機器的網線插到同一個交換機上,這時候以太網的CRC是有用的)。但是,如果兩台機器之間經過了多級路由器呢?
上圖中Client向Server發了一個TCP segment,這個segment先被封裝成一個IP packet,再被封裝成ethernet frame,發送到路由器(圖中消息a)。Router收到ethernet frame (b),轉發到另一個網段(c),最后Server收到d,通知應用程序。
Ethernet CRC能保證a和b相同,c和d相同;TCP header check sum的強度不足以保證收發payload的內容一樣。另外,如果把Router換成NAT,那么NAT自己會構造c(替換掉源地址),這時候a和d的payload不能用tcp header checksum校驗。
路由器可能出現硬件故障,比方說它的內存故障(或偶然錯誤)導致收發IP報文出現多bit的反轉或雙字節交換,這個反轉如果發生在payload區,那么無法用鏈路層、網絡層、傳輸層的check sum查出來,只能通過應用層的check sum來檢測。
這個現象在開發的時候不會遇到,因為開發用的幾台機器很可能都連到同一個交換機,ethernet CRC能防止錯誤。開發和測試的時候數據量不大,錯誤很難發生。之后大規模部署到生產環境,網絡環境復雜,這時候出個錯就讓人措手不及。
有一篇論文《When the CRC and TCP checksum disagree》分析了這個問題。另外《The Limitations of the Ethernet CRC and TCP/IP checksums for error detection》(http://noahdavids.org/self_published/CRC_and_checksum.html)也值得一讀。
這個情況真的會發生嗎?會的,Amazon S3 在2008年7月就遇到過,單bit反轉導致了一次嚴重線上事故,所以他們吸取教訓加了 check sum。見http://status.aws.amazon.com/s3-20080720.html
另外一個例證:下載大文件的時候一般都會附上MD5,這除了有安全方面的考慮(防止篡改),也說明應用層應該自己設法校驗數據的正確性。這是end-to-end principle的一個例證。
TCP作為一個可靠的傳輸層協議,其核心有三點:
1. Positive acknowledgement with retransmission (重傳)
2. Flow control using sliding window(包括Nagle 算法等,批量傳)
3. Congestion control(包括slow start、congestion avoidance、fast retransmit等)
第一點已經足以滿足“可靠性”要求(為什么?);
第二點是為了提高吞吐量,充分利用鏈路層帶寬;
第三點是防止過載造成丟包。
換言之,第二點是避免發得太慢,第三點是避免發得太快,二者相互制約。從反饋控制的角度看,TCP像是一個自適應的節流閥,根據管道的擁堵情況自動調整閥門的流量。
Java Byte之Adler32算法對數據流的校驗
Java Byte之Adler32算法對數據流的校驗代碼如下:
import
java.util.zip.Adler32;
import
java.util.zip.Checksum;
/**
* @from www.everycoding.com
* @Description:Java Byte之Adler32算法對數據流的校驗
*/
public
class
ComputeAdler {
public
static
void
main(String[] argv)
throws
Exception {
/**
* 可用於計算數據流的 Adler-32 校驗和的類。Adler-32 校驗和幾乎與
* CRC-32 一樣可靠,但是能夠更快地計算出來。
*/
byte
[] bytes =
"www.everycoding.com"
.getBytes();
Checksum checksumEngine =
new
Adler32();
checksumEngine.update(bytes,
0
, bytes.length);
long
checksum = checksumEngine.getValue();
System.out.println(
"生成用於比對數據完整性的校驗碼:"
+checksum);
}
}
執行結果如下:
CRC全稱Cyclic Redundancy Check,又叫循環冗余校驗。它是一種散列函數(HASH,把任意長度的輸入通過散列算法,最終變換成固定長度的摘要輸出,其結果就是散列值,按照HASH算法,HASH具有單向性,不可逆性),用來檢測或校驗傳輸或保存的數據錯誤,在通信領域廣泛地用於實現差錯控制,比如通信系統多使用CRC12和CRC16,XMODEM使用CRC16等等(12、16、32等值均是指多項式的最高階N次冪),天緣早前在做通信方面工作時也是最常用到這個校驗方法,因為其編解碼方法都非常簡單,運算時間也很短。
但從理論角度,CRC不能完全可靠的驗證數據完整性,因為CRC多項式是線性結構,很容易通過改變數據方式達到CRC碰撞,天緣這里給一個更加通俗的解釋,假設一串帶有CRC校驗的代碼在傳輸中,如果連續出現差錯,當出錯次數達到一定次數時,那么幾乎可以肯定會出現一次碰撞(值不對但CRC結果正確),但隨着CRC數據位增加,碰撞幾率會顯著降低,比如CRC32比CRC16具有更可靠的驗證性,CRC64又會比CRC32更可靠,當然這都是按照ITU規范標准條件下。
正因為CRC具有以上特點,對於網絡上傳輸的文件類很少只使用CRC作為校驗依據,文件傳輸相比通信底層傳輸風險更大,很容易受到人為干預影響。
從奇偶校驗說起
所謂通訊過程的校驗是指在通訊數據后加上一些附加信息,通過這些附加信息來判斷接收到的數據是否和發送出的數據相同。比如說RS232串行通訊可以設置奇偶校驗位,所謂奇偶校驗就是在發送的每一個字節后都加上一位,使得每個字節中1的個數為奇數個或偶數個。比如我們要發送的字節是0x1a,二進制表示為0001 1010。【】
采用奇校驗,則在數據后補上個0,數據變為0001 1010 0,數據中1的個數為奇數個(3個)
采用偶校驗,則在數據后補上個1,數據變為0001 1010 1,數據中1的個數為偶數個(4個)
接收方通過計算數據中1個數是否滿足奇偶性來確定數據是否有錯。
奇偶校驗的缺點也很明顯,首先,它對錯誤的檢測概率大約只有50%。也就是只有一半的錯誤它能夠檢測出來。另外,每傳輸一個字節都要附加一位校驗位,對傳輸效率的影響很大。因此,在高速數據通訊中很少采用奇偶校驗。奇偶校驗優點也很明顯,它很簡單,因此可以用硬件來實現,這樣可以減少軟件的負擔。因此,奇偶校驗也被廣泛的應用着。
奇偶校驗就先介紹到這來,之所以從奇偶校驗說起,是因為這種校驗方式最簡單,而且后面將會知道奇偶校驗其實就是CRC 校驗的一種(CRC-1)。
累加和校驗
另一種常見的校驗方式是累加和校驗。所謂累加和校驗實現方式有很多種,最常用的一種是在一次通訊數據包的最后加入一個字節的校驗數據。這個字節內容為前面數據包中全部數據的忽略進位的按字節累加和。比如下面的例子:
我們要傳輸的信息為: 6、23、4
加上校驗和后的數據包:6、23、4、33
這里 33 為前三個字節的校驗和。接收方收到全部數據后對前三個數據進行同樣的累加計算,如果累加和與最后一個字節相同的話就認為傳輸的數據沒有錯誤。
累加和校驗由於實現起來非常簡單,也被廣泛的采用。但是這種校驗方式的檢錯能力也比較一般,對於單字節的校驗和大概有1/256 的概率將原本是錯誤的通訊數據誤判為正確數據。之所以這里介紹這種校驗,是因為CRC校驗在傳輸數據的形式上與累加和校驗是相同的,都可以表示為:通訊數據 校驗字節(也可能是多個字節)
初識 CRC 算法
CRC 算法的基本思想是將傳輸的數據當做一個位數很長的數。將這個數除以另一個數。得到的余數作為校驗數據附加到原數據后面。還以上面例子中的數據為例:
6、23、4 可以看做一個2進制數: 0000011000010111 00000010
假如被除數選9,二進制表示為:1001