本節(jié)中和大家講解了"我們?cè)跒g覽器中輸入一個(gè)URL,具體發(fā)生了什么",整個(gè)過(guò)程中分析了應(yīng)用層(HTTP、DNS)、傳輸層(TCP)、網(wǎng)絡(luò)層(IP)等網(wǎng)絡(luò)分層的各個(gè)協(xié)議的作用,也對(duì)服務(wù)器解析HTTP的基本流程進(jìn)行了闡述,本小節(jié)需要大家掌握訪問(wèn) URL 時(shí)每個(gè)步驟的基本功能,能夠通過(guò)對(duì)調(diào)用鏈路的分析,向面試官展示自己的計(jì)算機(jī)網(wǎng)絡(luò)功底。
WeaveScope 默認(rèn)啟動(dòng)時(shí)在 4040 端口,我們可以在宿主機(jī)上打開 http://127.0.0.1:4040 進(jìn)行登錄查看:
第三方的訪問(wèn)控制是用到了auth_request模塊,該模塊的功能是向上游服務(wù)轉(zhuǎn)發(fā)請(qǐng)求,如果上游服務(wù)返回的相應(yīng)碼是 2xx,則通過(guò)認(rèn)證,繼續(xù)向后執(zhí)行;若返回的 401 或者 403,則將響應(yīng)返回給客戶端。auth_request 模塊默認(rèn)是未編譯進(jìn) Nginx 中的,因此我們需要使用 --with-http_auth_reques_module將該模塊編譯進(jìn) Nginx 中,然后我們才能使用該模塊。Syntax: auth_request uri | off;Default: auth_request off;Context: http, server, locationSyntax: auth_request_set $variable value;Default: —Context: http, server, location官方示例:location /private/ { auth_request /auth; ...}location = /auth { # 上游認(rèn)證服務(wù)地址 proxy_pass ... proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri;}
Asynchronous JavaScript + XML(異步JavaScript和XML), 其本身不是一種新技術(shù),而是一個(gè)在 2005年被Jesse James Garrett提出的新術(shù)語(yǔ),用來(lái)描述一種使用現(xiàn)有技術(shù)集合的‘新’方法。(MDN)AJAX 是2005年提出的一種術(shù)語(yǔ),并不代表某個(gè)特定的技術(shù)。其譯名 異步JavaScript和XML 描述出了核心,就是使用 JavaScript 發(fā)送異步 HTTP 請(qǐng)求,這樣就擺脫了想要和服務(wù)端交互,必須刷新頁(yè)面的痛點(diǎn)。學(xué)習(xí) AJAX 相關(guān)內(nèi)容前,建議有一些簡(jiǎn)單的 HTTP 相關(guān)知識(shí)的儲(chǔ)備,否則很難理解其工作流程。
參數(shù)類型說(shuō)明dataObject/String/ArrayBuffer服務(wù)器返回的數(shù)據(jù)statusCodeNumber服務(wù)器返回的 HTTP 狀態(tài)碼headerObject服務(wù)器返回的 HTTP Response HeadercookiesArray.服務(wù)器返回的 cookies,格式為字符串?dāng)?shù)組上面我們?cè)?success 參數(shù)中打印了返回的數(shù)據(jù)。success(res) { console.log('服務(wù)器返回的信息:',res)}打印信息格式如下:{ "data":"{"id":1,"name": "imooc"}", "header": { "Server": "nginx/1.14.0", "Date": "Thu, 10 Apr 2020 03:08:20 GMT", "Content-Type": "application/json;charset=utf-8;", "Transfer-Encoding": "chunked", "Connection": "keep-alive" }, "statusCode": 200, "cookies": [], "errMsg": "request:ok"}statusCode 為 200,說(shuō)明請(qǐng)求成功了,后面我們?cè)偬幚韽姆?wù)器傳回來(lái)的 data 信息就可以了。
實(shí)例:// config.js插件內(nèi)容const host = 'http://imooc.com'export default host// 引入插件并調(diào)用<script>import host from '../../common/config.js';export default {onLoad() {console.log(‘打印出js插件的內(nèi)容’,host)} }</script>打印結(jié)果:http://imooc.com調(diào)用插件的時(shí)候,明明文件已經(jīng)成功引入了,卻不會(huì)成功打印出js插件中的內(nèi)容。這個(gè)時(shí)候可以檢查一下是否在js插件中使用 export 將變量暴露出去了。因?yàn)閖s插件是獨(dú)立的文件,該文件內(nèi)部的所有的變量外部都無(wú)法獲取。如果希望獲取某個(gè)變量,必須通過(guò)export輸出,不然將會(huì)讀取失敗。
Nginx 最強(qiáng)大的地方是在于其 HTTP 請(qǐng)求的反向代理,也即常說(shuō)的七層反向代理。在這一層代理中,通過(guò) Nginx 框架提供的相關(guān)配置,我們能在該層將發(fā)送過(guò)來(lái)的 http 協(xié)議轉(zhuǎn)換成各種其他的協(xié)議比如 fastcgi 協(xié)議、uwsgi協(xié)議、grpc、http(高版本協(xié)議)、websocket協(xié)議等。這樣使用 Nginx 框架,我們可以支持多種應(yīng)用服務(wù)(java web、python web等)的反向代理。Nginx 從1.9.0開始,新增加了一個(gè) stream 模塊,用來(lái)實(shí)現(xiàn)四層協(xié)議( TCP 或 UDP 協(xié)議)的轉(zhuǎn)發(fā)、代理或者負(fù)載均衡。這層比較簡(jiǎn)單,只是單純將 TCP 或 UDP 層的流量轉(zhuǎn)發(fā)到上游服務(wù)器中。接下來(lái),我們將分別介紹這兩種反向代理的基本用法。
Nginx 的 proxy 模塊中定義了許多和 cache 相關(guān)的模塊,這是配置 http 請(qǐng)求代理的緩存功能。通常情況下,我們使用 proxy_cache 指令開啟 Nginx 緩存功能,用 proxy_cache_path 指令來(lái)設(shè)置緩存的路徑和其他配置。兩個(gè)指令的用法如下:Syntax: proxy_cache zone | off;Default: proxy_cache off;Context: http, server, locationSyntax: proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time] [purger=on|off] [purger_files=number] [purger_sleep=time] [purger_threshold=time];Default: —Context: httpproxy_cache_path 指令中有較多的參數(shù),部分重要參數(shù)說(shuō)明如下:path: 定義緩存存放的位置;levels: 定義緩存路徑的目錄等級(jí),最多3級(jí)use_temp_path:on: 使用proxy_temp_path定義的目錄off:keys_zone:name: 共享內(nèi)存名size: 共享內(nèi)存大小max_size: 設(shè)置最大的緩存文件大小其余的重要的緩存指令有:proxy_cache_key: 配置緩存的關(guān)鍵字,格式如下:Syntax: proxy_cache_key string;Default: proxy_cache_key $scheme$proxy_host$request_uri;Context: http, server, location示例:proxy_cache_key "$host$request_uri $cookie_user";proxy_cache_valid: 配置緩存什么樣的響應(yīng),緩存多長(zhǎng)時(shí)間。注意,如果只設(shè)置了緩存時(shí)間,只緩存只針對(duì)相應(yīng)碼200, 301和302的請(qǐng)求 。格式如下:Syntax: proxy_cache_valid [code ...] time;Default: —Context: http, server, location示例:proxy_cache_valid 200 302 10m;proxy_cache_valid 404 1m;# 只設(shè)置了緩存時(shí)間,只對(duì)200,301和302有效proxy_cache_valid 5m;proxy_cache_valid 200 302 10m;proxy_cache_valid 301 1h;# any表示所有相應(yīng)碼proxy_cache_valid any 1m;proxy_cache_methods: 對(duì)哪種 method 的請(qǐng)求使用緩存返回響應(yīng)。Syntax: proxy_cache_methods GET | HEAD | POST ...;Default: proxy_cache_methods GET HEAD;Context: http, server, location
LOG 是處理完請(qǐng)求后的日志記錄階段,如 access_log 模塊。Tips: 所有的 Http請(qǐng)求必須都是從上到下,一個(gè)接一個(gè)階段執(zhí)行的。
安裝完成訪問(wèn) http://IP:5601 即可,注意 IP 地址為按照 ELK 的服務(wù)器 IP 地址。
在我們從發(fā)送網(wǎng)絡(luò)情況到顯示網(wǎng)頁(yè)的這個(gè)過(guò)程中,系統(tǒng)主要進(jìn)行了下面幾步操作:(1)進(jìn)行域名解析:系統(tǒng)會(huì)根據(jù)域名找到服務(wù)器的IP地址;(2)建立TCP連接:確保數(shù)據(jù)可以有效的傳輸;(3)客戶端發(fā)起 HTTP 請(qǐng)求:TCP建立連接后,客戶端才會(huì)正式發(fā)起 HTTP 請(qǐng)求,帶著請(qǐng)求數(shù)據(jù)發(fā)給服務(wù)器;(4)服務(wù)器響應(yīng)HTTP請(qǐng)求:服務(wù)器會(huì)接收并處理上一步從客戶端發(fā)過(guò)來(lái)的數(shù)據(jù),不管是否處理成功,都會(huì)返回一個(gè)響應(yīng)消息給客戶端,包括 HTML 文件或者其他格式的數(shù)據(jù),還有響應(yīng)狀態(tài)碼等,響應(yīng)狀態(tài)碼是判斷我們是否請(qǐng)求成功最直觀的數(shù)據(jù),我們最常見(jiàn)的有200 OK請(qǐng)求成功、404 Not Found 請(qǐng)求失敗。(5)瀏覽器解析 HTML 文件:瀏覽器拿到html文件后,就會(huì)開始解析并渲染其中的html代碼,將相應(yīng)的頁(yè)面顯示給用戶。
告知服務(wù)端客戶側(cè)能夠處理的媒體類型,一般是 類型/子類型的格式,支持多種類型根據(jù)優(yōu)先級(jí)排序。GET /9_Q4simg2RQJ8t7jm9iCKT-xh_/s.gif HTTP/1.1Accept: image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5
Spring Security 作為一種安全框架,本身并不處理是否使用 HTTP 方式連接,也就是說(shuō)它不能直接建立 HTTPS 連接。但是它提供了一系列方法,使我們的 HTTPS 操作更加遍歷。
接下來(lái)講到的一種是服務(wù)端代理的方式。要問(wèn)為什么采取服務(wù)端代理的方式呢?很簡(jiǎn)單,因?yàn)闉g覽器端 Ajax 請(qǐng)求有跨域的限制,那我們就把請(qǐng)求不同域的操作放在服務(wù)端好了,畢竟服務(wù)端是沒(méi)有跨域限制這一說(shuō)的。3.2.1 服務(wù)端代理原理瀏覽器端發(fā)送請(qǐng)求到同域的服務(wù)端;服務(wù)端接收到請(qǐng)求之后,進(jìn)行轉(zhuǎn)發(fā),請(qǐng)求不同域的另外一個(gè)服務(wù)端;服務(wù)端間進(jìn)行交互數(shù)據(jù)后,同域服務(wù)端返回?cái)?shù)據(jù)給瀏覽器端。3.2.2 具體例子舉一個(gè)服務(wù)端代理的例子,這里我使用了一個(gè) Express 的中間件,叫做 express-http-proxy 。當(dāng)然同學(xué)們也可以在同域服務(wù)端接收到請(qǐng)求的時(shí)候,發(fā)起 http 請(qǐng)求訪問(wèn)不同域的服務(wù)端來(lái)模擬這一代理行為。前端方面我使用了 jQuery 的 Ajax 方法。3.2.2.1 javaScript 關(guān)鍵代碼$.ajax({ url: '/proxy/proxy_get', method: 'GET', data: { a: '123', b: '234' }}).done(data => { console.log(data)})很簡(jiǎn)單,我們就是向同域的服務(wù)器發(fā)送了一個(gè)請(qǐng)求。3.2.2.2 同域服務(wù)器關(guān)鍵代碼const proxy = require('express-http-proxy'); // 引入代理中間件// ... 一些代碼app.use('/proxy', proxy('http://localhost:8082/')); // 注冊(cè),之后 /proxy 都會(huì)代理到 http://localhost:8082/ 上3.2.2.3 不同域的服務(wù)器關(guān)鍵代碼router.get("/proxy_get", function(req, res) { const {a, b} = req.query res.send(`參數(shù)是:${a} 和 $`)});這是目標(biāo)服務(wù)器的響應(yīng)方法,返回一個(gè) 處理后的字符串。3.2.2.4 效果3.2.3 服務(wù)端代理小結(jié)服務(wù)端代理通過(guò)服務(wù)端和服務(wù)端之間的交互來(lái)避免瀏覽器和不同域的服務(wù)端之間直接進(jìn)行交互,從而避免了跨域的問(wèn)題。當(dāng)然這種方法要求我們有一個(gè)中間服務(wù)器的存在。
from flask import Flaskfrom datetime import timedeltaapp = Flask(__name__)app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1) app.config['SECRET_KEY'] = 'hard to guess string'在程序 app.py 中創(chuàng)建 Flask 實(shí)例 app,并進(jìn)行兩項(xiàng)配置:config[‘SEND_FILE_MAX_AGE_DEFAULT’],配置緩存的有效時(shí)間;config[‘SECRET_KEY’],在程序中使用到了 Session,需要使用 SECRET_KEY 進(jìn)行加密。
在前面介紹完 post-read、server-rewrite、find-config、rewrite 和 post-rewrite 階段后,我們將繼續(xù)學(xué)習(xí) preaccess 和 access 兩個(gè)階段,中間會(huì)涉及部分模塊,一同進(jìn)行說(shuō)明。
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="30dp"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_marginTop="37dp" android:text="錄音" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@+id/button" android:layout_centerHorizontal="true" android:text="停止" /> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@+id/button2" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:text="回放" /> <Button android:id="@+id/button4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/button2" android:layout_centerHorizontal="true" android:layout_marginTop="10dp" android:text="結(jié)束回放" /></RelativeLayout>對(duì)于兩大功能:“錄音”和“回放”,各提供兩個(gè)按鍵控制,分別對(duì)應(yīng)開始和結(jié)束。最終編譯效果:
可能部分同學(xué)會(huì)有疑問(wèn),上面的兩段請(qǐng)求內(nèi)容跟瀏覽器的截圖不一樣,原因是我上面的是 Http 協(xié)議標(biāo)準(zhǔn)的定義,瀏覽器畢竟面向的是用戶,對(duì)請(qǐng)求的信息有做了解析,更人性化地展示了請(qǐng)求的內(nèi)容。下面的內(nèi)容是某個(gè)后臺(tái)服務(wù)接收到瀏覽器請(qǐng)求的具體報(bào)文信息,借助 TCPflow 這個(gè)工具在 Linux 服務(wù)器上面監(jiān)聽(tīng)的?;蛘吆?jiǎn)單的 curl -v http://www.baidu.com/ 也可以看到協(xié)議內(nèi)容。
realip 模塊默認(rèn)沒(méi)有被編譯進(jìn) Nginx 的,我們需要在源碼編譯階段使用–with-http_realip_module,將 realip 模塊編譯進(jìn)來(lái)后方可使用。接下來(lái),我們做個(gè)簡(jiǎn)單測(cè)試,首先準(zhǔn)備一個(gè) server 塊如下:server { listen 8007; server_name localhost; set_real_ip_from 218.19.206.164; real_ip_recursive off; # real_ip_recursive on; real_ip_header X-Forwarded-For; location / { return 200 "client real ip: $remote_addr\n"; }}首先,我們將 real_ip_recursive 設(shè)置為 off,然后做一次請(qǐng)求:$ curl -H "X-Forwarded-For: 1.1.1.1,218.19.206.164" http://主機(jī)ip:8007 client real ip: 218.19.206.164 這里返回的是頭部參數(shù) X-Forwarded-For 中最后一個(gè) ip,如果將 real_ip_recursive 設(shè)置為 on,此時(shí),由于 set_real_ip_from 中設(shè)置218.19.206.164為信任的方向代理 ip,那么 Nginx 會(huì)往前找一位,認(rèn)為 1.1.1.1 是用戶的真實(shí)ip。$ ./nginx -s reload$ curl -H "X-Forwarded-For: 1.1.1.1,218.19.206.164" http://主機(jī)ip:8007 client real ip: 1.1.1.1
端口號(hào)是用 16 bit 無(wú)符號(hào)整數(shù)表示的,取值范圍是 0~65535,總共可以分配 65536 個(gè)端口號(hào)。端口號(hào)屬于稀缺資源,是由 Internet Assigned Numbers Authority (IANA)統(tǒng)一管理和分配的。端口號(hào)當(dāng)前分配狀況:0 ~ 1023此區(qū)間內(nèi)的端口號(hào)叫做知名端口號(hào),已經(jīng)被系統(tǒng)或者是一些知名的服務(wù)所占用,比如:端口號(hào)用途20 , 21用于 FTP 協(xié)議23用于 telnet 協(xié)議80用于著名的 HTTP 服務(wù)443用于 HTTPS 服務(wù)1023 ~ 65535此區(qū)間端口號(hào)也有很多被知名的應(yīng)用占有,比如:端口號(hào)用途1433用于 SQL Server 服務(wù)等等1935用于 RTMP 服務(wù)3306用于 MySQL 服務(wù)8080作為 HTTP 服務(wù)的另外一個(gè)端口號(hào)
列表布局類似 ListView 里面的 item 布局,但是由于 ExpandableListView 有主類和子類區(qū)分,所以這里需要提供兩套布局以適應(yīng)主列表和展開后的子列表:主列表布局 list_group.xml : <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/listTitle" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft" android:paddingTop="10dp" android:paddingBottom="10dp" android:textColor="@android:color/black" />為了突出大分類,字體設(shè)置為黑體。子列表布局 list_item.xml :<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/expandedListItem" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft" android:paddingTop="10dp" android:paddingBottom="10dp" />
直接在程序入口,也就是 main 函數(shù)所在的文件中導(dǎo)入 pprof。 import _ "net/http/pprof"示例代碼如下:package mainimport ( "fmt" "net/http" _ "net/http/pprof" "time")func main() { //打印數(shù)字 go printNum() //打印字符 go printChar() http.ListenAndServe("0.0.0.0:9300", nil)//啟動(dòng)一個(gè)服務(wù)用于查看性能分析可視化頁(yè)面}func printChar() { for i := '0'; ; i++ { fmt.Println("printChar:", string(i)) time.Sleep(time.Second) }}func printNum() { for i := 0; ; i++ { fmt.Println("printNum:", i) time.Sleep(time.Second) }}上述代碼啟動(dòng),pprof 會(huì)在這個(gè)服務(wù)上自動(dòng)創(chuàng)建路由:debug/pprof/在瀏覽器中輸入127.0.0.1:9300/debug/pprof/,會(huì)出現(xiàn)如下頁(yè)面:這個(gè)路由下還有幾個(gè)子頁(yè)面:allocs:內(nèi)存分配情況;block:獲取導(dǎo)致阻塞的 goroutine 堆棧(如 channel, mutex 等);cmdline:當(dāng)前程序激活的命令行啟動(dòng)參數(shù);goroutine:當(dāng)前當(dāng)前運(yùn)行的 goroutine 的堆棧信息;heap:存活對(duì)象的內(nèi)存分配情況;mutex :互斥鎖的競(jìng)爭(zhēng)持有者的堆棧跟蹤;profile:默認(rèn)進(jìn)行 30s 的 CPU Profiling,得到一個(gè)分析用的 profile 文件;threadcreate:操作系統(tǒng)線程跟蹤;trace:得到一個(gè)分析用的 trace 文件。
在 Python 爬蟲中,我們使用的最多的就是 requests 庫(kù), 截止到 2020年6月,request 庫(kù)最新的版本為 v2.24.0。來(lái)看放放文檔介紹:Requests is an elegant and simple HTTP library for Python, built for human beings.Requests 是 Python 中的一個(gè)簡(jiǎn)潔優(yōu)雅的第三方庫(kù),且其比較符合人們的使用習(xí)慣,這也是大部分人會(huì)使用 Requests 來(lái)模擬 Http 請(qǐng)求的原因。接下來(lái)我們會(huì)從使用和源碼角度來(lái)談一談 Requests 庫(kù)。
Response 類主要是封裝了前面請(qǐng)求的響應(yīng)結(jié)果,爬蟲的一個(gè)很重要的部分就是解析這些 Response,得到我們想要的結(jié)果。這一部分內(nèi)容我們就來(lái)深入分析 Response 類以及擴(kuò)展類。Response類相關(guān)代碼翻看源碼,我們可以得到如下信息:__init__.py 中定義了 Response 基類;text.pym 中定義的 TextResponse 類直接繼承 Response 類并進(jìn)行了一系列擴(kuò)展和重載部分方法;html.py 和 xml.py 中分別定義的 HtmlResponse 和 XmlResponse 都只繼承了 TextResponse ,并未做過(guò)多的修改,只是分別取了個(gè)別名:# 源碼位置:scrapy/http/response/html.pyfrom scrapy.http.response.text import TextResponseclass HtmlResponse(TextResponse): pass# 源碼位置:scrapy/http/response/xml.pyfrom scrapy.http.response.text import TextResponseclass XmlResponse(TextResponse): pass接下來(lái)我們的重點(diǎn)就是學(xué)習(xí) Response 類和 TextResponse 類。Response 類有如下幾個(gè)常用屬性值:headers:頭部信息;status:返回狀態(tài)碼;body:響應(yīng)內(nèi)容;url:請(qǐng)求的 url;request:對(duì)應(yīng)的 request 請(qǐng)求;ip_address:請(qǐng)求的 ip 地址。我們還是通過(guò) Scrapy Shell 請(qǐng)求廣州鏈家二手房的地址來(lái)看看真實(shí)的 Response 并打印上述值:(scrapy-test) [root@server ~]# scrapy shell https://gz.lianjia.com/ershoufang/ --nolog[s] Available Scrapy objects:[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)[s] crawler <scrapy.crawler.Crawler object at 0x7f9890d5a100>[s] item {}[s] request <GET https://gz.lianjia.com/ershoufang/>[s] response <200 https://gz.lianjia.com/ershoufang/>[s] settings <scrapy.settings.Settings object at 0x7f9890d57bb0>[s] spider <DefaultSpider 'default' at 0x7f989034dd90>[s] Useful shortcuts:[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)[s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help)[s] view(response) View response in a browser>>> response.headers{b'Server': [b'Lianjia'], b'Date': [b'Sun, 12 Jul 2020 07:37:16 GMT'], b'Content-Type': [b'text/html; charset=UTF-8'], b'Vary': [b'Accept-Encoding'], b'Set-Cookie': [b'select_city=440100; expires=Mon, 13-Jul-2020 07:37:16 GMT; Max-Age=86400; path=/; domain=.lianjia.com', b'lianjia_ssid=a0980b19-93f6-4942-a898-96ea722d524d; expires=Sun, 12-Jul-20 08:07:16 GMT; Max-Age=1800; domain=.lianjia.com; path=/', b'lianjia_uuid=12165c9c-6c66-4996-9e2c-623a838efd4a; expires=Wed, 10-Jul-30 07:37:16 GMT; Max-Age=315360000; domain=.lianjia.com; path=/'], b'Via': [b'web05-online.zeus.ljnode.com']}>>> response.status200>>> response.url'https://gz.lianjia.com/ershoufang/'>>> response.ip_addressIPv4Address('211.159.232.241')>>> >>> response.request<GET https://gz.lianjia.com/ershoufang/>>>> 注意:關(guān)于這個(gè) response,我們前面在分析 scrapy shell [url] 命令的執(zhí)行過(guò)程中說(shuō)過(guò),如果命令后面帶上要爬取的 URL 地址,那么在交互式的 shell 生成前,會(huì)將一些得到的基本的環(huán)境變量包括請(qǐng)求 URL 的響應(yīng)結(jié)果 (response) 放到該環(huán)境變量中,這就是為什么我們能在該交互模式下直接使用 response 獲取請(qǐng)求結(jié)果的原因。來(lái)看看 Response 類中預(yù)留的一些方法:# 源碼位置:scrapy/http/response/__init__.py# ...class Response(object_ref): def __init__(self, url, status=200, headers=None, body=b'', flags=None, request=None, certificate=None, ip_address=None): self.headers = Headers(headers or {}) self.status = int(status) self._set_body(body) self._set_url(url) self.request = request self.flags = [] if flags is None else list(flags) self.certificate = certificate self.ip_address = ip_address # ... @property def text(self): """For subclasses of TextResponse, this will return the body as str """ raise AttributeError("Response content isn't text") def css(self, *a, **kw): """Shortcut method implemented only by responses whose content is text (subclasses of TextResponse). """ raise NotSupported("Response content isn't text") def xpath(self, *a, **kw): """Shortcut method implemented only by responses whose content is text (subclasses of TextResponse). """ raise NotSupported("Response content isn't text") # ...上面這些預(yù)留的 text 屬性、css() 方法以及 xpath() 方法都會(huì)在 TextResponse 中有相應(yīng)的實(shí)現(xiàn)。接下來(lái)我們仔細(xì)分析 TextResponse 的這些屬性和的方法:# 源碼位置: class TextResponse(Response): _DEFAULT_ENCODING = 'ascii' _cached_decoded_json = _NONE def __init__(self, *args, **kwargs): self._encoding = kwargs.pop('encoding', None) self._cached_benc = None self._cached_ubody = None self._cached_selector = None super(TextResponse, self).__init__(*args, **kwargs) # ...從 __init__() 方法中可以看到,TextResponse 的屬性和父類基本沒(méi)變化,只是增加了一些用于緩存的屬性。接下來(lái)我們?cè)倏磶讉€(gè)重要的屬性和方法:# ...class TextResponse(Response): .... @property def text(self): """ Body as unicode """ # access self.encoding before _cached_ubody to make sure # _body_inferred_encoding is called benc = self.encoding if self._cached_ubody is None: charset = 'charset=%s' % benc self._cached_ubody = html_to_unicode(charset, self.body)[1] return self._cached_ubody # ...上面這段代碼的邏輯就是將 body 屬性中的值轉(zhuǎn)成 str,我們可以在 Scrapy Shell 模式下復(fù)現(xiàn)這一操作:(scrapy-test) [root@server ~]# scrapy shell https://www.baidu.com --nolog[s] Available Scrapy objects:[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)[s] crawler <scrapy.crawler.Crawler object at 0x7faf4f318190>[s] item {}[s] request <GET https://www.baidu.com>[s] response <200 https://www.baidu.com>[s] settings <scrapy.settings.Settings object at 0x7faf4f315b50>[s] spider <DefaultSpider 'default' at 0x7faf4e9122b0>[s] Useful shortcuts:[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)[s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help)[s] view(response) View response in a browser>>> response.bodyb'<!DOCTYPE html>\r\n<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>\xe7\x99\xbe\xe5\xba\xa6\xe4\xb8\x80\xe4\xb8\x8b\xef\xbc\x8c\xe4\xbd\xa0\xe5\xb0\xb1\xe7\x9f\xa5\xe9\x81\x93</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=\xe7\x99\xbe\xe5\xba\xa6\xe4\xb8\x80\xe4\xb8\x8b class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>\xe6\x96\xb0\xe9\x97\xbb</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>\xe5\x9c\xb0\xe5\x9b\xbe</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>\xe8\xa7\x86\xe9\xa2\x91</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>\xe8\xb4\xb4\xe5\x90\xa7</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>\xe7\x99\xbb\xe5\xbd\x95</a> </noscript> <script>document.write(\'<a + encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ \'" name="tj_login" class="lb">\xe7\x99\xbb\xe5\xbd\x95</a>\');\r\n </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">\xe6\x9b\xb4\xe5\xa4\x9a\xe4\xba\xa7\xe5\x93\x81</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>\xe5\x85\xb3\xe4\xba\x8e\xe7\x99\xbe\xe5\xba\xa6</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>\xe4\xbd\xbf\xe7\x94\xa8\xe7\x99\xbe\xe5\xba\xa6\xe5\x89\x8d\xe5\xbf\x85\xe8\xaf\xbb</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>\xe6\x84\x8f\xe8\xa7\x81\xe5\x8f\x8d\xe9\xa6\x88</a> \xe4\xba\xacICP\xe8\xaf\x81030173\xe5\x8f\xb7 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n'>>> type(response.body)<class 'bytes'>>>> from w3lib.encoding import html_to_unicode>>> html_to_unicode("charset=None", response.body)('utf-8', '<!DOCTYPE html>\r\n<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新聞</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地圖</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>視頻</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>貼吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登錄</a> </noscript> <script>document.write(\'<a + encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ \'" name="tj_login" class="lb">登錄</a>\');\r\n </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多產(chǎn)品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>關(guān)于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必讀</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意見(jiàn)反饋</a> 京ICP證030173號(hào) <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n')可以看到 ,Response 中的 body 屬性值是 bytes 類型,通過(guò) html_to_unicode() 方法可以將其轉(zhuǎn)成 str,然后我們得到的網(wǎng)頁(yè)文本就是 str 類型:>>> text = html_to_unicode("charset=None", response.body)>>> type(text[1])<class 'str'>接下來(lái)的這三個(gè)方法我們?cè)谏弦还?jié)介紹過(guò),正是由于有了這些屬性和方法,我們便可以使用 response.xpath() 或者 response.css() 這樣的寫法提取網(wǎng)頁(yè)數(shù)據(jù)。# ...class TextResponse(Response): .... @property def selector(self): from scrapy.selector import Selector if self._cached_selector is None: self._cached_selector = Selector(self) return self._cached_selector def xpath(self, query, **kwargs): return self.selector.xpath(query, **kwargs) def css(self, query): return self.selector.css(query)TextResponse 類比較重要的屬性和方法就這些,其他的則需要自行深入去研究相關(guān)的方法及其作用。我們現(xiàn)在來(lái)解答上一節(jié)提出的問(wèn)題:為什么 Scrapy 的 TextResponse 實(shí)例可以使用這樣的表達(dá)式:response.xpath(...).extrat()[0] 或者 response.xpath(...).extrat_first()?接下來(lái)我們帶著這個(gè)問(wèn)題來(lái)繼續(xù)追蹤下代碼。我們以上一節(jié)的例子為例,打印 response.xpath() 的返回類型:(scrapy-test) [root@server ~]# scrapy shell https://gz.lianjia.com/ershoufang/ --nolog...>>> data = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()')>>> type(data)<class 'scrapy.selector.unified.SelectorList'>>>> 可以看到結(jié)果是 SelectorList 實(shí)例,我們來(lái)看對(duì)應(yīng)定義的代碼:# 源碼位置:scrapy/selector/unified.pyfrom parsel import Selector as _ParselSelector# ...class SelectorList(_ParselSelector.selectorlist_cls, object_ref): """ The :class:`SelectorList` class is a subclass of the builtin ``list`` class, which provides a few additional methods. """它直接繼承的是 parsel 模塊中的 selectorlist_cls。繼續(xù)看這個(gè)值的定義:# 源碼位置:parsel/selector.pyclass Selector(object): # ... selectorlist_cls = SelectorList # ...# 源碼位置:parsel/selector.pyclass SelectorList(list): # ... def getall(self): """ Call the ``.get()`` method for each element is this list and return their results flattened, as a list of unicode strings. """ return [x.get() for x in self] extract = getall def get(self, default=None): """ Return the result of ``.get()`` for the first element in this list. If the list is empty, return the default value. """ for x in self: return x.get() return default extract_first = get是不是找到了 extract() 和 extract_first() 方法?注意理解這段代碼:for x in self: return x.get()return defaultself 表示的是 SelectorList 的實(shí)例,它其實(shí)也是一個(gè)列表,列表中的元素是 Selector 的實(shí)例。這個(gè) for 循環(huán)相當(dāng)于取的是一個(gè)元素,然后直接返回,返回的值是 x.get(),這里又會(huì)涉及 Selector 類的 get() 方法 :# 源碼位置:parsel/selector.pyfrom lxml import etree, html# ...class Selector(object): # ... def get(self): """ Serialize and return the matched nodes in a single unicode string. Percent encoded content is unquoted. """ try: return etree.tostring(self.root, method=self._tostring_method, encoding='unicode', with_tail=False) except (AttributeError, TypeError): if self.root is True: return u'1' elif self.root is False: return u'0' else: return six.text_type(self.root) # ...我們可以同樣在 Scrapy Shell 中來(lái)繼續(xù)做個(gè)測(cè)試:>>> data_list = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()')>>> type(data_list)<class 'scrapy.selector.unified.SelectorList'>>>> data = data_list[0]>>> type(data)<class 'scrapy.selector.unified.Selector'>>>> data.get()'地鐵口 總價(jià)低 精裝實(shí)用小兩房'Selector 的 get() 方法最后提取出了我們匹配的文本,因此在 SelectorList 中的 extract()[0] 和 ``extract_first()` 方法將得到同樣的結(jié)果:>>> response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()').extract_first()'地鐵口 總價(jià)低 精裝實(shí)用小兩房'>>> response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()').extract()[0]'地鐵口 總價(jià)低 精裝實(shí)用小兩房'這樣一步步追蹤和實(shí)驗(yàn),源碼里面很多的語(yǔ)句就會(huì)清晰明了,我們?cè)谑褂?Request 和 Response 類時(shí)便會(huì)顯得更加得心應(yīng)手。Request 實(shí)例化時(shí)需要哪些參數(shù),Response 的實(shí)例有哪些方法可用, 這些疑惑在源碼面前都會(huì)迎刃而解。
1. 創(chuàng)建工程2. 引入依賴<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <!-- Spring jdbc 使用的依賴--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency> </dependencies>3. 準(zhǔn)備代碼實(shí)體類代碼/** * 賬戶的實(shí)體類 */public class Account implements Serializable { //數(shù)據(jù)id private Integer id; //賬號(hào)編碼 private String accountNum; //賬號(hào)金額 private Float money;}持久層接口代碼/** * 賬戶的持久層接口 */public interface IAccountDao { /** * 根據(jù)Id查詢賬戶 * @param accountId * @return */ Account findAccountById(Integer accountId); /** * 保存賬戶 * @param account */ void saveAccount(Account account); /** * 更新賬戶 * @param account */ void updateAccount(Account account);}持久層實(shí)現(xiàn)類代碼/** * 賬戶的持久層實(shí)現(xiàn)類 */@Repositorypublic class AccountDaoImpl implements IAccountDao { //jdbc模板類屬性 @Autowired private JdbcTemplate jdbcTemplate; //根據(jù)id查找 public Account findAccountById(Integer accountId) { List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId); return accounts.isEmpty()?null:accounts.get(0); } public void saveAccount(Account account) { jdbcTemplate.update("insert into account values(?,?,?)", account.getId(),account.getAccountNum(),account.getMoney()); } public void updateAccount(Account account) { jdbcTemplate.update("update account set accountnum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }}業(yè)務(wù)層接口代碼/** * @Auther: wyan */public interface UserService { /** * 賬戶轉(zhuǎn)賬 * @param fromId toId */ public void transMoney(Integer fromId, Integer toId, Integer money);}業(yè)務(wù)層實(shí)現(xiàn)類代碼/** * @Auther: wyan * @Description: */@Service@Transactionalpublic class UserServiceImpl implements UserService { @Autowired private IAccountDao accountDao; public void transMoney(Integer fromId, Integer toId, Integer money) { Account fromAccount = accountDao.findAccountById(fromId); Account toAccount = accountDao.findAccountById(toId); //原始賬號(hào)減錢 fromAccount.setMoney(fromAccount.getMoney()-money); accountDao.updateAccount(fromAccount); //拋出異常 int i=1/0; //轉(zhuǎn)賬賬號(hào)加錢 toAccount.setMoney(toAccount.getMoney()+money); accountDao.updateAccount(toAccount); }}Tips: 此時(shí)需要注意注解 @Transactional 的含義。Transactional 就是表示事務(wù),那么在此類上面加入注解,說(shuō)明需要 Spring 框架針對(duì)此類的方法做事務(wù)的增強(qiáng)行為,也就是說(shuō)此注解其實(shí)是相當(dāng)于我們?cè)谂渲梦募信渲玫墓?jié)點(diǎn) tx:advice。那么這時(shí)候有的細(xì)心的同學(xué)可能會(huì)有些疑問(wèn):我們?cè)?xml 文件中可以配置事務(wù)的傳播行為與隔離級(jí)別,那么這一個(gè)注解如何制定事務(wù)的傳播行為與隔離級(jí)別呢?一個(gè)類中如果定義方法過(guò)多,而實(shí)際上需要增強(qiáng)控制事務(wù)的方法只有一部分,如何縮小粒度,只控制需要事務(wù)的方法呢?ok,大家。這里有必要跟大家解釋下此注解的其余使用方式:?jiǎn)栴}一答疑:在注解后面可以通過(guò)括號(hào)內(nèi)的參數(shù)設(shè)置隔離級(jí)別與傳播行為。比如:@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED) 此表達(dá)式的含義是事務(wù)一定需要,并且是讀已提交。問(wèn)題二答疑:在方法上使用注解。類上面可以不使用 @Transactional 注解,而是將注解寫在需要用到事務(wù)的方法之上。4. 配置文件<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--配置JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置數(shù)據(jù)源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///transmoney"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--包路徑掃描--> <context:component-scan base-package="com.offcn"></context:component-scan> <!--事務(wù)管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置數(shù)據(jù)源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///transmoney"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--包路徑掃描--> <context:component-scan base-package="com.offcn"></context:component-scan> <!--事務(wù)管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--注解事務(wù)驅(qū)動(dòng)--> <tx:annotation-driven/></beans>Tips: 此處需要注意 tx:annotation-driven 節(jié)點(diǎn)無(wú)需配置通知節(jié)點(diǎn)與切面節(jié)點(diǎn),而是使用 tx:annotation-driven 節(jié)點(diǎn)表示,事務(wù)的支持方式為聲明式事務(wù)。5. 測(cè)試代碼public class AccountServiceTest { public static void main(String[] args) { //1.獲取容器 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //2.獲取業(yè)務(wù)對(duì)象 UserService userService = ac.getBean(UserService.class); //3.從id為1的賬號(hào)轉(zhuǎn)成1000到2賬號(hào) userService.transMoney(1,2,1000); System.out.println("轉(zhuǎn)賬完成.."); }}6. 測(cè)試結(jié)果:ok, 大家,我們繼續(xù)測(cè)試之前的轉(zhuǎn)賬代碼,依然得到錯(cuò)誤的異常信息。同時(shí)數(shù)據(jù)庫(kù)的金額并沒(méi)有發(fā)生改變,因?yàn)槭聞?wù)的控制,保證了數(shù)據(jù)的一致性原子性。那么也證明我們聲明式事務(wù)的案例測(cè)試成功。
SERVER_REWRITE 和后面的 REWRITE 階段一般是使用 rewrite 模塊修改 Http請(qǐng)求的 uri,實(shí)現(xiàn)請(qǐng)求的控制。
Http 規(guī)定了會(huì)話是由客戶端發(fā)起,服務(wù)端響應(yīng)。發(fā)起和響應(yīng)的消息格式如下:
啟動(dòng) Nginx 非常簡(jiǎn)單,在終端輸入 Nginx 就可以了:nginx我們打開瀏覽器輸入http://localhost,就可以看到 Nginx 的歡迎頁(yè)面了!
指的是 HTTP 的 Headers 部分比較容易擴(kuò)展。只需要在頭部中加入擴(kuò)展的新功能,就可以很方便的實(shí)現(xiàn)擴(kuò)展。