TCP常見知識點整理

來自專欄 Java後端&大數據9 人贊了文章

首發於簡書:TCP常見知識點整理(長文)

作者簡書ID:小北覓

從簡書導出的markdown放在知乎上版式有點亂,如果介意請移步簡書

參考資料: blog.csdn.net/qzcsu/art cnblogs.com/duan2/p/918 《計算機網路 第六版》謝希仁編著

難免有遺漏,希望大佬們在評論中指出還有那些需要添加的點。感謝!感謝!感謝!

本文內容如下:

-TCP頭部結構和欄位介紹

- TCP三次握手過程

- TCP三次握手問題補充

- TCP四次揮手過程

- TCP四次揮手問題補充

- TCP流量控制 滑動窗口

- TCP擁塞控制 慢啟動、擁塞避免、快重傳、快恢復

- TCP超時重傳

- TCP的四種定時器

- TCP粘包/拆包問題

一、TCP首部結構詳解

圖片來自《TCP/IP詳解卷1》

TCP數據封裝在一個IP數據報中:

下圖是TCP報文數據格式。TCP首部如果不計選項和填充欄位,它通常是20個位元組。

下面分別對其中的欄位進行介紹: - 源埠和目的埠:各佔2個位元組,這兩個值加上IP首部中的源端IP地址和目的端IP地址唯一確定一個TCP連接。有時一個IP地址和一個埠號也稱為socket(插口)。

  • 序號:佔4個位元組,是本報文段所發送的數據項目組第一個位元組的序號。在TCP傳送的數據流中,每一個位元組都有一個序號。例如,一報文段的序號為300,而且數據共100位元組,則下一個報文段的序號就是400;序號是32bit的無符號數,序號到達2^32-1後從0開始。(註:如何防止從0開始後序號相同的問題)
  • 確認序號:佔4位元組,是期望收到對方下次發送的數據的第一個位元組的序號,也就是期望收到的下一個報文段的首部中的序號;確認序號應該是上次已成功收到數據位元組序號+1。只有ACK標誌為1時,確認序號纔有效。
  • 數據偏移:佔4比特,表示數據開始的地方離TCP段的起始處有多遠。實際上就是TCP段首部的長度。由於首部長度不固定,因此數據偏移欄位是必要的。數據偏移以32位為長度單位,也就是4個位元組,因此TCP首部的最大長度是60個位元組。即偏移最大為15個長度單位=1532位=154位元組。

  • 保留: 6比特,供以後應用,現在置為0。
  • 6個標誌位比特: ①URG:當URG=1時,註解此報文應儘快傳送,而不要按本來的列隊次序來傳送。與「緊急指針」欄位共同應用,緊急指針指出在本報文段中的緊急數據的最後一個位元組的序號,使接管方可以知道緊急數據共有多長; ②ACK:只有當ACK=1時,確認序號欄位纔有效; ③PSH:當PSH=1時,接收方應該儘快將本報文段立即傳送給其應用層。 ④RST:當RST=1時,表示出現連接錯誤,必須釋放連接,然後再重建傳輸連接。複位比特還用來拒絕一個不法的報文段或拒絕打開一個連接; ⑤SYN:SYN=1,ACK=0時表示請求建立一個連接,攜帶SYN標誌的TCP報文段為同步報文段; ⑥FIN:發端完成發送任務。
  • 窗口:TCP通過滑動窗口的概念來進行流量控制。設想在發送端發送數據的速度很快而接收端接收速度卻很慢的情況下,為了保證數據不丟失,顯然需要進行流量控制, 協調好通信雙方的工作節奏。所謂滑動窗口,可以理解成接收端所能提供的緩衝區大小。TCP利用一個滑動的窗口來告訴發送端對它所發送的數據能提供多大的緩 沖區。窗口大小為位元組數,起始於確認序號欄位指明的值(這個值是接收端正期望接收的位元組)。窗口大小是一個16bit欄位,因而窗口大小最大為65535位元組。
  • 檢驗和:檢驗和覆蓋了整個TCP報文段:TCP首部和數據。這是一個強制性的欄位,一定是由發端計算和存儲,並由收端進行驗證。
  • 緊急指針:只有當URG標誌置1時緊急指針纔有效。緊急指針是一個正的偏移量,和序號欄位中的值相加表示緊急數據最後一個位元組的序號。

二、TCP三次握手過程

百度圖片,侵刪。

①客戶端向伺服器發出連接請求報文,這時報文首部中的同部位SYN=1,同時選擇一個初始序列號 seq=J ,此時,TCP客戶端進程進入了 SYN-SENT(同步已發送狀態)狀態。TCP規定,SYN報文段(SYN=1的報文段)不能攜帶數據,但需要消耗掉一個序號。 ②TCP伺服器收到請求報文後,如果同意連接,則發出確認報文。確認報文中應該 ACK=1,SYN=1,確認號是ack=J+1,同時也要為自己初始化一個序列號 seq=K,此時,TCP伺服器進程進入了SYN-RCVD(同步收到)狀態。這個報文也不能攜帶數據,但是同樣要消耗一個序號。 ③TCP客戶進程收到確認後,還要向伺服器給出確認。確認報文的ACK=1,ack=K+1,[自己的序列號seq=J+1],此時,TCP連接建立,客戶端進入ESTABLISHED(已建立連接)狀態。TCP規定,ACK報文段可以攜帶數據,但是如果不攜帶數據則不消耗序號。 ④當伺服器收到客戶端的確認後也進入ESTABLISHED狀態,此後雙方就可以開始通信了。

三、TCP三次握手問題補充

2.1為什麼需要三次握手

首先我們要知道信道是不可靠的,但是我們要建立可靠的連接發送可靠的數據,也就是數據傳輸是需要可靠的。在這個時候三次握手是一個理論上的最小值,並不是說是tcp協議要求的,而是為了滿足在不可靠的信道上傳輸可靠的數據所要求的。 在《計算機網路》一書中其中有提到,三次握手的目的是「為了防止已經失效的連接請求報文段突然又傳到服務端,因而產生錯誤」,這種情況是:一端(client)A發出去的第一個連接請求報文並沒有丟失,而是因為某些未知的原因在某個網路節點上發生滯留,導致延遲到連接釋放以後的某個時間纔到達另一端(server)B。本來這是一個早已失效的報文段,但是B收到此失效的報文之後,會誤認為是A再次發出的一個新的連接請求,於是B端就向A又發出確認報文,表示同意建立連接。如果不採用「三次握手」,那麼只要B端發出確認報文就會認為新的連接已經建立了,但是A端並沒有發出建立連接的請求,因此不會去向B端發送數據,B端沒有收到數據就會一直等待,這樣B端就會白白浪費掉很多資源。如果採用「三次握手」的話就不會出現這種情況,B端收到一個過時失效的報文段之後,向A端發出確認,此時A並沒有要求建立連接,所以就不會向B端發送確認,這個時候B端也能夠知道連接沒有建立。

四、TCP四次揮手過程

①客戶端進程發出連接釋放報文,並且停止發送數據。釋放數據報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的數據的最後一個位元組的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即使不攜帶數據,也要消耗一個序號。

②伺服器收到連接釋放報文,發出確認報文,ACK=1,ack=u+1,並且帶上自己的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP伺服器通知高層的應用進程,客戶端向伺服器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,但是伺服器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。

③客戶端收到伺服器的確認請求後,此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待伺服器發送連接釋放報文(在這之前還需要接受伺服器發送的最後的數據)。

④伺服器將最後的數據發送完畢後,就向客戶端發送連接釋放報文,FIN=1,ack=u+1,由於在半關閉狀態,伺服器很可能又發送了一些數據,假定此時的序列號為seq=w,此時,伺服器就進入了LAST-ACK(最後確認)狀態,等待客戶端的確認。

⑤客戶端收到伺服器的連接釋放報文後,必須發出確認,ACK=1,ack=w+1,而自己的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連接還沒有釋放,必須經過2??MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。

⑥伺服器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB後,就結束了這次的TCP連接。可以看到,伺服器結束TCP連接的時間要比客戶端早一些。

五、TCP四次揮手問題補充

4.1 為什麼需要四次握手?

為了確保數據能夠完成傳輸。 關閉連接時,當收到對方的FIN報文通知時,它僅僅表示對方沒有數據發送給你了;但未必你所有的數據都全部發送給對方了,所以你可以未必會馬上會關閉SOCKET,也即你可能還需要發送一些數據給對方之後,再發送FIN報文給對方來表示你同意現在可以關閉連接了,所以它這裡的ACK報文和FIN報文多數情況下都是分開發送的。

4.2 釋放連接時為什麼TIME-WAIT狀態必須等待2MSL時間?

首先說下什麼是MSL: MSL是Maximum Segment Lifetime英文的縮寫,中文可以譯為「報文最大生存時間」,他是任何報文在網路上存在的最長時間,超過這個時間報文將被丟棄。

第一,為了保證A發送的最後一個ACK報文能夠到達B。這個ACK報文段有可能丟失,因而使處在LAST-ACK狀態的B收不到對已發送的FIN+ACK報文段的確認。B會超時重傳這個FIN+ACK報文段,而A就能在2MSL時間內收到這個重傳的FIN+ACK報文段,重置時間等待計時器(2MSL)。如果A在TIME-WAIT狀態不等待一段時間,而是在發送完ACK報文段後就立即釋放連接,就無法收到B重傳的FIN+ACK報文段,因而也不會再發送一次確認報文段。這樣,B就無法按照正常的步驟進入CLOSED狀態。

第二,A在發送完ACK報文段後,再經過2MSL時間,就可以使本連接持續的時間所產生的所有報文段都從網路中消失。這樣就可以使下一個新的連接中不會出現這種舊的連接請求的報文段。

4.3 客戶端突然掛掉了怎麼辦?

正常連接時,客戶端突然掛掉了,如果沒有措施處理這種情況,那麼就會出現客戶端和伺服器端出現長時期的空閑。解決辦法是在伺服器端設置保活計時器,每當伺服器收到客戶端的消息,就將計時器複位。超時時間通常設置為2小時。若伺服器超過2小時沒收到客戶的信息,他就發送探測報文段。若發送了10個探測報文段,每一個相隔75秒,還沒有響應就認為客戶端出了故障,因而終止該連接。

TCP超時重傳

原理是在發送某一個數據以後就開啟一個計時器,在一定時間內如果沒有得到發送的數據報的ACK報文,那麼就重新發送數據,直到發送成功為止。

影響超時重傳機制協議效率的一個關鍵參數是重傳超時時間(RTO,Retransmission TimeOut)。RTO的值被設置過大過小都會對協議造成不利影響。   (1)RTO設長了,重發就慢,沒有效率,性能差。   (2)RTO設短了,重發的就快,會增加網路擁塞,導致更多的超時,更多的超時導致更多的重發。

連接往返時間(RTT,Round Trip Time),指發送端從發送TCP包開始到接收它的立即響應所消耗的時間。

RTO理論上最好是網路 RTT 時間,但又受制於網路距離與瞬態時延變化,所以實際上使用自適應的動態演算法(例如 Jacobson 演算法和 Karn 演算法等)來確定超時時間。

六、TCP流量控制

TCP流量控制主要是針對接收端的處理速度不如發送端發送速度快的問題,消除發送方使接收方緩存溢出的可能性。

TCP流量控制主要使用滑動窗口協議,滑動窗口是接受數據端使用的窗口大小,用來告訴發送端接收端的緩存大小,以此可以控制發送端發送數據的大小,從而達到流量控制的目的。

接收端通過TCP首部的窗口大小欄位反饋當前可接收的位元組數。

  • 發送方接收到了對方發來的報文 ack = 33, win = 10,知道對方收到了 33 號前的數據,現在期望接收 [33, 43) 號數據。發送方連續發送了 4 個報文段假設為 A, B, C, D, 分別攜帶 [33, 35), [35, 36), [36, 38), [38, 41) 號數據。
  • 接收方接收到了報文段 A, C,但是沒收到 B 和 D,也就是隻收到了 [33, 35) 和 [36, 38) 號數據。接收方發送回對報文段 A 的確認:ack = 35, win = 10。
  • 發送方收到了 ack = 35, win = 10,對方期望接收 [35, 45) 號數據。接著發送了一個報文段 E,它攜帶了 [41, 44) 號數據。
  • 接收方接收到了報文段 B: [35, 36), D:[38, 41),接收方發送對 D 的確認:ack = 41, win = 10. (這是一個累積確認)
  • 發送方收到了 ack = 41, win = 10,對方期望接收 [41, 51) 號數據。
  • …… 需要注意的是,接收方接收 tcp 報文的順序是不確定的,並非是一定先收到 35 再收到 36,也可能是先收到 36,37,再收到 35.

對於發送端的數據緩衝區有這些量:LastByteSent是目前發送的最後1位元組的數據編號;LastByteAckd是目前接收到確認的最後1位元組的數據編號;Rcvwin是窗口大小。鑒於每次發送方都是收到ACK之後滑動窗口繼續發送,發送到LastByteSent這個位置,LastByteSent-LastByteAckd也就是這次發送數據的多少,那麼只要滿足:LastByteSent–LastByteAckd<=RcvWin(接收端空閑窗口大小) 就會保證不會溢出了。

那麼接收端RcvWin怎麼算呢?假設接收端緩衝區大小為RcvBuffer。LastByteRead:上層應用程序接收的最後一個位元組序號,LastByteRcvd:接收端從網路接收的最後一個位元組序號,那麼LastByteRcvd–LastByteRead就是已經接受但是還沒有傳遞給上層的數據。所以空閑區域RcvWin= RcvBuffer-(LastByteRcvd–LastByteRead).

TCP擁塞控制

TCP發送方可能因為IP網路的擁塞而被遏制,TCP擁塞控制就是為瞭解決這個問題(注意和TCP流量控制的區別)。 TCP擁塞控制的幾種方法:慢啟動,擁塞避免,快重傳和快恢復。

這裡引入了一個擁塞窗口的概念; 擁塞窗口:發送方維持一個叫做擁塞窗口 cwnd的狀態變數。擁塞窗口的大小取決於網路的擁塞程度,並且動態變化。發送方的讓自己的發送窗口=min(cwnd,接受端接收窗口大小)。

發送方控制擁塞窗口的原則是:只要網路沒有出現擁塞,擁塞窗口就增大一些,以便把更多的分組發送出去。但只要網路出現擁塞,擁塞窗口就減小一些,以減少注入到網路中的分組數。

下面將討論擁塞窗口cwnd的大小是怎麼變化的。

慢啟動

當主機開始發送數據時,如果立即所大量數據位元組注入到網路,那麼就有可能引起網路擁塞,因為現在並不清楚網路的負荷情況。因此,較好的方法是 先探測一下,即由小到大逐漸增大發送窗口,也就是說,由小到大逐漸增大擁塞窗口數值。通常在剛剛開始發送報文段時,先把擁塞窗口 cwnd 設置為一個最大報文段MSS的數值。而在每收到一個對新的報文段的確認後,把擁塞窗口增加至多一個MSS的數值。用這樣的方法逐步增大發送方的擁塞窗口 cwnd ,可以使分組注入到網路的速率更加合理。

每經過一個傳輸輪次,擁塞窗口 cwnd 就加倍。一個傳輸輪次所經歷的時間其實就是往返時間RTT。不過「傳輸輪次」更加強調:把擁塞窗口cwnd所允許發送的報文段都連續發送出去,並收到了對已發送的最後一個位元組的確認。

另外,慢開始的「慢」並不是指cwnd的增長速率慢,而是指在TCP開始發送報文段時先設置cwnd=1,使得發送方在開始時只發送一個報文段(目的是試探一下網路的擁塞情況),然後再逐漸增大cwnd。

為了防止擁塞窗口cwnd增長過大引起網路擁塞,還需要設置一個慢開始門限ssthresh狀態變數。慢開始門限ssthresh的用法如下: 當 cwnd < ssthresh 時,使用上述的慢開始演算法。 當 cwnd > ssthresh 時,停止使用慢開始演算法而改用擁塞避免演算法。 當 cwnd = ssthresh 時,既可使用慢開始演算法,也可使用擁塞控制避免演算法。

擁塞避免

讓擁塞窗口cwnd緩慢地增大,即每經過一個往返時間RTT就把發送方的擁塞窗口cwnd加1,而不是加倍。這樣擁塞窗口cwnd按線性規律緩慢增長,比慢開始演算法的擁塞窗口增長速率緩慢得多。 無論在慢開始階段還是在擁塞避免階段,只要發送方判斷網路出現擁塞(其根據就是沒有收到確認),就要把慢開始門限ssthresh設置為出現擁塞時的發送 方窗口值的一半(但不能小於2)。然後把擁塞窗口cwnd重新設置為1,執行慢開始演算法。這樣做的目的就是要迅速減少主機發送到網路中的分組數,使得發生 擁塞的路由器有足夠時間把隊列中積壓的分組處理完畢。

如下圖,用具體數值說明瞭上述擁塞控制的過程。現在發送窗口的大小和擁塞窗口一樣大。

<1>當TCP連接進行初始化時,把擁塞窗口cwnd置為1。前面已說過,為了便於理解,圖中的窗口單位不使用位元組而使用報文段的個數。慢開始門限的初始值設置為16個報文段,即 cwnd = 16 。

<2>在執行慢開始演算法時,擁塞窗口 cwnd 的初始值為1。以後發送方每收到一個對新報文段的確認ACK,就把擁塞窗口值加1,然後開始下一輪的傳輸(圖中橫坐標為傳輸輪次)。因此擁塞窗口cwnd 隨著傳輸輪次按指數規律增長。當擁塞窗口cwnd增長到慢開始門限值ssthresh時(即當cwnd=16時),就改為執行擁塞控制演算法,擁塞窗口按線 性規律增長。

<3>假定擁塞窗口的數值增長到24時,網路出現超時(這很可能就是網路發生擁塞了)。更新後的ssthresh值變為12(即變為出現超時時的擁塞窗口數值 24的一半),擁塞窗口再重新設置為1,並執行慢開始演算法。當cwnd=ssthresh=12時改為執行擁塞避免演算法,擁塞窗口按線性規律增長,每經過 一個往返時間增加一個MSS的大小。

強調:「擁塞避免」並非指完全能夠避免了擁塞。利用以上的措施要完全避免網路擁塞還是不可能的。「擁塞避免」是說在擁塞避免階段將擁塞窗口控制為按線性規律增長,使網路比較不容易出現擁塞。

快重傳

在超時重傳中,重點是定時器溢出超時了才認為發送的數據包丟失,快速重傳機制,實現了另外的一種丟包評定標準,即如果我連續收到3次重複ACK,發送方就認為這個seq的包丟失了,立刻進行重傳,這樣如果接收端回復及時的話,基本就是在重傳定時器到期之前,提高了重傳的效率。

例如:M1,M2,M3 -----> M1,M3,缺失M2,則向發送方發送M2重複確認,當發送方收到M2的三次重複確認,則認為M2報文丟失,啟動快重傳機制,重傳數據,其他數據發送數據放入隊列,待快重傳結束後再正常傳輸。

快恢復

與快重傳配合使用的還有快恢復演算法,其主要有以下兩個要點: 1)當發送方連續收到接收方發來的三個重複確認時,就執行「乘法減小」演算法,把慢開始門限減半(這個減半指的是變成發生阻塞時的阻塞窗口大小的一半),這是為了預防網路發生擁塞。(注意:接下來不執行慢開始演算法)

2)由於發送方現在認為網路很可能沒有發生擁塞,因此現在不執行慢開始演算法,而是把cwnd(擁塞窗口)值設置為慢開始門限減半後的值,然後開始執行擁塞避免演算法,使擁塞窗口緩慢的線性增大。

TCP的四種定時器

TCP中有四種計時器(Timer),分別為: 1.重傳計時器:Retransmission Timer 2.堅持計時器:Persistent Timer 3.保活計時器:Keeplive Timer 4.時間等待計時器:Timer_Wait Timer

(1)重傳計時器  大家都知道TCP是保證數據可靠傳輸的。怎麼保證呢?帶確認的重傳機制。在滑動窗口協議中,接受窗口會在連續收到的包序列中的最後一個包向接收端發送一個ACK,當網路擁堵的時候,發送端的數據包和接收端的ACK包都有可能丟失。TCP為了保證數據可靠傳輸,就規定在重傳的「時間片」到了以後,如果還沒有收到對方的ACK,就重發此包,以避免陷入無限等待中。   當TCP發送報文段時,就創建該特定報文的重傳計時器。可能發生兩種情況:   1.若在計時器截止時間到之前收到了對此特定報文段的確認,則撤銷此計時器。   2.若在收到了對此特定報文段的確認之前計時器截止時間到,則重傳此報文段,並將計時器複位。

(2)堅持計時器 專門對付零窗口通知而設立的,   先來考慮一下情景:發送端向接收端發送數據包知道接受窗口填滿了,然後接受窗口告訴發送方接受窗口填滿了停止發送數據。此時的狀態稱為「零窗口」狀態,發送端和接收端窗口大小均為0.直到接受TCP發送確認並宣佈一個非零的窗口大小。但這個確認會丟失。我們知道TCP中,對確認是不需要發送確認的。若確認丟失了,接受TCP並不知道,而是會認為他已經完成了任務,並等待著發送TCP接著會發送更多的報文段。但發送TCP由於沒有收到確認,就等待對方發送確認來通知窗口大小。雙方的TCP都在永遠的等待著對方。 要打開這種死鎖,TCP為每一個鏈接使用一個持久計時器。當發送TCP收到窗口大小為0的確認時,就堅持啟動計時器。當堅持計時器期限到時,發送TCP就發送一個特殊的報文段,叫做探測報文。這個報文段只有一個位元組的數據。他有一個序號,但他的序號永遠不需要確認;甚至在計算機對其他部分的數據的確認時該序號也被忽略。探測報文段提醒接受TCP:確認已丟失,必須重傳。 堅持計時器的值設置為重傳時間的數值。但是,若沒有收到從接收端來的響應,則需發送另一個探測報文段,並將堅持計時器的值加倍和複位。發送端繼續發送探測報文段,將堅持計時器設定的值加倍和複位,直到這個值增大到門限值(通常是60秒)為止。在這以後,發送端每個60秒就發送一個探測報文,直到窗口重新打開。 (3)保活計時器 保活計時器使用在某些實現中,用來防止在兩個TCP之間的連接出現長時間的空閑。假定客戶打開了到伺服器的連接,傳送了一些數據,然後就保持靜默了。也許這個客戶出故障了。在這種情況下,這個連接將永遠的處理打開狀態。 要解決這種問題,在大多數的實現中都是使伺服器設置保活計時器。每當伺服器收到客戶的信息,就將計時器複位。通常設置為兩小時。若伺服器過了兩小時還沒有收到客戶的信息,他就發送探測報文段。若發送了10個探測報文段(每一個像個75秒)還沒有響應,就假定客戶除了故障,因而就終止了該連接。 這種連接的斷開當然不會使用四次握手,而是直接硬性的中斷和客戶端的TCP連接。 (4)時間等待計時器   時間等待計時器是在四次握手的時候使用的。四次握手的簡單過程是這樣的:假設客戶端準備中斷連接,首先向伺服器端發送一個FIN的請求關閉包(FIN=final),然後由established過渡到FIN-WAIT1狀態。伺服器收到FIN包以後會發送一個ACK,然後自己有established進入CLOSE-WAIT.此時通信進入半雙工狀態,即留給伺服器一個機會將剩餘數據傳遞給客戶端,傳遞完後伺服器發送一個FIN+ACK的包,表示我已經發送完數據可以斷開連接了,就這便進入LAST_ACK階段。客戶端收到以後,發送一個ACK表示收到並同意請求,接著由FIN-WAIT2進入TIME-WAIT階段。伺服器收到ACK,結束連接。此時(即客戶端發送完ACK包之後),客戶端還要等待2MSL(MSL=maxinum segment lifetime最長報文生存時間,2MSL就是兩倍的MSL)才能真正的關閉連接。

TCP 粘包拆包問題

TCP粘包,拆包及解決方法 - This is bill的專屬博客 - CSDN博客

推薦閱讀:

相關文章