TCP 協議--詳解--來源於兩篇博客

TCP概述

前一篇講述了IP,它是一個不可靠的,無連接的,無序的,無流控的,只顧尋找最佳路由進行轉發,提供最好的傳輸,既然IP不管這些,是因爲這些都由TCP來完成,IP層只需要傳送,不管到達,複雜度在於路由選擇,TCP接管了有序,有連接的,可靠的服務,複雜度也就在於如何有序,如何控制流量,使得傳輸可靠,兩個協議側重面不同,但卻又相輔相承,TCP保證正確的傳輸,IP保證最佳路徑傳輸,IP不保證到達,TCP保證到達。想要知道TCPIP不一樣的地方就需要了解它的頭部和IP有什麼區別。

TCP頭部

Source port 源端口和Destination port 目的端口16位,告知主機該報文段是來自哪個源端口以及傳給哪個上層協議(應用層)目的端口的,進行TCP通信,客戶端通常使用系統自動選擇的臨時端口號,而服務器則使用一些知名服務的端口號。

應用程序的端口號和應用程序所在主機的IP地址統稱爲socket(套接字),IP:XX, 在互聯網上socket唯一標識每一個應用程序,源端口+源IP+目的端口+目的IP稱爲套接字對,一對套接字就是一個連接,一個客戶端與服務器之間的連接。

Sequence Number 序列號:32位,一次TCP通信(建立到斷開)過程中某一個傳輸方向上的字節流的每個字節的編號,它保證了TCP通信的有序性,解決網絡包亂序的問題,由於有了這個編號,接收端可以根據這個序號進行確認,可以保證每個分段在原始數據包中的位置,初始序列號由自己定,而後緒的序列號由對端的ACK決定:SN_x = ACK_y (x的序列號=y發給xACK)

Acknowledgement Number確認號:32位,用來另一方發送來的TCP報文段的確認響應,其值是收到的TCP報文段的序號+1,也是對端下一次發報文段過來的序號,期望對端以這個序號開始發送自己的分組,這樣可以保證發送過來的報文是有序的,否則發送端不能確認之前的報文是否有被收到,而要決定是否重傳, ACK_y = SN_x+TCP_len+Flag(ACK的值是由對方的tcp_len+對方的seq值以及flag的值來決定,syn,fin都消耗一個序號,而ack無需任何代價,因爲確認序號與ACK標誌是在一起的,屬於TCP頭部一部分,不消耗序號)

Head Length 報頭長度:4位,標識該TCP頭部有多少個32bit字節,這個跟IP報文中的HL一樣,都是計算頭部長度,最小IPTCP頭部都是20字節,值爲5,即4*5=20TCP最大頭部爲60字節, 16*4,一般情況下TCP頭部爲32字節,20字節頭+12字節的options,這個字段有時又叫offset,數據偏移,它確定了TCP數據在一個分組中從何開始

Flag標誌位:

·       URG標誌: urgent pointer是否有效,1表示該分段包含緊急數據

·         ACK  標誌:表示確認號是否有效,攜帶ACK標誌的TCP報文段爲確認報文段

·         PSH標誌:發送方使用該標誌通知接收方將所收到的數據全部提交給接收進程,這裏的數據包括PUSH過去的,以及接收方已經接收的那些沒有PUSH標誌的其它數據,目的是讓接收端立即提交進程處理而不要判斷是否還會有額外的數據到達。提示接收應用程序立即從TCP接收緩衝器中讀走數據,通常對時效性比較高的服務,如telnet。也常見TCP分片中,一個報文段發不完,此時需要清空緩存,以備後面的大數據到來

·         RST 標誌:表示要求對方重新建立連接,通常發生在對端端口沒打開,對端會發給一個帶R標誌的復位報文段,TCP提供了一個異常終止的方法,就是發TCP報文段,還有一種情況就是連接屬於半打開狀態,此時寫數據,會收到RST,因爲連接並不真實存在

·         SYN 標誌:請求建立一個連接,主要是協商ISN,初始序列號,完成三次握手

·         FIN 標誌:表示通知對方,本端數據已傳完,要關閉連接,完成四次握手,成功關閉,與RST不同的是,這是正常的關閉,而不是異常

Window Size窗口大小:16位,它是TCP流量控制的一個手段,這裏說的窗口是接收通告窗口,即告訴對方本端TCP接收緩衝區還能容納多少字節的數據,這樣對方就需要控制發送數據的速度

Checksum 校驗和:TCP的校驗和由發送端填充,由接收端執行CRC算法是否得到全1,以檢驗TCP報文段在傳輸過程中是否損壞,它與IP的校驗和不同,IP是頭校驗,而TCP的校驗和不僅是TCP頭部,還有TCP的數據,以及IP源地址,目的地址,協議(0x06,TCPsegment Length)計算而來,每兩個字節爲一單位進行反碼求和,其中協議爲低字節,TCP報文長度也爲單低字節進行反碼求和,高位溢出加到低位

#/usr/bin/env python

def checksum(*aList):
    sum = 0x0
    for i in range(len(aList[0])):                         #aList[0]=alist
        if i%2 == 0:
            aList[0][i]=aList[0][i]<<8                     #奇數位左移8位,成爲高8位字節
    for i in aList[0]:
        sum = sum+i                                        #求和
        sum=(sum>>16)+(sum&0xffff)                         #循環相加,溢出就加到低位
        result = 2**16-1-sum                               #算反碼=2**模-原碼
        result = hex(result)                               #十六進制轉換
    return result

 

alist = [0xc0,0xa8,0x02,0x65,                              #192.168.2.101 源IP

         0xc0,0xa8,0x02,0x6f,                              #192.168.2.111 目的IP

         0x11,0xbf,0x1f,0x40,0x12,                         #從0x11開始到0x02都是TCP

         0xad,0xd2,0xf9,0x00,0x00,0x00,0x00,

         0x80,0x02,0xff,0xff,0x00,0x00,0x00,               #0xff後面的0x00,0x00是因爲發端算校驗和先填校驗碼爲0,隨後算出來的值再到收端進行計算可得ffff

         0x00,0x02,0x04,0x05,0xac,0x01,0x03,

         0x03,0x02,0x01,0x01,0x04,0x02,                    

         0x00,0x20,0x00,0x06]                              #0x00,0x20 TCP報文長度(頭和數據),由於現在沒有數據,因此只有頭+選項共32字節,0x20,作爲低字節,因此前面加0x00
                                                           #0x00,0x06 TCP協議號,作爲低字節,前面補0x00
print checksum(alist)

 ---------------------------------------------------------------------------

0xd253

Urgent Point 緊急指針:16位,是一個正的偏移量,它和序號字段的值相加表示最後一個緊急數據的下一字節的序號,因此確切的說是一個緊急偏移,TCP是字節流不存在優先級,但是可以設置緊急位表示該報文有緊急數據,通過緊急指針知道這個報文中緊急數據的最後一個字節,但是並不知道這個報文中緊急數據的具體位置,因爲初始位置不能確定,有可能前面有普通數據。

options選項:TCP的選項是可變的,但是最多隻包含40個字節,因爲TCP頭部最大60字節,固定部分20字節,選項的結構如下,所有的選項都遵循下面的格式

kind(1字節) length(1字節)                         info(n字節)           

選項的第一個字段爲Kind類型,有的選項沒有lengthinfo, 有的都有,但總共分爲7種選項

Kind=0是選項表結束選項

Kind=1是空操作NOP,一般用於將TCP的頭部填充爲4字節的整數倍

Kind=2是最大報文段長度,TCP連接初始化時會協商MSS(max segment size),TCP模塊通常設置爲1460,因爲以太網最大數據報爲1500IP20 TCP20,最大段長就爲1460,這也就避免了IP分片,也就是我們說的在TCP應用中一般看不到IP分片的原因

Kind=3窗口擴大因子,雖然窗口大小在連接時可以協商,但窗口遠不止65535,當窗口大小爲N,當移位數爲M時,窗口大小在實際傳輸中是N*2**M

Kind=4是選擇性確認(Selective Acknowledgement, SACKTCP重傳機制指的是如果某個TCP報文丟失,TCP會重傳最後確認的TCP報文段的後緒報文段,但之前正確傳輸的報文段也要重傳,這就降低了性能,SACK可以使TCP只重傳丟失的報文段,而不用把所有未確認的報文都重傳,連接時會選擇是否啓用SACK技術

Kind=5SACK實際工作的選項,該選項的參數告訴發送方已經收到並緩存的不連續的數據塊,從而讓發送端可以據此檢查並重發丟失的數據塊,每個塊邊沿包含4個字節,每個塊的左邊沿表示不連續塊的的第一個數據號,右邊沿不連續塊的最後一個數據的序號的下一個序號,這樣一對參數之間的數據就是沒有收到的塊,一個塊信息佔用8字節,TCP頭部最多隻包含4個這樣的不連續塊,因爲4*8+2<40

Kind=8是時間戳選項,該項提供較爲準確的,通信雙方之間的迴路時間(RTT),爲流量控制提供重要信息,並且也爲SN的迴繞提供信息,在一個帶寬很高的環境中,充號最大爲65535,很短的時間,序號就會耗盡從而從0開始,有了timestamp後,我們就可以避免同樣的序號的包如何辨別先後的問題

其實,網絡上的傳輸是沒有連接的,包括TCP也是一樣的。而TCP所謂的“連接”,其實只不過是在通訊的雙方維護一個“連接狀態”,讓它看上去好像有連接一樣。所以,TCP的狀態變換是非常重要的。

下面是:“TCP協議的狀態機”(圖片來源) 和 “TCP建鏈接”、“TCP斷鏈接”、“傳數據” 的對照圖,我把兩個圖並排放在一起,這樣方便在你對照着看。另外,下面這兩個圖非常非常的重要,你一定要記牢。(吐個槽:看到這樣複雜的狀態機,就知道這個協議有多複雜,複雜的東西總是有很多坑爹的事情,所以TCP協議其實也挺坑爹的)

 

很多人會問,爲什麼建鏈接要3次握手,斷鏈接需要4次揮手?

  • 對於建鏈接的3次握手,主要是要初始化Sequence Number 的初始值。通信的雙方要互相通知對方自己的初始化的Sequence Number(縮寫爲ISN:Inital Sequence Number)——所以叫SYN,全稱Synchronize Sequence Numbers。也就上圖中的 x 和 y。這個號要作爲以後的數據通信的序號,以保證應用層接收到的數據不會因爲網絡上的傳輸的問題而亂序(TCP會用這個序號來拼接數據)。
  • 對於4次揮手,其實你仔細看是2次,因爲TCP是全雙工的,所以,發送方和接收方都需要Fin和Ack。只不過,有一方是被動的,所以看上去就成了所謂的4次揮手。如果兩邊同時斷連接,那就會就進入到CLOSING狀態,然後到達TIME_WAIT狀態。下圖是雙方同時斷連接的示意圖(你同樣可以對照着TCP狀態機看):


兩端同時斷連接(圖片來源

 

另外,有幾個事情需要注意一下:

  • 關於建連接時SYN超時。試想一下,如果server端接到了clien發的SYN後回了SYN-ACK後client掉線了,server端沒有收到client回來的ACK,那麼,這個連接處於一箇中間狀態,即沒成功,也沒失敗。於是,server端如果在一定時間內沒有收到的TCP會重發SYN-ACK。在Linux下,默認重試次數爲5次,重試的間隔時間從1s開始每次都翻售,5次的重試時間間隔爲1s, 2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP纔會把斷開這個連接。
  • 關於SYN Flood攻擊。一些惡意的人就爲此製造了SYN Flood攻擊——給服務器發了一個SYN後,就下線了,於是服務器需要默認等63s纔會斷開連接,這樣,攻擊者就可以把服務器的syn連接的隊列耗盡,讓正常的連接請求不能處理。於是,Linux下給了一個叫tcp_syncookies的參數來應對這個事——當SYN隊列滿了後,TCP會通過源地址端口、目標地址端口和時間戳打造出一個特別的Sequence Number發回去(又叫cookie),如果是攻擊者則不會有響應,如果是正常連接,則會把這個 SYN Cookie發回來,然後服務端可以通過cookie建連接(即使你不在SYN隊列中)。請注意,請先千萬別用tcp_syncookies來處理正常的大負載的連接的情況。因爲,synccookies是妥協版的TCP協議,並不嚴謹。對於正常的請求,你應該調整三個TCP參數可供你選擇,第一個是:tcp_synack_retries 可以用他來減少重試次數;第二個是:tcp_max_syn_backlog,可以增大SYN連接數;第三個是:tcp_abort_on_overflow 處理不過來乾脆就直接拒絕連接了。
  • 關於ISN的初始化。ISN是不能hard code的,不然會出問題的——比如:如果連接建好後始終用1來做ISN,如果client發了30個segment過去,但是網絡斷了,於是 client重連,又用了1做ISN,但是之前連接的那些包到了,於是就被當成了新連接的包,此時,client的Sequence Number 可能是3,而Server端認爲client端的這個號是30了。全亂了。RFC793中說,ISN會和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過2^32,又從0開始。這樣,一個ISN的週期大約是4.55個小時。因爲,我們假設我們的TCP Segment在網絡上的存活時間不會超過Maximum Segment Lifetime(縮寫爲MSL – Wikipedia語條),所以,只要MSL的值小於4.55小時,那麼,我們就不會重用到ISN。
  • 關於 MSL 和 TIME_WAIT。通過上面的ISN的描述,相信你也知道MSL是怎麼來的了。我們注意到,在TCP的狀態圖中,從TIME_WAIT狀態到CLOSED狀態,有一個超時設置,這個超時設置是 2*MSL(RFC793定義了MSL爲2分鐘,Linux設置成了30s)爲什麼要這有TIME_WAIT?爲什麼不直接給轉成CLOSED狀態呢?主要有兩個原因:1)TIME_WAIT確保有足夠的時間讓對端收到了ACK,如果被動關閉的那方沒有收到Ack,就會觸發被動端重發Fin,一來一去正好2個MSL,2)有足夠的時間讓這個連接不會跟後面的連接混在一起(你要知道,有些自做主張的路由器會緩存IP數據包,如果連接被重用了,那麼這些延遲收到的包就有可能會跟新連接混在一起)。你可以看看這篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems
  • 關於TIME_WAIT數量太多。從上面的描述我們可以知道,TIME_WAIT是個很重要的狀態,但是如果在大併發的短鏈接下,TIME_WAIT 就會太多,這也會消耗很多系統資源。只要搜一下,你就會發現,十有八九的處理方式都是教你設置兩個參數,一個叫tcp_tw_reuse,另一個叫tcp_tw_recycle的參數,這兩個參數默認值都是被關閉的,後者recyle比前者resue更爲激進,resue要溫柔一些。另外,如果使用tcp_tw_reuse,必需設置tcp_timestamps=1,否則無效。這裏,你一定要注意,打開這兩個參數會有比較大的坑——可能會讓TCP連接出一些詭異的問題(因爲如上述一樣,如果不等待超時重用連接的話,新的連接可能會建不上。正如官方文檔上說的一樣“It should not be changed without advice/request of technical experts”)。
    • 關於tcp_tw_reuse。官方文檔上說tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)可以保證協議的角度上的安全,但是你需要tcp_timestamps在兩邊都被打開(你可以讀一下tcp_twsk_unique的源碼 )。我個人估計還是有一些場景會有問題。
    • 關於tcp_tw_recycle。如果是tcp_tw_recycle被打開了話,會假設對端開啓了tcp_timestamps,然後會去比較時間戳,如果時間戳變大了,就可以重用。但是,如果對端是一個NAT網絡的話(如:一個公司只用一個IP出公網)或是對端的IP被另一臺重用了,這個事就複雜了。建鏈接的SYN可能就被直接丟掉了(你可能會看到connection time out的錯誤)(如果你想觀摩一下Linux的內核代碼,請參看源碼 tcp_timewait_state_process)。
    • 關於tcp_max_tw_buckets。這個是控制併發的TIME_WAIT的數量,默認值是180000,如果超限,那麼,系統會把多的給destory掉,然後在日誌裏打一個警告(如:time wait bucket table overflow),官網文檔說這個參數是用來對抗DDoS攻擊的。也說的默認值180000並不小。這個還是需要根據實際情況考慮。

Again,使用tcp_tw_reuse和tcp_tw_recycle來解決TIME_WAIT的問題是非常非常危險的,因爲這兩個參數違反了TCP協議(RFC 1122) 

其實,TIME_WAIT表示的是你主動斷連接,所以,這就是所謂的“不作死不會死”。試想,如果讓對端斷連接,那麼這個破問題就是對方的了,呵呵。另外,如果你的服務器是於HTTP服務器,那麼設置一個HTTP的KeepAlive有多重要(瀏覽器會重用一個TCP連接來處理多個HTTP請求),然後讓客戶端去斷鏈接(你要小心,瀏覽器可能會非常貪婪,他們不到萬不得已不會主動斷連接)。

數據傳輸中的Sequence Number

下圖是我從Wireshark中截了個我在訪問coolshell.cn時的有數據傳輸的圖給你看一下,SeqNum是怎麼變的。(使用Wireshark菜單中的Statistics ->Flow Graph… )

你可以看到,SeqNum的增加是和傳輸的字節數相關的。上圖中,三次握手後,來了兩個Len:1440的包,而第二個包的SeqNum就成了1441。然後第一個ACK回的是1441,表示第一個1440收到了。

注意:如果你用Wireshark抓包程序看3次握手,你會發現SeqNum總是爲0,不是這樣的,Wireshark爲了顯示更友好,使用了Relative SeqNum——相對序號,你只要在右鍵菜單中的protocol preference 中取消掉就可以看到“Absolute SeqNum”了

TCP重傳機制

TCP要保證所有的數據包都可以到達,所以,必需要有重傳機制。

注意,接收端給發送端的Ack確認只會確認最後一個連續的包,比如,發送端發了1,2,3,4,5一共五份數據,接收端收到了1,2,於是回ack 3,然後收到了4(注意此時3沒收到),此時的TCP會怎麼辦?我們要知道,因爲正如前面所說的,SeqNum和Ack是以字節數爲單位,所以ack的時候,不能跳着確認,只能確認最大的連續收到的包,不然,發送端就以爲之前的都收到了。

超時重傳機制

一種是不回ack,死等3,當發送方發現收不到3的ack超時後,會重傳3。一旦接收方收到3後,會ack 回 4——意味着3和4都收到了。

但是,這種方式會有比較嚴重的問題,那就是因爲要死等3,所以會導致4和5即便已經收到了,而發送方也完全不知道發生了什麼事,因爲沒有收到Ack,所以,發送方可能會悲觀地認爲也丟了,所以有可能也會導致4和5的重傳。

對此有兩種選擇:

  • 一種是僅重傳timeout的包。也就是第3份數據。
  • 另一種是重傳timeout後所有的數據,也就是第3,4,5這三份數據。

這兩種方式有好也有不好。第一種會節省帶寬,但是慢,第二種會快一點,但是會浪費帶寬,也可能會有無用功。但總體來說都不好。因爲都在等timeout,timeout可能會很長(在下篇會說TCP是怎麼動態地計算出timeout的)

快速重傳機制

於是,TCP引入了一種叫Fast Retransmit 的算法,不以時間驅動,而以數據驅動重傳。也就是說,如果,包沒有連續到達,就ack最後那個可能被丟了的包,如果發送方連續收到3次相同的ack,就重傳。Fast Retransmit的好處是不用等timeout了再重傳。

比如:如果發送方發出了1,2,3,4,5份數據,第一份先到送了,於是就ack回2,結果2因爲某些原因沒收到,3到達了,於是還是ack回2,後面的4和5都到了,但是還是ack回2,因爲2還是沒有收到,於是發送端收到了三個ack=2的確認,知道了2還沒有到,於是就馬上重轉2。然後,接收端收到了2,此時因爲3,4,5都收到了,於是ack回6。示意圖如下:

Fast Retransmit只解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是,是重傳之前的一個還是重傳所有的問題。對於上面的示例來說,是重傳#2呢還是重傳#2,#3,#4,#5呢?因爲發送端並不清楚這連續的3個ack(2)是誰傳回來的?也許發送端發了20份數據,是#6,#10,#20傳來的呢。這樣,發送端很有可能要重傳從2到20的這堆數據(這就是某些TCP的實際的實現)。可見,這是一把雙刃劍。

SACK 方法

另外一種更好的方式叫:Selective Acknowledgment (SACK)(參看RFC 2018),這種方式需要在TCP頭裏加一個SACK的東西,ACK還是Fast Retransmit的ACK,SACK則是彙報收到的數據碎版。參看下圖:

這樣,在發送端就可以根據回傳的SACK來知道哪些數據到了,哪些沒有到。於是就優化了Fast Retransmit的算法。當然,這個協議需要兩邊都支持。在 Linux下,可以通過tcp_sack參數打開這個功能(Linux 2.4後默認打開)。

這裏還需要注意一個問題——接收方Reneging,所謂Reneging的意思就是接收方有權把已經報給發送端SACK裏的數據給丟了。這樣幹是不被鼓勵的,因爲這個事會把問題複雜化了,但是,接收方這麼做可能會有些極端情況,比如要把內存給別的更重要的東西。所以,發送方也不能完全依賴SACK,還是要依賴ACK,並維護Time-Out,如果後續的ACK沒有增長,那麼還是要把SACK的東西重傳,另外,接收端這邊永遠不能把SACK的包標記爲Ack。

注意:SACK會消費發送方的資源,試想,如果一個攻擊者給數據發送方發一堆SACK的選項,這會導致發送方開始要重傳甚至遍歷已經發出的數據,這會消耗很多發送端的資源。詳細的東西請參看《TCP SACK的性能權衡

Duplicate SACK – 重複收到數據的問題

Duplicate SACK又稱D-SACK,其主要使用了SACK來告訴發送方有哪些數據被重複接收了RFC-2883 裏有詳細描述和示例。下面舉幾個例子(來源於RFC-2883

D-SACK使用了SACK的第一個段來做標誌,

  • 如果SACK的第一個段的範圍被ACK所覆蓋,那麼就是D-SACK
  • 如果SACK的第一個段的範圍被SACK的第二個段覆蓋,那麼就是D-SACK

示例一:ACK丟包

下面的示例中,丟了兩個ACK,所以,發送端重傳了第一個數據包(3000-3499),於是接收端發現重複收到,於是回了一個SACK=3000-3500,因爲ACK都到了4000意味着收到了4000之前的所有數據,所以這個SACK就是D-SACK——旨在告訴發送端我收到了重複的數據,而且我們的發送端還知道,數據包沒有丟,丟的是ACK包。

1
2
3
4
5
6
7
Transmitted  Received    ACK Sent
Segment      Segment     (Including SACK Blocks)
 
3000-3499    3000-3499   3500 (ACK dropped)
3500-3999    3500-3999   4000 (ACK dropped)
3000-3499    3000-3499   4000, SACK=3000-3500
                                    ---------

 示例二,網絡延誤

下面的示例中,網絡包(1000-1499)被網絡給延誤了,導致發送方沒有收到ACK,而後面到達的三個包觸發了“Fast Retransmit算法”,所以重傳,但重傳時,被延誤的包又到了,所以,回了一個SACK=1000-1500,因爲ACK已到了3000,所以,這個SACK是D-SACK——標識收到了重複的包。

這個案例下,發送端知道之前因爲“Fast Retransmit算法”觸發的重傳不是因爲發出去的包丟了,也不是因爲迴應的ACK包丟了,而是因爲網絡延時了。

1
2
3
4
5
6
7
8
9
10
11
Transmitted    Received    ACK Sent
Segment        Segment     (Including SACK Blocks)
 
500-999        500-999     1000
1000-1499      (delayed)
1500-1999      1500-1999   1000, SACK=1500-2000
2000-2499      2000-2499   1000, SACK=1500-2500
2500-2999      2500-2999   1000, SACK=1500-3000
1000-1499      1000-1499   3000
               1000-1499   3000, SACK=1000-1500
                                      ---------

 

可見,引入了D-SACK,有這麼幾個好處:

1)可以讓發送方知道,是發出去的包丟了,還是回來的ACK包丟了。

2)是不是自己的timeout太小了,導致重傳。

3)網絡上出現了先發的包後到的情況(又稱reordering)

4)網絡上是不是把我的數據包給複製了。

 知道這些東西可以很好得幫助TCP瞭解網絡情況,從而可以更好的做網絡上的流控


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章