传输层的七七八八
TCP
TCP提供一種面向連接、可靠的字節流服務。
面向連接:兩端各自維護一份數據結構,傳輸數據之前,先進行數據結構部分信息的狀態同步,就是去建立連接,建立好之后才能傳輸數據,不需要的時候斷開連接,然后釋放相關數據結構
可靠性:
- 由TCP將報文段分段為合適的大小后交給IP層
- TCP發出段后啟動定時器,目的端在定時器到期之前沒有確認應答,TCP會重發該段
- 接收端收到數據段后需要確認應答,告知發送端數據已經接收到哪里了(通過Sequence Number和acknowledgement Number記錄)
- TCP的首部校驗和由發送端計算和存儲,接收端如果校驗出錯,將包丟棄不發送確認應答,等待重發
- TCP分段后委托下層發送數據段,到達目的端如果出現亂序,TCP會重排序后交給應用層
- 如果數據重復,則丟棄
- 接收端會告知發送端能接受的最大數據段,實現流量控制
首部格式
各字段含義
- Source Port: 發送端端口號,例如http:80
- Destionation Port:接收端端口號
- Sequence Number(Seq):初始值由主機隨機生成,TCP流服務會對每個字節進行編號,對傳輸的Data部分進行計數(不包含數據鏈路層、IP首部、TCP首部)。目的端的ACK會將Seq+Tcp Segment Data的值返回到發送端,這個值就是發送端下次發送時的seq值。
- 注:雖然SYN、FIN在首部,但傳輸他們時仍然會計數,單位為1byte,所以三次握手和四次揮手時,雖然Tcp Segment Data的長度為0,回復的ACK值仍然要加1。
- Acknowledgement Number:Seq + Tcp Segment Data計算值后返回給發送端,表明該值和上次Seq值之間的數據我已經收到了,下次發送時以這個值作為Seq值發送
- Data offset:TCP首部的長度,如果沒有選項內容,首部長度為固定20 bytes,添加選項后最大首部長度為60 bytes(受限于該字段長度:4 bit,單位為4bytes)
- Reserved:該字段為了以后擴展使用,通常設置為0
- Control Flags:每一位代表一個標志,順序如上圖:
- CWR(Congestion Window Reduced):在網絡層的七七八八聊過,ECN(Explicit Congestion Notificat)的實現依靠IP首部記錄路由器是否遇到擁塞,在返回包的TCP首部中通知發生擁塞。CWR標志和ECE標志設置為1時,會通知對方網絡擁塞
- ECE(ECN-Echo)
- URG(Urgent Flag):為1時表示該數據段中有需要緊急處理的數據
- ACK(Acknowledgement Flag):TCP規定除了SYN包之外,該標志都設置為1,表示應答有效
- PSH(Push Flag):為1時表示將數據立即傳給上層協議,不進行緩存
- RST(Reset Flag):強制斷開連接
- SYN(Synchronize Flag):為1時表示想要建立連接,并設置Sequence Number的初始值(握手)
- FIN(Fin Flag):為1時表示不再發送數據,希望斷開連接。主機收到設置FIN標志的包后,兩端主機對對方的FIN標志包進行確認應答。不必立即回復,可以等待緩沖區中的所有數據發送成功并刪除后再回復(揮手)
- Window Size:從ACK Number的位置開始,最大可以接收的數據,發送發發送的數據不能超過該窗口大小。窗口為0時,對端可以發送窗口探測包。(1 byte)
- Checksum:校驗數據是否正確,覆蓋TCP首部和Data部分
- Urgent Pointer:在URG標志位1時該字段有效,緊急數據是從數據部分的首位到緊急指針所指的位置為止。Telnet Ctrl + C時會有URG為1的包
- Options:用于提高TCP的傳輸性能,長度最大為40bytes(首部最大60bytes - 固定部分20bytes),padding同IP首部的padding一樣,作為對options的填充,調整為32 bit的整數倍。options分為多種類型:
- 類型2:MSS(Maximum Segment Size),在建立連接時(發送SYN標志的報文段)中指定MSS,表示本端能接收的最大長度的報文段,通常來說MSS越大,網絡利用率越高。長度為 (MTU - IP首部 - TCP首部),對于以太網MSS長度可達1460bytes,默認為536bytes
- 類型3:WSOPT-Window Scale,可以提高TCP吞吐量,首部的windows size長度為16位,只能發送最大64KBytes,使用該選項可以擴展到1GBytes字節,提升了單位RTT的數據傳輸量,從而增加吞吐。
- 類型8:上面介紹Sequence Number對傳輸數據的字節進行計數,受32位長度的影響,如果高速傳輸一個很大的數據包,Sequence Number超出了內核解決序號回繞問題的范圍(回繞幅度2^31 - 1),那么接收端就無法判斷正確的序號了,加上該選項可以區分新老序號
數據傳輸
在主機網卡抓包繁雜信息太多,在虛擬機起一個基本的tcp server,回顯客戶端發送的消息后關閉,代碼如下:
import socket
import sys
HOST = "0.0.0.0"
PORT = 8888
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(10)
connection, addr = server.accept()
data = connection.recv(1024)
print(data.decode())
connection.sendall(data)
connection.close()
server.close()
客戶端輸入消息發送,并接收服務端的消息打印
import socket
HOST = "10.211.55.3"
PORT = 8888
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST,PORT))
send_info = input("send message: ")
client.send(send_info.encode("utf-8"))
recv_info = client.recv(1024).decode("utf-8")
print(recv_info)
client.close()
三次握手建立連接
建立連接過程
- 客戶端發包(active open):開啟SYN標志,客戶端的initial seq = x
客戶端向服務端發起帶SYN標志的段,端口就是我們server.py中定義的8888,TCP Segment Len為0表示沒有傳輸數據,Acknowledgment Number為0(第一次發起,沒什么好應答的),客戶端生成的Sequence Number為4176525122,wireshark幫我們分析的relative sequence number為0(下文使用relative sequence number),客戶端seq = 0
- 服務器回包(passive open):開啟SYN標志,服務端的initial seq = y和ack,ack的值為x+1(SYN占用一個序號)
服務端收到請求后,向客戶端發送SYN標志的段,數據段長度為0,服務端的seq為2608063952,同樣relative值為0,因為上一個包的SYN是占用sequence number的,所以ack = 1(客戶端的seq 0 + SYN標志位1,看raw值的話就是客戶端的seq4176525122 + 1 = 41766525123)
- 客戶端回包:開啟ACK標志,ack的值為服務端的y+1,seq為x+1
客戶端收到服務端的回包后,發送ACK段,數據段長度為0,客戶端seq為1,同樣ack = 1(服務端seq 0 + SYN標志位 1,raw值是2608063952 + 1 = 2608063953)
通過三次握手,客戶端與服務端協商好,客戶端下一次從2608063953處接收,服務端從41766525123處接收。
狀態變遷
-
server.py啟動后服務端處于LISTEN狀態,客戶端client.py發送SYN標志包后處于SYN_SENT狀態 - 服務端收到該包后會返回SYN標志的應答包,從LISTEN切換為SYN_RCVD狀態
- 客戶端發送ACK標志的應答包,切換為ESTABLISHED狀態
- 服務端收到ACK標志的包后,切換為ESTABLISHED狀態
之后就可以進行數據傳輸了。
數據傳輸
- 使用我們的client.py給server.py發送hello,數據段長度為5,根據上面的握手協商,本次發送數據的seq為41766525123,ack不變
- 服務端接收到數據后給客戶端發送ack,ack的值為 客戶端seq 41766525123 + tcp segment len 5 = 41766525128,服務端的seq為握手協商好的2608063953
- 應答后我們的server.py要將hello發回客戶端,于是數據段長度為5,seq為2608063953,ack跟第2步一樣
- 客戶端收到包后給服務端發送ack應答,ack值為服務端seq 2608063953 + 數據段長度 5 = 2608063958
四次揮手斷開連接
TCP是雙向傳輸的(全雙工),兩端都各自維護一份連接狀態,因此每個方向必須單獨的進行關閉,所以終止連接需要四次揮手(每一方都需要給對端發送FIN標志位的包,并且都需要給對方回復一個應答包):
- 為了實現四次揮手,TCP提供了半關閉的能力,即客戶端發送FIN包后表示不會再發送數據,但是仍然可以接收數據,服務器會應答該FIN包。
- 期間服務端可以繼續向客戶端發送未完成的數據,服務端也不需要再發送時,會向客戶端發送FIN包,客戶端應答該FIN包,雙方連接徹底關閉。
斷開連接過程
斷開的請求可以由任意一端發起,server.py是讓服務端將客戶端發來的內容發送出去后直接關閉連接,所以本次抓包是從服務端發起斷開的
- 服務端發起關閉:開啟FIN標志,seq = x,len = 0,上一個包沒有數據,ACK不變
因為之前應答過客戶端的hello,服務端seq變為2608063958,之前數據段的應答包已經發送過了(有時候會合并發送),所以ack不變為4176525128
- 客戶端回包:對FIN標志段進行應答,數據段長度為0,ack = x + 1,seq = y,len = 0
同樣因為應答過服務端的hello,客戶端seq為4176525128,FIN標志占用一個seq,所以ack為服務端seq 2608063958 + FIN標志 1 = 2608063959
- 客戶端發起關閉:開啟FIN標志,seq = y, ack = x + 1,len = 0
客戶端發起關閉,開啟FIN標志,seq為4176525128,第2步回包已經應答過服務端的FIN包,ack不變
- 服務端回包:對FIN標志段進行應答,seq = x + 1, ack = y + 1, len = 0
服務端發起應答,第2步客戶端會FIN包后告訴服務端下次從seq = 2608063959發送,所以seq = 2608063959,ack為客戶端seq 4176525128 + FIN標志 1 = 4176525129
至此,雙方連接徹底關閉。
狀態
借用劉超老師的圖,根據我們抓包的情況把左邊看成服務端,右邊看成客戶端
- 傳輸數據過程中客戶端和服務端都是ESTABLISHED狀態,服務端發出FIN標志數據段后進入FIN_WAIT_1,等待客戶端回包。如果服務端收不到ACK回包,會重傳該報文(重傳次數由
tcp_orphan_retries控制),超時會斷開連接。 - 客戶端收到FIN包后,發送ACK包并進入CLOSE_WAIT狀態,如果這個ACK丟失,服務端沒有收到,服務端會重傳FIN包再次等待客戶端的ACK。
- 服務端收到ACK包后進入FIN_WAIT_2狀態,等待客戶端的FIN包發來。
- 如果調用close關閉連接,超過
tcp_fin_timeout規定的時間,客戶端沒有發來FIN包,那么服務端會直接關閉 - 如果服務端調用shutdown來關閉連接,仍然可以接收數據,如果客戶端沒有發送FIN標志的包,那么服務端會一直處于FIN_WAIT_2狀態
- 如果調用close關閉連接,超過
- 客戶端發送FIN包后進入LAST_ACK,等待服務端的ACK,如果等不到會重傳FIN包,超過
tcp_orphan_retries就會斷開連接 - 服務端收到客戶端的FIN包,并發送ACK回包后進入TIME_WAIT狀態,等待2MSL的時間后關閉連接,如果中間收到了客戶端重發的FIN包,會重置2MSL的定時器。如果沒有進行2MSL的等待直接退出,可能會出現客戶端的FIN包無法收到ACK的情況,這時客戶端再次發送FIN包會收到服務端RST的回包(Connection reset by peer)
- 客戶端收到服務端的ACK后徹底關閉
TIME_WAIT的2MSL
MSL(Maximum Segment Lifetime)是報文最大生存時間,MSL>=TTL的時間,確保了超過該時間報文會在網絡傳輸中被丟棄,TIME_WAIT設置為2倍MSL的設值允許報文至少被丟棄1次,linux停留在TIME_WAIT的時間為60秒,所以我們的server.py啟動并運行完之后立馬再次啟動會bind失敗,告知端口占用。
如果TIME_WAIT等待的時間不夠可能會將舊的seq插入新的連接數據中(
序列號回繞并延遲到達)
異常斷開連接
TCP是通過內核管理的,應用層需要通過send/recv來發送和接受數據,如果連接斷開后應用層繼續recv,會收到Connection reset by peer(之前的項目中,Prometheus相關的日志會出現大量的Connection reset by peer,原因是后端收集數據太慢,而server又使用python的WSGI server,無法支持長連接,導致讀取時對端已經關閉的情況),如果是send則會收到Broken pipe
無數據傳輸異常斷開(tcp keepalive)
socket通過設置SO_KEEPALIVE啟動keepalive,可以通過sysctl -a查看系統設置,若開啟了keepalive,兩端沒有數據傳輸,一端崩潰,另一端發送探測包經過 7200 + 75 * 9 = 7875秒后認為對端掛了
# 過了7200秒后無數據交互啟動探測
net.ipv4.tcp_keepalive_time = 7200
# 探測間隔時間為75秒
net.ipv4.tcp_keepalive_intvl = 75
# 一共探測9次,一直無響應就認為對端掛了,關閉連接
net.ipv4.tcp_keepalive_probes = 9
如果服務端沒有開啟keepalive,這個tcp連接會一直處于ESTABLISHED狀態,服務端重啟失效
端口失效(RST標志)
一般異常關閉連接的時候會使用RST標志,發出或收到該標志的內核會清理該連接相應的內存資源、端口。接收到RST的一端會收到Connection reset或者Connection refused。大概情況:
- 端口現在不可用
- socket關閉
- 客戶端消息發送完之前關閉了socket,會發一個RST到服務端
- 服務端關閉了socket,客戶端再發送消息,服務端會回復一個RST包
進程崩潰
上面說到TCP棧是由操作系統管理的,如果一方進程發生了崩潰并被系統感知,操作系統會與對端進行四次揮手結束連接
數據傳輸中主機崩潰
- 客戶端崩潰后重啟,服務端利用超時重傳機制重傳報文,客戶端之前連接的上下文都不存在了,會發送RST包到服務端關閉連接
- 客戶端永久下線,服務端超時重傳達到最大超時時間或最大重傳次數,服務端會斷開連接并通過socket發送ETIMEOUT到應用程序
TCP狀態機
該狀態圖中A為客戶端,B為服務端。由客戶端發起連接,也由客戶端發起關閉。
- (1)(2)(3)(4)(5)表示發起連接的狀態,可以對照三次握手
- (一)(二)(三)(四)(五)(六)表示斷開連接的狀態,可以對照四次揮手
- 實線為客戶端A,虛線為服務端B
觸發重傳機制
超時重傳
上面的連接建立、傳輸數據、連接關閉都會出現數據包未被接收的情況,發送端觸發重傳機制,TCP提供可靠傳輸依靠確認接收端已經收到了數據,也就是說通過數據發出去到收到接收端發來的ack報文才算是完成這一報文段的傳輸,用收到ack的時間戳減去發送數據的時間戳得到的差值就是這個包的RTT(Round-Trip Time),其中的問題是出去的包可能會丟失,對端收到數據后返回的ack也可能丟失。TCP通過在發送時設定一個定時器,如果超過定時器就重傳數據,具體的問題就在于如何設置定時器間隔時間和重傳的頻率。
- 如果定時器時間設置過大,會出現網絡利用率低的情況,丟了很久了才重傳
- 如果定時器時間設置過小,可能網絡只是延遲略大,第一個包還沒到,觸發重傳的第二個包就發出來了,給鏈路增加了不必要的負載
所以重傳定時器的時間應略大于RTT比較合適,而網絡環境的速率是經常變化的。所以跟蹤測量RTT并且根據該值來動態設置重傳定時器RTO (Retransmission Time Out)
快速重傳
快速重傳的機制擁塞控制會用到,如果發送端接收到了3個相同的ack后,會在超時重傳的定時器過期之前重傳丟失的seq,比如發送了seq1~seq5,但是seq2、seq3都丟失了,接收端回復ack時回復的都是seq2的ack,再收到seq3的3個ack后,才能再重傳seq3的ack。網絡利用率嚴重下降。現在Linux中會開啟net.ipv4.tcp_sack=1和net.ipv4.tcp_dsack=1,分別對應SACK( Selective Acknowledgment)重傳機制和Duplicate SACK重傳機制。
SACK
在TCP首部options里設置SACK,接收端將已經收到的數據信息發送給發送端,這樣發送端在收到三次重復ACK后啟動快速重傳機制,但是根據這個字段可以看到丟失的數據,重發則發送丟失的那些seq就可以了
Duplicate SACK
SACK主要是告訴發送端,哪些數據是重復發送了。可以判斷出是ack應答丟了導致的重發,還是發送方的數據包延時到達導致的重發。
流量控制
滑動窗口
在我們數據傳輸部分的抓包中,本地網絡棧加上數據包較小,都是一發一答的順序來進行的。如果是遠端服務器,RRT時間較長的話傳輸會變得低效,應答包不承載數據,只是告知發送端我的數據接收到哪里了,你下次從哪個seq開始發送,如果應答包丟了,發送端還得等著超時重傳再收到ack后發送下一段。所以如果要提高傳輸效率,引入了滑動窗口的概念:接收端可以告訴客戶端,我的緩沖區能放多少數據,你看著發。至于接收端發回的ack,如果中間的某次ack走丟了,比如200299的ack收到了,300399的走丟了,400~499的收到了,那么發送端就認為,500之前的所有數據接收端都收到了,不用重傳,繼續發送。或者是接收端先不發送ack,直接發送一個500的ack,這種方式叫做累計應答。發送端和接收端都要維護一個窗口用來限制收發數據的大小。
發送端窗口1
- seq 1、2、3都發送并收到了確認
- seq 4~9是已經發送但是未收到ack確認的
- seq 10~12是發送端可以接著發送的
- seq 13~15超過了當前窗口大小,發送端不能發送,否則發出去也會被接收端丟掉
接下來發送端繼續發送10、11、12
服務端接收窗口1
- 藍色部分已經接收并發送了ack,但是應用層還沒有讀取,不占用窗口大小
- 橙色部分是已經收到了數據,還沒有確認,此時可以直接確認ack=7,發送端收到ack后就會知道4、5、6的數據包接收端已經接收到了。因為存在7、8、9還沒有收到,所以無法發送ack=11的確認。
- 紅色部分超出窗口大小,無法接收
接下來服務端發送ack=7確認4、5、6已經收到,窗口滑動后如下
服務端接收窗口2
- 4、5、6已經ack,窗口向右移動3個
- 之前不能接收的13、14、15現在可以接收
客戶端發送窗口2
- 4、5、6已經ack,之前不可發送13、14、15已經發送等待服務端確認
窗口大小變化
應用程序無法及時讀取緩存內容
- 窗口的大小是通過兩端交互數據段中TCP首部的windows指定的,窗口大小即當前系統給TCP分配的緩存區大小受系統繁忙程度的影響,系統繁忙,應用層無法及時讀取TCP緩沖區中的內容(圖中藍色部分),那么TCP的窗口大小就要減小,告訴發送端,那么發送端下次發送的數據就減少。
- 極端情況下減小到0,發送端不能再發送任何數據,這時會啟動定時器來發送探測包看窗口何時變大,如果回復的windows大小仍然為0,就重新啟動定時器。
操作系統減小TCP緩存
第一種情況緩沖區大小不變,只是改變接收窗口的大小。而更糟糕的情況是操作系統減小接收端緩沖區大小,TCP規定必須先減小窗口大小,然后才能減小緩沖區。如果發送端按照上次窗口的大小發送了120字節的數據,而應用層還沒有處理緩沖區中的數據,操作系統將緩沖區減小60字節,此時接收端的窗口為60字節,發送窗口通告告訴客戶端,但是消息已經發出,120字節的數據超過了當前窗口大小,發生丟包。
糊涂窗口綜合癥(Silly Window Syndrome)
接收端會通告一個小窗口,比方5字節的窗口,TCP首部的固定長度就有20字節,再加上IP首部等長度,發送端的一個包實際傳輸了5字節,但是包大小就有幾十字節。顯然網絡利用率大大降低,這個癥狀就是糊涂窗口綜合癥。
為了避免該現象發生,根據TCP首部選項里的MSS大小做控制:
- 發送端滿足以下條件之一才能發送
- 可以發送>=MSS長度的報文段
- 數據長度至少為接收端通告窗口大小的一半
- 之前數據的ack接收到之后
- 服務端通告窗口大小的方式
- 如果窗口大小小于MSS與1/2緩存大小中最小的一個,關閉窗口
- 窗口大小至少增長到MSS,或者超過1/2緩存大小,打開窗口
擁塞控制
流量控制是根據主機緩沖區大小調節滑動窗口來限制客戶端的發送,而擁塞控制相當于慢慢試探網絡帶寬有多大,擁塞狀況如何來調整發送端的發送速率,這里引入了擁塞窗口,實際的發送窗口等于滑動窗口和擁塞窗口中最小的一個。
擁塞窗口
擁塞窗口是由發送端決定的,試探的意思就是慢慢的增大擁塞窗口,如果觸發了超時重傳,就認為是網絡擁塞,較小擁塞窗口
慢啟動
建立連接后,每收到一個ACK就將擁塞窗口加1(單位為1個MSS):
- 收到第一個ACK時,擁塞窗口為1+1 = 2.
- 發送兩個報文,收到兩個ACK,擁塞窗口為2+2=4
- 4+4 = 8,指數型增長
增長到ssthresh(slow start threshold)65535 bytes這個值后,啟用擁塞避免
擁塞避免
- 上面擁塞窗口增長到8,收到8個ACK后,每個增長1/8
- 下次發送9個,收到9個ACK后,每個增長1/9
- 下次發送10個,線性增長
增長到觸發重傳機制后,表示網絡發生擁塞,啟動快速重傳
快速重傳
比如當前窗口是12,將之前的擁塞窗口減半為6,再將減半的值設置給ssthresh=6,再進入快速重傳:
- 如果收到了3個重復的ACK(類似快速重傳算法),擁塞窗口+3(為了盡快將丟失的包重傳)
- 重傳丟失的包
- 再收到重復的ACK后,把擁塞窗口+1
- 直到收到了新的ACK,再將ssthresh設置為進入快速重傳之前的值6
- 進入避免擁塞算法
UDP
與TCP不同,UDP的首部格式簡單,傳輸時也不需要事先建立連接,即不需要客戶端和服務端維護雙方交互的狀態;因此TCP可以以數據流的形式發送,而進程產生一個UDP數據報,組裝成一份待發送的IP數據報,只能發送一個數據報或者接收一個數據報,加上UDP并無控制可言,所以很多行為與IP層類似。
首部格式
各字段含義
- Source Port:發送端端口號,如果不需要回復消息,該字段設置為0
- Destination Port:接收端端口號
- Length:UDP首部長度+Data長度(因為IP數據報的最大長度為65535,UDP頭+Data實際長度不大于 65535 - IP頭20 = 65515)
- Checksum:與TCP相同,校驗和覆蓋UDP的首部和Data部分,校驗數據包的正確性。與TCP不同的是,UDP可以設置為0表示不校驗(包括IP首部的地址和UDP首部都不校驗)。與IP層相同,如果發送端沒有計算校驗和而接收端發現校驗和有差錯,那么數據包會被直接丟棄而不產生差錯報文
場景
- UDP適用于對丟包不敏感并要求時延極低的應用,不考慮網絡擁塞,一股腦往出發。
- 因為UDP并不需要維護端對端連接,可以應用于廣播或者多播協議。例如基于UDP協議的TFTP、DHCP、VXLAN等
- 因為自身的簡單,只承載傳輸的任務。所以應用層可以更靈活的按照自己的需求來做定制開發,把需要維護的狀態放在應用層來做。
傳輸層的端口號
傳輸層的端口號用來分辨交給哪個應用程序處理
端口的使用
- 對于UDP和TCP在內核中是獨立存在的,所以可以綁定相同的地址和端口
- 如果兩個TCP程序要綁定相同的端口,那么需要綁定不同的IP地址
- 對于TCP,如果上文重啟server.py后會提示
Address already in use,開啟多路復用(socket設置SO_REUSEPORT)允許第一個socket處于TIME_WAIT的情況下,第二個socket可以使用該地址和端口
端口范圍
知名端口號(0~1023)
相當于一種約定,例如HTTP服務用80端口,HTTPs用443端口,FTP用21端口,SSH用22端口
登記端口號(1024~49151)
我們自己實現的服務器,從該范圍內申請端口長時間使用
客戶端端口號(49152~65535)
操縱系統動態分派,客戶端通信臨時使用的,用完之后連接關閉,操作系統回收端口號,分配給其他的進程使用
學習自:
《趣談網絡協議》劉超
《圖解TCP/IP》
《圖解HTTP》
《網絡是怎樣連接的》
《TCP/IP詳解 卷一》
小林coding
https://www.xiaolincoding.com/network/3_tcp/tcp_down_and_crash.html
https://xiaolincoding.com/network/3_tcp/tcp_feature.html
https://blog.csdn.net/GV7lZB0y87u7C/article/details/121186808
總結
- 上一篇: 整合 Disney + 及 Hulu 两
- 下一篇: 明海法师:寺院生活仪轨的意义