終極解密輸入網址按回車到底發生了什麼
詳解輸入網址點選回車,後臺到底發生了什麼。透析 HTTP 協議與 TCP 連線之間的千絲萬縷的關係。掌握為何是三次握手四次揮手? time_wait 存在的意義是什麼?全面圖解重點問題,再也不用擔心面試問這個問題。
大致流程
- URL 解析,解析 http 協議、埠、資源地址。
- DNS 查詢:首先查詢本地 host,再訪問 DNS 伺服器將 域名解析成 ip 地址。
- 建立 TCP 連線。
- 伺服器收到請求後處理,並且構造響應返回給客戶端。
- 客戶端接收 HTTP 報文響應。
- 渲染頁面,最後有可能會四次揮手斷開連線,也可能不會而是複用連線。
重點來了:
- 如何理解 TCP 的三次握手與四次揮手?每次握手客戶端與服務端是怎樣的狀態?
- 為何揮手會出現 2MSL,遇到大量 Socket 處在 TIME_WAIT 或者 CLOSE_WAIT 狀態是什麼問題?
- 三次握手與四次揮手的過程是怎樣的?
- HTTP 的報文格式又是怎樣的?
繼續閱讀本文,且聽碼哥位元組答疑解惑,微信搜尋 “碼哥位元組”,關注公眾號更多硬核。
URL 解析
比如 【碼哥位元組】在思否釋出的一篇文章的地址:https://segmentfault.com/a/1190000023475177。url 遵守的規則是這個樣子
scheme://host.domain:port/path/filename
每個名詞的含義如下解釋:
- scheme 定義應用層協議型別,比如 http、https、 ftp 等;
- host 定義域主機(http 的預設主機是 www);
- domain 定義因特網域名,比如 segmentfault.com;
- port 主機的埠,http 預設是 80, https 預設是 443;
- path 伺服器上的資源路徑;
- filename - 定義文件/資源的名稱;
DNS 查詢
瀏覽器不能直接通過域名找到伺服器,只能通過 IP 地址。
那瀏覽器是如何通過域名查詢到我們輸入的 url 對應的 IP 呢?
- 瀏覽器快取:按照一定頻率快取 DNS 資料。
- 作業系統快取:如果瀏覽器快取好啊不到記錄則去作業系統中找。
- 路由快取:路由器 DNS 快取。
- ISP 的 DNS 伺服器:ISP 是網際網路服務提供商(Internet Service Provider)的簡稱,ISP 有專門的 DNS 伺服器應對 DNS 查詢請求。
- 根伺服器:ISP 的 DNS 伺服器還找不到的話,它就會向根伺服器發出請求,進行遞迴查詢(DNS 伺服器先問根域名伺服器.com 域名伺服器的 IP 地址,然後再問 .baidu 域名伺服器,依次類推)
TCP 連線建立與斷開
通過域名解析出 IP 地址以後就要建立 TCP/IP 連線了。
TCP/IP 分為四層,每一層都會加上一個頭部再傳送給下一層。到了接收方後,對應的每一層則把對應層的頭部解析拆除,丟上上一層,跟傳送端的過程反過來。
應用層:傳送 HTTP 請求
瀏覽器從位址列得到伺服器 IP,接著構造一個 HTTP 報文,其中包括:
-
請求行包含請求方法、URL、協議版本
-
請求報頭(Request Header):由 “關鍵字: 值”對組成,每行一對,關鍵字與值使用英文 “:” 分割
-
請求體:請求引數,並不是所有的請求有又請求引數。一般 get 引數 的格式
name=tom&password=1234&realName=tomson
,也可以將引數放在 body 裡面。
傳輸層:TCP 傳輸報文
在傳輸報文之前會先建立 TCP/IP 連線,也就是後面我們要說的三次握手。
在這一層解決了資料可靠傳輸、及流量控制、擁塞控制。
可靠傳輸
對於傳送方傳送的資料,接收方在接受到資料之後必須要給予確認,確認它收到了資料。如果在規定時間內,沒有給予確認則意味著接收方沒有接受到資料,然後傳送方對資料進行重發。
TCP的可靠傳輸是通過確認和超時重傳的機制來實現的,而確認和超時重傳的具體的實現是通過以位元組為單位的滑動視窗機制來完成。
TCP擁塞控制
TCP協議通過慢啟動機制、擁塞避免機制、加速遞減機制、快重傳和快恢復機制來共同實現擁塞控制。
流量控制
採用通知視窗實現對傳送端的流量控制,通知視窗大小的單位是位元組。TCP通過在TCP資料段首部的視窗欄位中填入當前設定的接收視窗(即通知視窗)的大小,用來告知對方 '我方當前的接收視窗大小',以實現流量控制。
通訊雙方的傳送視窗大小由雙方在連線建立的時候商定,在通訊過程,雙方可以動態地根據自己的情況調整對方的傳送視窗大小。
網路層:IP 協議查詢 MAC 地址
將資料段打包,並加入源及目標的 IP 地址,並且負責尋找傳輸路線。判斷目標地址是否與當前地址處於同一網路中,是的話直接根據 Mac 地址傳送,否則使用路由表查詢下一跳地址,以及使用 ARP 協議查詢它的 Mac 地址。
鏈路層:乙太網協議
根據乙太網協議將資料分為以“幀”為單位的資料包,每一幀分為兩個部分:
- 標頭:資料包的傳送者、接受者、資料型別
- 資料:資料包具體內容
Mac 地址
乙太網規定了連入網路的所有裝置都必須具備“網路卡”介面,資料包都是從一塊網路卡傳遞到另一塊網路卡,網路卡的地址就是 Mac 地址。每一個 Mac 地址都是獨一無二的,具備了一對一的能力。
三次握手
在傳輸層傳輸資料之前需要建立連線,也就是三次握手建立可靠連線。
首先建立連結前需要 Server 端先監聽埠,因此 Server 端建立連結前的初始狀態就是 LISTEN 狀態,這時 Client 端準備建立連結,先傳送一個 SYN 同步包,傳送完同步包後,Client 端的連結狀態變成了 SYN_SENT 狀態。Server 端收到 SYN 後,同意建立連結,會向 Client 端回覆一個 ACK。
由於 TCP 是雙工傳輸,Server 端也會同時向 Client 端傳送一個 SYN,申請 Server 向 Client 方向建立連結。傳送完 ACK 和 SYN 後,Server 端的連結狀態就變成了 SYN_RCVD。
Client 收到 Server 的 ACK 後,Client 端的連結狀態就變成了 ESTABLISHED 狀態,同時,Client 向 Server 端傳送 ACK,回覆 Server 端的 SYN 請求。
Server 端收到 Client 端的 ACK 後,Server 端的連結狀態也就變成了的 ESTABLISHED 狀態,此時建連完成,雙方隨時可以進行資料傳輸。
在面試時需要明白三次握手是為了建立雙向的連結,需要記住 Client 端和 Server 端的連結狀態變化。另外回答建連的問題時,可以提到 SYN 洪水攻擊發生的原因,就是 Server 端收到 Client 端的 SYN 請求後,傳送了 ACK 和 SYN,但是 Client 端不進行回覆,導致 Server 端大量的連結處在 SYN_RCVD 狀態,進而影響其他正常請求的建連。可以設定 tcp_synack_retries = 0 加快半連結的回收速度,或者調大 tcp_max_syn_backlog 來應對少量的 SYN 洪水攻擊
四次揮手
我們只要關注 80 埠與 13743 埠建立的連線斷開過程,瀏覽器通過 13747 埠傳送 [FIN, ACK] 這裡是不是跟很多網上看到的不一樣?
-
其實是客戶端在傳送 [FIN] 報文的時候順帶發了一個 [ACK] 確認上次傳輸確認。
-
接著服務端通過 80 埠響應了 [ACK] ,然後立馬響應 [FIN, ACK] 表示資料傳輸完了,可以關閉連線。
-
最後瀏覽器通過 13743 埠 傳送 [ACK] 包給服務端,客服端與服務端連線就關閉了。
具體流程如下圖抓包所示:
三次握手與四次揮手
客戶端:
- SYN_SENT - 客戶端發起第 1 次握手後,連線狀態為 SYN_SENT ,等待服務端核心進行應答,如果服務端來不及處理(例如服務端的 backlog 佇列已滿)就可以看到這種狀態的連線。
- ESTABLISHED - 表示連線處於正常狀態,可以進行資料傳送。客戶端收到伺服器回覆的 SYN+ACK 後,對服務端的 SYN 單獨回覆(第 3 次握手),連線建立完成,進入 ESTABLISHED 狀態。服務端程式收到第 3 次握手包後,也進入 ESTABLISHED 狀態。
- FIN_WAIT_1 - 客戶端傳送了關閉連線的 FIN 報文後,等待服務端回覆 ACK 確認。
- FIN_WAIT_2 - 表示我方已關閉連線,正在等待服務端關閉。客戶端發了關閉連線的 FIN 報文後,伺服器發回 ACK 應答,但是沒進行關閉,就會處於這種狀態。
- TIME_WAIT - 雙方都正常關閉連線後,客戶端會維持 TIME_WAIT 一段時間,以確保最後一個 ACK 能成功傳送到伺服器端。停留時長為 2 倍的 MSL (報文最大生存時間),Linux 下大約是 60 秒。所以在一個頻繁建立短連線的伺服器上通常可以看到成千上萬的 TIME_WAIT 連線。
服務端:
- LISTEN - 表示當前程式正在監聽某個埠時。
- SYN_RCVD - 服務端收到第 1 次握手後,進入 SYN_RCVD 狀態,並回復一個 SYN+ACK(第 2 次握手),再等待對方確認。
- ESTABLISHED - 表示連線處於正常狀態,可以進行資料傳送。完成 TCP3 次握手後,連線建立完成,進入 ESTABLISHED 狀態。
- CLOSE_WAIT - 表示客戶端已經關閉連線,但是本地還沒關閉,正在等待本地關閉。有時客戶端程式已經退出了,但服務端程式由於異常或 BUG 沒有呼叫 close()函式對連線進行關閉,那在伺服器這個連線就會一直處於 CLOSE_WAIT 狀態,而在客戶機已經不存在這個連線了。
- LAST_ACK - 表示正在等待客戶端對服務端的關閉請求進行最終確認。
TIME_WAIT 狀態存在的理由:
劃重點了
- 可靠地實現 TCP 全雙工連線的終止 在進行關閉連線四路握手協議時,最後的 ACK 是由主動關閉端發出的,如果這個最終的 ACK 丟失,伺服器將重發最終的 FIN,因此客戶端必須維護狀態資訊允 許它重發最終的 ACK。如 果不維持這個狀態資訊,那麼客戶端將響應 RST 分節,伺服器將此分節解釋成一個錯誤( 在 java 中會丟擲 connection reset 的 SocketException)。因而,要實現 TCP 全雙工連線的正常終 止,必須處理終止序列四個分節中任何一個分節的丟失情況,主動關閉 的客戶端必須維持狀 態資訊進入 TIME_WAIT 狀態。
- 允許老的重複分節在網路中消逝 TCP 分節可能由於路由器異常而“迷途”,在迷途期間,TCP 傳送端可能因確認超時而重發這個 分節,迷途的分節在路由器修復後也會被送到最終目的地,這個 原來的迷途分節就稱為 lost duplicate。在關閉一個 TCP 連線後,馬上又重新建立起一個相同的 IP 地址和埠之間的 TCP 連線,後一個連線被稱為前一個連線的化身 ( incarnation),那麼有可能出現這種情況,前一 個連線的迷途重複分組在前一個連線終止後出現,從而被誤解成從屬於新的化身。為了避免 這個情 況,TCP 不允許處於 TIME_WAIT 狀態的連線啟動一個新的化身,因為 TIME_WAIT 狀 態持續 2MSL,就可以保證當成功建立一個 TCP 連線的時 候,來自連線先前化身的重複分組已 經在網路中消逝。
另外回答斷鏈的問題時,可以提到實際應用中有可能遇到大量 Socket 處在 TIME_WAIT 或者 CLOSE_WAIT 狀態的問題。一般開啟 tcp_tw_reuse 和 tcp_tw_recycle 能夠加快 TIME-WAIT 的 Sockets 回收;而大量 CLOSE_WAIT 可能是被動關閉的一方存在程式碼 bug,沒有正確關閉連結導致的。
簡單地說就是
- 保證 TCP 協議的全雙工連線能夠可靠關閉;
- 保證這次連線的重複資料段從網路中消失,防止埠被重用時可能產生資料混淆;
伺服器處理請求並響應 HTTP 報文
深入分析下 HTTP 報文到底是什麼玩意。資料傳輸都是通過 TCP/IP 協議負責底層的傳輸工作, HTTP 協議基本不用操心,所謂的 “超文字傳輸協議” 似乎不怎麼例會 “傳輸” 這個事情,那 HTTP 的核心又是什麼呢?
比圖 TCP 報文,它在實際要傳輸的資料之前附加了一個 20 位元組的頭部資料,儲存 TCP 協議必須的額外資訊,例如傳送方的埠號、接收方的埠號、包序號、標誌位等等。
有了這個附加的 TCP 頭,資料包才能夠正確傳輸,到了目的地後把頭部去掉,就可以拿到真正的資料。這個很容易理解,設定起點與終點,不同協議貼上不同的頭部,到了對應目的地就拆下這個頭部,提取真正的資料。
與 TCP/UDP 類似需要在傳輸資料前設定一些請求頭,不同的是 HTTP 是一個 “純文字” 的協議,所有的頭都是 ASCII 碼的文字,很容易看出來是什麼。
再者就是他的請求報文與響應報文的結構基本一樣,主要三大部分組成:
- 起始行(Start Line):描述請求或者響應的基本資訊。
- Header:使用 key-value 的形式詳細說明報文資訊。
- 空行。
- 訊息正文(Entity):傳輸的資料,圖片、視訊、文字等都可以。
這其中前兩部分起始行和頭部欄位經常又合稱為“請求頭”或“響應頭”,訊息正文又稱為“實體”,但與“header”對應,很多時候就直接稱為“body”。
敲黑板了
HTTP 協議規定報文必須包含 Header,而且之後必須有一個 “空行”,也就是“CRLF”,十六進位制的“0D0A”,可以沒有 “body”。
報文結構如下圖所示:
擷取一段報文:
請求頭-起始行
請求行由請求方法欄位、URL 欄位和 HTTP 協議版本欄位 3 個欄位組成,它們用空格分隔。例如,GET / HTTP/1.1。
HTTP 協議的請求方法有 GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
。
GET 是請求方法, “/” 是請求的目標資源,“HTTP/1.1” 請求協議版本號。
GET / HTTP/1.1
翻譯成文字大概就是:“hello,伺服器,我要請求根目錄下的預設檔案使用的是 HTTP 1.1 協議版本”。
頭部 Header
第二部分就是 Header,組成形式是 key:value,使用自定義頭需要注意事項:
- header 欄位不區分大小寫,通常是首字母大寫;
- 欄位名不允許有空格,可以使用 “-”,不能使用 “_”;
- 欄位名必須緊接著 “:”,不能有空格,但是 “:” 後面可以有空格。
- 欄位名順序沒有意義;
瀏覽器接收響應並渲染資料
接收到響應文字 HTML,則開始執行瀏覽器渲染機制。
不同的瀏覽器渲染可能有所差異,但是基本按照以下步驟執行:
- 根據 HTML 解析 DOM 樹;
- 根據 CSS 解析出 CSS 規則樹;
- 結合 DOM 樹與 CSS 規則樹,生成渲染樹;
- 根據生成的渲染樹計算每個節點的資訊;
- 根據節點資訊繪製畫面展示給使用者。
推薦閱讀
以下幾篇文章閱讀量與讀者反饋都很好,推薦大家閱讀:
如果覺得閱讀後對你有幫助,希望多多分享、點贊與在看素質三連不做白嫖者。關注 【碼哥位元組】解鎖更多硬核。
我的個人微信:MageByte1024