從輸入 URL 到浏覽器接收的過程中發生了什麼事情
從輸入 URL 到浏覽器接收的過程中發生了什麼事情?
原文:http://www.codeceo.com/article/url-cpu-broswer.html
從觸屏到 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/interrupts
CPU0
17: 0 GIC dg_timer
294: 1973609 msmgpioelan-ktf3k
314:679msmgpio KEY_POWER
因為 Nexus 7 使用了 ELAN 的觸屏控制器,是以結果中的 elan-ktf3k 就是觸屏的中斷請求資訊,其中 294 是中斷号,1973609 是觸發的次數(手指單擊時會産生兩次中斷,但滑動時會産生上百次中斷)。
為了簡化這裡不考慮優先級問題,以 ARMv7 架構的處理器為例,當中斷發生時,CPU 會停下目前運作的程式,儲存目前執行狀态(如 PC 值),進入 IRQ 狀态),然後跳轉到對應的中斷處理程式執行,這個程式一般由第三方核心驅動來實作.
這個驅動程式将讀取 I²C 總線中傳來的位置資料,然後通過核心的 input_report_abs 等方法記錄觸屏按下坐标等資訊,最後由核心中的 input 子子產品将這些資訊都寫進 /dev/input/event0 這個裝置檔案中.
從作業系統 GUI 到浏覽器
前面提到 Linux 核心已經完成了對硬體的抽象,其它程式隻需要通過監聽 /dev/input/event0 檔案的變化就能知道使用者進行了哪些觸摸操作,不過如果每個程式都這麼做實在太麻煩了,是以在圖像作業系統中都會包含 GUI 架構來友善應用程式開發,比如 Linux 下著名的 X。
但 Android 并沒有使用 X,而是自己實作了一套 GUI 架構,其中有個 EventHub 的服務會通過 epoll 方式監聽 /dev/input/ 目錄下的檔案,然後将這些資訊傳遞到 Android 的視窗管理服務(WindowManagerService)中,它會根據位置資訊來查找相應的 app,然後調用其中的監聽函數(如 onTouch 等)。
就這樣,我們解答了第一個問題,不過由于時間有限,這裡省略了很多細節,想進一步學習的讀者推薦閱讀以下書籍。
擴充學習
《計算機體系結構》
《計算機體系結構:量化研究方法》
《計算機組成與設計:硬體/軟體接口》
《編碼》
《CPU自制入門》
《作業系統概念》
《ARMv7-AR 體系結構參考手冊》
《Linux核心設計與實作》
《精通Linux裝置驅動程式開發》
浏覽器如何向網卡發送資料?
從浏覽器到浏覽器核心
前面提到作業系統 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 指令得到的結果(省略了一些):
; <<>> DiG 9.8.3-P1
<<>> +trace fex.baidu.com
;; global options: +cmd
. 11157 INNS g.root-servers.net.
. 11157 INNS i.root-servers.net.
. 11157 INNS j.root-servers.net.
. 11157 INNS a.root-servers.net.
. 11157 INNS l.root-servers.net.
;; Received 228 bytes from
8.8.8.8#53(8.8.8.8) in 220 ms
1. 172800 IN NS a.gtld-servers.net.
2. 172800 IN NS c.gtld-servers.net.
3. 172800 IN NS m.gtld-servers.net.
4. 172800 IN NS h.gtld-servers.net.
5. 172800 IN NS e.gtld-servers.net.
;; Received 503 bytes from 192.36.148.17#53(192.36.148.17) in 185 msbaidu.com.
172800 IN NS dns.baidu.com.
baidu.com. 172800 IN NS ns2.baidu.com.
baidu.com. 172800 IN NS ns3.baidu.com.
baidu.com. 172800 IN NS ns4.baidu.com.
baidu.com. 172800 IN NS ns7.baidu.com.
;; Received 201 bytes from 192.48.79.30#53(192.48.79.30) in 1237
msfex.baidu.com. 7200 IN CNAME fexteam.duapp.com.
fexteam.duapp.com. 300 IN CNAME duapp.n.shifen.com.
n.shifen.com. 86400 IN NS ns1.n.shifen.com.
n.shifen.com. 86400 IN NS ns4.n.shifen.com.
n.shifen.com. 86400 IN NS ns2.n.shifen.com.
n.shifen.com. 86400 IN NS ns5.n.shifen.com.
n.shifen.com. 86400 IN NS ns3.n.shifen.com.
;; Received 258 bytes from 61.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 的抓包序号,并不是網絡協定
就這樣,我們解答了第二個問題,不過其實這裡面還有很多很多細節沒介紹,建議大家通過下面的書籍進一步學習。
擴充學習
《計算機網絡:自頂向下方法與Internet特色》
《計算機網絡》
《Web性能權威指南》
資料如何從本機網卡發送到伺服器?
從核心到網絡擴充卡(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 次握手)。
IDC 内網
資料通過光纖最終會來到伺服器所在的 IDC 機房,進入 IDC 内網,這時可以先通過分光器将流量鏡像一份出來友善進行安全檢查等分析,還能用來進行。。。
這裡的帶寬成本很高,是按照峰值來結算的,以每月每 Gbps(注意這裡指的是 bit,而不是
Byte)為機關,北京這邊價格在十萬人民币以上,一般網站使用 1G 到 10G 不等。
接下來光纖中的資料将進入叢集(Cluster)交換機,然後再轉發到機架(Rack)頂部的交換機,最後通過這個交換機的端口将資料發往機架中的伺服器,可以參考下圖(來自 Open Compute):
上圖左邊是正面,右邊是側面,可以看到頂部為交換機所留的位置。
以前這些交換機的内部實作是封閉的,相關廠商(如思科、Juniper 等)會使用特定的處理器和作業系統,外界難以進行靈活控制,甚至有時候需要手工配置,但這幾年随着 OpenFlow 技術的流行,也出現了開放交換機硬體(Open Switch Hardware),比如 Intel 的網絡平台,推薦感興趣的讀者建議看看它的視訊,比文字描述清晰多了。
需要注意的是,一般網絡書中提到的交換機都隻具備二層(MAC 協定)的功能,但在 IDC 中的交換器基本上都具備三層(IP
協定)的功能,是以不需要有專門的路由了。
最後,因為 CPU 處理的是電氣信号,是以光纖中的光線需要先使用相關裝置通過光電效應将光信号轉成電信号,然後進入伺服器網卡。
伺服器 CPU
前面說到資料已經到達伺服器網卡了,接着網卡會将資料拷貝到記憶體中(DMA),然後通過中斷來通知 CPU,目前伺服器端的 CPU 基本上都是 Intel Xeon,不過這幾年出現了一些新的架構,比如在存儲領域,百度使用 ARM 架構來提升存儲密度,因為 ARM 的功耗比 Xeon 低得多。而在高性能領域,Google 最近在嘗試基于 POWER 架構的 CPU 來開發的伺服器,最新的 POWER8 處理器可以并行執行 96 個線程,是以對高并發的應用應該很有幫助。
擴充學習
The Datacenter as a Computer
Open Computer
《軟體定義網絡》
《大話無線通信》
伺服器接收到資料後會進行哪些處理?
為了避免重複,這裡将不再介紹作業系統,而是直接進入後端服務程序,由于這方面有太多技術選型,是以我隻挑幾個常見的公共部分來介紹。
負載均衡
請求在進入到真正的應用伺服器前,可能還會先經過負責負載均衡的機器,它的作用是将請求合理地配置設定到多個伺服器上,同時具備具備防攻擊等功能。
負載均衡具體實作有很多種,有直接基于硬體的 F5,有作業系統傳輸層(TCP)上的 LVS,也有在應用層(HTTP)實作的反向代理(也叫七層代理),接下來将介紹 LVS 及反向代理。
負載均衡的政策也有很多,如果後面的多個伺服器性能均衡,最簡單的方法就是挨個循環一遍(Round-Robin),其它政策就不一一介紹了,可以參考 LVS 中的算法。
LVS
LVS 的作用是從對外看來隻有一個 IP,而實際上這個 IP 後面對應是多台機器,是以也被成為 Virtual IP。
前面提到的 NAT 也是一種 LVS 中的工作模式,除此之外還有 DR 和 TUNNEL,具體細節這裡就不展開了,它們的缺點是無法跨網段,是以百度自己開發了 BVS 系統。
反向代理
方向代理是工作在 HTTP 上的,具體實作可以基于 HAProxy 或 Nginx,因為反向代理能了解 HTTP 協定,是以能做非常多的事情,比如:
進行很多統一處理,比如防攻擊政策、放抓取、SSL、gzip、自動性能優化等
應用層的分流政策都能在這裡做,比如對 /xx 路徑的請求分到 a 伺服器,對 /yy 路徑的請求分到 b 伺服器,或者按照 cookie 進行小流量測試等
緩存,并在後端服務挂掉的時候顯示友好的 404 頁面
監控後端服務是否異常
⋯⋯
Nginx 的代碼寫得非常優秀,從中能學到很多,對高性能服務端開發感興趣的讀者一定要看看。
Web Server 中的處理
請求經過前面的負載均衡後,将進入到對應伺服器上的 Web Server,比如 Apache、Tomcat、Node.JS 等。
以 Apache 為例,在接收到請求後會交給一個獨立的程序來處理,我們可以通過編寫 Apache 擴充來處理,但這樣開發起來太麻煩了,是以一般會調用 PHP 等腳本語言來進行處理,比如在 CGI 下就是将 HTTP 中的參數放到環境變量中,然後啟動 PHP 程序來執行,或者使用 FastCGI 來預先啟動程序。
(等後續有空再單獨介紹 Node.JS 中的處理)
進入後端語言
前面說到 Web Server 會調用後端語言程序來處理 HTTP 請求(這個說法不完全正确,有很多其它可能),那麼接下來就是後端語言的處理了,目前大部分後端語言都是基于虛拟機的,如 PHP、Java、JavaScript、Python 等,但這個領域的話題非常大,難以講清楚,對 PHP 感興趣的讀者可以閱讀我之前寫的 HHVM 介紹文章,其中提到了很多虛拟機的基礎知識。
Web 架構(Framework)
如果你的 PHP 隻是用來做簡單的個人首頁「Personal Home Page」,倒沒必要使用 Web 架構,但如果随着代碼的增加會變得越來越難以管理,是以一般網站都會會基于某個 Web 架構來開發,是以在後端語言執行時首先進入 Web 架構的代碼,然後由架構再去調用應用的實作代碼。
可選的 Web 架構非常多,這裡就不一一介紹了。
讀取資料
這部分不展開了,從簡單的讀寫檔案到資料中間層,這裡面可選的方案實在太多。
擴充學習
《深入了解Nginx》
《Python源碼剖析》
《深入了解Java虛拟機》
《資料庫系統實作》
伺服器傳回資料後浏覽器如何處理?
前面說到服務端處理完請求後,結果将通過網絡發回用戶端的浏覽器,從本節開始将介紹浏覽器接收到資料後的處理,值得一提的是這方面之前有一篇不錯的文章 How Browsers Work,是以很多内容我不想再重複介紹,是以将重點放在那篇文章所忽略的部分。
從 01 到字元
HTTP 請求傳回的 HTML 傳遞到浏覽器後,如果有 gzip 會先解壓,然後接下來最重要的問題是要知道它的編碼是什麼,比如同樣一個「中」字,在 UTF-8 編碼下它的内容其實是「11100100 10111000 10101101」也就是「E4 B8 AD」,而在 GBK 下則是「11010110 11010000」,也就是「D6 D0」,如何才能知道檔案的編碼?可以有很多判斷方法:
使用者設定,在浏覽器中可以指定頁面編碼
HTTP 協定中
<meta>中的 charset 屬性值
對于 JS 和 CSS
對于 iframe
如果在這些地方都沒指明,浏覽器就很難處理,在它看來就是一堆「0」和「1」,比如「中文」,它在 UTF-8 下有 6 個位元組,如果按照 GBK 可以當成「涓枃」這 3 個漢字來解釋,浏覽器怎麼知道到底是「中文」還是「涓枃」呢?
不過正常人一眼就能認出「涓枃」是錯的,因為這 3 個字太不常見了,是以有人就想到通過判斷常見字的方法來檢測編碼,典型的比如 Mozilla 的 UniversalCharsetDetection,不過這東東誤判率也很高,是以還是指明編碼的好。
這樣後續對文本的操作就是基于「字元」(Character)的了,一個漢字就是一個字元,不用再關心它究竟是 2 個位元組還是 3 個位元組。
JavaScript 的執行
(後續再單獨介紹,推薦大家看 R 大去年整理的這個文章,裡面有非常多相關資料,另外我兩年前曾講過 JavaScript 引擎中的性能優化,雖然有些内容不太正确了,但也可以看看)
從字元到圖檔
二維渲染中最複雜的要數文字顯示了,雖然想想似乎很簡單,不就是将某個文字對應的字形(glyph)找出來麼?在中文和英文中這樣做是沒問題的,因為一個字元就對應一個字形(glyph),在字型檔案中找到字形,然後畫上去就可以了,但在阿拉伯語中是不行的,因為它有有連體形式。
(以後續再單獨介紹,這裡非常複雜)
跨平台 2D 繪制庫
在不同作業系統中都提供了自己的圖形繪制 API,比如 Mac OS X 下的 Quartz,Windows 下的 GDI 以及 Linux 下的 Xlib,但它們互相不相容,是以為了友善支援跨平台繪圖,在 Chrome 中使用了 Skia 庫。
(以後再單獨介紹,Skia 内部實作調用層級太多,直接講代碼可能不适合初學者)
GPU 合成
(以後續再單獨介紹,雖然簡單來說就是靠貼圖,但還得介紹 OpenGL 以及 GPU 晶片,内容太長)
擴充學習
這節内容是我最熟悉,結果反而因為這樣才想花更多時間寫好,是以等到以後再發出來好了,大家先可以先看看以下幾個站點:
Chromium
Mozilla
Hacks
Surfin’
Safari
浏覽器如何将頁面展現出來?
前面提到浏覽器已經将頁面渲染成一張圖檔了,接下來的問題就是如何将這張圖檔展示在螢幕上。
Framebuffer
以 Linux 為例,在應用中控制螢幕最直接的方法是将圖像的 bitmap 寫入 /dev/fb0 檔案中,這個檔案實際上一個記憶體區域的映射,這段記憶體區域稱為 Framebuffer。
需要注意的是在硬體加速下,如 OpenGL 是不經過 Framebuffer 的。
從記憶體到 LCD
在手機的 SoC 中通常都會有一個 LCD 控制器,當 Framebuffer 準備好後,CPU 會通過 AMBA 内部總線通知 LCD 控制器,然後這個控制器讀取 Framebuffer 中的資料,進行格式轉換、伽馬校正等操作,最終通過 DSI、HDMI 等接口發往 LCD 顯示器。
以 OMAP5432 為例,下圖是它所支援的一種并行資料傳輸:
LCD 顯示
最後簡單介紹一下 LCD 的顯示原理。
首先,要想讓人眼能看見,就必須有光線進入,要麼通過反射、要麼有光源,比如 Kindle 所使用的 E-ink 螢幕本身是不發光的,是以必須在有光線的地方才能閱讀,它的優點是省電,但限制太大,是以幾乎所有 LCD 都會自帶光源。
目前 LCD 中通常使用 LED 作為光源,LED 接上電源後,在電壓的作用下,内部的正負電子結合會釋放光子,進而産生光,這種實體現象叫電緻發光(Electroluminescence),這在前面介紹光纖時也介紹過。
以下是 iPod Touch 2 拆開後的樣子:(來自 Wikipedia):
在上圖中可以看到 6 盞 LED,這就是整個螢幕的光源,這些光源将通過反射的反射輸出到螢幕中。
有了光源還得有色彩,在 LED 中通常做法是使用彩色濾光片(Color filter)來将 LED 光源轉成不同顔色。
另外直接使用三種顔色的 LED 也是可行的,它能避免了濾光導緻的光子浪費,降低耗電,很适用于智能手表這樣的小螢幕,Apple 收購的
LuxVue 公司就采用的是這種方式,感興趣的話可以去研究它的專利
LCD 螢幕上的每個實體像素點實際上是由紅、綠、藍 3 種色彩的點組成,每個顔色點能單獨控制,下面是用顯微鏡放大後的情況(來自 Wikipedia):
從上圖可以看到每 3 種顔色的濾光片都全亮的時候就是白色,都滅就是黑色,如果你仔細看還能看到有些點并不是完全黑,這是字型上的反鋸齒效果。
通過這 3 種顔色亮度的不同組合就能産生出各種色彩,如果每個顔色點能産生 256 種亮度,就能生成 256 256 256 = 16777216 種色彩。
并不是所有顯示器的亮度都能達到 256,在選擇顯示器時有個參數是 8-Bit 或 6-Bit 面闆,其中 8-Bit 的面闆能在實體上達到
256 種亮度,而 6-Bit 的則隻有 64 種,它需要靠重新整理率控制(Frame rate control)技術來達到 256 的效果。
如何控制這些顔色點的亮度?這就要靠液晶體了,液晶體的特性是當有電流通過時會發生旋轉,進而将部分光線擋住,是以隻要通過電壓控制液晶體的轉動就能控制這個顔色點的亮度,目前手機螢幕中通常使用 TFT 控制器來對其進行控制,在 TFT 中最著名的要數 IPS 面闆。
這些過濾後的光線大部分會直接進入眼睛,有些光還會在其它表面上經過漫(diffuse)反射或鏡面(specular)反射後再進入眼睛,加上環境光的影響,要真正算出有多少光到眼睛是一個積分問題,感興趣的讀者可以研究基于實體的渲染。
當光線進入眼睛後,接下來就是生物學的領域了,是以我們到此結束。
擴充學習
《Computer Graphics, 3rd Edition :
Principles and Practices》
《互動式計算機圖形學》
本文所忽略的内容
為了編寫友善,前面的介紹中将很多底層細節實作忽略了,比如:
記憶體相關
堆,這裡的配置設定政策有很多,比如 malloc 的實作
棧,函數調用,已經有很多優秀的文章或書籍介紹了
記憶體映射,動态庫加載等
隊列幾乎無處不在,但這些細節和原理沒太大關系
各種緩存
CPU 的緩存、作業系統的緩存、HTTP 緩存、後端緩存等等
各種監控
很多日志會儲存下來以便後續分析
FAQ
從微網誌回報來看,有些問題被經常問到,我就在這裡統一回答吧,如果有其它問題請在評論中問。
Q:學那麼多有什麼用?根本用不着
A:計算機是人類最強大的工具,你不想了解它是如何運作的麼?
Q:什麼都了解一點,還不如精通一項吧?
A:非常認同,初期肯定需要先在某個領域精通,然後再去了解周邊領域的知識,這樣還能讓你對之前那個領域有更深刻的了解。
Q:曬出來培養一堆面霸跟自己過不去?
A:本文其實寫得很淺,每個部分都能再深入展開。
Q:這題要把人累死啊,說幾天都說不完的
A:哈哈哈,大神你暴露了,題目隻是手段,目的是将你這樣的大牛挖掘出來。
來自為知筆記(Wiz)