有不少小夥伴私聊我說更新太慢,奪命吹吹吹。一週一次,還不能知足大家嗎?html
好,那我不寫了。╭(╯^╰)╮!!!java
額,儘可能保持完成審覈後就交貨發佈啦。實際上前面的章節都是週末完成等到週三才發的。編程
上一節咱們細說了網絡模型分層。知道了七層分法實際如今只有五層了。今天咱們展開來學習傳輸層UDP和TCP的協議。由於這兩個協議關聯性比較大,且篇幅不高。故合併層一個小節學習。緩存
它是一種高速但數據不可靠的協議,爲何說它不可靠呢?是由於與之對應的TCP是很是可靠和穩定的。咱們先看一下UDP的報文結構再講解傳遞數據過程的原理。安全
能夠看到很簡單,沒有什麼東西須要特別說明的,根據前幾節的知識就能夠推斷出來。UDP的效率高並不是在報文上,而是它的傳輸機制就是隻管往外發,對方能不能接收到(是否存在),並不關心。服務器
你必定還記得網絡不通的時候ping一下,這種小秀的騷操做的把?沒有錯它的底層就是依靠UDP協議直接實現的,ping的過程就是客戶端組裝UDP報文,和DNS服務器接收解析UDP並響應客戶端的過程。當ping不通的時候就會出現超時,即DNS服務器不回覆客戶端。網絡
因此UDP的傳輸可理解成來數據了,就無論三七二一就是一個走你,拜拜了您勒。也不去校驗到底有沒有收到。socket
正常狀況下一條報文會被路由、交換機通過一層層轉發,最後到該接收的位置。他們依靠上一節提到的數據鏈路層和網絡層來尋找主機。UDP協議只關心端口。這種最典型的UDP稱爲單播UDP,除此以外還有組播和廣播。關於組播和廣播,咱們後續的章節講到作查找設備時會詳細講解,並作一個小demo。工具
相對UDP來講TCP協議就要穩定不少了。一樣咱們先看報文結構,再學習傳輸原理。學習
咱們重點看幾個字段說明:
序號: 表示發送端當前包位於整組數據的第幾個字節,也叫流水號。用於確保數據組的穩定性。
ack號: 也叫確認號,表示接收方收到了多少個字節,表示期待下一組數據的字節序號。
數據偏移: 通常不用,可是出如今TCP頭裏若是可選字段增長後,就要在數據偏移中指明偏移的位置,最多能夠將20字節的頭擴展到60個字節。
控制位: 6個標誌位URG ACK PSH RST SYN FIN每個表示一個控制功能,也就是告訴對方當前的包是作什麼用的標識。
窗口: 接收端告訴發送端自身的緩存大小的。避免過大的數據包致使接收端接受不過來而丟失數據。
URG: 緊急指針標誌是否有效。
ACK: 確認序號是否有效。
PSH: 刷新緩存標誌1有效,0忽略,要求把數據儘快給應用,而不要放在緩存裏。(java裏的flush方法)
RST: 異常標誌,表示強制斷開鏈接,在異常的狀況下。
SYN: 鏈接過程同步序號標誌。
FIN: finish標誌,用於釋放鏈接1表示關閉鏈接。
咋一看你會發現TCP的報文比UDP複雜了好多,一大堆東西。個人老夥計,別擔憂。那麼底層的事情又不要你處理。你只須要知道它們在幹嗎。而且值得說的是在Java上作socket編程時,你根本就感知不到底層字段的狀態。到上層在處理數據的時候僅僅只有一個輸入流和輸出流了。這樣極大的方便了應用層的簡便開發。
咱們知道TCP是面向鏈接的,它嚴格的把控住了每一次傳輸的數據的穩定性。那麼它是如何作到的呢?乾巴巴的說太過於抽象,如今咱們實際動手寫一下代碼(kotlin
)搭配利刃wireshark
分析一下。
fun main(args: Array<String>) {
val ss= ServerSocket(22222);
while (true){
val accept = ss.accept()
val dataInputStream = DataInputStream(accept.getInputStream())
val dataOutputStream = DataOutputStream(accept.getOutputStream())
val s = dataInputStream.readUTF()
dataOutputStream.writeUTF("hello Server")
dataInputStream.close()
dataOutputStream.close()
accept.close()
println(s)
}
}
複製代碼
fun main(args: Array<String>) {
val socket = Socket("192.168.0.5", 22222);
val dataOutputStream = DataOutputStream(socket.getOutputStream())
val dataInputStream = DataInputStream(socket.getInputStream())
dataOutputStream.writeUTF("hello")
val s = dataInputStream.readUTF()
dataInputStream.close()
dataOutputStream.close()
socket.close()
println(s)
}
複製代碼
值得注意的是wireshark沒法直接抓到TCP在本地的迴路包,可是咱們可使用命令行配置一下就能夠了,否則抓不到本機的迴路包。
給路由表添加一條 route add 本機IP mask 255.255.255.255 路由IP metric 1
抓完刪除route delete 本機IP
爲了方便演示,我這裏使用了兩塊網卡。因此往路由裏添加了兩次迴路IP。
這裏首先咱們開啓服務端,進入了循環監聽狀態,隨後開啓了客戶端鏈接上後發送一個hello
,服務端收到後回覆了一個hello Server
。再沒有作更多的事情了。咱們來看一下經過wireshark抓到的包。
192.168.0.4
端口:55705
(操做系統協議棧分配)192.168.0.5
端口:22222
(咱們定義的)1.啓動服務端後,操做系統將開始對22222
端口的SYN
包進行監聽。
2.客戶端啓動,並嘗試鏈接服務端,由操做系統協議棧隨機分配一個發送端口。
3.客戶端組裝一組SYN
報文發送至服務端22222
號端,標記客戶端seq
爲0
。(完成第一次握手)
4.服務端收到SYN
包,並組裝一組SYN、ACK
報文,內容是告訴客戶端我確認了你的SYN
包,並期待你的下一組數據從1
開始,標記服務端seq
爲0
。(完成第二次握手)
5.客戶端收到服務端返回的SYN、ACK
包報文,,標記客戶端seq
爲1
,並組裝一組ACK
報文,內容是告訴服務端指望收到的下一組ack
從1
開始。(完成第三次握手)
6.客戶端組裝了一組PSH、ACK
包,並帶上ack=1
,內容爲hello
,發送個服務端。
7.服務端收到了內容爲hello
的包,確認後將ack
加上收到的內容的長度。組裝一組PSH、ACK
並帶上ack=8
發送給客戶端。
8.客戶端收到服務端的消息,更新本身的seq=8
,並組裝一組FIN、ACK
包,並帶上ack=15
確認號,嘗試請求斷開鏈接。(客戶端第一次主動揮手)
9.服務端收到客戶端的FIN
包,更新本身的seq=15
,並組裝一組FIN、ACK
包,並帶上ack=8
發送給客戶端。(服務端確認,開始第二次揮手。)
10.客戶端收到服務端的ack
包並檢測seq
是否一致,組裝一組ACK
包發送給服務端。(第三次揮手)
11.服務端收到客戶端的ack
並檢測seq
是否一致,組裝一組ACK
包發送給客戶端。(第四次揮手)
上面描述的這個過程從字面上去看並無太大意義,我但願你真正打開wireshark和代碼,調試着玩玩。
握手: 客戶端和服務端會先進行三次握手,握手的時候會告訴對方自身的窗體大小和ack確認號。這個過包的功能是從ACK、SYN
來達到認識的。
傳輸數據: 在接受對方的數據時,在下一組包裏帶上ACK
標記告知對方我已經收到了多少內容,期待下一組內容的起始位置。
揮手: 相互確認包是否發送完整,再雙方確認了ACK
後。才真正揮手完畢。
丟包或包校驗不對狀況: 操做系統協議棧會根據ACK
和SEQ
號對控制位爲ACK
的包進行校驗,若是對不上則要求重發上一組包。
到此UDP&TCP
的協議咱們就學習完了。這裏沒有去演示UDP
包的抓取過程,由於若是TCP
都會了UDP
就不是什麼難題。再一個沒有展現wireshark
的具體使用過程,由於網上實在有不少優秀的教程了。
即使如此,我敢打賭的是若是讀者是第一次接觸TCP/IP
絕對會由於不少概念和發來發去的字段搞得頭暈或者只知其一;不知其二。由於個人能力還不足以讓你看看文章就徹底懂了。(原話出自凱哥 ^o^
)
聽我一句勸,這個時候不要放棄,成熱打鐵趕快打開分析工具,照着我前面的代碼跑起來。再拿出紙筆寫寫畫一畫。深挖一下真正的去感覺它們的巧妙之處。你會感受這個設計是很是棒的。
話說你們看得真的有收穫嗎?若是以爲不錯,又想獎勵我一下戳這裏打賞(據說打賞有吹更效果噢)。