前面的文章介紹了TCP狀態變遷,以及TCP狀態變遷圖中的一些特殊狀態。html
本文主要看看TCP數據傳輸過程當中須要瞭解的一些重要點:jquery
在開始介紹上面列出的內容以前,先看看實驗程序的運行。網絡
本文的例子代碼是基於"動手學習TCP:客戶端狀態變遷"文章中的例子。tcp
首先,修改了"BuildTcpPacket"這個函數,增長了兩個功能:函數
public static Packet BuildTcpPacket(EndPointInfo endPointInfo, TcpControlBits tcpControlBits, List<TcpOption> tcpOptionList = null, bool withPayload = false, string payloadData = "") { EthernetLayer ethernetLayer = new EthernetLayer { Source = new MacAddress(endPointInfo.SourceMac), Destination = new MacAddress(endPointInfo.DestinationMac), EtherType = EthernetType.None, // Will be filled automatically. }; IpV4Layer ipV4Layer = new IpV4Layer { Source = new IpV4Address(endPointInfo.SourceIp), CurrentDestination = new IpV4Address(endPointInfo.DestinationIp), Fragmentation = IpV4Fragmentation.None, HeaderChecksum = null, // Will be filled automatically. Identification = 123, Options = IpV4Options.None, Protocol = null, // Will be filled automatically. Ttl = 10, TypeOfService = 0, }; TcpLayer tcpLayer = new TcpLayer { SourcePort = endPointInfo.SourcePort, DestinationPort = endPointInfo.DestinationPort, Checksum = null, // Will be filled automatically. SequenceNumber = seqNum, AcknowledgmentNumber = ackNum, ControlBits = tcpControlBits, Window = windowSize, UrgentPointer = 0, Options = (tcpOptionList == null) ? TcpOptions.None : new TcpOptions(tcpOptionList), }; PacketBuilder builder; if (withPayload) { PayloadLayer payloadLayer = new PayloadLayer { Data = new Datagram(System.Text.Encoding.ASCII.GetBytes(payloadData)), }; builder = new PacketBuilder(ethernetLayer, ipV4Layer, tcpLayer, payloadLayer); return builder.Build(DateTime.Now); } builder = new PacketBuilder(ethernetLayer, ipV4Layer, tcpLayer); return builder.Build(DateTime.Now); }
代碼其他的改動發生在"PacketHandler"函數中:學習
private static void PacketHandler(PacketCommunicator communicator, EndPointInfo endPointInfo, bool clientToSendFin = true)
增長了對於"ESTABLISHED"狀態下收到數據包的處理,主要做用就是發送一個[ACK]包對收到的數據包進行確認。大數據
case TcpControlBits.Acknowledgment: if (tcpStatus == TCPStatus.FIN_WAIT_1) { tcpStatus = TCPStatus.FIN_WAIT_2; Utils.PacketInfoPrinter(packet, tcpStatus); } else if (tcpStatus == TCPStatus.LAST_ACK) { tcpStatus = TCPStatus.CLOSED; Utils.PacketInfoPrinter(packet, tcpStatus); running = false; } else if (tcpStatus == TCPStatus.ESTABLISHED) { //print the data received from server Console.WriteLine(packet.Ethernet.IpV4.Tcp.Payload.ToString()); communicator.SendPacket(Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment)); } break; case (TcpControlBits.Acknowledgment | TcpControlBits.Push): if (tcpStatus == TCPStatus.ESTABLISHED) { //print the data received from server Console.WriteLine(packet.Ethernet.IpV4.Tcp.Payload.ToString()); communicator.SendPacket(Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment)); } break;
代碼修改好以後,運行代碼。ui
經過console端能夠看到,在鏈接爲"ESTABLISHED"狀態下,客戶端收到的來自服務端的字節數。spa
經過Wireshark抓包能夠看到,在鏈接創建請求包[SYN]中增長了MSS的設置,而且之後的數據傳出中,TCP數據包的payload長度最大就是MSS的值。3d
下面就開始介紹上面實驗中涉及的TCP數據傳輸的知識點。
在網絡上傳輸的數據包是有大小限制,這裏就須要知道TCP分段和IP分片的概念了。
跟這兩個概念緊密相關的就是MSS(Maximum Segment Size)和MTU(Maximum Transmission Unit)這兩個指標了,這兩個指標的值大小直接決定了TCP分段和IP分片。
下面分別看看MSS和MTU。
首先來看看MTU。
以太網和802.3對數據幀的長度都有一個限制,最大值分別是1500和1492個字節。鏈路層的這個指標稱做MTU(注意MTU是鏈路層的概念),不一樣類型的網絡大多數都有一個上限。
若是網絡層(IP層)有一個數據報須要傳輸,且數據的長度比鏈路層的 MTU還大,那麼網絡層(IP層)就要進行分片(fragmentation),把數據報分紅若干片,保證每個分片都小於MTU;目的端的網絡層(IP層)會對收到的分片進行從新組裝。
也就是說,分片和從新組裝過程發生在網絡層(IP層),因此對運輸層(TCP/UDP)是透明的。
下面看看經過ping命令演示IP分片,ping命令屬於ICMP(Internet Control Messages Protocol)協議:
Wireshark的結果爲下,這5000個字節的數據被分別放在了四個IP分片中,每一個分片(最後一個分片除外)中的數據長度等於1480(1500 – 20[IP header]):
IP分片的問題:IP分片有一個很大的問題,因爲IP層自己沒有超時重傳機制,即便只丟失一片數據也要從新傳整個數據報。也就是說,對於上面截圖中的4個Frame,任何一個丟失了,另外3個都須要進行重傳。
使用UDP和ICMP的時候很容易致使IP分片,由於UDP和ICMP是不考慮MTU和分片的,而是把這些工做都丟給了網絡層(IP層);可是,爲了減小IP分片對TCP的影響,在TCP中提出了MSS來試圖避免IP分片。
MSS就是TCP數據包每次可以傳輸的最大數據分段。
爲了達到最佳的傳輸效能TCP協議在創建鏈接的時候一般要協商雙方的MSS值,這個值TCP協議在實現的時候每每用MTU值代替(須要減去IP數據包首部的大小20Bytes和TCP數據段的首部20Bytes),因此每每MSS爲1460。通信雙方會根據雙方提供的MSS值得最小值肯定爲此次鏈接的最大MSS值。
回到本文開始的例子,在創建TCP鏈接的時候,客戶端指定了MSS爲800,服務端指定的MSS爲1460。通過協商後,雙方採用了較小的MSS,因此之後的數據包長度最到爲800字節。TCP就是經過這種方式來避免IP分片的。
再看一個MSS的例子,經過Wireshark抓取了一段HTTP請求,經過GET方法請求jquery的一組數據包。
經過下面能夠看到,當應用層有一個超過MSS的數據須要發送的時候,TCP會把應用層的數據分紅多個TCP分段而後發送出去。每個分段包都包含TCP首部,而後傳遞給網絡層進一步增長IP首部。
經過上面能夠看到TCP分段和IP分片有下面的主要區別:
TCP傳輸的可靠性是經過Seq和Ack號來進行保證的,因此能夠看出Seq和Ack號的重要性。
文章開始的實驗中,另外一個須要注意的地方就是Seq和Ack號的變化。
在前面TCP鏈接的相關文章中已經介紹了鏈接創建和終止時候Seq和Ack號的變化,能夠總結獲得下面公式:
確認包的Ack = 待確認包(特殊標誌包)的Seq + 1
從Wireshark的截圖中能夠看到在數據傳輸中Seq和Ack號的變化。
對於數據包的確認,可使用下面的方式進行計算:
確認包的Ack = 待確認數據包的Seq + 待確認數據包的數據長度(Len)
在介紹TCP終止鏈接的時候,提到了因爲TCP是全雙工的,因此須要通過四次揮手才能關閉TCP鏈接。
TCP中有一個半鏈接的概念,就是TCP鏈接的一端在結束它的發送後,還能接收來自另外一端數據。
仍是回到文章開始的例子,服務端發出了終止TCP鏈接的請求[FIN, ACK],客戶端進行了確認,到此服務端到客戶端方向的TCP鏈接就關閉了。
可是,隨後客戶端向服務端發送了一段長度爲480字節的數據,而後才關閉客戶端到服務端方向的TCP鏈接。
本文主要介紹了TCP數據傳輸中的幾個重要的概念。
經過這篇文章,必定能很好的認識TCP分段和IP分片的區別,以及MSS和MTU的關係。