動手學(xué)習(xí)TCP系列之?dāng)?shù)據(jù)傳輸
前面的文章介紹了TCP狀態(tài)變遷,以及TCP狀態(tài)變遷圖中的一些特殊狀態(tài)。
本文主要看看TCP數(shù)據(jù)傳輸過程中需要了解的一些重要點:
MSS(Maximum Segment Size)
Seq號和Ack號的計算
TCP半連接
TCP數(shù)據(jù)傳輸實驗
在開始介紹上面列出的內(nèi)容之前,先看看實驗程序的運行。
本文的例子代碼是基于"動手學(xué)習(xí)TCP:客戶端狀態(tài)變遷"文章中的例子。
首先,修改了"BuildTcpPacket"這個函數(shù),增加了兩個功能:
正常情況下TCP首部是20個字節(jié),但是TCP首部支持一些特殊"Options"(MSS就是其中一個);所以,***個改動就是支持創(chuàng)建帶特殊選項的TCP包
第二個改動是可以通過參數(shù)設(shè)置為TCP包增加Payload,這樣就可以通過TCP包傳輸數(shù)據(jù)了。
public static Packet BuildTcpPacket(EndPointInfo endPointInfo, TcpControlBits tcpControlBits, ListtcpOptionList = 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); }
代碼其余的改動發(fā)生在"PacketHandler"函數(shù)中:
private static void PacketHandler(PacketCommunicator communicator, EndPointInfo endPointInfo, bool clientToSendFin = true)
增加了對于"ESTABLISHED"狀態(tài)下收到數(shù)據(jù)包的處理,主要作用就是發(fā)送一個[ACK]包對收到的數(shù)據(jù)包進(jìn)行確認(rèn)。
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;
運行效果
代碼修改好之后,運行代碼。
通過console端可以看到,在連接為"ESTABLISHED"狀態(tài)下,客戶端收到的來自服務(wù)端的字節(jié)數(shù)。
#p#
通過Wireshark抓包可以看到,在連接建立請求包[SYN]中增加了MSS的設(shè)置,并且以后的數(shù)據(jù)傳出中,TCP數(shù)據(jù)包的payload長度***就是MSS的值。
下面就開始介紹上面實驗中涉及的TCP數(shù)據(jù)傳輸?shù)闹R點。
TCP分段和IP分片
在網(wǎng)絡(luò)上傳輸?shù)臄?shù)據(jù)包是有大小限制,這里就需要知道TCP分段和IP分片的概念了。
跟這兩個概念緊密相關(guān)的就是MSS(Maximum Segment Size)和MTU(Maximum Transmission Unit)這兩個指標(biāo)了,這兩個指標(biāo)的值大小直接決定了TCP分段和IP分片。
下面分別看看MSS和MTU。
MTU
首先來看看MTU。
以太網(wǎng)和802.3對數(shù)據(jù)幀的長度都有一個限制,***值分別是1500和1492個字節(jié)。鏈路層的這個指標(biāo)稱作MTU(注意MTU是鏈路層的概念),不同類型的網(wǎng)絡(luò)大多數(shù)都有一個上限。
如果網(wǎng)絡(luò)層(IP層)有一個數(shù)據(jù)報需要傳輸,且數(shù)據(jù)的長度比鏈路層的 MTU還大,那么網(wǎng)絡(luò)層(IP層)就要進(jìn)行分片(fragmentation),把數(shù)據(jù)報分成若干片,保證每一個分片都小于MTU;目的端的網(wǎng)絡(luò)層(IP層)會對收到的分片進(jìn)行重新組裝。
也就是說,分片和重新組裝過程發(fā)生在網(wǎng)絡(luò)層(IP層),所以對運輸層(TCP/UDP)是透明的。
下面看看通過ping命令演示IP分片,ping命令屬于ICMP(Internet Control Messages Protocol)協(xié)議:
Wireshark的結(jié)果為下,這5000個字節(jié)的數(shù)據(jù)被分別放在了四個IP分片中,每個分片(***一個分片除外)中的數(shù)據(jù)長度等于1480(1500 – 20[IP header]):
IP分片的問題:IP分片有一個很大的問題,由于IP層本身沒有超時重傳機(jī)制,即使只丟失一片數(shù)據(jù)也要重新傳整個數(shù)據(jù)報。也就是說,對于上面截圖中的4個Frame,任何一個丟失了,另外3個都需要進(jìn)行重傳。
使用UDP和ICMP的時候很容易導(dǎo)致IP分片,因為UDP和ICMP是不考慮MTU和分片的,而是把這些工作都丟給了網(wǎng)絡(luò)層(IP層);但是,為了減少IP分片對TCP的影響,在TCP中提出了MSS來試圖避免IP分片。
MSS
MSS就是TCP數(shù)據(jù)包每次能夠傳輸?shù)?**數(shù)據(jù)分段。
為了達(dá)到***的傳輸效能TCP協(xié)議在建立連接的時候通常要協(xié)商雙方的MSS值,這個值TCP協(xié)議在實現(xiàn)的時候往往用MTU值代替(需要減去IP數(shù)據(jù)包首部的大小20Bytes和TCP數(shù)據(jù)段的首部20Bytes),所以往往MSS為1460。通訊雙方會根據(jù)雙方提供的MSS值得最小值確定為這次連接的***MSS值。
回到本文開始的例子,在建立TCP連接的時候,客戶端指定了MSS為800,服務(wù)端指定的MSS為1460。經(jīng)過協(xié)商后,雙方采用了較小的MSS,所以以后的數(shù)據(jù)包長度最到為800字節(jié)。TCP就是通過這種方式來避免IP分片的。
再看一個MSS的例子,通過Wireshark抓取了一段HTTP請求,通過GET方法請求jquery的一組數(shù)據(jù)包。
通過下面可以看到,當(dāng)應(yīng)用層有一個超過MSS的數(shù)據(jù)需要發(fā)送的時候,TCP會把應(yīng)用層的數(shù)據(jù)分成多個TCP分段然后發(fā)送出去。每一個分段包都包含TCP首部,然后傳遞給網(wǎng)絡(luò)層進(jìn)一步增加IP首部。
區(qū)別
通過上面可以看到TCP分段和IP分片有下面的主要區(qū)別:
TCP分段發(fā)生在傳輸層,分段的依據(jù)是MSS;IP分片發(fā)生在網(wǎng)絡(luò)層,分片的依據(jù)是MTU
TCP分段是在傳輸層完成,并在傳輸層進(jìn)行重組;IP分片由網(wǎng)絡(luò)層完成,也在網(wǎng)絡(luò)層進(jìn)行重組
再看Seq和Ack號
TCP傳輸?shù)目煽啃允峭ㄟ^Seq和Ack號來進(jìn)行保證的,所以可以看出Seq和Ack號的重要性。
文章開始的實驗中,另一個需要注意的地方就是Seq和Ack號的變化。
在前面TCP連接的相關(guān)文章中已經(jīng)介紹了連接建立和終止時候Seq和Ack號的變化,可以總結(jié)得到下面公式:
確認(rèn)包的Ack = 待確認(rèn)包(特殊標(biāo)志包)的Seq + 1
從Wireshark的截圖中可以看到在數(shù)據(jù)傳輸中Seq和Ack號的變化。
對于數(shù)據(jù)包的確認(rèn),可以使用下面的方式進(jìn)行計算:
確認(rèn)包的Ack = 待確認(rèn)數(shù)據(jù)包的Seq + 待確認(rèn)數(shù)據(jù)包的數(shù)據(jù)長度(Len)
關(guān)于TCP半連接
在介紹TCP終止連接的時候,提到了由于TCP是全雙工的,所以需要經(jīng)過四次揮手才能關(guān)閉TCP連接。
TCP中有一個半連接的概念,就是TCP連接的一端在結(jié)束它的發(fā)送后,還能接收來自另一端數(shù)據(jù)。
還是回到文章開始的例子,服務(wù)端發(fā)出了終止TCP連接的請求[FIN, ACK],客戶端進(jìn)行了確認(rèn),到此服務(wù)端到客戶端方向的TCP連接就關(guān)閉了。
但是,隨后客戶端向服務(wù)端發(fā)送了一段長度為480字節(jié)的數(shù)據(jù),然后才關(guān)閉客戶端到服務(wù)端方向的TCP連接。
總結(jié)
本文主要介紹了TCP數(shù)據(jù)傳輸中的幾個重要的概念。
MSS(Maximum Segment Size)
Seq號和Ack號
TCP半連接
通過這篇文章,一定能很好的認(rèn)識TCP分段和IP分片的區(qū)別,以及MSS和MTU的關(guān)系。