1. 程式人生 > >靈魂拷問!瀏覽器輸入「xxxxhub」的背後.....

靈魂拷問!瀏覽器輸入「xxxxhub」的背後.....

![](https://z3.ax1x.com/2021/03/30/cPRJrF.png) Hey guys 各位讀者姥爺們大家好,這裡是程式設計師 cxuan 計算機網路連載系列的第 13 篇文章。 到現在為止,我們算是把應用層、運輸層、網路層和資料鏈路層都介紹完了,那麼現在是時候把這些內容都串起來,做一個全面的回顧了。那麼我這就以 Web 頁面的請求歷程為例,來和你聊聊計算機網路中這些協議是怎樣工作的、資料包是怎麼收發的,從輸入 URL 、敲擊會車到最終完成頁面呈現在你面前的這個過程。 首先,我打開了 Web Browser ,然後在 Google 瀏覽器 URL 位址列中輸入了 `maps.google.com`。 ![](https://z3.ax1x.com/2021/03/30/cPcwPP.png) 然後 ...... ## 查詢 DNS 快取 瀏覽器在這個階段會檢查四個地方是否存在快取,第一個地方是瀏覽器快取,這個快取就是 DNS 記錄。 瀏覽器會為你訪問過的網站在固定期限內維護 DNS 記錄。因此,它是第一個執行 DNS 查詢的地方。 瀏覽器首先會檢查這個網址在瀏覽器中是否有一條對應的 DNS 記錄,用來找到目標網址的 IP 地址。 我是 chrome 瀏覽器,所以在 mac 中,無法使用 **chrome://net-internals/#dns** 找到對應的 IP 地址,在 windows 中是可以找到的。 >那麼 mac 怎麼查詢 DNS 記錄呢?你可以使用 `nslookup` 命令來查詢,但這不是我們討論的重點。 `DNS(Domain Name System)` 是一個分散式的資料庫,它用於維護網址 URL 到其 IP 地址的對映關係。在網際網路中,IP 地址是計算機所能夠理解的一種地址,而 DNS 的這種別名地址是我們人類能夠理解和記憶的地址,DNS 就負責把人類記憶的地址對映成計算機能夠理解的地址,每個 URL 都有唯一的 IP 地址進行對應。 舉個例子,google 的官網是 www.google.com ,而 google 的 ip 地址是 216.58.200.228 ,這兩個地址你在 URL 上輸入哪個都能訪問,但是 IP 地址不好記憶,而 google.com 簡單明瞭。DNS 就相當於是我們幾年前使用的家庭電話薄,比如你想給 cxuan 打電話,你有可能記不住 cxuan 的電話號碼,此時你需要查詢電話薄來找到 cxuan 的電話號碼。 瀏覽器第二個需要檢查的地方就是**作業系統快取**。如果 DNS 記錄不在瀏覽器快取中,那麼瀏覽器將對作業系統發起系統呼叫,Windows 下就是 `getHostName`。 >在 Linux 和大部分 UNIX 系統上,除非安裝了 `nscd`,否則作業系統可能沒有 DNS 快取。 > >nscd 是 Linux 系統上的一種**名稱服務快取程式**。 瀏覽低第三個需要檢查的地方是**路由器快取**,如果 DNS 記錄不在自己電腦上的話,瀏覽器就會和與之相連的路由器共同維護 DNS 記錄。 如果與之相連的路由器也沒有 DNS 記錄的話,瀏覽器就會檢查 `ISP` 中是否有快取。ISP 快取就是你本地通訊服務商的快取,因為 ISP 維護著自己的 DNS 伺服器,它快取 DNS 記錄的本質也是為了降低請求時間,達到快速響應的效果。一旦你訪問過某些網站,你的 ISP 可能就會快取這些頁面,以便下次快速訪問。對於經常看小電影的你是否感到震驚呢?如果家裡還安裝了一個可以聯網的攝像頭的話,那就有點嗨皮了。 >你肯定比較困惑為什麼第一步瀏覽器需要檢查這麼多快取,你可能會感到不舒服,因為快取可能會透露我們的隱私,但是這些快取在調節網路流量和縮短資料傳輸時間等方面至關重要。 所以,上面涉及到 DNS 快取的查詢過程如下。 ![](https://z3.ax1x.com/2021/03/30/cPc08f.png) 如果上面四個步驟中都不存在 DNS 記錄,那麼就表示不存在 DNS 快取,這個時候就需要發起 DNS 查詢,以查詢目標網址(本示例中是 maps.google.com)的 IP 地址。 ## 發起 DNS 查詢 如上所述,如果想要使我的計算機和 maps.google.com 建立連線並進行通訊的話,我需要知道 maps.google.com 的 IP 地址,由於 DNS 的設計原因,本地 DNS 可能無法給我提供正確的 IP 地址,那麼它就需要在網際網路上搜索多個 DNS 伺服器,來找到網站的正確 IP 地址。 >這裡有個疑問,為什麼我需要搜尋多個 DNS 伺服器的來找到網站的 IP 地址呢?一臺伺服器不行嗎? 因為 DNS 是分散式域名伺服器,每臺伺服器只維護一部分 IP 地址到網路地址的對映,沒有任何一臺伺服器能夠維持全部的對映關係。 在 DNS 的早期設計中只有一臺 DNS 伺服器。這臺伺服器會包含所有的 DNS 對映。這是一種`集中式`的設計,這種設計並不適用於當今的網際網路,因為網際網路有著**數量巨大並且持續增長**的主機,這種集中式的設計會存在以下幾個問題 - `單點故障(a single point of failure)`,如果 DNS 伺服器崩潰,那麼整個網路隨之癱瘓。 - `通訊容量(traaffic volume)`,單個 DNS 伺服器不得不處理所有的 DNS 查詢,這種查詢級別可能是上百萬上千萬級,一臺伺服器很難滿足。 - `遠距離集中式資料庫(distant centralized database)`,單個 DNS 伺服器不可能 `鄰近` 所有的使用者,假設在美國的 DNS 伺服器不可能臨近讓澳大利亞的查詢使用,其中查詢請求勢必會經過低速和擁堵的鏈路,造成嚴重的時延。 - `維護(maintenance)`,維護成本巨大,而且還需要頻繁更新。 所以在當今網路情況下 DNS 不可能集中式設計,因為它完全沒有可擴充套件能力,所以採用`分散式設計`,這種設計的特點如下 **分散式、層次資料庫**。 首先分散式設計首先解決的問題就是 DNS 伺服器的擴充套件性問題,因此 DNS 使用了大量的 DNS 伺服器,它們的組織模式一般是層次方式,並且分佈在全世界範圍內。**沒有一臺 DNS 伺服器能夠擁有因特網上所有主機的對映**。相反,這些對映分佈在所有的 DNS 伺服器上。 大致來說有三種 DNS 伺服器:`根 DNS 伺服器`、 `頂級域(Top-Level Domain, TLD) DNS 伺服器` 和 `權威 DNS 伺服器` 。這些伺服器的層次模型如下圖所示 ![](https://z3.ax1x.com/2021/03/30/cPca5t.png) ![](https://z3.ax1x.com/2021/03/30/cPcUUI.png) - `根 DNS 伺服器` ,有 400 多個根域名伺服器遍及全世界,這些根域名伺服器由 13 個不同的組織管理。根域名伺服器的清單和組織機構可以在 https://root-servers.org/ 中找到,根域名伺服器提供 TLD 伺服器的 IP 地址。 - `頂級域 DNS 伺服器`,對於每個頂級域名比如 com、org、net、edu 和 gov 和所有的國家級域名 uk、fr、ca 和 jp 都有 TLD 伺服器或伺服器叢集。所有的頂級域列表參見 https://tld-list.com/ 。TDL 伺服器提供了權威 DNS 伺服器的 IP 地址。 - `權威 DNS 伺服器`,在因特網上具有公共可訪問的主機,如 Web 伺服器和郵件伺服器,這些主機的組織機構必須提供可供訪問的 DNS 記錄,這些記錄將這些主機的名字對映為 IP 地址。一個組織機構的權威 DNS 伺服器收藏了這些 DNS 記錄。 在瞭解了 DNS 伺服器的設計理念之後,我們回到 DNS 查詢的步驟上來,DNS 的查詢方式主要分為三種 DNS 查詢中會出現三種類型的查詢。通過組合使用這些查詢,**優化的 DNS 解析過程可縮短傳輸距離**。在理想情況下,可以使用快取的記錄資料,從而使 DNS 域名伺服器能夠直接使用非遞迴查詢。 * `遞迴查詢`:在遞迴查詢中,DNS 客戶端要求 DNS 伺服器(一般為 DNS 遞迴解析器)將使用所請求的資源記錄響應客戶端,或者如果解析器無法找到該記錄,則返回錯誤訊息。 ![](https://z3.ax1x.com/2021/03/30/cPcNVA.png) * `迭代查詢`:在迭代查詢中,如果所查詢的 DNS 伺服器與查詢名稱不匹配,則其將返回對較低級別域名空間具有權威性的 DNS 伺服器的引用。然後,DNS 客戶端將對引用地址進行查詢。此過程繼續使用查詢鏈中的其他 DNS 伺服器,直至發生錯誤或超時為止。 ![](https://z3.ax1x.com/2021/03/30/cPcYbd.png) * `非遞迴查詢`:當 DNS 解析器客戶端查詢 DNS 伺服器以獲取其有權訪問的記錄時通常會進行此查詢,因為其對該記錄具有權威性,或者該記錄存在於其快取內。DNS 伺服器通常會快取 DNS 記錄,查詢到來後能夠直接返回快取結果,以防止更多頻寬消耗和上游伺服器上的負載。 上面負責開始 DNS 查詢的介質就是 **DNS 解析器**,它一般是 ISP 維護的 DNS 伺服器,它的主要職責就是通過向網路中其他 DNS 伺服器詢問正確的 IP 地址。 如果想要了解更多關於 DNS 的訊息,請查閱 [萬字長文爆肝 DNS 協議!](https://mp.weixin.qq.com/s?__biz=MzI0ODk2NDIyMQ==&mid=2247487880&idx=1&sn=fd38ce30ae82fa7d08e5f83fabb9d497&chksm=e999e49adeee6d8c1adacbfe27dc59097e4cb9d39c6a04802b0fe61877653330e75721cbde0b&token=1399988516&lang=zh_CN#rd) 所以對於 maps.google.com 這個域名來說,如果 ISP 維護的伺服器沒有 DNS 快取記錄,它就會向 DNS 根伺服器地址發起查詢,根名稱伺服器會將其重定向到 .com 頂級域名伺服器。 .com 頂級域名伺服器會將其重定向到google.com 權威伺服器。google.com 名稱伺服器將在其 DNS 記錄中找到 maps.google.com 匹配的 IP 地址,並將其返回給您的 DNS 解析器,然後將其傳送回你的瀏覽器。 這裡值得注意的是,DNS 查詢報文會經過許多路由器和裝置才會達到根域名等伺服器,每經過一個裝置或者路由器都會使用`路由表` 來確定哪種路徑是資料包達到目的地最快的選擇。這裡面涉及到路由選擇演算法,如果小夥伴們想要了解路由選擇演算法,可以看看這篇文章 https://www.cisco.com/c/en/us/support/docs/ip/border-gateway-protocol-bgp/13753-25.html#anc3 ## ARP 請求 我看了很多篇文章都沒有提到這一點,那就是 ARP 請求的這個過程。 >什麼時候需要傳送 ARP 請求呢? 這裡其實有個前提條件 - 如果 DNS 伺服器和我們的主機在同一個子網內,系統會按照下面的 ARP 過程對 DNS 伺服器進行 ARP 查詢 - 如果 DNS 伺服器和我們的主機在不同的子網,系統會按照下面的 ARP 過程對預設閘道器進行查詢 ARP 協議的全稱是 `Address Resolution Protocol(地址解析協議)`,它是一個通過**用於實現從 IP 地址到 MAC 地址的對映,即詢問目標 IP 對應的 MAC 地址** 的一種協議。 簡而言之,ARP 就是一種解決地址問題的協議,它以 IP 地址為線索,定位下一個應該接收資料分包的主機 MAC 地址。如果目標主機不在同一個鏈路上,那麼會查詢下一跳路由器的 MAC 地址。 >關於為什麼有了 IP 地址,還要有 MAC 地址概述可以參看知乎這個回答 https://www.zhihu.com/question/21546408 ARP 的大致工作流程如下 假設 A 和 B 位於同一鏈路,不需要經過路由器的轉換,主機 A 向主機 B 傳送一個 IP 分組,主機 A 的地址是 192.168.1.2 ,主機 B 的地址是 192.168.1.3,它們都不知道對方的 MAC 地址是啥,主機 C 和 主機 D 是同一鏈路的其他主機。 ![](https://z3.ax1x.com/2021/03/30/cPcGKe.png) 主機 A 想要獲取主機 B 的 MAC 地址,通過主機 A 會通過`廣播` 的方式向乙太網上的所有主機發送一個 `ARP 請求包`,這個 ARP 請求包中包含了主機 A 想要知道的主機 B 的 IP 地址的 MAC 地址。 ![](https://z3.ax1x.com/2021/03/30/cPc3vD.png) 主機 A 傳送的 ARP 請求包會被同一鏈路上的所有主機/路由器接收並進行解析。每個主機/路由器都會檢查 ARP 請求包中的資訊,如果 ARP 請求包中的`目標 IP 地址` 和自己的相同,就會將自己主機的 MAC 地址寫入響應包返回主機 A ![](https://z3.ax1x.com/2021/03/30/cPcJDH.png) 由此,可以通過 ARP 從 IP 地址獲取 MAC 地址,實現同一鏈路內的通訊。 所以,要想傳送 ARP 廣播,我們需要有一個目標 IP 地址,同時還需要知道用於傳送 ARP 廣播的介面的 MAC 地址。 這裡會涉及到 **ARP 快取**的概念。 現在你知道了傳送一次 IP 分組前通過傳送一次 ARP 請求就能夠確定 MAC 地址。那麼是不是每傳送一次都得經過廣播 -> 封裝 ARP 響應 -> 返回給主機這一系列流程呢? 想想看,瀏覽器是如何做的?瀏覽器內建了快取能夠快取你最近經常使用的地址,那麼 ARP 也是一樣的。ARP 高效執行的關鍵就是維護每個主機和路由器上的 `ARP 快取(或表)`。這個快取維護著每個 IP 到 MAC 地址的對映關係。通過把第一次 ARP 獲取到的 MAC 地址作為 IP 對 MAC 的對映關係到一個 ARP 快取表中,下一次再向這個地址傳送資料報時就不再需要重新發送 ARP 請求了,而是直接使用這個快取表中的 MAC 地址進行資料報的傳送。每傳送一次 ARP 請求,快取表中對應的對映關係都會被清除。 通過 ARP 快取,降低了網路流量的使用,在一定程度上防止了 ARP 的大量廣播。 ![](https://z3.ax1x.com/2021/03/30/cPc1gO.png) 一般來說,傳送過一次 ARP 請求後,再次傳送相同請求的機率比較大,因此使用 ARP 快取能夠減少 ARP 包的傳送,除此之外,不僅僅 ARP 請求的傳送方能夠快取 ARP 接收方的 MAC 地址,接收方也能夠快取 ARP 請求方的 IP 和 MAC 地址,如下所示 ![](https://z3.ax1x.com/2021/03/30/cPcl8K.png) 不過,**MAC 地址的快取有一定期限,超過這個期限後,快取的內容會被清除**。 深入理解 ARP 協議的話,可以參考 cxuan 的這篇文章。 [ARP,這個隱匿在計網背後的男人](https://mp.weixin.qq.com/s?__biz=MzI0ODk2NDIyMQ==&mid=2247487804&idx=1&sn=f001a24a308053b3723dfb12d36045ee&chksm=e999e42edeee6d383fbb411792e22e4028bb8c2441255786f50cf848443af7b1bd5e382078dc&token=1623097963&lang=zh_CN#rd) *** 所以,瀏覽器會首先查詢 ARP 快取,如果快取命中,我們返回結果:目標 IP = MAC。 如果快取沒有命中: - 檢視路由表,看看目標 IP 地址是不是在本地路由表中的某個子網內。是的話,使用跟那個子網相連的介面,否則使用與預設閘道器相連的介面。 - 查詢選擇的網路介面的 MAC 地址 - 我們傳送一個數據鏈路層的 ARP 請求: ![](https://z3.ax1x.com/2021/03/30/cPcK4x.png) 根據連線主機和路由器的硬體型別不同,可以分為以下幾種情況: 直連: - 如果我們和路由器是直接連線的,路由器會返回一個 `ARP Reply` (見下面)。 集線器: - 如果我們連線到一個集線器,集線器會把 ARP 請求向所有其它埠廣播,如果路由器也連線在其中,它會返回一個 `ARP Reply` 。 交換機: - 如果我們連線到了一個交換機,交換機會檢查本地 CAM/MAC 表,看看哪個埠有我們要找的那個 MAC 地址,如果沒有找到,交換機會向所有其它埠廣播這個 ARP 請求。 - 如果交換機的 MAC/CAM 表中有對應的條目,交換機會向有我們想要查詢的 MAC 地址的那個埠傳送 ARP 請求 - 如果路由器也`連線`在其中,它會返回一個 `ARP Reply` `ARP Reply`: ![](https://z3.ax1x.com/2021/03/30/cPcAvF.png) 現在我們有了 DNS 伺服器或者預設閘道器的 IP 地址,我們可以繼續 DNS 請求了: - 使用 53 埠向 DNS 伺服器傳送 UDP 請求包,如果響應包太大,會使用 TCP 協議 - 如果本地/ISP DNS 伺服器沒有找到結果,它會發送一個遞迴查詢請求,一層一層向高層 DNS 伺服器做查詢,直到查詢到起始授權機構,如果找到會把結果返回。 (上述均來自:https://github.com/skyline75489/what-happens-when-zh_CN#dns) *** ## 封裝 TCP 資料包 瀏覽器得到目標伺服器的 IP 地址後,根據 URL 中的埠可以知道埠號 (http 協議預設埠號是 80, https 預設埠號是 443),會準備 TCP 資料包。資料包的封裝會經過下面的層層處理,資料到達目標主機後,目標主機會解析資料包,完整的請求和解析過程如下。 ![](https://z3.ax1x.com/2021/03/30/cPceb9.png) 這裡就不再詳細介紹了,讀者朋友們可以閱讀 cxuan 的這篇文章 [TCP/IP 基礎知識詳解](https://mp.weixin.qq.com/s?__biz=MzI0ODk2NDIyMQ==&mid=2247486408&idx=1&sn=c332ae7ae448f3eb98865003ecade589&chksm=e999fedadeee77cc6281d1b170bd906b58220d6cd83054bc741821f4167f1f18ceee9ba0e449&token=1623097963&lang=zh_CN#rd)詳細瞭解。 ## 瀏覽器與目標伺服器建立 TCP 連線 在經過上述 DNS 和 ARP 查詢流程後,瀏覽器就會收到一個目標伺服器的 IP 和 MAC地址,然後瀏覽器將會和目標伺服器建立連線來傳輸資訊。這裡可以使用很多種 Internet 協議,但是 HTTP 協議建立連線所使用的運輸層協議是 TCP 協議。所以這一步驟是瀏覽器與目標伺服器建立 TCP 連線的過程。 TCP 的連線建立需要經過 TCP/IP 的三次握手,三次握手的過程其實就是瀏覽器和伺服器交換 SYN 同步和 ACK 確認訊息的過程。 假設圖中左端是客戶端主機,右端是服務端主機,一開始,兩端都處於`CLOSED(關閉)`狀態。 ![](https://z3.ax1x.com/2021/03/30/cPcuU1.png) 1. 服務端程序準備好接收來自外部的 TCP 連線。然後服務端程序處於 `LISTEN` 狀態,等待客戶端連線請求。 2. 客戶端向伺服器發出連線請求,請求中首部同步位 SYN = 1,同時選擇一個初始序號 sequence ,簡寫 seq = x。SYN 報文段不允許攜帶資料,只消耗一個序號。此時,客戶端進入 `SYN-SEND` 狀態。 3. 伺服器收到客戶端連線後,,需要確認客戶端的報文段。在確認報文段中,把 SYN 和 ACK 位都置為 1 。確認號是 ack = x + 1,同時也為自己選擇一個初始序號 seq = y。請注意,這個報文段也不能攜帶資料,但同樣要消耗掉一個序號。此時,TCP 伺服器進入 `SYN-RECEIVED(同步收到)` 狀態。 4. 客戶端在收到伺服器發出的響應後,還需要給出確認連線。確認連線中的 ACK 置為 1 ,序號為 seq = x + 1,確認號為 ack = y + 1。TCP 規定,這個報文段可以攜帶資料也可以不攜帶資料,如果不攜帶資料,那麼下一個資料報文段的序號仍是 seq = x + 1。這時,客戶端進入 `ESTABLISHED (已連線)` 狀態 5. 伺服器收到客戶的確認後,也進入 `ESTABLISHED` 狀態。 這樣三次握手建立連線的階段就完成了,雙方可以直接通訊了。 ## 瀏覽器傳送 HTTP 請求到 web 伺服器 一旦 TCP 連線建立完成後,就開始直接傳輸資料辦正事了!此時瀏覽器會發送 `GET` 請求,要求目標伺服器提供 maps.google.com 的網頁,如果你填寫的是表單,則發起的是 `POST` 請求,在 HTTP 中,GET 請求和 POST 請求是最常見的兩種請求,基本上佔據了所有 HTTP 請求的九成以上。 除了請求型別外,HTTP 請求還包含很多很多資訊,最常見的有 Host、Connection 、User-agent、Accept-language 等 ![](https://z3.ax1x.com/2021/03/30/cPcVu4.png) 首先 Host 表示的是物件所在的主機。`Connection: close` 表示的是瀏覽器需要告訴伺服器使用的是`非持久連線`。它要求伺服器在傳送完響應的物件後就關閉連線。`User-agent`: 這是請求頭用來告訴 Web 伺服器,瀏覽器使用的型別是 `Mozilla/5.0`,即 Firefox 瀏覽器。`Accept-language` 告訴 Web 伺服器,瀏覽器想要得到物件的法語版本,前提是伺服器需要支援法語型別,否則將會發送伺服器的預設版本。下面我們針對主要的實體欄位進行介紹(具體的可以參考 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers MDN 官網學習) HTTP 的請求標頭分為四種: `通用標頭`、`請求標頭`、`響應標頭` 和 `實體標頭`。 這四種標頭又分別有很多內容,如果你想要深入理解一下關於 HTTP 請求頭的相關內容,可以參考 cxuan 的這篇文章 [深入理解 HTTP 標頭](https://mp.weixin.qq.com/s?__biz=MzkwMDE1MzkwNQ==&mid=2247496023&idx=1&sn=7281feaf0853d5465d7b082329b7f2d7&chksm=c04ae609f73d6f1f2bac28a3222f83ce24ef199e9869e887aa23f627f194d9d9cb43e1683ddb&token=1307572136&lang=zh_CN#rd) ## 伺服器處理請求併發回一個響應 這個伺服器包含一個 Web 伺服器,也就是 Apache 伺服器,伺服器會從瀏覽器接收請求並將其傳遞給請求處理程式並生成響應。 >請求處理程式也是一個程式,它一般是用 .net 、php、ruby 等語言編寫,用於讀取請求,檢查請求內容,cookie,必要時更新伺服器上的資訊的這麼一個程式。它會以特定的格式比如 JSON、XML、HTML 組合響應。 ## 伺服器傳送回一個 HTTP 響應 伺服器響應包含你請求的網頁以及狀態程式碼,壓縮型別(Content-Encoding),如何快取頁面(Cache-Control),要設定的 cookie,隱私資訊等。 比如下面就是一個響應體 ![](https://z3.ax1x.com/2021/03/30/cPcZDJ.png) 關於深入理解 HTTP 請求和響應,可以參考這篇文章 [看完這篇HTTP,跟面試官扯皮就沒問題了](https://mp.weixin.qq.com/s?__biz=MzkwMDE1MzkwNQ==&mid=2247496030&idx=1&sn=82f56874f82f372af71e23a8e385f8cd&chksm=c04ae600f73d6f16d707c1d32b00e3f0d47e893c9cf59a2eb60ace418943aeb5c5c679cb27ea&token=1307572136&lang=zh_CN#rd) ## 瀏覽器顯示 HTML 的相關內容 瀏覽器會分階段顯示 HTML 內容。 首先,它將渲染裸露的 HTML 骨架。 然後它將檢查 HTML 標記併發送 GET 請求以獲取網頁上的其他元素,例如影象,CSS 樣式表,JavaScript 檔案等。這些靜態檔案由瀏覽器快取,因此你再次訪問該頁面時,不用重新再請求一次。最後,您會看到 maps.google.com 顯示的內容出現在你的瀏覽器中。 **我自己肝了六本 PDF,微信搜尋「程式設計師cxuan」關注公眾號後,在後臺回覆 cxuan ,領取全部 PDF,這些 PDF 如下** [六本 PDF 連結](https://s3.ax1x.com/2020/11/30/DgOK6f.png) ![](https://img2020.cnblogs.com/blog/1515111/202011/1515111-20201130090550310-1032998206.png) ![](https://img2020.cnblogs.com/blog/1515111/202103/1515111-20210330095116003-1507401691.png)