TCP是什麼php
首先看一下OSI七層模型:前端
而後數據從應用層發下來,會在每一層都加上頭部信息進行封裝,而後再發送到數據接收端,這個基本的流程中每一個數據都會通過數據的封裝和解封的過程,流程以下圖所示:java
在OSI七層模型中,每一層的做用和對應的協議以下圖所示:linux
說回TCP,簡單說TCP(Transmission Control Protocol)即傳輸控制協議,是一種面向鏈接的、可靠的、基於ip的傳輸層協議。緩存
TCP協議頭部格式服務器
要學習TCP協議,首先得知道TCP協議頭部的格式,我在網上找了一張以爲畫得比較好的TCP協議頭部格式的圖片:網絡
這張圖把TCP協議頭部格式的每部分都描述得比較清楚:socket
從圖上咱們能夠看到,TCP頭部的固定大小爲20個字節,不過因爲有可選字段,實際上TCP頭部的大小有可能超過20字節。tcp
TCP三次握手函數
TCP三次握手是TCP一個比較重點的內容,來學習一下。
TCP三次握手其實就是TCP鏈接創建的過程,三次握手的目的是同步鏈接雙方的序列號和確認號並交換TCP窗口大小信息。下面是TCP三次握手的流程圖:
畫得很清晰,惋惜不是我畫的。整個流程爲:
爲何在第3步中客戶端還要再進行一次確認呢?這主要是爲了防止已經失效的鏈接請求報文段忽然又傳回到服務端而產生錯誤的場景:
所謂"已失效的鏈接請求報文段"是這樣產生的。正常來講,客戶端發出鏈接請求,但由於鏈接請求報文丟失而未收到確認。因而客戶端再次發出一次鏈接請求,後來收到了確認,創建了鏈接。數據傳輸完畢後,釋放了鏈接,客戶端一共發送了兩個鏈接請求報文段,其中第一個丟失,第二個到達了服務端,沒有"已失效的鏈接請求報文段"。 如今假定一種異常狀況,即客戶端發出的第一個鏈接請求報文段並無丟失,只是在某些網絡節點長時間滯留了,以致於延誤到鏈接釋放之後的某個時間點纔到達服務端。原本這個鏈接請求已經失效了,可是服務端收到此失效的鏈接請求報文段後,就誤認爲這是客戶端又發出了一次新的鏈接請求。因而服務端又向客戶端發出請求報文段,贊成創建鏈接。假定不採用三次握手,那麼只要服務端發出確認,鏈接就創建了。
因爲如今客戶端並無發出鏈接創建的請求,所以不會理會服務端的確認,也不會向服務端發送數據,可是服務端卻覺得新的傳輸鏈接已經創建了,並一直等待客戶端發來數據,這樣服務端的許多資源就這樣白白浪費了。
採用三次握手的辦法能夠防止上述現象的發生。好比在上述的場景下,客戶端不向服務端的發出確認請求,服務端因爲收不到確認,就知道客戶端並無要求創建鏈接。
TCP四次握手
TCP三次握手是TCP鏈接創建的過程,TCP四次握手則是TCP鏈接釋放的過程。下面是TCP四次握手的流程圖:
當客戶端沒有數據再須要發送給服務端時,就須要釋放客戶端的鏈接,這整個過程爲:
這裏的一個問題是,爲何TCP鏈接的創建只須要三次握手而TCP鏈接的釋放須要四次握手呢:
由於服務端在LISTEN狀態下,收到創建請求的SYN報文後,把ACK和SYN放在一個報文裏發送給客戶端。而鏈接關閉時,當收到對方的FIN報文時,僅僅表示對方沒有須要發送的數據了,可是還能接收數據,己方未必數據已經所有發送給對方了,因此己方能夠當即關閉,也能夠將應該發送的數據所有發送完畢後再發送FIN報文給客戶端來表示贊成如今關閉鏈接。
從這個角度而言,服務端的ACK和FIN通常都會分開發送。
使用Wireshark抓包驗證TCP三次握手過程
爲了加深對TCP三次握手的理解,抓包看一下TCP三次握手的過程。我這裏訪問的是咱們公司本身的網站,不打廣告,訪問的具體什麼頁面、哪一個ip就不透露了。
抓包下來的內容爲:
這裏多說一句,因爲wireshark抓包針對的是網卡,所以只要某張網卡上有網絡訪問,就會有數據包,這會致使Wireshark的抓包結果裏面會有大量數據包,而大多數都不是想要的,這種狀況可使用Wireshark的過濾規則。我這裏因爲知道目標ip,所以使用的是"ip.src == xxx.xxx.xxx.xxx or ip.dst == xxx.xxx.xxx.xxx"這條規則只過濾特定的ip。
從抓包結果看來,整個過程符合TCP三次握手的預期:
至於Sequence Number和Acknowledge Number就不看了,可是注意,前面說了Sequence Number是隨機產生的一個值,可是這裏確是0,不光這裏是0,抓其餘的任何包這個值都是0。但其實這裏並非真的0,而是Wireshark爲了顯示更好閱讀,使用了relative sequence number相對序號,Sequence Number具體值咱們也是能夠看到的:
第一個紅框就是上面說的relative sequence number,第二個紅框就是Sequence Number的真實值0xc978aa7e,轉換爲十進制爲3380128382,就是隨機產生的Sequence Number。
順便能看到,下一個數據包就是HTTP的數據包,由於TCP三次握手已完成,鏈接創建,正式傳輸應用層數據,傳輸的HTTP內容大小爲704字節。
TCP的backlog
在學習TCP的時候發現的一個比較重要的知識點。
在TCP鏈接創建的過程當中有以下的流程和隊列:
如圖所示,這裏面有兩個隊列,分別爲syns queue(半鏈接隊列)與accept queue(全鏈接隊列)。整個流程總結用文字以下:
backlog的定義是已鏈接但未進行accept處理的socket隊列大小,若是這個隊列滿了,將會發送一個ECONNREFUSED錯誤信息給到客戶端,即 linux 頭文件 /usr/include/asm-generic/errno.h中定義的「Connection refused」。
Java支持原生的Socket,咱們能夠寫一段代碼來驗證一下。首先是一個普通的客戶端Socket,模擬向本地的8888端口發起鏈接:
1 public class ClientSocketClass { 2 3 private static Socket[] clients = new Socket[30]; 4 5 public static void main(String[] args) throws Exception { 6 for (int i = 0; i < 10; i++) { 7 clients[i] = new Socket("127.0.0.1", 8888); 8 System.out.println("Client:" + i); 9 } 10 } 11 12 }
接着是服務端Socket,監聽8888端口,ServerSocket構造函數的第二個參數就是backlog的大小,若是backlog小於1或者不傳會給一個默認值50,代碼很簡單:
1 public class ServerSocketClass { 2 3 public static void main(String[] args) throws Exception { 4 ServerSocket server = new ServerSocket(8888, 5); 5 6 while (true) { 7 // server.accept(); 8 } 9 } 10 11 }
先把註釋關閉,運行ServerSocketClass,先發起監聽,再運行ClientSocketClass,運行結果爲:
1 Client:0 2 Client:1 3 Client:2 4 Client:3 5 Client:4 6 Exception in thread "main" java.net.ConnectException: Connection refused: connect 7 at java.net.DualStackPlainSocketImpl.connect0(Native Method) 8 at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79) 9 at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339) 10 at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200) 11 at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182) 12 at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172) 13 at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) 14 at java.net.Socket.connect(Socket.java:579) 15 at java.net.Socket.connect(Socket.java:528) 16 at java.net.Socket.<init>(Socket.java:425) 17 at java.net.Socket.<init>(Socket.java:208) 18 at org.xrq.test.socket.ClientSocketClass.main(ClientSocketClass.java:11)
看到Client只發起了五個請求,第六個請求發起被拒絕了,由於三次握手創建後,前五個請求佔據了全鏈接隊列並無被處理,因而第六個請求進來,全鏈接隊列中沒有它的位置了,所以請求被拒絕。
若是註釋打開,又是不同的效果:
1 Client:0 2 Client:1 3 Client:2 4 Client:3 5 Client:4 6 Client:5 7 Client:6 8 Client:7 9 Client:8 10 Client:9
這裏全部的十個客戶端請求所有被接受,由於accept()方法從全鏈接隊列中取出了鏈接請求進行處理。看得出來,backlog提供了容量限制功能,避免過多的客戶端Socket佔據大量的服務端資源。
全鏈接隊列大小的問題
接着說說全鏈接隊列大小的問題。首先上面提到了backlog,不一樣的應用對backlog的默認值定義不一樣,好比:
Tomcat能夠經過server.xml配置文件中<Connector />節點中的acceptCount來修改backlog。若是請求量不是很大,使用Tomcat默認的100也能夠,但若是訪問量比較大,建議這個值設置得大一些,好比1024或者更大。若是Tomcat前一層對SYC FLOOD攻擊的防護沒有把握的話,最好將SYN COOKIE防護也開啓。
可是,全鏈接隊列的大小未必是backlog的值,它是backlog與somaxconn(一個os級別的系統參數)的較小值。Linux環境下能夠經過執行"cat /proc/sys/net/core/somaxconn"來查看:
這個值系統默認的是128,假如傳入的backlog是10,取128和10的較小值,那麼最終的全鏈接隊列大小就是10。一樣,若是要修改Linux系統默認的全鏈接隊列大小的話,能夠經過修改/proc/sys/net/core路徑下的somaxconn。
半鏈接隊列大小的問題
說完了全鏈接隊列大小的問題,接着說一下半鏈接隊列大小的問題,它是64與tcp_max_syn_backlog的較大值。
能夠經過"cat /proc/sys/net/ipv4/tcp_max_syn_backlog"命令或者"cat /etc/sysctl.conf"命令來查看半鏈接隊列的大小。之後者爲例,其實就是打開了/ect/sysctl.conf這個文件:
標紅的即tcp_max_syn_backlog默認值,默認值爲1024,能夠經過修改這個值來修改系統默認的半鏈接隊列大小。
經過ss查看Socket統計狀態
前面說了這麼多全鏈接隊列,那麼如何查看全鏈接隊列大小?
在Linux環境下能夠經過ss命令查看,ss命令全稱爲Socket Statistics,顧名思義它用於統計Socket。netstat命令其實也能夠顯示相似內容,可是ss命令相比netstat命令可以顯示更多更詳細的有關TCP和鏈接狀態的信息,並且比netstat更快速更高效。
ss命令的參數就不列舉了,能夠本身上網查看,這裏使用ss -lnt,即查看處於LISTEN狀態的TCP套接字,且不解析服務名稱:
Send-Q表示當前端口的全鏈接隊列大小,Recv-Q表示全鏈接隊列當前使用了多少。
從Send-Q能夠看到,它的值只有三種:12八、50、1。這也印證了咱們的結論,全鏈接隊列的大小爲傳入的backlog與somaxconn的較小值。
參考文章
http://blog.csdn.net/oney139/article/details/8103223
http://www.jellythink.com/archives/705
http://jm.taobao.org/2017/05/25/525-1/