背景 本文來自於之前我發的一篇微博:
不過寫這篇文章並不是為了幫大家準備面試,而是想借這道題來介紹計算機和網際網路的基礎知識,讓讀者瞭解它們之間是如何關聯起來的。
為了便於理解,我將整個過程分為了六個問題來展開。
第一個問題:從輸入 URL 到瀏覽器接收的過程中發生了什麼事情?
從觸屏到 CPU
首先是「輸入 URL」,大部分人的第一反應會是鍵盤,不過為了與時俱進,這裡將介紹觸控式螢幕裝置的互動。
觸控式螢幕一種感測器,目前大多是基於電容(Capacitive)來實現的,以前都是直接覆蓋在螢幕上的,不過最近出現了 3 種嵌入到螢幕中的技術,第一種是 iPhone 5 的 In-cell,它能減小了 0.5 毫米的厚度,第二種是三星使用的 On-cell 技術,第三種是國內廠商喜歡用的 OGS 全貼合技術。
當手指在這個感測器上觸控時,有些電子會傳遞到手上,從而導致該區域的電壓變化,觸控式螢幕控制器晶片根據這個變化就能計算出所觸控的位置,然後透過匯流排介面將訊號傳到 CPU 的引腳上。
以 Nexus 5 為例,它所使用的觸屏控制器是Synaptics S3350B,匯流排介面為I²C,以下是 Synaptics 觸控式螢幕和處理器連線的示例:
左邊是處理器,右邊是觸控式螢幕控制器,中間的 SDA 和 SCL 連線就是 I²C 匯流排介面。
CPU 內部的處理
移動裝置中的 CPU 並不是一個單獨的晶片,而是和 GPU 等晶片整合在一起,被稱為 SoC(片上系統)。
前面提到了觸屏和 CPU 的連線,這個連線和大部分計算機內部的連線一樣,都是透過電氣訊號來進行通訊的,也就是電壓高低的變化,如下面的時序圖:
在時鐘的控制下,這些電流會經過MOSFET電晶體,電晶體中包含 N 型半導體和 P 型半導體,透過電壓就能控制線路開閉,然後這些 MOSFET 構成了CMOS,接著再由 CMOS 實現「與」「或」「非」等邏輯電路門,最後由邏輯電路門上就能實現加法、位移等計算,整體如下圖所示(來自《計算機體系結構》):
除了計算,在 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來透過具體例子講解,以下是我請求百度首頁時抓取到的網路資料:
最底下是實際的二進位制資料,中間是解析出來的各個欄位值,可以看到其中最底部為 HTTP 協議(Hypertext Transfer Protocol),在 HTTP 之前有 54 位元組(0×36),這就是底層網路協議所帶來的開銷,我們接下來對這些協議進行分析。
在 HTTP 之上是 TCP 協議(Transmission Control Protocol),它的具體內容如下圖所示:
透過底部的二進位制資料,可以看到 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 協議中定義的,如下圖所示:
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 位元組,如下所示:
當一臺電腦加入網路時,需要透過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 次握手)。