從輸入URL到頁面載入完成的過程中都發生了什麼?

佚名發表於2015-10-12

背景 本文來自於之前我發的一篇微博:

頁面載入 網站響應流程 網站URL

不過寫這篇文章並不是為了幫大家準備面試,而是想借這道題來介紹計算機和網際網路的基礎知識,讓讀者瞭解它們之間是如何關聯起來的。

為了便於理解,我將整個過程分為了六個問題來展開。

第一個問題:從輸入 URL 到瀏覽器接收的過程中發生了什麼事情?

從觸屏到 CPU

首先是「輸入 URL」,大部分人的第一反應會是鍵盤,不過為了與時俱進,這裡將介紹觸控式螢幕裝置的互動。

觸控式螢幕一種感測器,目前大多是基於電容(Capacitive)來實現的,以前都是直接覆蓋在螢幕上的,不過最近出現了 3 種嵌入到螢幕中的技術,第一種是 iPhone 5 的 In-cell,它能減小了 0.5 毫米的厚度,第二種是三星使用的 On-cell 技術,第三種是國內廠商喜歡用的 OGS 全貼合技術。

當手指在這個感測器上觸控時,有些電子會傳遞到手上,從而導致該區域的電壓變化,觸控式螢幕控制器晶片根據這個變化就能計算出所觸控的位置,然後透過匯流排介面將訊號傳到 CPU 的引腳上。

以 Nexus 5 為例,它所使用的觸屏控制器是Synaptics S3350B,匯流排介面為I²C,以下是 Synaptics 觸控式螢幕和處理器連線的示例:

頁面載入 網站響應流程 網站URL

左邊是處理器,右邊是觸控式螢幕控制器,中間的 SDA 和 SCL 連線就是 I²C 匯流排介面。

CPU 內部的處理

移動裝置中的 CPU 並不是一個單獨的晶片,而是和 GPU 等晶片整合在一起,被稱為 SoC(片上系統)。

前面提到了觸屏和 CPU 的連線,這個連線和大部分計算機內部的連線一樣,都是透過電氣訊號來進行通訊的,也就是電壓高低的變化,如下面的時序圖:

頁面載入 網站響應流程 網站URL

在時鐘的控制下,這些電流會經過MOSFET電晶體,電晶體中包含 N 型半導體和 P 型半導體,透過電壓就能控制線路開閉,然後這些 MOSFET 構成了CMOS,接著再由 CMOS 實現「與」「或」「非」等邏輯電路門,最後由邏輯電路門上就能實現加法、位移等計算,整體如下圖所示(來自《計算機體系結構》):

頁面載入 網站響應流程 網站URL

除了計算,在 CPU 中還需要儲存單元來載入和儲存資料,這個儲存單元一般透過觸發器(Flip-flop)來實現,稱為暫存器。

以上這些概念都比較抽象,推薦閱讀「How to Build an 8-Bit Computer」這篇文章,作者基於電晶體、二極體、電容等原件製作了一個 8 位的計算機,支援簡單彙編指令和結果輸出,雖然現代 CPU 的實現要比這個複雜得多,但基本原理還是一樣的。

另外其實我也是剛開始學習 CPU 晶片的實現,所以就不在這誤人子弟了,感興趣的讀者請閱讀本節後面推薦的書籍。

從 CPU 到作業系統核心

前面說到觸屏控制器將電氣訊號傳送到 CPU 對應的引腳上,接著就會觸發 CPU 的中斷機制,以 Linux 為例,每個外部裝置都有一識別符號,稱為中斷請求(IRQ)號,可以透過/proc/interrupts檔案來檢視系統中所有裝置的中斷請求號,以下是 Nexus 7 (2013) 的部分結果:

shell@flo:/$cat/proc/interruptsCPU017:0 GICdg_timer294:1973609 msmgpioelan-ktf3k314:679 msmgpioKEY_POWER

因為 Nexus 7 使用了 ELAN 的觸屏控制器,所以結果中的 elan-ktf3k 就是觸屏的中斷請求資訊,其中 294 是中斷號,1973609 是觸發的次數(手指單擊時會產生兩次中斷,但滑動時會產生上百次中斷)。

為了簡化這裡不考慮優先順序問題,以 ARMv7 架構的處理器為例,當中斷髮生時,CPU 會停下當前執行的程式,儲存當前執行狀態(如 PC 值),進入 IRQ 狀態),然後跳轉到對應的中斷處理程式執行,這個程式一般由第三方核心驅動來實現,比如前面提到的 Nexus 7 的驅動原始碼在這裡touchscreen/ektf3k.c。

這個驅動程式將讀取 I²C 匯流排中傳來的位置資料,然後透過核心的input_report_abs等方法記錄觸屏按下座標等資訊,最後由核心中的input 子模組將這些資訊都寫進/dev/input/event0這個裝置檔案中,比如下面展示了一次觸控事件所產生的資訊:

130|shell@flo:/$getevent-lt/dev/input/event0[414624.658986]EV_ABS ABS_MT_TRACKING_ID 0000835c[414624.659017]EV_ABS ABS_MT_TOUCH_MAJOR 0000000b[414624.659047]EV_ABS ABS_MT_PRESSURE0000001d[414624.659047]EV_ABS ABS_MT_POSITION_X000003f0[414624.659078]EV_ABS ABS_MT_POSITION_Y00000588[414624.659078]EV_SYN SYN_REPORT 00000000[414624.699239]EV_ABS ABS_MT_TRACKING_ID ffffffff[414624.699270]EV_SYN SYN_REPORT 00000000

從作業系統 GUI 到瀏覽器

前面提到 Linux 核心已經完成了對硬體的抽象,其它程式只需要透過監聽/dev/input/event0檔案的變化就能知道使用者進行了哪些觸控操作,不過如果每個程式都這麼做實在太麻煩了,所以在影像作業系統中都會包含 GUI 框架來方便應用程式開發,比如 Linux 下著名的X。

但 Android 並沒有使用 X,而是自己實現了一套 GUI 框架,其中有個EventHub的服務會透過epoll方式監聽/dev/input/目錄下的檔案,然後將這些資訊傳遞到 Android 的視窗管理服務(WindowManagerService)中,它會根據位置資訊來查詢相應的 app,然後呼叫其中的監聽函式(如 onTouch 等)。

就這樣,我們解答了第一個問題,不過由於時間有限,這裡省略了很多細節。

第二個問題:瀏覽器如何向網路卡傳送資料?

從瀏覽器到瀏覽器核心

前面提到作業系統 GUI 將輸入事件傳遞到了瀏覽器中,在這過程中,瀏覽器可能會做一些預處理,比如 Chrome 會根據歷史統計來預估所輸入字元對應的網站,比如輸入了「ba」,根據之前的歷史發現 90% 的機率會訪問「www.baidu.com 」,因此就會在輸入回車前就馬上開始建立 TCP 連結甚至渲染了,這裡面還有很多其它策略,感興趣的讀者推薦閱讀 High Performance Networking in Chrome。

接著是輸入 URL 後的「回車」,這時瀏覽器會對 URL 進行檢查,首先判斷協議,如果是 http 就按照 Web 來處理,另外還會對這個 URL 進行安全檢查,然後直接呼叫瀏覽器核心中的對應方法,比如WebView中的 loadUrl 方法。

在瀏覽器核心中會先檢視快取,然後設定 UA 等 HTTP 資訊,接著呼叫不同平臺下網路請求的方法。

需要注意瀏覽器和瀏覽器核心是不同的概念,瀏覽器指的是 Chrome、Firefox,而瀏覽器核心則是 Blink、Gecko,瀏覽器核心只負責渲染,GUI 及網路連線等跨平臺工作則是瀏覽器實現的

HTTP 請求的傳送

因為網路的底層實現是和核心相關的,所以這一部分需要針對不同平臺進行處理,從應用層角度看主要做兩件事情:透過 DNS 查詢 IP、透過 Socket 傳送資料,接下來就分別介紹這兩方面的內容。

DNS 查詢

應用程式可以直接呼叫 Libc 提供的getaddrinfo()方法來實現 DNS 查詢。

DNS 查詢其實是基於 UDP 來實現的,這裡我們透過一個具體例子來了解它的查詢過程,以下是使用dig +trace fex.baidu.com命令得到的結果(省略了一些):

;<<>>DiG9.8.3-P1<<>>+trace fex.baidu.com;;globaloptions:+cmd. 11157 INNSg.root-servers.net.. 11157 INNSi.root-servers.net.. 11157 INNSj.root-servers.net.. 11157 INNSa.root-servers.net.. 11157 INNSl.root-servers.net.;;Received228bytes from8.8.8.8#53(8.8.8.8) in 220 mscom.172800INNSa.gtld-servers.net.com.172800INNSc.gtld-servers.net.com.172800INNSm.gtld-servers.net.com.172800INNSh.gtld-servers.net.com.172800INNSe.gtld-servers.net.;;Received503bytes from192.36.148.17#53(192.36.148.17) in 185 msbaidu.com.172800INNSdns.baidu.com.baidu.com.172800INNSns2.baidu.com.baidu.com.172800INNSns3.baidu.com.baidu.com.172800INNSns4.baidu.com.baidu.com.172800INNSns7.baidu.com.;;Received201bytes from192.48.79.30#53(192.48.79.30) in 1237 msfex.baidu.com.7200INCNAME fexteam.duapp.com.fexteam.duapp.com.300INCNAME duapp.n.shifen.com.n.shifen.com. 86400 INNSns1.n.shifen.com.n.shifen.com. 86400 INNSns4.n.shifen.com.n.shifen.com. 86400 INNSns2.n.shifen.com.n.shifen.com. 86400 INNSns5.n.shifen.com.n.shifen.com. 86400 INNSns3.n.shifen.com.;;Received258bytes from61.135.165.235#53(61.135.165.235) in 2 ms

可以看到這是一個逐步縮小範圍的查詢過程,首先由本機所設定的 DNS 伺服器(8.8.8.8)向 DNS 根節點查詢負責 .com 區域的域務器,然後透過其中一個負責 .com 的伺服器查詢負責 baidu.com 的伺服器,最後由其中一個 baidu.com 的域名伺服器查詢 fex.baidu.com 域名的地址。

可能你在查詢某些域名的時會發現和上面不一樣,最底將看到有個奇怪的伺服器搶先返回結果。。。

這裡為了方便描述,忽略了很多不同的情況,比如 127.0.0.1 其實走的是loopback,和網路卡裝置沒關係;比如 Chrome 會在瀏覽器啟動的時預先查詢 10 個你有可能訪問的域名;還有 Hosts 檔案、快取時間 TTL(Time to live)的影響等。

透過 Socket 傳送資料

有了 IP 地址,就可以透過 Socket API 來傳送資料了,這時可以選擇 TCP 或 UDP 協議,具體使用方法這裡就不介紹了,推薦閱讀 Beej’s Guide to Network Programming。

HTTP 常用的是 TCP 協議,由於 TCP 協議的具體細節到處都能看到,所以本文就不介紹了,這裡談一下 TCP 的 Head-of-line blocking 問題:假設客戶端的傳送了 3 個 TCP 片段(segments),編號分別是 1、2、3,如果編號為 1 的包傳輸時丟了,即便編號 2 和 3 已經到達也只能等待,因為 TCP 協議需要保證順序,這個問題在 HTTP pipelining 下更嚴重,因為 HTTP pipelining 可以讓多個 HTTP 請求透過一個 TCP 傳送,比如傳送兩張圖片,可能第二張圖片的資料已經全收到了,但還得等第一張圖片的資料傳到。

為了解決 TCP 協議的效能問題,Chrome 團隊去年提出了QUIC協議,它是基於 UDP 實現的可靠傳輸,比起 TCP,它能減少很多來回(round trip)時間,還有前向糾錯碼(Forward Error Correction)等功能。目前 Google Plus、 Gmail、Google Search、blogspot、Youtube 等幾乎大部分 Google 產品都在使用 QUIC,可以透過chrome://net-internals/#spdy頁面來發現。

雖然目前除了 Google 還沒人用 QUIC,但我覺得挺有前景的,因為最佳化 TCP 需要升級系統核心(比如Fast Open)。

瀏覽器對同一個域名有連線數限制,大部分是 6,我以前認為將這個連線數改大後會提升效能,但實際上並不是這樣的,Chrome 團隊有做過實驗,發現從 6 改成 10 後效能反而下降了,造成這個現象的因素有很多,如建立連線的開銷、擁塞控制等問題,而像 SPDY、HTTP 2.0 協議儘管只使用一個 TCP 連線來傳輸資料,但效能反而更好,而且還能實現請求優先順序。

另外,因為 HTTP 請求是純文字格式的,所以在 TCP 的資料段中可以直接分析 HTTP 的文字,如果發現。。。

Socket 在核心中的實現

前面說到瀏覽器的跨平臺庫透過呼叫 Socket API 來傳送資料,那麼 Socket API 是如何實現的呢?

以 Linux 為例,它的實現在這裡socket.c,目前我還不太瞭解,推薦讀者看看Linux kernel map,它標註出了關鍵路徑的函式,方便學習從協議棧到網路卡驅動的實現。

底層網路協議的具體例子

接下來如果繼續介紹 IP 協議和 MAC 協議可能很多讀者會暈,所以本節將使用Wireshark來透過具體例子講解,以下是我請求百度首頁時抓取到的網路資料:

頁面載入 網站響應流程 網站URL

最底下是實際的二進位制資料,中間是解析出來的各個欄位值,可以看到其中最底部為 HTTP 協議(Hypertext Transfer Protocol),在 HTTP 之前有 54 位元組(0×36),這就是底層網路協議所帶來的開銷,我們接下來對這些協議進行分析。

在 HTTP 之上是 TCP 協議(Transmission Control Protocol),它的具體內容如下圖所示:

頁面載入 網站響應流程 網站URL

透過底部的二進位制資料,可以看到 TCP 協議是加在 HTTP 文字前面的,它有 20 個位元組,其中定義了本地埠(Source port)和目標埠(Destination port)、順序序號(Sequence Number)、視窗長度等資訊,以下是 TCP 協議各個部分資料的完整介紹:

0 1 2 3

01234567890123456789012345678901+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Source Port| Destination Port|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Sequence Number|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Acknowledgment Number|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Data| |U|A|E|R|S|F| ||Offset|Reserved|R|C|O|S|Y|I|Window || | |G|K|L|T|N|N| |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Checksum| Urgent Pointer|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Options|Padding|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| data|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

具體各個欄位的作用這裡就不介紹了,感興趣的讀者可以閱讀RFC 793,並結合抓包分析來理解。

需要注意的是,在 TCP 協議中並沒有 IP 地址資訊,因為這是在上一層的 IP 協議中定義的,如下圖所示:

頁面載入 網站響應流程 網站URL

IP 協議同樣是在 TCP 前面的,它也有 20 位元組,在這裡指明瞭版本號(Version)為 4,源(Source) IP 為192.168.1.106,目標(Destination) IP 為119.75.217.56,因此 IP 協議最重要的作用就是確定 IP 地址。

因為 IP 協議中可以檢視到目標 IP 地址,所以如果發現某些特定的 IP 地址,某些路由器就會。。。

但是,光靠 IP 地址是無法進行通訊的,因為 IP 地址並不和某臺裝置繫結,比如你的筆記本的 IP 在家中是192.168.1.1,但到公司就變成172.22.22.22了,所以在底層通訊時需要使用一個固定的地址,這就是 MAC(media Access control) 地址,每個網路卡出廠時的 MAC 地址都是固定且唯一的。

因此再往上就是 MAC 協議,它有 14 位元組,如下所示:

頁面載入 網站響應流程 網站URL

當一臺電腦加入網路時,需要透過ARP協議告訴其它網路裝置它的 IP 及對應的 MAC 地址是什麼,這樣其它裝置就能透過 IP 地址來查詢對應的裝置了。

最頂上的 Frame 是代表 Wireshark 的抓包序號,並不是網路協議。

就這樣,我們解答了第二個問題,不過其實這裡面還有很多很多細節沒介紹。

第三個問題:資料如何從本機網路卡傳送到伺服器?

從核心到網路介面卡(Network Interface Card)

前面說到呼叫 Socket API 後核心會對資料進行底層協議棧的封裝,接下來啟動DMA控制器,它將從記憶體中讀取資料寫入網路卡。

以 Nexus 5 為例,它使用的是博通BCM4339晶片通訊,介面採用了 SD 卡一樣的SDIO,但這個晶片的細節並沒有公開資料,所以這裡就不討論了。

連線 Wi-Fi 路由

Wi-Fi 網路卡需要透過 Wi-Fi 路由來與外部通訊,原理是基於無線電,透過電流變化來產生無線電,這個過程也叫「調製」,而反過來無線電可以引起電磁場變化,從而產生電流變化,利用這個原理就能將無線電中的資訊解讀出來就叫「解調」,其中單位時間內變化的次數就稱為頻率,目前在 Wi-Fi 中所採用的頻率分為 2.4 GHz 和 5 GHz 兩種。

在同一個 Wi-Fi 路由下,因為採用的頻率相同,同時使用時會發生衝突,為了解決這個問題,Wi-Fi 採用了被稱為CSMA/CA的方法,簡單來說就是在傳輸前先確認通道是否已被使用,沒有才傳送資料。

而同樣基於無線電原理的 2G/3G/LTE 也會遇到類似的問題,但它並沒有採用 Wi-Fi 那樣的獨佔方案,而是透過頻分(FDMA)、時分(TDMA)和碼分(CDMA)來進行復用,具體細節這裡就不展開了。

小米路由為例,它使用的晶片是BCM 4709,這個晶片由 ARM Cortex-A9 處理器及流量(Flow)硬體加速組成,使用硬體晶片可以避免經過作業系統中斷、上下文切換等操作,從而提升了效能。

路由器中的作業系統可以基於OpenWrt或DD-WRT來開發的,具體細節我不太瞭解,所以就不展開了。

因為內網裝置的 IP 都是類似192.168.1.x這樣的內網地址,外網無法直接向這個地址傳送資料,所以網路資料在經過路由時,路由會修改相關地址和埠,這個操作稱為NAT對映。

最後家庭路由一般會透過雙絞線連線到運營商網路的。

運營商網路內的路由

資料過雙絞線傳送到運營商網路後,還會經過很多箇中間路由轉發,讀者可以透過 traceroute 命令或者線上視覺化工具來檢視這些路由的 ip 和位置。

當資料傳遞到這些路由器後,路由器會取出包中目的地址的字首,透過內部的轉發表查詢對應的輸出鏈路,而這個轉發表是如何得到的呢?這就是路由器中最重要的選路演算法了,可選的有很多,我對這方面並不太瞭解,看起來維基百科上的詞條列得很全。

主幹網間的傳輸

對於長線的資料傳輸,通常使用光纖作為介質,光纖是基於光的全反射來實現的,使用光纖需要專門的發射器透過電致發光(比如 LED)將電訊號轉成光,比起前面介紹的無線電和雙絞線,光纖訊號的抗干擾性要強得多,而且能耗也小很多。

既然是基於光來傳輸資料,資料傳輸速度也就取決於光的速度,在真空中的光速接近於 30 萬千米/秒,由於光纖包層(cladding)中的折射率(refractive index)為 1.52,所以實際光速是 20 萬千米/秒左右,從首都機場飛往廣州白雲機場的距離是 1967 千米,按照這個距離來算需要花費 10 毫秒才能抵達。這意味著如果你在北京,伺服器在廣州,等你發出資料到伺服器返回資料至少得等 20 毫秒,實際情況預計是 2- 3 倍,因為這其中還有各個節點路由處理的耗時,比如我測試了一個廣州的 IP 發現平均延遲為 60 毫秒。

這個延遲是現有科技無法解決的(除非找到超過光速的方法),只能透過 CDN 來讓傳輸距離變短,或儘量減少序列的來回請求(比如 TCP 建立連線所需的 3 次握手)。

相關文章