Nginx從聽說到學(xué)會

第一章 Nginx簡介

Nginx是什么

沒有聽過Nginx膝擂?那么一定聽過它的“同行”Apache吧允趟!Nginx同Apache一樣都是一種WEB服務(wù)器丹弱。基于REST架構(gòu)風(fēng)格悯森,以統(tǒng)一資源描述符(Uniform Resources Identifier)URI或者統(tǒng)一資源定位符(Uniform Resources Locator)URL作為溝通依據(jù),通過HTTP協(xié)議提供各種網(wǎng)絡(luò)服務(wù)义郑。

然而喳坠,這些服務(wù)器在設(shè)計之初受到當(dāng)時環(huán)境的局限,例如當(dāng)時的用戶規(guī)模脱茉,網(wǎng)絡(luò)帶寬懦傍,產(chǎn)品特點等局限并且各自的定位和發(fā)展都不盡相同。這也使得各個WEB服務(wù)器有著各自鮮明的特點芦劣。

Apache的發(fā)展時期很長粗俱,而且是毫無爭議的世界第一大服務(wù)器。它有著很多有點:穩(wěn)定虚吟、開源寸认、跨平臺等等。但是由于它出現(xiàn)的時間太長了串慰。它興起的年代偏塞,互聯(lián)網(wǎng)產(chǎn)業(yè)遠(yuǎn)比不上現(xiàn)在。所以它被設(shè)計為一個重量級的邦鲫。不支持高并發(fā)的服務(wù)器灸叼。在Apache上運(yùn)行數(shù)以萬計的并發(fā)訪問,會導(dǎo)致服務(wù)器消耗大量內(nèi)存庆捺。操作系統(tǒng)對其進(jìn)行進(jìn)程或線程間的切換也消耗了大量的CPU資源古今,導(dǎo)致HTTP請求的平均響應(yīng)速度降低。

這些都決定了Apache不可能成為高性能WEB服務(wù)器滔以,輕量級高并發(fā)服務(wù)器Nginx和Lighttpd就應(yīng)運(yùn)而生了捉腥。

Nginx產(chǎn)生

又是拜大神的時候了,這次被選中的人是俄羅斯的工程師Igor Sysoev你画,他在為Rambler Media工作期間抵碟,使用C語言開發(fā)了Nginx桃漾。Nginx作為WEB服務(wù)器一直為Rambler Media提供出色而又穩(wěn)定的服務(wù)。

然后呢拟逮,Igor Sysoev將Nginx代碼開源撬统,并且賦予自由軟件許可證。

由于:

  • Nginx使用基于事件驅(qū)動架構(gòu)敦迄,使得其可以支持?jǐn)?shù)以百萬級別的TCP連接
  • 高度的模塊化和自由軟件許可證是的第三方模塊層出不窮(這是個開源的時代啊~)
  • Nginx是一個跨平臺服務(wù)器恋追,可以運(yùn)行在Linux, FreeBSD, Solaris, AIX, Mac OS, Windows等操作系統(tǒng)上
  • 這些優(yōu)秀的設(shè)計帶來的極大的穩(wěn)定性。

于是颅崩,duang的一下几于。Nginx火了。

三大WEB服務(wù)器對比

lighttpd

Lighttpd是一個具有非常低的內(nèi)存開銷沿后,cpu占用率低沿彭,效能好,以及豐富的模塊等特點尖滚。lighttpd是眾多OpenSource輕量級的web server中較為優(yōu)秀的一個喉刘。支持FastCGI, CGI, Auth, 輸出壓縮(output compress), URL重寫, Alias等重要功能。

Lighttpd使用fastcgi方式運(yùn)行PHP,它會使用很少的PHP進(jìn)程響應(yīng)很大的并發(fā)量漆弄。

Fastcgi的優(yōu)點在于:

  • 從穩(wěn)定性上看, fastcgi是以獨立的進(jìn)程池運(yùn)行來cgi,單獨一個進(jìn)程死掉,系統(tǒng)可以很輕易的丟棄,然后重新分配新的進(jìn)程來運(yùn)行邏輯.
  • 從安全性上看, fastcgi和宿主的server完全獨立, fastcgi怎么down也不會把server搞垮,
  • 從性能上看, fastcgi把動態(tài)邏輯的處理從server中分離出來, 大負(fù)荷的IO處理還是留給宿主server, 這樣宿主server可以一心一意作IO,對于一個普通的動態(tài)網(wǎng)頁來說, 邏輯處理可能只有一小部分, 大量的圖片等靜態(tài)IO處理完全不需要邏輯程序的參與
  • 從擴(kuò)展性上講, fastcgi是一個中立的技術(shù)標(biāo)準(zhǔn), 完全可以支持任何語言寫的處理程序php,Java,Python

Apache

apache是世界排名第一的web服務(wù)器, 根據(jù)netcraft所作的調(diào)查,世界上百分之五十以上的web服務(wù)器在使用apache.

1995年4月, 最早的apache(0.6.2版)由apache group公布發(fā)行. apache group 是一個完全通過internet進(jìn)行運(yùn)作的非盈利機(jī)構(gòu), 由它來決定apache web服務(wù)器的標(biāo)準(zhǔn)發(fā)行版中應(yīng)該包含哪些內(nèi)容. 準(zhǔn)許任何人修改隱錯, 提供新的特征和將它移植到新的平臺上, 以及其它的工作. 當(dāng)新的代碼被提交給apache group時, 該團(tuán)體審核它的具體內(nèi)容, 進(jìn)行測試 如果認(rèn)為滿意, 該代碼就會被集成到apache的主要發(fā)行版中睦裳。

**apache 的特性: **

  • 幾乎可以運(yùn)行在所有的計算機(jī)平臺上
  • 支持最新的http/1.1協(xié)議
  • 簡單而且強(qiáng)有力的基于文件的配置(httpd.conf)
  • 支持通用網(wǎng)關(guān)接口(cgi)
  • 支持虛擬主機(jī)
  • 支持http認(rèn)證
  • 集成perl
  • 集成的代理服務(wù)器
  • 可以通過web瀏覽器監(jiān)視服務(wù)器的狀態(tài), 可以自定義日志
  • 支持服務(wù)器端包含命令(ssi)
  • 支持安全socket層(ssl)
  • 具有用戶會話過程的跟蹤能力
  • 支持fastcgi
  • 支持Java

Nginx

Nginx是俄羅斯人編寫的十分輕量級的HTTP服務(wù)器,Nginx,它的發(fā)音為“engine X”撼唾, 是一個高性能的HTTP和反向代理服務(wù)器廉邑,同時也是一個IMAP/POP3/SMTP 代理服務(wù)器.Nginx是由俄羅斯人 Igor Sysoev為俄羅斯訪問量第二的 Rambler.ru站點開發(fā).

Nginx以事件驅(qū)動的方式編寫,所以有非常好的性能倒谷,同時也是一個非常高效的反向代理蛛蒙、負(fù)載平衡。其擁有匹配 Lighttpd的性能渤愁,同時還沒有Lighttpd的內(nèi)存泄漏問題牵祟,而且Lighttpd的mod_proxy也有一些問題并且很久沒有更新。但是Nginx并不支持cgi方式運(yùn)行抖格,原因是可以減少因此帶來的一些程序上的漏洞诺苹。所以必須使用FastCGI方式來執(zhí)行PHP程序。

nginx做為HTTP服務(wù)器雹拄,有以下幾項基本特性:

  • 處理靜態(tài)文件收奔,索引文件以及自動索引;打開文件描述符緩沖
  • 無緩存的反向代理加速办桨,簡單的負(fù)載均衡和容錯
  • FastCGI筹淫,簡單的負(fù)載均衡和容錯
  • 模塊化的結(jié)構(gòu)。包括gzipping, byte ranges, chunked responses,以及 SSI-filter等filter呢撞。如果由FastCGI或其它代理服務(wù)器處理單頁中存在的多個SSI损姜,則這項處理可以并行運(yùn)行,而不需要相互等待殊霞。

Nginx專為性能優(yōu)化而開發(fā)摧阅,性能是其最重要的考量,實現(xiàn)上非常注重效率。它支持內(nèi)核Poll模型绷蹲,能經(jīng)受高負(fù)載的考驗,有報告表明能支持高達(dá) 50,000個并發(fā)連接數(shù)棒卷。

Nginx具有很高的穩(wěn)定性。其它HTTP服務(wù)器祝钢,當(dāng)遇到訪問的峰值比规,或者有人惡意發(fā)起慢速連接時,也很可能會導(dǎo)致服務(wù)器物理內(nèi)存耗盡頻繁交換拦英,失去響應(yīng)蜒什,只能重啟服務(wù)器。例如當(dāng)前apache一旦上到200個以上進(jìn)程疤估,web響應(yīng)速度就明顯非常緩慢了灾常。而Nginx采取了分階段資源分配技術(shù),使得它的CPU與內(nèi)存占用率非常低铃拇。nginx官方表示保持10,000個沒有活動的連接钞瀑,它只占2.5M內(nèi)存,所以類似DOS這樣的攻擊對nginx來說基本上是毫無用處的慷荔。就穩(wěn)定性而言,nginx比lighthttpd更勝一籌雕什。

Nginx支持熱部署。它的啟動特別容易, 并且?guī)缀蹩梢宰龅?*24不間斷運(yùn)行显晶,即使運(yùn)行數(shù)個月也不需要重新啟動贷岸。你還能夠在不間斷服務(wù)的情況下,對軟件版本進(jìn)行進(jìn)行升級吧碾。

三種服務(wù)器比較

server Apache Nginx Lighttpd
Proxy代理 非常好 非常好 一般
Rewriter 非常好 一般
Fcgi 不好 非常好
熱部署 不支持 支持 不支持
系統(tǒng)壓力比較 很大 很小 比較小
穩(wěn)定性 非常好 不好
安全性 一般 一般
靜態(tài)文件處理 一般 非常好
反向代理 一般 非常好 一般

第二章 Nginx安裝教程

Nginx的安裝

模塊依賴性Nginx需要依賴下面3個包

  1. gzip 模塊需要 zlib 庫 ( 點擊下載 )
  2. rewrite 模塊需要 pcre 庫 ( 點擊下載 )
  3. ssl 功能需要 openssl 庫 ( 點擊下載 )

Nginx包下載: http://nginx.org/en/download.html

依賴包安裝順序依次為:openssl凰盔、zlib、pcre, 最后安裝Nginx包倦春。

圖解教程

第一步: 下載安裝所需包

openssl-fips-2.0.2.tar.gz
zlib-1.2.7.tar.gz
pcre-8.21.tar.gz
nginx-1.2.6.tar.gz

第二步:依次安裝

1.安裝openssl-fips-2.0.2.tar.gz

[root@localhost mrms]# tar -zxvf openssl-fips-2.0.2.tar.gz 
[root@localhost mrms]# cd openssl-fips-2.0.2
[root@localhost openssl-fips-2.0.2]# ./config 
[root@localhost openssl-fips-2.0.2]# make
[root@localhost openssl-fips-2.0.2]# make install

2.安裝zlib-1.2.7.tar.gz

[root@localhost mrms]# tar -zxvf zlib-1.2.7.tar.gz
[root@localhost mrms]# cd zlib-1.2.7
[root@localhost zlib-1.2.7]# ./configure 
[root@localhost zlib-1.2.7]# make
[root@localhost zlib-1.2.7]# make install

3.安裝pcre-8.21.tar.gz

[root@localhost mrms]# tar -zxvf pcre-8.21.tar.gz
[root@localhost mrms]# cd pcre-8.21
[root@localhost pcre-8.21]# ./configure 
[root@localhost pcre-8.21]# make
[root@localhost pcre-8.21]# make install

** 4.安裝 nginx-1.2.6.tar.gz**

[root@localhost mrms]# tar -zxvf nginx-1.2.6.tar.gz 
[root@localhost mrms]# cd nginx-1.2.6
[root@localhost nginx-1.2.6]# ./configure --with-pcre=../pcre-8.21 --with-zlib=../zlib-1.2.7 --with-openssl=../openssl-fips-2.0.2
[root@localhost nginx-1.2.6]# make
[root@localhost nginx-1.2.6]# make install

至此Nginx的安裝完成!

第三步:檢測是否安裝成功

[root@localhost nginx-1.2.6]# cd  /usr/local/nginx/sbin
[root@localhost sbin]# ./nginx -t

出現(xiàn)如下所示提示,表示安裝成功

安裝成功提示

啟動nginx
[root@localhost sbin]# ./nginx
查看端口
[root@localhost sbin]# netstat -ntlp
結(jié)果如下

查看結(jié)果

第三章 Nginx基本概念

靜態(tài)HTTP服務(wù)器

首先户敬,Nginx是一個HTTP服務(wù)器,可以將服務(wù)器上的靜態(tài)文件(如HTML睁本、圖片)通過HTTP協(xié)議展現(xiàn)給客戶端尿庐。
配置:

server {
    listen 80; # 端口號
    location / {
        root /usr/share/nginx/html; # 靜態(tài)文件路徑
    }
}

反向代理服務(wù)器

什么是反向代理?

客戶端本來可以直接通過HTTP協(xié)議訪問某網(wǎng)站應(yīng)用服務(wù)器呢堰,如果網(wǎng)站管理員在中間加上一個Nginx抄瑟,客戶端請求Nginx,Nginx請求應(yīng)用服務(wù)器枉疼,然后將結(jié)果返回給客戶端皮假,此時Nginx就是反向代理服務(wù)器鞋拟。


反向代理配置:

server {
    listen 80;
    location / {
        proxy_pass http://192.168.0.112:8080; # 應(yīng)用服務(wù)器HTTP地址
    }
}

既然服務(wù)器可以直接HTTP訪問,為什么要在中間加上一個反向代理惹资,不是多此一舉嗎贺纲?反向代理有什么作用?繼續(xù)往下看褪测,下面的負(fù)載均衡猴誊、虛擬主機(jī),都基于反向代理實現(xiàn)侮措,當(dāng)然反向代理的功能也不僅僅是這些懈叹。

負(fù)載均衡

當(dāng)網(wǎng)站訪問量非常大,也攤上事兒了分扎。因為網(wǎng)站越來越慢澄成,一臺服務(wù)器已經(jīng)不夠用了。于是將相同的應(yīng)用部署在多臺服務(wù)器上笆包,將大量用戶的請求分配給多臺機(jī)器處理环揽。同時帶來的好處是,其中一臺服務(wù)器萬一掛了庵佣,只要還有其他服務(wù)器正常運(yùn)行歉胶,就不會影響用戶使用。
Nginx可以通過反向代理來實現(xiàn)負(fù)載均衡巴粪。


負(fù)載均衡配置:

upstream myapp {
    server 192.168.0.111:8080; # 應(yīng)用服務(wù)器1
    server 192.168.0.112:8080; # 應(yīng)用服務(wù)器2
}
server {
    listen 80;
    location / {
        proxy_pass http://myweb;
    }
}

虛擬主機(jī)

有的網(wǎng)站訪問量大通今,需要負(fù)載均衡。然而并不是所有網(wǎng)站都如此出色肛根,有的網(wǎng)站辫塌,由于訪問量太小,需要節(jié)省成本派哲,將多個網(wǎng)站部署在同一臺服務(wù)器上臼氨。

例如將www.aaa.comwww.bbb.com兩個網(wǎng)站部署在同一臺服務(wù)器上,兩個域名解析到同一個IP地址芭届,但是用戶通過兩個域名卻可以打開兩個完全不同的網(wǎng)站储矩,互相不影響,就像訪問兩個服務(wù)器一樣褂乍,所以叫兩個虛擬主機(jī)持隧。

配置:

server {
    listen 80 default_server;
    server_name _;
    return 444; # 過濾其他域名的請求,返回444狀態(tài)碼
}
server {
    listen 80;
    server_name www.aaa.com; # www.aaa.com域名
    location / {
        proxy_pass http://localhost:8080; # 對應(yīng)端口號8080
    }
}
server {
    listen 80;
    server_name www.bbb.com; # www.bbb.com域名
    location / {
        proxy_pass http://localhost:8081; # 對應(yīng)端口號8081
    }
}

在服務(wù)器8080和8081分別開了一個應(yīng)用逃片,客戶端通過不同的域名訪問屡拨,根據(jù)server_name可以反向代理到對應(yīng)的應(yīng)用服務(wù)器。

虛擬主機(jī)的原理是通過HTTP請求頭中的Host是否匹配server_name來實現(xiàn)的,有興趣的同學(xué)可以研究一下HTTP協(xié)議呀狼。

另外裂允,server_name配置還可以過濾有人惡意將某些域名指向你的主機(jī)服務(wù)器。

FastCGI

Nginx本身不支持PHP等語言赠潦,但是它可以通過FastCGI來將請求扔給某些語言或框架處理(例如PHP叫胖、Python草冈、Perl)她奥。

server {
    listen 80;
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /PHP文件路徑$fastcgi_script_name; # PHP文件路徑
        fastcgi_pass 127.0.0.1:9000; # PHP-FPM地址和端口號
        # 另一種方式:fastcgi_pass unix:/var/run/php5-fpm.sock;
    }
}

配置中將.php結(jié)尾的請求通過FashCGI交給PHP-FPM處理,PHP-FPM是PHP的一個FastCGI管理器怎棱。有關(guān)FashCGI可以查閱其他資料哩俭,本篇不再介紹。

fastcgi_pass和proxy_pass有什么區(qū)別拳恋?
下面一張圖帶你看明白:

第四章 Nginx常用命令

1. 啟動 Nginx

poechant@ubuntu:sudo ./sbin/nginx

2. 停止 Nginx

poechant@ubuntu:sudo ./sbin/nginx -s stoppoechant@ubuntu:sudo ./sbin/nginx -s quit

-s都是采用向 Nginx 發(fā)送信號的方式凡资。

3. Nginx 重載配置

poechant@ubuntu:sudo ./sbin/nginx -s reload

上述是采用向 Nginx 發(fā)送信號的方式,或者使用:

poechant@ubuntu:service nginx reload

4. 指定配置文件

poechant@ubuntu:sudo ./sbin/nginx -c /usr/local/nginx/conf/nginx.conf

-c表示configuration谬运,指定配置文件隙赁。

5. 查看 Nginx 版本

有兩種可以查看 Nginx 的版本信息的參數(shù)。第一種如下:

poechant@ubuntu:/usr/local/nginx$ ./sbin/nginx -v
nginx: nginx version: nginx/1.0.0

另一種顯示的是詳細(xì)的版本信息:

poechant@ubuntu:/usr/local/nginx$ ./sbin/nginx -V
nginx: nginx version: nginx/1.0.0
nginx: built by gcc 4.3.3 (Ubuntu 4.3.3-5ubuntu4) 
nginx: TLS SNI support enabled
nginx: configure arguments: --with-http_ssl_module --with-openssl=/home/luming/openssl-1.0.0d/

6. 檢查配置文件是否正確

poechant@ubuntu:/usr/local/nginx$ ./sbin/nginx -t
nginx: [alert] could not open error log file: open() "/usr/local/nginx/logs/error.log" failed (13: Permission denied)
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
2012/01/09 16:45:09 [emerg] 23898#0: open() "/usr/local/nginx/logs/nginx.pid" failed (13: Permission denied)
nginx: configuration file /usr/local/nginx/conf/nginx.conf test failed

如果出現(xiàn)如上的提示信息梆暖,表示沒有訪問錯誤日志文件和進(jìn)程伞访,可以sudo(super user do)一下:

poerchant@ubuntu:/usr/local/nginx$ sudo ./sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

如果顯示如上,則表示配置文件正確轰驳。否則厚掷,會有相關(guān)提示。

7. 顯示幫助信息

poechant@ubuntu:/user/local/nginx$ ./sbin/nginx -h

或者:

poechant@ubuntu:/user/local/nginx$ ./sbin/nginx -?

以上這些涵蓋了 Nginx 日常維護(hù)的所有基本操作级解,另外還有向 master 進(jìn)程發(fā)送信號的相關(guān)命令冒黑,我們會在后續(xù)看到。

第五章 初探nginx架構(gòu)

進(jìn)程模型

眾所周知勤哗,nginx性能高抡爹,而nginx的高性能與其架構(gòu)是分不開的。那么nginx究竟是怎么樣的呢芒划?這一節(jié)我們先來初識一下nginx框架吧冬竟。

nginx在啟動后,在unix系統(tǒng)中會以daemon的方式在后臺運(yùn)行腊状,后臺進(jìn)程包含一個master進(jìn)程和多個worker進(jìn)程诱咏。我們也可以手動地關(guān)掉后臺模式,讓nginx在前臺運(yùn)行缴挖,并且通過配置讓nginx取消master進(jìn)程袋狞,從而可以使nginx以單進(jìn)程方式運(yùn)行。

很顯然,生產(chǎn)環(huán)境下我們肯定不會這么做苟鸯,所以關(guān)閉后臺模式同蜻,一般是用來調(diào)試用的,在后面的章節(jié)里面早处,我們會詳細(xì)地講解如何調(diào)試nginx湾蔓。所以,我們可以看到砌梆,nginx是以多進(jìn)程的方式來工作的默责,當(dāng)然nginx也是支持多線程的方式的,只是我們主流的方式還是多進(jìn)程的方式咸包,也是nginx的默認(rèn)方式桃序。nginx采用多進(jìn)程的方式有諸多好處,所以我就主要講解nginx的多進(jìn)程模式吧烂瘫。

剛才講到媒熊,nginx在啟動后,會有一個master進(jìn)程和多個worker進(jìn)程坟比。

master進(jìn)程主要用來管理worker進(jìn)程芦鳍,包含:接收來自外界的信號,向各worker進(jìn)程發(fā)送信號,監(jiān)控worker進(jìn)程的運(yùn)行狀態(tài),當(dāng)worker進(jìn)程退出后(異常情況下)氨鹏,會自動重新啟動新的worker進(jìn)程。而基本的網(wǎng)絡(luò)事件茄茁,則是放在worker進(jìn)程中來處理了。多個worker進(jìn)程之間是對等的巩割,他們同等競爭來自客戶端的請求裙顽,各進(jìn)程互相之間是獨立的。一個請求宣谈,只可能在一個worker進(jìn)程中處理愈犹,一個worker進(jìn)程,不可能處理其它進(jìn)程的請求闻丑。worker進(jìn)程的個數(shù)是可以設(shè)置的漩怎,一般我們會設(shè)置與機(jī)器cpu核數(shù)一致,這里面的原因與nginx的進(jìn)程模型以及事件處理模型是分不開的嗦嗡。nginx的進(jìn)程模型勋锤,可以由下圖來表示:


nginx進(jìn)程模型

nginx進(jìn)程操作

在nginx啟動后,如果我們要操作nginx侥祭,要怎么做呢叁执?

從上文中我們可以看到茄厘,master來管理worker進(jìn)程,所以我們只需要與master進(jìn)程通信就行了谈宛。master進(jìn)程會接收來自外界發(fā)來的信號次哈,再根據(jù)信號做不同的事情。所以我們要控制nginx吆录,只需要通過kill向master進(jìn)程發(fā)送信號就行了窑滞。

比如kill -HUP pid,則是告訴nginx恢筝,從容地重啟nginx哀卫,我們一般用這個信號來重啟nginx,或重新加載配置滋恬,因為是從容地重啟聊训,因此服務(wù)是不中斷的。

master進(jìn)程在接收到HUP信號后是怎么做的呢恢氯?首先master進(jìn)程在接到信號后,會先重新加載配置文件鼓寺,然后再啟動新的worker進(jìn)程勋拟,并向所有老的worker進(jìn)程發(fā)送信號,告訴他們可以光榮退休了妈候。新的worker在啟動后敢靡,就開始接收新的請求,而老的worker在收到來自master的信號后苦银,就不再接收新的請求啸胧,并且在當(dāng)前進(jìn)程中的所有未處理完的請求處理完成后,再退出幔虏。

當(dāng)然纺念,直接給master進(jìn)程發(fā)送信號,這是比較老的操作方式想括,nginx在0.8版本之后陷谱,引入了一系列命令行參數(shù),來方便我們管理瑟蜈。比如烟逊,./nginx -s reload,就是來重啟nginx铺根,./nginx -s stop宪躯,就是來停止nginx的運(yùn)行。如何做到的呢位迂?我們還是拿reload來說访雪,我們看到予颤,執(zhí)行命令時,我們是啟動一個新的nginx進(jìn)程冬阳,而新的nginx進(jìn)程在解析到reload參數(shù)后蛤虐,就知道我們的目的是控制nginx來重新加載配置文件了,它會向master進(jìn)程發(fā)送信號肝陪,然后接下來的動作驳庭,就和我們直接向master進(jìn)程發(fā)送信號一樣了。

事件模型

現(xiàn)在氯窍,我們知道了當(dāng)我們在操作nginx的時候饲常,nginx內(nèi)部做了些什么事情,那么狼讨,worker進(jìn)程又是如何處理請求的呢贝淤?我們前面有提到,worker進(jìn)程之間是平等的政供,每個進(jìn)程播聪,處理請求的機(jī)會也是一樣的。當(dāng)我們提供80端口的http服務(wù)時布隔,一個連接請求過來离陶,每個進(jìn)程都有可能處理這個連接,怎么做到的呢衅檀?

首先招刨,每個worker進(jìn)程都是從master進(jìn)程fork過來,在master進(jìn)程里面哀军,先建立好需要listen的socket(listenfd)之后沉眶,然后再fork出多個worker進(jìn)程。所有worker進(jìn)程的listenfd會在新連接到來時變得可讀杉适,為保證只有一個進(jìn)程處理該連接谎倔,所有worker進(jìn)程在注冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進(jìn)程注冊listenfd讀事件淘衙,在讀事件里調(diào)用accept接受該連接传藏。當(dāng)一個worker進(jìn)程在accept這個連接之后,就開始讀取請求彤守,解析請求毯侦,處理請求,產(chǎn)生數(shù)據(jù)后具垫,再返回給客戶端侈离,最后才斷開連接,這樣一個完整的請求就是這樣的了筝蚕。我們可以看到卦碾,一個請求铺坞,完全由worker進(jìn)程來處理,而且只在一個worker進(jìn)程中處理洲胖。

那么济榨,nginx采用這種進(jìn)程模型有什么好處呢?當(dāng)然绿映,好處肯定會很多了擒滑。首先,對于每個worker進(jìn)程來說叉弦,獨立的進(jìn)程丐一,不需要加鎖,所以省掉了鎖帶來的開銷淹冰,同時在編程以及問題查找時库车,也會方便很多。其次樱拴,采用獨立的進(jìn)程柠衍,可以讓互相之間不會影響,一個進(jìn)程退出后疹鳄,其它進(jìn)程還在工作拧略,服務(wù)不會中斷,master進(jìn)程則很快啟動新的worker進(jìn)程瘪弓。當(dāng)然,worker進(jìn)程的異常退出禽最,肯定是程序有bug了腺怯,異常退出,會導(dǎo)致當(dāng)前worker上的所有請求失敗川无,不過不會影響到所有請求呛占,所以降低了風(fēng)險。當(dāng)然懦趋,好處還有很多晾虑,大家可以慢慢體會。

nginx事件處理

上面講了很多關(guān)于nginx的進(jìn)程模型仅叫,接下來帜篇,我們來看看nginx是如何處理事件的。

有人可能要問了诫咱,nginx采用多worker的方式來處理請求笙隙,每個worker里面只有一個主線程,那能夠處理的并發(fā)數(shù)很有限啊坎缭,多少個worker就能處理多少個并發(fā)竟痰,何來高并發(fā)呢签钩?非也,這就是nginx的高明之處坏快,nginx采用了異步非阻塞的方式來處理請求铅檩,也就是說,nginx是可以同時處理成千上萬個請求的莽鸿。

想想apache的常用工作方式(apache也有異步非阻塞版本昧旨,但因其與自帶某些模塊沖突,所以不常用)富拗,每個請求會獨占一個工作線程臼予,當(dāng)并發(fā)數(shù)上到幾千時,就同時有幾千的線程在處理請求了啃沪。這對操作系統(tǒng)來說粘拾,是個不小的挑戰(zhàn),線程帶來的內(nèi)存占用非常大创千,線程的上下文切換帶來的cpu開銷很大缰雇,自然性能就上不去了,而這些開銷完全是沒有意義的追驴。

為什么nginx可以采用異步非阻塞的方式來處理呢械哟,或者異步非阻塞到底是怎么回事呢?

我們先回到原點暇咆,看看一個請求的完整過程爸业。首先扯旷,請求過來索抓,要建立連接,然后再接收數(shù)據(jù),接收數(shù)據(jù)后,再發(fā)送數(shù)據(jù)。具體到系統(tǒng)底層租冠,就是讀寫事件,而當(dāng)讀寫事件沒有準(zhǔn)備好時肉渴,必然不可操作,如果不用非阻塞的方式來調(diào)用朱灿,那就得阻塞調(diào)用了侣灶,事件沒有準(zhǔn)備好咏雌,那就只能等了,等事件準(zhǔn)備好了,你再繼續(xù)吧岳链。

阻塞調(diào)用會進(jìn)入內(nèi)核等待举户,cpu就會讓出去給別人用了供填,對單線程的worker來說粘捎,顯然不合適娩缰,當(dāng)網(wǎng)絡(luò)事件越多時,大家都在等待呢,cpu空閑下來沒人用蹦锋,cpu利用率自然上不去了憎妙,更別談高并發(fā)了抚垃。好吧罕伯,你說加進(jìn)程數(shù)湿酸,這跟apache的線程模型有什么區(qū)別蜂奸,注意祖屏,別增加無謂的上下文切換期丰。所以,在nginx里面,最忌諱阻塞的系統(tǒng)調(diào)用了狸捕。

不要阻塞混槐,那就非阻塞嘍件舵。非阻塞就是临梗,事件沒有準(zhǔn)備好,馬上返回EAGAIN锉屈,告訴你雾家,事件還沒準(zhǔn)備好呢敬飒,你慌什么阴孟,過會再來吧哥牍。好吧愿题,你過一會,再來檢查一下事件吹艇,直到事件準(zhǔn)備好了為止路克,在這期間樟结,你就可以先去做其它事情,然后再來看看事件好了沒精算。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態(tài)灰羽,你可以做更多的事情了驮履,但帶來的開銷也是不小的鱼辙。

所以,才會有了異步非阻塞的事件處理機(jī)制玫镐,具體到系統(tǒng)調(diào)用就是像select/poll/epoll/kqueue這樣的系統(tǒng)調(diào)用倒戏。它們提供了一種機(jī)制,讓你可以同時監(jiān)控多個事件恐似,調(diào)用他們是阻塞的杜跷,但可以設(shè)置超時時間,在超時時間之內(nèi)矫夷,如果有事件準(zhǔn)備好了葛闷,就返回。

這種機(jī)制正好解決了我們上面的兩個問題双藕,拿epoll為例(在后面的例子中淑趾,我們多以epoll為例子,以代表這一類函數(shù))忧陪,當(dāng)事件沒準(zhǔn)備好時扣泊,放到epoll里面,事件準(zhǔn)備好了赤嚼,我們就去讀寫旷赖,當(dāng)讀寫返回EAGAIN時,我們將它再次加入到epoll里面。這樣,只要有事件準(zhǔn)備好了影兽,我們就去處理它,只有當(dāng)所有事件都沒準(zhǔn)備好時俯萌,才在epoll里面等著。這樣上枕,我們就可以并發(fā)處理大量的并發(fā)了咐熙,當(dāng)然,這里的并發(fā)請求辨萍,是指未處理完的請求棋恼,線程只有一個,所以同時能處理的請求當(dāng)然只有一個了锈玉,只是在請求間進(jìn)行不斷地切換而已爪飘,切換也是因為異步事件未準(zhǔn)備好,而主動讓出的拉背。這里的切換是沒有任何代價师崎,你可以理解為循環(huán)處理多個準(zhǔn)備好的事件,事實上就是這樣的椅棺。

與多線程相比犁罩,這種事件處理方式是有很大的優(yōu)勢的齐蔽,不需要創(chuàng)建線程,每個請求占用的內(nèi)存也很少床估,沒有上下文切換含滴,事件處理非常的輕量級。并發(fā)數(shù)再多也不會導(dǎo)致無謂的資源浪費(fèi)(上下文切換)丐巫。更多的并發(fā)數(shù)蛙吏,只是會占用更多的內(nèi)存而已。 我之前有對連接數(shù)進(jìn)行過測試鞋吉,在24G內(nèi)存的機(jī)器上,處理的并發(fā)請求數(shù)達(dá)到過200萬±常現(xiàn)在的網(wǎng)絡(luò)服務(wù)器基本都采用這種方式谓着,這也是nginx性能高效的主要原因。

我們之前說過坛掠,推薦設(shè)置worker的個數(shù)為cpu的核數(shù)赊锚,在這里就很容易理解了,更多的worker數(shù)屉栓,只會導(dǎo)致進(jìn)程來競爭cpu資源了舷蒲,從而帶來不必要的上下文切換。而且友多,nginx為了更好的利用多核特性牲平,提供了cpu親緣性的綁定選項,我們可以將某一個進(jìn)程綁定在某一個核上域滥,這樣就不會因為進(jìn)程的切換帶來cache的失效纵柿。像這種小的優(yōu)化在nginx中非常常見,同時也說明了nginx作者的苦心孤詣启绰。比如昂儒,nginx在做4個字節(jié)的字符串比較時,會將4個字符轉(zhuǎn)換成一個int型委可,再作比較渊跋,以減少cpu的指令數(shù)等等。

現(xiàn)在着倾,知道了nginx為什么會選擇這樣的進(jìn)程模型與事件模型了拾酝。對于一個基本的web服務(wù)器來說,事件通常有三種類型屈呕,網(wǎng)絡(luò)事件微宝、信號、定時器虎眨。從上面的講解中知道蟋软,網(wǎng)絡(luò)事件通過異步非阻塞可以很好的解決掉镶摘。如何處理信號與定時器?

首先岳守,信號的處理凄敢。對nginx來說,有一些特定的信號湿痢,代表著特定的意義涝缝。信號會中斷掉程序當(dāng)前的運(yùn)行,在改變狀態(tài)后譬重,繼續(xù)執(zhí)行拒逮。如果是系統(tǒng)調(diào)用,則可能會導(dǎo)致系統(tǒng)調(diào)用的失敗臀规,需要重入滩援。關(guān)于信號的處理,大家可以學(xué)習(xí)一些專業(yè)書籍塔嬉,這里不多說玩徊。對于nginx來說,如果nginx正在等待事件(epoll_wait時)谨究,如果程序收到信號恩袱,在信號處理函數(shù)處理完后,epoll_wait會返回錯誤胶哲,然后程序可再次進(jìn)入epoll_wait調(diào)用畔塔。

另外,再來看看定時器纪吮。由于epoll_wait等函數(shù)在調(diào)用的時候是可以設(shè)置一個超時時間的俩檬,所以nginx借助這個超時時間來實現(xiàn)定時器。nginx里面的定時器事件是放在一顆維護(hù)定時器的紅黑樹里面碾盟,每次在進(jìn)入epoll_wait前棚辽,先從該紅黑樹里面拿到所有定時器事件的最小時間,在計算出epoll_wait的超時時間后進(jìn)入epoll_wait冰肴。

所以屈藐,當(dāng)沒有事件產(chǎn)生,也沒有中斷信號時熙尉,epoll_wait會超時联逻,也就是說,定時器事件到了检痰。這時包归,nginx會檢查所有的超時事件,將他們的狀態(tài)設(shè)置為超時铅歼,然后再去處理網(wǎng)絡(luò)事件公壤。由此可以看出换可,當(dāng)我們寫nginx代碼時,在處理網(wǎng)絡(luò)事件的回調(diào)函數(shù)時厦幅,通常做的第一個事情就是判斷超時沾鳄,然后再去處理網(wǎng)絡(luò)事件。
我們可以用一段偽代碼來總結(jié)一下nginx的事件處理模型:

while (true)
 { 
for t in run_tasks: t.handler(); 
update_time(&now); 
timeout = ETERNITY; 
for t in wait_tasks: /* sorted already */ if (t.time <= now) 
{ 
t.timeout_handler();
 } else
 { 
timeout = t.time - now; break; 
} nevents = poll_function(events, timeout); 
for i in nevents: task t; 
if (events[i].type == READ) 
{
 t.handler = read_handler; 
} else 
{
 /* events[i].type == WRITE */ t.handler = write_handler; 
} run_tasks_add(t);}

好确憨,本節(jié)我們講了進(jìn)程模型译荞,事件模型,包括網(wǎng)絡(luò)事件休弃,信號吞歼,定時器事件。

nginx基礎(chǔ)概念

connection

在nginx中connection就是對tcp連接的封裝塔猾,其中包括連接的socket浆熔,讀事件,寫事件桥帆。利用nginx封裝的connection,我們可以很方便的使用nginx來處理與連接相關(guān)的事情慎皱,比如老虫,建立連接,發(fā)送與接受數(shù)據(jù)等茫多。而nginx中的http請求的處理就是建立在connection之上的祈匙,所以nginx不僅可以作為一個web服務(wù)器,也可以作為郵件服務(wù)器天揖。當(dāng)然夺欲,利用nginx提供的connection,我們可以與任何后端服務(wù)打交道今膊。

結(jié)合一個tcp連接的生命周期些阅,我們看看nginx是如何處理一個連接的。首先斑唬,nginx在啟動時市埋,會解析配置文件,得到需要監(jiān)聽的端口與ip地址恕刘,然后在nginx的master進(jìn)程里面缤谎,先初始化好這個監(jiān)控的socket(創(chuàng)建socket,設(shè)置addrreuse等選項褐着,綁定到指定的ip地址端口坷澡,再listen),然后再fork出多個子進(jìn)程出來含蓉,然后子進(jìn)程會競爭accept新的連接频敛。此時项郊,客戶端就可以向nginx發(fā)起連接了。當(dāng)客戶端與服務(wù)端通過三次握手建立好一個連接后姻政,nginx的某一個子進(jìn)程會accept成功呆抑,得到這個建立好的連接的socket,然后創(chuàng)建nginx對連接的封裝汁展,即ngx_connection_t結(jié)構(gòu)體鹊碍。接著,設(shè)置讀寫事件處理函數(shù)并添加讀寫事件來與客戶端進(jìn)行數(shù)據(jù)的交換食绿。最后侈咕,nginx或客戶端來主動關(guān)掉連接,到此器紧,一個連接就壽終正寢了耀销。

當(dāng)然,nginx也是可以作為客戶端來請求其它server的數(shù)據(jù)的(如upstream模塊)铲汪,此時熊尉,與其它server創(chuàng)建的連接,也封裝在ngx_connection_t中掌腰。作為客戶端狰住,nginx先獲取一個ngx_connection_t結(jié)構(gòu)體,然后創(chuàng)建socket齿梁,并設(shè)置socket的屬性( 比如非阻塞)催植。然后再通過添加讀寫事件,調(diào)用connect/read/write來調(diào)用連接勺择,最后關(guān)掉連接创南,并釋放ngx_connection_t。

在nginx中省核,每個進(jìn)程會有一個連接數(shù)的最大上限稿辙,這個上限與系統(tǒng)對fd的限制不一樣。在操作系統(tǒng)中气忠,通過ulimit -n邓深,我們可以得到一個進(jìn)程所能夠打開的fd的最大數(shù),即nofile笔刹,因為每個socket連接會占用掉一個fd芥备,所以這也會限制我們進(jìn)程的最大連接數(shù),當(dāng)然也會直接影響到我們程序所能支持的最大并發(fā)數(shù)舌菜,當(dāng)fd用完后萌壳,再創(chuàng)建socket時,就會失敗。

nginx通過設(shè)置worker_connectons來設(shè)置每個進(jìn)程支持的最大連接數(shù)袱瓮。如果該值大于nofile缤骨,那么實際的最大連接數(shù)是nofile,nginx會有警告尺借。nginx在實現(xiàn)時绊起,是通過一個連接池來管理的,每個worker進(jìn)程都有一個獨立的連接池燎斩,連接池的大小是worker_connections虱歪。這里的連接池里面保存的其實不是真實的連接,它只是一個worker_connections大小的一個ngx_connection_t結(jié)構(gòu)的數(shù)組栅表。并且笋鄙,nginx會通過一個鏈表free_connections來保存所有的空閑ngx_connection_t,每次獲取一個連接時怪瓶,就從空閑連接鏈表中獲取一個萧落,用完后,再放回空閑連接鏈表里面洗贰。

在這里找岖,很多人會誤解worker_connections這個參數(shù)的意思,認(rèn)為這個值就是nginx所能建立連接的最大值敛滋。其實不然宣增,這個值是表示每個worker進(jìn)程所能建立連接的最大值,所以矛缨,一個nginx能建立的最大連接數(shù),應(yīng)該是worker_connections * worker_processes帖旨。當(dāng)然箕昭,這里說的是最大連接數(shù),對于HTTP請求本地資源來說解阅,能夠支持的最大并發(fā)數(shù)量是worker_connections * worker_processes落竹,而如果是HTTP作為反向代理來說,最大并發(fā)數(shù)量應(yīng)該是worker_connections * worker_processes/2货抄。因為作為反向代理服務(wù)器述召,每個并發(fā)會建立與客戶端的連接和與后端服務(wù)的連接,會占用兩個連接蟹地。

那么积暖,我們前面有說過一個客戶端連接過來后,多個空閑的進(jìn)程怪与,會競爭這個連接夺刑,很容易看到,這種競爭會導(dǎo)致不公平,如果某個進(jìn)程得到accept的機(jī)會比較多遍愿,它的空閑連接很快就用完了存淫,如果不提前做一些控制,當(dāng)accept到一個新的tcp連接后沼填,因為無法得到空閑連接桅咆,而且無法將此連接轉(zhuǎn)交給其它進(jìn)程,最終會導(dǎo)致此tcp連接得不到處理坞笙,就中止掉了岩饼。很顯然,這是不公平的羞海,有的進(jìn)程有空余連接忌愚,卻沒有處理機(jī)會,有的進(jìn)程因為沒有空余連接却邓,卻人為地丟棄連接硕糊。

那么,如何解決這個問題呢腊徙?首先简十,nginx的處理得先打開accept_mutex選項,此時撬腾,只有獲得了accept_mutex的進(jìn)程才會去添加accept事件螟蝙,也就是說,nginx會控制進(jìn)程是否添加accept事件民傻。nginx使用一個叫ngx_accept_disabled的變量來控制是否去競爭accept_mutex鎖胰默。

在第一段代碼中,計算ngx_accept_disabled的值漓踢,這個值是nginx單進(jìn)程的所有連接總數(shù)的八分之一创泄,減去剩下的空閑連接數(shù)量籍铁,得到的這個ngx_accept_disabled有一個規(guī)律,當(dāng)剩余連接數(shù)小于總連接數(shù)的八分之一時,其值才大于0酪碘,而且剩余的連接數(shù)越小扣囊,這個值越大吉嫩。

再看第二段代碼其掂,當(dāng)ngx_accept_disabled大于0時,不會去嘗試獲取accept_mutex鎖扁耐,并且將ngx_accept_disabled減1暇检,于是,每次執(zhí)行到此處時婉称,都會去減1占哟,直到小于0心墅。不去獲取accept_mutex鎖,就是等于讓出獲取連接的機(jī)會榨乎,很顯然可以看出怎燥,當(dāng)空余連接越少時,ngx_accept_disable越大蜜暑,于是讓出的機(jī)會就越多铐姚,這樣其它進(jìn)程獲取鎖的機(jī)會也就越大。不去accept肛捍,自己的連接就控制下來了隐绵,其它進(jìn)程的連接池就會得到利用,這樣拙毫,nginx就控制了多進(jìn)程間連接的平衡了依许。

ngx_accept_disabled = ngx_cycle->connection_n / 8
    - ngx_cycle->free_connection_n;

if (ngx_accept_disabled > 0) {
    ngx_accept_disabled--;

} else {
    if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
        return;
    }

    if (ngx_accept_mutex_held) {
        flags |= NGX_POST_EVENTS;

    } else {
        if (timer == NGX_TIMER_INFINITE
                || timer > ngx_accept_mutex_delay)
        {
            timer = ngx_accept_mutex_delay;
        }
    }
}

好了,連接就先介紹到這缀蹄,本章的目的是介紹基本概念峭跳,知道在nginx中連接是個什么東西就行了,而且連接是屬于比較高級的用法缺前,在后面的模塊開發(fā)高級篇會有專門的章節(jié)來講解連接與事件的實現(xiàn)及使用蛀醉。

request

這節(jié)我們講request,在nginx中我們指的是http請求衅码,具體到nginx中的數(shù)據(jù)結(jié)構(gòu)是ngx_http_request_t拯刁。ngx_http_request_t是對一個http請求的封裝。 我們知道逝段,一個http請求垛玻,包含請求行、請求頭奶躯、請求體帚桩、響應(yīng)行、響應(yīng)頭巫糙、響應(yīng)體。

http請求是典型的請求-響應(yīng)類型的的網(wǎng)絡(luò)協(xié)議颊乘,而http是文件協(xié)議参淹,所以我們在分析請求行與請求頭,以及輸出響應(yīng)行與響應(yīng)頭乏悄,往往是一行一行的進(jìn)行處理浙值。如果我們自己來寫一個http服務(wù)器,通常在一個連接建立好后檩小,客戶端會發(fā)送請求過來开呐。然后我們讀取一行數(shù)據(jù),分析出請求行中包含的method、uri筐付、http_version信息卵惦。然后再一行一行處理請求頭,并根據(jù)請求method與請求頭的信息來決定是否有請求體以及請求體的長度瓦戚,然后再去讀取請求體沮尿。得到請求后,我們處理請求產(chǎn)生需要輸出的數(shù)據(jù)较解,然后再生成響應(yīng)行畜疾,響應(yīng)頭以及響應(yīng)體。在將響應(yīng)發(fā)送給客戶端之后印衔,一個完整的請求就處理完了啡捶。

當(dāng)然這是最簡單的webserver的處理方式,其實nginx也是這樣做的奸焙,只是有一些小小的區(qū)別瞎暑,比如,當(dāng)請求頭讀取完成后忿偷,就開始進(jìn)行請求的處理了金顿。nginx通過ngx_http_request_t來保存解析請求與輸出響應(yīng)相關(guān)的數(shù)據(jù)。

那接下來鲤桥,簡要講講nginx是如何處理一個完整的請求的揍拆。對于nginx來說,一個請求是從ngx_http_init_request開始的茶凳,在這個函數(shù)中嫂拴,會設(shè)置讀事件為ngx_http_process_request_line,也就是說贮喧,接下來的網(wǎng)絡(luò)事件筒狠,會由ngx_http_process_request_line來執(zhí)行。

從ngx_http_process_request_line的函數(shù)名箱沦,我們可以看到辩恼,這就是來處理請求行的,正好與之前講的谓形,處理請求的第一件事就是處理請求行是一致的灶伊。通過ngx_http_read_request_header來讀取請求數(shù)據(jù)。然后調(diào)用ngx_http_parse_request_line函數(shù)來解析請求行寒跳。nginx為提高效率聘萨,采用狀態(tài)機(jī)來解析請求行,而且在進(jìn)行method的比較時童太,沒有直接使用字符串比較米辐,而是將四個字符轉(zhuǎn)換成一個整型胸完,然后一次比較以減少cpu的指令數(shù),這個前面有說過翘贮。

很多人可能很清楚一個請求行包含請求的方法赊窥,uri,版本择膝,卻不知道其實在請求行中誓琼,也是可以包含有host的。比如一個請求GET http://www.taobao.com/uri HTTP/1.0這樣一個請求行也是合法的肴捉,而且host是www.taobao.com腹侣,這個時候,nginx會忽略請求頭中的host域齿穗,而以請求行中的這個為準(zhǔn)來查找虛擬主機(jī)傲隶。另外,對于對于http0.9版來說窃页,是不支持請求頭的跺株,所以這里也是要特別的處理。所以脖卖,在后面解析請求頭時乒省,協(xié)議版本都是1.0或1.1。整個請求行解析到的參數(shù)畦木,會保存到ngx_http_request_t結(jié)構(gòu)當(dāng)中袖扛。

在解析完請求行后,nginx會設(shè)置讀事件的handler為ngx_http_process_request_headers十籍,然后后續(xù)的請求就在ngx_http_process_request_headers中進(jìn)行讀取與解析蛆封。ngx_http_process_request_headers函數(shù)用來讀取請求頭,跟請求行一樣勾栗,還是調(diào)用ngx_http_read_request_header來讀取請求頭惨篱,調(diào)用ngx_http_parse_header_line來解析一行請求頭,解析到的請求頭會保存到ngx_http_request_t的域headers_in中围俘,headers_in是一個鏈表結(jié)構(gòu)砸讳,保存所有的請求頭。而HTTP中有些請求是需要特別處理的界牡,這些請求頭與請求處理函數(shù)存放在一個映射表里面簿寂,即ngx_http_headers_in,在初始化時欢揖,會生成一個hash表陶耍,當(dāng)每解析到一個請求頭后奋蔚,就會先在這個hash表中查找她混,如果有找到烈钞,則調(diào)用相應(yīng)的處理函數(shù)來處理這個請求頭。比如:Host頭的處理函數(shù)是ngx_http_process_host坤按。

當(dāng)nginx解析到兩個回車換行符時毯欣,就表示請求頭的結(jié)束,此時就會調(diào)用ngx_http_process_request來處理請求了臭脓。ngx_http_process_request會設(shè)置當(dāng)前的連接的讀寫事件處理函數(shù)為ngx_http_request_handler酗钞,然后再調(diào)用ngx_http_handler來真正開始處理一個完整的http請求。

這里可能比較奇怪来累,讀寫事件處理函數(shù)都是ngx_http_request_handler砚作,其實在這個函數(shù)中,會根據(jù)當(dāng)前事件是讀事件還是寫事件嘹锁,分別調(diào)用ngx_http_request_t中的read_event_handler或者是write_event_handler葫录。由于此時,我們的請求頭已經(jīng)讀取完成了领猾,之前有說過米同,nginx的做法是先不讀取請求body,所以這里面我們設(shè)置read_event_handler為ngx_http_block_reading摔竿,即不讀取數(shù)據(jù)了面粮。

剛才說到,真正開始處理數(shù)據(jù)继低,是在ngx_http_handler這個函數(shù)里面熬苍,這個函數(shù)會設(shè)置write_event_handler為ngx_http_core_run_phases,并執(zhí)行ngx_http_core_run_phases函數(shù)郁季。ngx_http_core_run_phases這個函數(shù)將執(zhí)行多階段請求處理冷溃,nginx將一個http請求的處理分為多個階段,那么這個函數(shù)就是執(zhí)行這些階段來產(chǎn)生數(shù)據(jù)梦裂。因為ngx_http_core_run_phases最后會產(chǎn)生數(shù)據(jù)似枕,所以我們就很容易理解,為什么設(shè)置寫事件的處理函數(shù)為ngx_http_core_run_phases了年柠。

在這里凿歼,我簡要說明了一下函數(shù)的調(diào)用邏輯,我們需要明白最終是調(diào)用ngx_http_core_run_phases來處理請求冗恨,產(chǎn)生的響應(yīng)頭會放在ngx_http_request_t的headers_out中答憔,這一部分內(nèi)容,我會放在請求處理流程里面去講掀抹。nginx的各種階段會對請求進(jìn)行處理虐拓,最后會調(diào)用filter來過濾數(shù)據(jù),對數(shù)據(jù)進(jìn)行加工傲武,如truncked傳輸蓉驹、gzip壓縮等城榛。

這里的filter包括header filter與body filter,即對響應(yīng)頭或響應(yīng)體進(jìn)行處理态兴。filter是一個鏈表結(jié)構(gòu)狠持,分別有header filter與body filter,先執(zhí)行header filter中的所有filter瞻润,然后再執(zhí)行body filter中的所有filter喘垂。在header filter中的最后一個filter,即ngx_http_header_filter绍撞,這個filter將會遍歷所有的響應(yīng)頭正勒,最后需要輸出的響應(yīng)頭在一個連續(xù)的內(nèi)存,然后調(diào)用ngx_http_write_filter進(jìn)行輸出傻铣。ngx_http_write_filter是body filter中的最后一個昭齐,所以nginx首先的body信息,在經(jīng)過一系列的body filter之后矾柜,最后也會調(diào)用ngx_http_write_filter來進(jìn)行輸出(有圖來說明)阱驾。

這里要注意的是,nginx會將整個請求頭都放在一個buffer里面怪蔑,這個buffer的大小通過配置項client_header_buffer_size來設(shè)置里覆,如果用戶的請求頭太大,這個buffer裝不下缆瓣,那nginx就會重新分配一個新的更大的buffer來裝請求頭喧枷,這個大buffer可以通過large_client_header_buffers來設(shè)置,這個large_buffer這一組buffer弓坞,比如配置4 8k隧甚,就是表示有四個8k大小的buffer可以用。

注意渡冻,為了保存請求行或請求頭的完整性戚扳,一個完整的請求行或請求頭,需要放在一個連續(xù)的內(nèi)存里面族吻,所以帽借,一個完整的請求行或請求頭,只會保存在一個buffer里面超歌。這樣砍艾,如果請求行大于一個buffer的大小,就會返回414錯誤巍举,如果一個請求頭大小大于一個buffer大小脆荷,就會返回400錯誤。在了解了這些參數(shù)的值,以及nginx實際的做法之后蜓谋,在應(yīng)用場景苔严,我們就需要根據(jù)實際的需求來調(diào)整這些參數(shù),來優(yōu)化我們的程序了孤澎。

處理流程圖:


請求處理流程

以上這些,就是nginx中一個http請求的生命周期了欠窒。我們再看看與請求相關(guān)的一些概念吧覆旭。

keepalive

當(dāng)然,在nginx中岖妄,對于http1.0與http1.1也是支持長連接的型将。

什么是長連接呢?我們知道荐虐,http請求是基于TCP協(xié)議之上的七兜,那么,當(dāng)客戶端在發(fā)起請求前福扬,需要先與服務(wù)端建立TCP連接腕铸,而每一次的TCP連接是需要三次握手來確定的,如果客戶端與服務(wù)端之間網(wǎng)絡(luò)差一點铛碑,這三次交互消費(fèi)的時間會比較多狠裹,而且三次交互也會帶來網(wǎng)絡(luò)流量。當(dāng)然汽烦,當(dāng)連接斷開后涛菠,也會有四次的交互,當(dāng)然對用戶體驗來說就不重要了撇吞。而http請求是請求應(yīng)答式的俗冻,如果我們能知道每個請求頭與響應(yīng)體的長度,那么我們是可以在一個連接上面執(zhí)行多個請求的牍颈,這就是所謂的長連接迄薄,但前提條件是我們先得確定請求頭與響應(yīng)體的長度。

對于請求來說煮岁,如果當(dāng)前請求需要有body噪奄,如POST請求,那么nginx就需要客戶端在請求頭中指定content-length來表明body的大小人乓,否則返回400錯誤勤篮。也就是說,請求體的長度是確定的色罚,那么響應(yīng)體的長度呢碰缔?先來看看http協(xié)議中關(guān)于響應(yīng)body長度的確定:

  1. 對于http1.0協(xié)議來說偿洁,如果響應(yīng)頭中有content-length頭逮诲,則以content-length的長度就可以知道body的長度了秸应,客戶端在接收body時荆针,就可以依照這個長度來接收數(shù)據(jù),接收完后梗肝,就表示這個請求完成了榛瓮。而如果沒有content-length頭,則客戶端會一直接收數(shù)據(jù)巫击,直到服務(wù)端主動斷開連接禀晓,才表示body接收完了。
  2. 而對于http1.1協(xié)議來說坝锰,如果響應(yīng)頭中的Transfer-encoding為chunked傳輸粹懒,則表示body是流式輸出,body會被分成多個塊顷级,每塊的開始會標(biāo)識出當(dāng)前塊的長度凫乖,此時,body不需要通過長度來指定弓颈。如果是非chunked傳輸帽芽,而且有content-length,則按照content-length來接收數(shù)據(jù)翔冀。否則嚣镜,如果是非chunked,并且沒有content-length橘蜜,則客戶端接收數(shù)據(jù)菊匿,直到服務(wù)端主動斷開連接。

從上面计福,我們可以看到跌捆,除了http1.0不帶content-length以及http1.1非chunked不帶content-length外,body的長度是可知的象颖。此時佩厚,當(dāng)服務(wù)端在輸出完body之后,會可以考慮使用長連接说订。能否使用長連接抄瓦,也是有條件限制的。如果客戶端的請求頭中的connection為close陶冷,則表示客戶端需要關(guān)掉長連接钙姊,如果為keep-alive,則客戶端需要打開長連接埂伦,如果客戶端的請求中沒有connection這個頭煞额,那么根據(jù)協(xié)議,如果是http1.0,則默認(rèn)為close膊毁,如果是http1.1胀莹,則默認(rèn)為keep-alive。如果結(jié)果為keepalive婚温,那么描焰,nginx在輸出完響應(yīng)體后,會設(shè)置當(dāng)前連接的keepalive屬性栅螟,然后等待客戶端下一次請求荆秦。

當(dāng)然,nginx不可能一直等待下去嵌巷,如果客戶端一直不發(fā)數(shù)據(jù)過來,豈不是一直占用這個連接室抽?所以當(dāng)nginx設(shè)置了keepalive等待下一次的請求時搪哪,同時也會設(shè)置一個最大等待時間,這個時間是通過選項keepalive_timeout來配置的坪圾,如果配置為0晓折,則表示關(guān)掉keepalive,此時兽泄,http版本無論是1.1還是1.0漓概,客戶端的connection不管是close還是keepalive,都會強(qiáng)制為close病梢。

如果服務(wù)端最后的決定是keepalive打開胃珍,那么在響應(yīng)的http頭里面,也會包含有connection頭域蜓陌,其值是”Keep-Alive”觅彰,否則就是”Close”。如果connection值為close钮热,那么在nginx響應(yīng)完數(shù)據(jù)后填抬,會主動關(guān)掉連接。所以隧期,對于請求量比較大的nginx來說飒责,關(guān)掉keepalive最后會產(chǎn)生比較多的time-wait狀態(tài)的socket。一般來說仆潮,當(dāng)客戶端的一次訪問宏蛉,需要多次訪問同一個server時,打開keepalive的優(yōu)勢非常大性置,比如圖片服務(wù)器檐晕,通常一個網(wǎng)頁會包含很多個圖片。打開keepalive也會大量減少time-wait的數(shù)量。

pipe

在http1.1中辟灰,引入了一種新的特性个榕,即pipeline。

那么什么是pipeline呢芥喇?pipeline其實就是流水線作業(yè)西采,它可以看作為keepalive的一種升華,因為pipeline也是基于長連接的继控,目的就是利用一個連接做多次請求械馆。如果客戶端要提交多個請求,對于keepalive來說武通,那么第二個請求霹崎,必須要等到第一個請求的響應(yīng)接收完全后,才能發(fā)起冶忱,這和TCP的停止等待協(xié)議是一樣的尾菇,得到兩個響應(yīng)的時間至少為2RTT。而對pipeline來說囚枪,客戶端不必等到第一個請求處理完后派诬,就可以馬上發(fā)起第二個請求。得到兩個響應(yīng)的時間可能能夠達(dá)到1RTT链沼。nginx是直接支持pipeline的默赂,但是,nginx對pipeline中的多個請求的處理卻不是并行的括勺,依然是一個請求接一個請求的處理缆八,只是在處理第一個請求的時候,客戶端就可以發(fā)起第二個請求疾捍。

這樣耀里,nginx利用pipeline減少了處理完一個請求后,等待第二個請求的請求頭數(shù)據(jù)的時間拾氓。其實nginx的做法很簡單冯挎,前面說到,nginx在讀取數(shù)據(jù)時咙鞍,會將讀取的數(shù)據(jù)放到一個buffer里面房官,所以,如果nginx在處理完前一個請求后续滋,如果發(fā)現(xiàn)buffer里面還有數(shù)據(jù)翰守,就認(rèn)為剩下的數(shù)據(jù)是下一個請求的開始,然后就接下來處理下一個請求疲酌,否則就設(shè)置keepalive蜡峰。

lingering_close

lingering_close了袁,字面意思就是延遲關(guān)閉,也就是說湿颅,當(dāng)nginx要關(guān)閉連接時载绿,并非立即關(guān)閉連接,而是先關(guān)閉tcp連接的寫油航,再等待一段時間后再關(guān)掉連接的讀崭庸。

為什么要這樣呢?我們先來看看這樣一個場景谊囚。nginx在接收客戶端的請求時怕享,可能由于客戶端或服務(wù)端出錯了,要立即響應(yīng)錯誤信息給客戶端镰踏,而nginx在響應(yīng)錯誤信息后函筋,大分部情況下是需要關(guān)閉當(dāng)前連接。nginx執(zhí)行完write()系統(tǒng)調(diào)用把錯誤信息發(fā)送給客戶端奠伪,write()系統(tǒng)調(diào)用返回成功并不表示數(shù)據(jù)已經(jīng)發(fā)送到客戶端跌帐,有可能還在tcp連接的write buffer里。接著如果直接執(zhí)行close()系統(tǒng)調(diào)用關(guān)閉tcp連接芳来,內(nèi)核會首先檢查tcp的read buffer里有沒有客戶端發(fā)送過來的數(shù)據(jù)留在內(nèi)核態(tài)沒有被用戶態(tài)進(jìn)程讀取含末,如果有則發(fā)送給客戶端RST報文來關(guān)閉tcp連接丟棄write buffer里的數(shù)據(jù)猜拾,如果沒有則等待write buffer里的數(shù)據(jù)發(fā)送完畢即舌,然后再經(jīng)過正常的4次分手報文斷開連接。

所以,當(dāng)在某些場景下出現(xiàn)tcp write buffer里的數(shù)據(jù)在write()系統(tǒng)調(diào)用之后到close()系統(tǒng)調(diào)用執(zhí)行之前沒有發(fā)送完畢挎袜,且tcp read buffer里面還有數(shù)據(jù)沒有讀顽聂,close()系統(tǒng)調(diào)用會導(dǎo)致客戶端收到RST報文且不會拿到服務(wù)端發(fā)送過來的錯誤信息數(shù)據(jù)。那客戶端肯定會想盯仪,這服務(wù)器好霸道紊搪,動不動就reset我的連接,連個錯誤信息都沒有全景。

在上面這個場景中耀石,我們可以看到,關(guān)鍵點是服務(wù)端給客戶端發(fā)送了RST包爸黄,導(dǎo)致自己發(fā)送的數(shù)據(jù)在客戶端忽略掉了滞伟。所以,解決問題的重點是炕贵,讓服務(wù)端別發(fā)RST包梆奈。再想想,我們發(fā)送RST是因為我們關(guān)掉了連接称开,關(guān)掉連接是因為我們不想再處理此連接了亩钟,也不會有任何數(shù)據(jù)產(chǎn)生了乓梨。對于全雙工的TCP連接來說,我們只需要關(guān)掉寫就行了清酥,讀可以繼續(xù)進(jìn)行扶镀,我們只需要丟掉讀到的任何數(shù)據(jù)就行了,這樣的話总处,當(dāng)我們關(guān)掉連接后狈惫,客戶端再發(fā)過來的數(shù)據(jù),就不會再收到RST了鹦马。當(dāng)然最終我們還是需要關(guān)掉這個讀端的胧谈,所以我們會設(shè)置一個超時時間,在這個時間過后荸频,就關(guān)掉讀菱肖,客戶端再發(fā)送數(shù)據(jù)來就不管了,作為服務(wù)端我會認(rèn)為旭从,都這么長時間了稳强,發(fā)給你的錯誤信息也應(yīng)該讀到了,再慢就不關(guān)我事了和悦,要怪就怪你RP不好了退疫。

當(dāng)然,正常的客戶端鸽素,在讀取到數(shù)據(jù)后褒繁,會關(guān)掉連接,此時服務(wù)端就會在超時時間內(nèi)關(guān)掉讀端馍忽。這些正是lingering_close所做的事情棒坏。協(xié)議棧提供 SO_LINGER 這個選項,它的一種配置情況就是來處理lingering_close的情況的遭笋,不過nginx是自己實現(xiàn)的lingering_close坝冕。lingering_close存在的意義就是來讀取剩下的客戶端發(fā)來的數(shù)據(jù),所以nginx會有一個讀超時時間瓦呼,通過lingering_timeout選項來設(shè)置喂窟,如果在lingering_timeout時間內(nèi)還沒有收到數(shù)據(jù),則直接關(guān)掉連接央串。

nginx還支持設(shè)置一個總的讀取時間磨澡,通過lingering_time來設(shè)置,這個時間也就是nginx在關(guān)閉寫之后蹋辅,保留socket的時間钱贯,客戶端需要在這個時間內(nèi)發(fā)送完所有的數(shù)據(jù),否則nginx在這個時間過后侦另,會直接關(guān)掉連接秩命。當(dāng)然尉共,nginx是支持配置是否打開lingering_close選項的,通過lingering_close選項來配置弃锐。 那么袄友,我們在實際應(yīng)用中,是否應(yīng)該打開lingering_close呢霹菊?這個就沒有固定的推薦值了剧蚣,如Maxim Dounin所說,lingering_close的主要作用是保持更好的客戶端兼容性旋廷,但是卻需要消耗更多的額外資源(比如連接會一直占著)鸠按。

這節(jié),我們介紹了nginx中饶碘,連接與請求的基本概念目尖,下節(jié),我們講基本的數(shù)據(jù)結(jié)構(gòu)扎运。

第六章 基本數(shù)據(jù)結(jié)構(gòu)(一)

nginx的作者為追求極致的高效瑟曲,自己實現(xiàn)了很多頗具特色的nginx風(fēng)格的數(shù)據(jù)結(jié)構(gòu)以及公共函數(shù)。比如豪治,nginx提供了帶長度的字符串洞拨,根據(jù)編譯器選項優(yōu)化過的字符串拷貝函數(shù)ngx_copy等。所以负拟,在我們寫nginx模塊時烦衣,應(yīng)該盡量調(diào)用nginx提供的api,盡管有些api只是對glibc的宏定義齿椅。本節(jié)琉挖,我們介紹string启泣、list涣脚、buffer、chain等一系列最基本的數(shù)據(jù)結(jié)構(gòu)及相關(guān)api的使用技巧以及注意事項寥茫。

ngx_str_t

在nginx源碼目錄的src/core下面的ngx_string.h|c里面遣蚀,包含了字符串的封裝以及字符串相關(guān)操作的api。nginx提供了一個帶長度的字符串結(jié)構(gòu)ngx_str_t纱耻,它的原型如下:

typedef struct { size_t len; u_char *data;} ngx_str_t;

在結(jié)構(gòu)體當(dāng)中芭梯,data指向字符串?dāng)?shù)據(jù)的第一個字符,字符串的結(jié)束用長度來表示弄喘,而不是由’\0’來表示結(jié)束玖喘。所以,在寫nginx代碼時蘑志,處理字符串的方法跟我們平時使用有很大的不一樣累奈,但要時刻記住贬派,字符串不以’\0’結(jié)束,盡量使用nginx提供的字符串操作的api來操作字符串澎媒。

那么搞乏,nginx這樣做有什么好處呢?首先戒努,通過長度來表示字符串長度请敦,減少計算字符串長度的次數(shù)。其次储玫,nginx可以重復(fù)引用一段字符串內(nèi)存侍筛,data可以指向任意內(nèi)存,長度表示結(jié)束撒穷,而不用去copy一份自己的字符串(因為如果要以’\0’結(jié)束勾笆,而不能更改原字符串,所以勢必要copy一段字符串)桥滨。我們在ngx_http_request_t結(jié)構(gòu)體的成員中窝爪,可以找到很多字符串引用一段內(nèi)存的例子,比如request_line齐媒、uri蒲每、args等等鬼癣,這些字符串的data部分矢劲,都是指向在接收數(shù)據(jù)時創(chuàng)建buffer所指向的內(nèi)存中,uri他膳,args就沒有必要copy一份出來唬血。這樣的話望蜡,減少了很多不必要的內(nèi)存分配與拷貝。

正是基于此特性拷恨,在nginx中脖律,必須謹(jǐn)慎的去修改一個字符串。在修改字符串時需要認(rèn)真的去考慮:是否可以修改該字符串腕侄;字符串修改后小泉,是否會對其它的引用造成影響。在后面介紹ngx_unescape_uri函數(shù)的時候冕杠,就會看到這一點微姊。但是,使用nginx的字符串會產(chǎn)生一些問題分预,glibc提供的很多系統(tǒng)api函數(shù)大多是通過’\0’來表示字符串的結(jié)束兢交,所以我們在調(diào)用系統(tǒng)api時,就不能直接傳入str->data了笼痹。此時配喳,通常的做法是創(chuàng)建一段str->len + 1大小的內(nèi)存飘诗,然后copy字符串,最后一個字節(jié)置為’\0’界逛。比較hack的做法是昆稿,將字符串最后一個字符的后一個字符backup一個,然后設(shè)置為’\0’息拜,在做完調(diào)用后溉潭,再由backup改回來,但前提條件是少欺,你得確定這個字符是可以修改的喳瓣,而且是有內(nèi)存分配,不會越界赞别,但一般不建議這么做畏陕。 接下來,看看nginx提供的操作字符串相關(guān)的api仿滔。

define ngx_string(str) { sizeof(str) - 1, (u_char *) str }

ngx_string(str)是一個宏惠毁,它通過一個以’\0’結(jié)尾的普通字符串str構(gòu)造一個nginx的字符串,鑒于其中采用sizeof操作符計算字符串長度崎页,因此參數(shù)必須是一個常量字符串鞠绰。

define ngx_null_string { 0, NULL }

定義變量時,使用ngx_null_string初始化字符串為空字符串飒焦,符串的長度為0蜈膨,data為NULL。

define ngx_str_set(str, text) \ (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text

ngx_str_set用于設(shè)置字符串str為text牺荠,由于使用sizeof計算長度翁巍,故text必須為常量字符串。

define ngx_str_null(str) (str)->len = 0; (str)->data = NULL

ngx_str_null用于設(shè)置字符串str為空串休雌,長度為0灶壶,data為NULL。
上面這四個函數(shù)挑辆,使用時一定要小心例朱,ngx_string與ngx_null_string是“{孝情,}”格式的鱼蝉,故只能用于賦值時初始化,如:

ngx_str_t str = ngx_string("hello world");
ngx_str_t str1 = ngx_null_string;

如果向下面這樣使用箫荡,就會有問題魁亦,這里涉及到c語言中對結(jié)構(gòu)體變量賦值操作的語法規(guī)則,在此不做介紹羔挡。

ngx_str_t str, str1;
str = ngx_string("hello world"); // 編譯出錯
str1 = ngx_null_string; // 編譯出錯

這種情況洁奈,可以調(diào)用ngx_str_set與ngx_str_null這兩個函數(shù)來做:

ngx_str_t str, str1;
ngx_str_set(&str, "hello world");
ngx_str_null(&str1);

按照C99標(biāo)準(zhǔn)间唉,您也可以這么做:

ngx_str_t str, str1;
str = (ngx_str_t) ngx_string("hello world");s
tr1 = (ngx_str_t) ngx_null_string;

另外要注意的是,ngx_string與ngx_str_set在調(diào)用時利术,傳進(jìn)去的字符串一定是常量字符串呈野,否則會得到意想不到的錯誤(因為ngx_str_set內(nèi)部使用了sizeof(),如果傳入的是u_char*印叁,那么計算的是這個指針的長度被冒,而不是字符串的長度)。如:

ngx_str_t str;
u_char *a = "hello world";
ngx_str_set(&str, a); // 問題產(chǎn)生

此外轮蜕,值得注意的是昨悼,由于ngx_str_set與ngx_str_null實際上是兩行語句,故在if/for/while等語句中單獨使用需要用花括號括起來跃洛,例如:

ngx_str_t str;
if (cond) ngx_str_set(&str, "true"); // 問題產(chǎn)生
else ngx_str_set(&str, "false"); // 問題產(chǎn)生
void ngx_strlow(u_char *dst, u_char *src, size_t n);

將src的前n個字符轉(zhuǎn)換成小寫存放在dst字符串當(dāng)中率触,調(diào)用者需要保證dst指向的空間大于等于n,且指向的空間必須可寫汇竭。操作不會對原字符串產(chǎn)生變動葱蝗。如要更改原字符串,可以:

ngx_strlow(str->data, str->data, str->len);

ngx_strncmp(s1, s2, n)

區(qū)分大小寫的字符串比較细燎,只比較前n個字符垒玲。

ngx_strcmp(s1, s2)

區(qū)分大小寫的不帶長度的字符串比較。

ngx_int_t ngx_strcasecmp(u_char *s1, u_char *s2);

不區(qū)分大小寫的不帶長度的字符串比較找颓。

ngx_int_t ngx_strncasecmp(u_char *s1, u_char *s2, size_t n);

不區(qū)分大小寫的帶長度的字符串比較合愈,只比較前n個字符。

u_char * ngx_cdecl ngx_sprintf(u_char *buf, const char *fmt, ...);
u_char * ngx_cdecl ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...);
u_char * ngx_cdecl ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...);

上面這三個函數(shù)用于字符串格式化击狮,ngx_snprintf的第二個參數(shù)max指明buf的空間大小佛析,ngx_slprintf則通過last來指明buf空間的大小。推薦使用第二個或第三個函數(shù)來格式化字符串彪蓬,ngx_sprintf函數(shù)還是比較危險的寸莫,容易產(chǎn)生緩沖區(qū)溢出漏洞。在這一系列函數(shù)中档冬,nginx在兼容glibc中格式化字符串的形式之外膘茎,還添加了一些方便格式化nginx類型的一些轉(zhuǎn)義字符,比如%V用于格式化ngx_str_t結(jié)構(gòu)酷誓。在nginx源文件的ngx_string.c中有說明:

/*
 * supported formats: 
* %[0][width][x][X]O off_t
* %[0][width]T time_t 
* %[0][width][u][x|X]z ssize_t/size_t 
* %[0][width][u][x|X]d int/u_int 
* %[0][width][u][x|X]l long 
* %[0][width|m][u][x|X]i ngx_int_t/ngx_uint_t 
* %[0][width][u][x|X]D int32_t/uint32_t 
* %[0][width][u][x|X]L int64_t/uint64_t 
* %[0][width|m][u][x|X]A ngx_atomic_int_t/ngx_atomic_uint_t 
* %[0][width][.width]f double, max valid number fits to %18.15f 
* %P ngx_pid_t 
* %M ngx_msec_t 
* %r rlim_t 
* %p void * 
* %V ngx_str_t * 
* %v ngx_variable_value_t * 
* %s null-terminated string 
* %*s length and string 
* %Z '\0' 
* %N '\n' 
* %c char 
* %% % * 
* reserved: 
* %t ptrdiff_t 
* %S null-terminated wchar string 
* %C wchar */

這里特別要提醒的是披坏,我們最常用于格式化ngx_str_t結(jié)構(gòu),其對應(yīng)的轉(zhuǎn)義符是%V盐数,傳給函數(shù)的一定要是指針類型棒拂,否則程序就會coredump掉。這也是我們最容易犯的錯。比如:

ngx_str_t str = ngx_string("hello world");
char buffer[1024];ngx_snprintf(buffer, 1024, "%V", &str); // 注意帚屉,str取地址
void ngx_encode_base64(ngx_str_t *dst, ngx_str_t *src);
ngx_int_t ngx_decode_base64(ngx_str_t *dst, ngx_str_t *src);

這兩個函數(shù)用于對str進(jìn)行base64編碼與解碼谜诫,調(diào)用前,需要保證dst中有足夠的空間來存放結(jié)果攻旦,如果不知道具體大小喻旷,可先調(diào)用ngx_base64_encoded_length與ngx_base64_decoded_length來預(yù)估最大占用空間。

uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size,
 ngx_uint_t type);

對src進(jìn)行編碼牢屋,根據(jù)type來按不同的方式進(jìn)行編碼掰邢,如果dst為NULL,則返回需要轉(zhuǎn)義的字符的數(shù)量伟阔,由此可得到需要的空間大小辣之。type的類型可以是:

#define NGX_ESCAPE_URI          0
#define NGX_ESCAPE_ARGS         1
#define NGX_ESCAPE_HTML         2
#define NGX_ESCAPE_REFRESH      3
#define NGX_ESCAPE_MEMCACHED    4
#define NGX_ESCAPE_MAIL_AUTH    5
void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, ngx_uint_t type);

對src進(jìn)行反編碼,type可以是0皱炉、NGX_UNESCAPE_URI怀估、NGX_UNESCAPE_REDIRECT這三個值。如果是0合搅,則表示src中的所有字符都要進(jìn)行轉(zhuǎn)碼多搀。如果是NGX_UNESCAPE_URI與NGX_UNESCAPE_REDIRECT,則遇到’?’后就結(jié)束了灾部,后面的字符就不管了康铭。而NGX_UNESCAPE_URI與NGX_UNESCAPE_REDIRECT之間的區(qū)別是NGX_UNESCAPE_URI對于遇到的需要轉(zhuǎn)碼的字符,都會轉(zhuǎn)碼赌髓,而NGX_UNESCAPE_REDIRECT則只會對非可見字符進(jìn)行轉(zhuǎn)碼从藤。

uintptr_t ngx_escape_html(u_char *dst, u_char *src, size_t size);

對html標(biāo)簽進(jìn)行編碼。
當(dāng)然锁蠕,我這里只介紹了一些常用的api的使用夷野,大家可以先熟悉一下,在實際使用過程中荣倾,遇到不明白的悯搔,最快最直接的方法就是去看源碼,看api的實現(xiàn)或看nginx自身調(diào)用api的地方是怎么做的舌仍,代碼就是最好的文檔妒貌。

ngx_pool_t

ngx_pool_t是一個非常重要的數(shù)據(jù)結(jié)構(gòu),在很多重要的場合都有使用铸豁,很多重要的數(shù)據(jù)結(jié)構(gòu)也都在使用它灌曙。那么它究竟是一個什么東西呢?簡單的說推姻,它提供了一種機(jī)制平匈,幫助管理一系列的資源(如內(nèi)存,文件等)藏古,使得對這些資源的使用和釋放統(tǒng)一進(jìn)行增炭,免除了使用過程中考慮到對各種各樣資源的什么時候釋放,是否遺漏了釋放的擔(dān)心拧晕。

例如對于內(nèi)存的管理隙姿,如果我們需要使用內(nèi)存,那么總是從一個ngx_pool_t的對象中獲取內(nèi)存厂捞,在最終的某個時刻输玷,我們銷毀這個ngx_pool_t對象,所有這些內(nèi)存都被釋放了靡馁。這樣我們就不必要對對這些內(nèi)存進(jìn)行malloc和free的操作欲鹏,不用擔(dān)心是否某塊被malloc出來的內(nèi)存沒有被釋放。因為當(dāng)ngx_pool_t對象被銷毀的時候臭墨,所有從這個對象中分配出來的內(nèi)存都會被統(tǒng)一釋放掉赔嚎。

再比如我們要使用一系列的文件,但是我們打開以后胧弛,最終需要都關(guān)閉尤误,那么我們就把這些文件統(tǒng)一登記到一個ngx_pool_t對象中,當(dāng)這個ngx_pool_t對象被銷毀的時候结缚,所有這些文件都將會被關(guān)閉损晤。

從上面舉的兩個例子中我們可以看出,使用ngx_pool_t這個數(shù)據(jù)結(jié)構(gòu)的時候红竭,所有的資源的釋放都在這個對象被銷毀的時刻尤勋,統(tǒng)一進(jìn)行了釋放,那么就會帶來一個問題茵宪,就是這些資源的生存周期(或者說被占用的時間)是跟ngx_pool_t的生存周期基本一致(ngx_pool_t也提供了少量操作可以提前釋放資源)斥黑。從最高效的角度來說,這并不是最好的眉厨。比如锌奴,我們需要依次使用A,B憾股,C三個資源鹿蜀,且使用完B的時候,A就不會再被使用了服球,使用C的時候A和B都不會被使用到茴恰。如果不使用ngx_pool_t來管理這三個資源,那我們可能從系統(tǒng)里面申請A斩熊,使用A往枣,然后在釋放A。接著申請B,使用B分冈,再釋放B圾另。最后申請C,使用C雕沉,然后釋放C集乔。但是當(dāng)我們使用一個ngx_pool_t對象來管理這三個資源的時候,A坡椒,B和C的釋放是在最后一起發(fā)生的扰路,也就是在使用完C以后。誠然倔叼,這在客觀上增加了程序在一段時間的資源使用量汗唱。但是這也減輕了程序員分別管理三個資源的生命周期的工作。這也就是有所得丈攒,必有所失的道理哩罪。實際上是一個取舍的問題,要看在具體的情況下肥印,你更在乎的是哪個识椰。

可以看一下在nginx里面一個典型的使用ngx_pool_t的場景,對于nginx處理的每個http request, nginx會生成一個ngx_pool_t對象與這個http request關(guān)聯(lián)深碱,所有處理過程中需要申請的資源都從這個ngx_pool_t對象中獲取腹鹉,當(dāng)這個http request處理完成以后,所有在處理過程中申請的資源敷硅,都將隨著這個關(guān)聯(lián)的ngx_pool_t對象的銷毀而釋放功咒。

ngx_pool_t相關(guān)結(jié)構(gòu)及操作被定義在文件src/core/ngx_palloc.h|c中。

typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s { ngx_pool_data_t d;
 size_t max; ngx_pool_t *current; 
ngx_chain_t *chain;
 ngx_pool_large_t *large; 
ngx_pool_cleanup_t *cleanup;
 ngx_log_t *log;};

從ngx_pool_t的一般使用者的角度來說绞蹦,可不用關(guān)注ngx_pool_t結(jié)構(gòu)中各字段作用力奋。所以這里也不會進(jìn)行詳細(xì)的解釋,當(dāng)然在說明某些操作函數(shù)的使用的時候幽七,如有必要景殷,會進(jìn)行說明。
下面我們來分別解釋下ngx_pool_t的相關(guān)操作澡屡。

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

創(chuàng)建一個初始節(jié)點大小為size的pool猿挚,log為后續(xù)在該pool上進(jìn)行操作時輸出日志的對象。 需要說明的是size的選擇驶鹉,size的大小必須小于等于NGX_MAX_ALLOC_FROM_POOL绩蜻,且必須大于sizeof(ngx_pool_t)。

選擇大于NGX_MAX_ALLOC_FROM_POOL的值會造成浪費(fèi)室埋,因為大于該限制的空間不會被用到(只是說在第一個由ngx_pool_t對象管理的內(nèi)存塊上的內(nèi)存办绝,后續(xù)的分配如果第一個內(nèi)存塊上的空閑部分已用完伊约,會再分配的)。
選擇小于sizeof(ngx_pool_t)的值會造成程序崩潰孕蝉。由于初始大小的內(nèi)存塊中要用一部分來存儲ngx_pool_t這個信息本身屡律。
當(dāng)一個ngx_pool_t對象被創(chuàng)建以后,該對象的max字段被賦值為size-sizeof(ngx_pool_t)和NGX_MAX_ALLOC_FROM_POOL這兩者中比較小的昔驱。后續(xù)的從這個pool中分配的內(nèi)存塊疹尾,在第一塊內(nèi)存使用完成以后上忍,如果要繼續(xù)分配的話骤肛,就需要繼續(xù)從操作系統(tǒng)申請內(nèi)存。當(dāng)內(nèi)存的大小小于等于max字段的時候窍蓝,則分配新的內(nèi)存塊腋颠,鏈接在d這個字段(實際上是d.next字段)管理的一條鏈表上。當(dāng)要分配的內(nèi)存塊是比max大的吓笙,那么從系統(tǒng)中申請的內(nèi)存是被掛接在large字段管理的一條鏈表上淑玫。我們暫且把這個稱之為大塊內(nèi)存鏈和小塊內(nèi)存鏈。

void *ngx_palloc(ngx_pool_t *pool, size_t size);

從這個pool中分配一塊為size大小的內(nèi)存面睛。注意絮蒿,此函數(shù)分配的內(nèi)存的起始地址按照NGX_ALIGNMENT進(jìn)行了對齊。對齊操作會提高系統(tǒng)處理的速度叁鉴,但會造成少量內(nèi)存的浪費(fèi)土涝。

void *ngx_pnalloc(ngx_pool_t *pool, size_t size);

從這個pool中分配一塊為size大小的內(nèi)存。但是此函數(shù)分配的內(nèi)存并沒有像上面的函數(shù)那樣進(jìn)行過對齊幌墓。

void *ngx_pcalloc(ngx_pool_t *pool, size_t size);

該函數(shù)也是分配size大小的內(nèi)存但壮,并且對分配的內(nèi)存塊進(jìn)行了清零。內(nèi)部實際上是轉(zhuǎn)調(diào)用ngx_palloc實現(xiàn)的常侣。

void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

按照指定對齊大小alignment來申請一塊大小為size的內(nèi)存蜡饵。此處獲取的內(nèi)存不管大小都將被置于大內(nèi)存塊鏈中管理。

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

對于被置于大塊內(nèi)存鏈胳施,也就是被large字段管理的一列內(nèi)存中的某塊進(jìn)行釋放溯祸。該函數(shù)的實現(xiàn)是順序遍歷large管理的大塊內(nèi)存鏈表。所以效率比較低下舞肆。如果在這個鏈表中找到了這塊內(nèi)存焦辅,則釋放,并返回NGX_OK胆绊。否則返回NGX_DECLINED氨鹏。

由于這個操作效率比較低下,除非必要压状,也就是說這塊內(nèi)存非常大仆抵,確應(yīng)及時釋放跟继,否則一般不需要調(diào)用。反正內(nèi)存在這個pool被銷毀的時候镣丑,總歸會都釋放掉的嘛舔糖!

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

ngx_pool_t中的cleanup字段管理著一個特殊的鏈表,該鏈表的每一項都記錄著一個特殊的需要釋放的資源莺匠。對于這個鏈表中每個節(jié)點所包含的資源如何去釋放金吗,是自說明的。這也就提供了非常大的靈活性趣竣。意味著摇庙,ngx_pool_t不僅僅可以管理內(nèi)存,通過這個機(jī)制遥缕,也可以管理任何需要釋放的資源彬檀,例如灯抛,關(guān)閉文件,或者刪除文件等等。下面我們看一下這個鏈表每個節(jié)點的類型:

typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
typedef void (*ngx_pool_cleanup_pt)(void *data);
struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; 
void *data; ngx_pool_cleanup_t *next;};

data:
指明了該節(jié)點所對應(yīng)的資源压怠。

handler:
是一個函數(shù)指針炼彪,指向一個可以釋放data所對應(yīng)資源的函數(shù)胎署。該函數(shù)只有一個參數(shù)颠悬,就是data。

next:
指向該鏈表中下一個元素鸡号。

看到這里转砖,ngx_pool_cleanup_add這個函數(shù)的用法,我相信大家都應(yīng)該有一些明白了膜蠢。但是這個參數(shù)size是起什么作用的呢堪藐?這個 size就是要存儲這個data字段所指向的資源的大小,該函數(shù)會為data分配size大小的空間挑围。

比如我們需要最后刪除一個文件礁竞。那我們在調(diào)用這個函數(shù)的時候,把size指定為存儲文件名的字符串的大小杉辙,然后調(diào)用這個函數(shù)給cleanup鏈表中增加一項模捂。該函數(shù)會返回新添加的這個節(jié)點。我們?nèi)缓蟀堰@個節(jié)點中的data字段拷貝為文件名蜘矢。把hander字段賦值為一個刪除文件的函數(shù)(當(dāng)然該函數(shù)的原型要按照void (*ngx_pool_cleanup_pt)(void *data))狂男。

void ngx_destroy_pool(ngx_pool_t *pool);

該函數(shù)就是釋放pool中持有的所有內(nèi)存,以及依次調(diào)用cleanup字段所管理的鏈表中每個元素的handler字段所指向的函數(shù)品腹,來釋放掉所有該pool管理的資源岖食。并且把pool指向的ngx_pool_t也釋放掉了,完全不可用了舞吭。

void ngx_reset_pool(ngx_pool_t *pool);

該函數(shù)釋放pool中所有大塊內(nèi)存鏈表上的內(nèi)存泡垃,小塊內(nèi)存鏈上的內(nèi)存塊都修改為可用析珊。但是不會去處理cleanup鏈表上的項目。

ngx_array_t

ngx_array_t是nginx內(nèi)部使用的數(shù)組結(jié)構(gòu)蔑穴。nginx的數(shù)組結(jié)構(gòu)在存儲上與大家認(rèn)知的C語言內(nèi)置的數(shù)組有相似性忠寻,比如實際上存儲數(shù)據(jù)的區(qū)域也是一大塊連續(xù)的內(nèi)存。但是數(shù)組除了存儲數(shù)據(jù)的內(nèi)存以外還包含一些元信息來描述相關(guān)的一些信息存和。下面我們從數(shù)組的定義上來詳細(xì)的了解一下奕剃。ngx_array_t的定義位于src/core/ngx_array.c|h里面。

typedef struct ngx_array_s ngx_array_t;
struct ngx_array_s { void *elts; 
ngx_uint_t nelts; 
size_t size; 
ngx_uint_t nalloc; 
ngx_pool_t *pool;};

elts:
指向?qū)嶋H的數(shù)據(jù)存儲區(qū)域捐腿。

nelts:
數(shù)組實際元素個數(shù)纵朋。

size:
數(shù)組單個元素的大小,單位是字節(jié)叙量。

nalloc:
數(shù)組的容量倡蝙。表示該數(shù)組在不引發(fā)擴(kuò)容的前提下九串,可以最多存儲的元素的個數(shù)绞佩。當(dāng)nelts增長到達(dá)nalloc 時,如果再往此數(shù)組中存儲元素猪钮,則會引發(fā)數(shù)組的擴(kuò)容品山。數(shù)組的容量將會擴(kuò)展到原有容量的2倍大小。實際上是分配新的一塊內(nèi)存烤低,新的一塊內(nèi)存的大小是原有內(nèi)存大小的2倍肘交。原有的數(shù)據(jù)會被拷貝到新的一塊內(nèi)存中。

pool:
該數(shù)組用來分配內(nèi)存的內(nèi)存池扑馁。

下面介紹ngx_array_t相關(guān)操作函數(shù)涯呻。

ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);

創(chuàng)建一個新的數(shù)組對象,并返回這個對象腻要。

p:
數(shù)組分配內(nèi)存使用的內(nèi)存池复罐;

n:
數(shù)組的初始容量大小,即在不擴(kuò)容的情況下最多可以容納的元素個數(shù)雄家。

size:
單個元素的大小效诅,單位是字節(jié)。

void ngx_array_destroy(ngx_array_t *a);

銷毀該數(shù)組對象趟济,并釋放其分配的內(nèi)存回內(nèi)存池乱投。

void *ngx_array_push(ngx_array_t *a);

在數(shù)組a上新追加一個元素,并返回指向新元素的指針顷编。需要把返回的指針使用類型轉(zhuǎn)換戚炫,轉(zhuǎn)換為具體的類型,然后再給新元素本身或者是各字段(如果數(shù)組的元素是復(fù)雜類型)賦值媳纬。

void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);

在數(shù)組a上追加n個元素双肤,并返回指向這些追加元素的首個元素的位置的指針叛甫。

static ngx_inline ngx_int_t ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size);

如果一個數(shù)組對象是被分配在堆上的,那么當(dāng)調(diào)用ngx_array_destroy銷毀以后杨伙,如果想再次使用其监,就可以調(diào)用此函數(shù)。
如果一個數(shù)組對象是被分配在棧上的限匣,那么就需要調(diào)用此函數(shù)抖苦,進(jìn)行初始化的工作以后,才可以使用米死。

注意事項: 由于使用ngx_palloc分配內(nèi)存锌历,數(shù)組在擴(kuò)容時,舊的內(nèi)存不會被釋放峦筒,會造成內(nèi)存的浪費(fèi)究西。因此,最好能提前規(guī)劃好數(shù)組的容量物喷,在創(chuàng)建或者初始化的時候一次搞定卤材,避免多次擴(kuò)容,造成內(nèi)存浪費(fèi)峦失。

ngx_hash_t

ngx_hash_t是nginx自己的hash表的實現(xiàn)扇丛。定義和實現(xiàn)位于src/core/ngx_hash.h|c中。ngx_hash_t的實現(xiàn)也與數(shù)據(jù)結(jié)構(gòu)教科書上所描述的hash表的實現(xiàn)是大同小異尉辑。對于常用的解決沖突的方法有線性探測帆精,二次探測和開鏈法等。ngx_hash_t使用的是最常用的一種隧魄,也就是開鏈法卓练,這也是STL中的hash表使用的方法。

但是ngx_hash_t的實現(xiàn)又有其幾個顯著的特點:

ngx_hash_t不像其他的hash表的實現(xiàn)购啄,可以插入刪除元素襟企,它只能一次初始化,就構(gòu)建起整個hash表以后,既不能再刪除,也不能在插入元素了。

ngx_hash_t的開鏈并不是真的開了一個鏈表,實際上是開了一段連續(xù)的存儲空間函匕,幾乎可以看做是一個數(shù)組。這是因為ngx_hash_t在初始化的時候,會經(jīng)歷一次預(yù)計算的過程,提前把每個桶里面會有多少元素放進(jìn)去給計算出來澜公,這樣就提前知道每個桶的大小了甚侣。那么就不需要使用鏈表,一段連續(xù)的存儲空間就足夠了。這也從一定程度上節(jié)省了內(nèi)存的使用草则。

從上面的描述,我們可以看出來,這個值越大,越造成內(nèi)存的浪費(fèi)。就兩步似扔,首先是初始化,然后就可以在里面進(jìn)行查找了。下面我們詳細(xì)來看一下袭艺。

ngx_hash_t的初始化驴党。

ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,ngx_uint_t nelts);

首先我們來看一下初始化函數(shù)。該函數(shù)的第一個參數(shù)hinit是初始化的一些參數(shù)的一個集合把还。 names是初始化一個ngx_hash_t所需要的所有key的一個數(shù)組。而nelts就是key的個數(shù)茸俭。下面先看一下ngx_hash_init_t類型吊履,該類型提供了初始化一個hash表所需要的一些基本信息。

typedef struct { 
ngx_hash_t *hash; 
ngx_hash_key_pt key; 
ngx_uint_t max_size; 
ngx_uint_t bucket_size;
 char *name; 
ngx_pool_t *pool; 
ngx_pool_t *temp_pool;
} ngx_hash_init_t;

hash:
該字段如果為NULL燕锥,那么調(diào)用完初始化函數(shù)后谐区,該字段指向新創(chuàng)建出來的hash表员帮。如果該字段不為NULL皆看,那么在初始的時候昵观,所有的數(shù)據(jù)被插入了這個字段所指的hash表中。

key:
指向從字符串生成hash值的hash函數(shù)惜辑。nginx的源代碼中提供了默認(rèn)的實現(xiàn)函數(shù)ngx_hash_key_lc片橡。

max_size:
hash表中的桶的個數(shù)。該字段越大蒂阱,元素存儲時沖突的可能性越小锻全,每個桶中存儲的元素會更少,則查詢起來的速度更快录煤。當(dāng)然鳄厌,這個值越大,越造成內(nèi)存的浪費(fèi)也越大妈踊,(實際上也浪費(fèi)不了多少)了嚎。

bucket_size:
每個桶的最大限制大小,單位是字節(jié)廊营。如果在初始化一個hash表的時候歪泳,發(fā)現(xiàn)某個桶里面無法存的下所有屬于該桶的元素,則hash表初始化失敗露筒。

name:
該hash表的名字呐伞。

pool:
該hash表分配內(nèi)存使用的pool。

temp_pool:
該hash表使用的臨時pool慎式,在初始化完成以后伶氢,該pool可以被釋放和銷毀掉趟径。

下面來看一下存儲hash表key的數(shù)組的結(jié)構(gòu)。

typedef struct { 
ngx_str_t key;
 ngx_uint_t key_hash; 
void *value;
} ngx_hash_key_t;

key和value的含義顯而易見癣防,就不用解釋了蜗巧。key_hash是對key使用hash函數(shù)計算出來的值。 對這兩個結(jié)構(gòu)分析完成以后蕾盯,我想大家應(yīng)該都已經(jīng)明白這個函數(shù)應(yīng)該是如何使用了吧幕屹。該函數(shù)成功初始化一個hash表以后,返回NGX_OK级遭,否則返回NGX_ERROR望拖。

void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);

在hash里面查找key對應(yīng)的value。實際上這里的key是對真正的key(也就是name)計算出的hash值装畅。len是name的長度靠娱。
如果查找成功,則返回指向value的指針掠兄,否則返回NULL像云。

ngx_hash_wildcard_t

nginx為了處理帶有通配符的域名的匹配問題,實現(xiàn)了ngx_hash_wildcard_t這樣的hash表蚂夕。他可以支持兩種類型的帶有通配符的域名迅诬。一種是通配符在前的,例如:“.abc.com”婿牍,也可以省略掉星號侈贷,直接寫成”.abc.com”。這樣的key等脂,可以匹配www.abc.com俏蛮,qqq.www.abc.com之類的。另外一種是通配符在末尾的上遥,例如:“mail.xxx.”搏屑,請?zhí)貏e注意通配符在末尾的不像位于開始的通配符可以被省略掉。這樣的通配符粉楚,可以匹配mail.xxx.com辣恋、mail.xxx.com.cn、mail.xxx.net之類的域名模软。

有一點必須說明伟骨,就是一個ngx_hash_wildcard_t類型的hash表只能包含通配符在前的key或者是通配符在后的key。不能同時包含兩種類型的通配符的key燃异。ngx_hash_wildcard_t類型變量的構(gòu)建是通過函數(shù)ngx_hash_wildcard_init完成的携狭,而查詢是通過函數(shù)ngx_hash_find_wc_head或者ngx_hash_find_wc_tail來做的。ngx_hash_find_wc_head是查詢包含通配符在前的key的hash表的回俐,而ngx_hash_find_wc_tail是查詢包含通配符在后的key的hash表的暑中。

下面詳細(xì)說明這幾個函數(shù)的用法壹瘟。

ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, 
ngx_uint_t nelts);

該函數(shù)迎來構(gòu)建一個可以包含通配符key的hash表。

hinit:
構(gòu)造一個通配符hash表的一些參數(shù)的一個集合鳄逾。關(guān)于該參數(shù)對應(yīng)的類型的說明,請參見ngx_hash_t類型中ngx_hash_init函數(shù)的說明灵莲。

names:
構(gòu)造此hash表的所有的通配符key的數(shù)組雕凹。特別要注意的是這里的key已經(jīng)都是被預(yù)處理過的。例如:“.abc.com”或者“.abc.com”被預(yù)處理完成以后政冻,變成了“com.abc.”枚抵。而“mail.xxx.”則被預(yù)處理為“mail.xxx.”。為什么會被處理這樣明场?這里不得不簡單地描述一下通配符hash表的實現(xiàn)原理汽摹。當(dāng)構(gòu)造此類型的hash表的時候,實際上是構(gòu)造了一個hash表的一個“鏈表”苦锨,是通過hash表中的key“鏈接”起來的逼泣。比如:對于“.abc.com”將會構(gòu)造出2個hash表,第一個hash表中有一個key為com的表項舟舒,該表項的value包含有指向第二個hash表的指針拉庶,而第二個hash表中有一個表項abc,該表項的value包含有指向.abc.com對應(yīng)的value的指針秃励。那么查詢的時候氏仗,比如查詢www.abc.com的時候,先查com夺鲜,通過查com可以找到第二級的hash表皆尔,在第二級hash表中,再查找abc币励,依次類推慷蠕,直到在某一級的hash表中查到的表項對應(yīng)的value對應(yīng)一個真正的值而非一個指向下一級hash表的指針的時候,查詢過程結(jié)束榄审。

這里有一點需要特別注意的砌们,就是names數(shù)組中元素的value值低兩位bit必須為0(有特殊用途)。如果不滿足這個條件搁进,這個hash表查詢不出正確結(jié)果浪感。

nelts:
names數(shù)組元素的個數(shù)。

該函數(shù)執(zhí)行成功返回NGX_OK饼问,否則NGX_ERROR影兽。

void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

該函數(shù)查詢包含通配符在前的key的hash表的。

hwc:
hash表對象的指針莱革。

name:
需要查詢的域名峻堰,例如: www.abc.com讹开。

len:
name的長度。

該函數(shù)返回匹配的通配符對應(yīng)value捐名。如果沒有查到旦万,返回NULL。

void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

該函數(shù)查詢包含通配符在末尾的key的hash表的镶蹋。 參數(shù)及返回值請參加上個函數(shù)的說明成艘。

ngx_hash_combined_t

組合類型hash表,該hash表的定義如下:

typedef struct { 
ngx_hash_t hash; 
ngx_hash_wildcard_t *wc_head; 
ngx_hash_wildcard_t *wc_tail;
} ngx_hash_combined_t;

從其定義顯見贺归,該類型實際上包含了三個hash表淆两,一個普通hash表,一個包含前向通配符的hash表和一個包含后向通配符的hash表拂酣。

nginx提供該類型的作用秋冰,在于提供一個方便的容器包含三個類型的hash表,當(dāng)有包含通配符的和不包含通配符的一組key構(gòu)建hash表以后婶熬,以一種方便的方式來查詢剑勾,你不需要再考慮一個key到底是應(yīng)該到哪個類型的hash表里去查了。

構(gòu)造這樣一組合hash表的時候尸诽,首先定義一個該類型的變量甥材,再分別構(gòu)造其包含的三個子hash表即可。
對于該類型hash表的查詢性含,nginx提供了一個方便的函數(shù)ngx_hash_find_combined洲赵。

void *ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key,
u_char *name, size_t len);

該函數(shù)在此組合hash表中,依次查詢其三個子hash表商蕴,看是否匹配叠萍,一旦找到,立即返回查找結(jié)果绪商,也就是說如果有多個可能匹配苛谷,則只返回第一個匹配的結(jié)果。

hash:
此組合hash表對象格郁。

key:
根據(jù)name計算出的hash值腹殿。

name:
key的具體內(nèi)容。

len:
name的長度例书。

返回查詢的結(jié)果锣尉,未查到則返回NULL。

第七章 基本數(shù)據(jù)結(jié)構(gòu)(二)

ngx_hash_keys_arrays_t

大家看到在構(gòu)建一個ngx_hash_wildcard_t的時候决采,需要對通配符的哪些key進(jìn)行預(yù)處理自沧。這個處理起來比較麻煩。而當(dāng)有一組key树瞭,這些里面既有無通配符的key拇厢,也有包含通配符的key的時候爱谁。我們就需要構(gòu)建三個hash表,一個包含普通的key的hash表孝偎,一個包含前向通配符的hash表访敌,一個包含后向通配符的hash表(或者也可以把這三個hash表組合成一個ngx_hash_combined_t)。在這種情況下邪媳,為了讓大家方便的構(gòu)造這些hash表捐顷,nginx提供給了此輔助類型。

該類型以及相關(guān)的操作函數(shù)也定義在src/core/ngx_hash.h|c里雨效。我們先來看一下該類型的定義。

typedef struct {
 ngx_uint_t hsize; 
ngx_pool_t *pool; 
ngx_pool_t *temp_pool; 
ngx_array_t keys; 
ngx_array_t *keys_hash; 
ngx_array_t dns_wc_head; 
ngx_array_t *dns_wc_head_hash; 
ngx_array_t dns_wc_tail; 
ngx_array_t *dns_wc_tail_hash;
} ngx_hash_keys_arrays_t;

hsize:
將要構(gòu)建的hash表的桶的個數(shù)废赞。對于使用這個結(jié)構(gòu)中包含的信息構(gòu)建的三種類型的hash表都會使用此參數(shù)徽龟。

pool:
構(gòu)建這些hash表使用的pool。

temp_pool:
在構(gòu)建這個類型以及最終的三個hash表過程中可能用到臨時pool唉地。該temp_pool可以在構(gòu)建完成以后据悔,被銷毀掉。這里只是存放臨時的一些內(nèi)存消耗耘沼。

keys:
存放所有非通配符key的數(shù)組极颓。

keys_hash:
這是個二維數(shù)組,第一個維度代表的是bucket的編號涧黄,那么keys_hash[i]中存放的是所有的key算出來的hash值對hsize取模以后的值為i的key茵典。假設(shè)有3個key,分別是key1,key2和key3假設(shè)hash值算出來以后對hsize取模的值都是i读整,那么這三個key的值就順序存放在keys_hash[i][0],keys_hash[i][1], keys_hash[i][2]。該值在調(diào)用的過程中用來保存和檢測是否有沖突的key值骇径,也就是是否有重復(fù)。

dns_wc_head:
放前向通配符key被處理完成以后的值者春。比如:“*.abc.com” 被處理完成以后破衔,變成 “com.abc.” 被存放在此數(shù)組中。

dns_wc_tail:
存放后向通配符key被處理完成以后的值钱烟。比如:“mail.xxx.*” 被處理完成以后晰筛,變成 “mail.xxx.” 被存放在此數(shù)組中。

dns_wc_head_hash:
該值在調(diào)用的過程中用來保存和檢測是否有沖突的前向通配符的key值拴袭,也就是是否有重復(fù)读第。

dns_wc_tail_hash:
該值在調(diào)用的過程中用來保存和檢測是否有沖突的后向通配符的key值,也就是是否有重復(fù)稻扬。

在定義一個這個類型的變量卦方,并對字段pool和temp_pool賦值以后,就可以調(diào)用函數(shù)ngx_hash_add_key把所有的key加入到這個結(jié)構(gòu)中了泰佳,該函數(shù)會自動實現(xiàn)普通key盼砍,帶前向通配符的key和帶后向通配符的key的分類和檢查尘吗,并將這個些值存放到對應(yīng)的字段中去, 然后就可以通過檢查這個結(jié)構(gòu)體中的keys浇坐、dns_wc_head睬捶、dns_wc_tail三個數(shù)組是否為空,來決定是否構(gòu)建普通hash表近刘,前向通配符hash表和后向通配符hash表了(在構(gòu)建這三個類型的hash表的時候擒贸,可以分別使用keys、dns_wc_head觉渴、dns_wc_tail三個數(shù)組)介劫。

構(gòu)建出這三個hash表以后,可以組合在一個ngx_hash_combined_t對象中案淋,使用ngx_hash_find_combined進(jìn)行查找座韵。或者是仍然保持三個獨立的變量對應(yīng)這三個hash表踢京,自己決定何時以及在哪個hash表中進(jìn)行查詢誉碴。

ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type);

初始化這個結(jié)構(gòu),主要是對這個結(jié)構(gòu)中的ngx_array_t類型的字段進(jìn)行初始化瓣距,成功返回NGX_OK黔帕。

ha:
該結(jié)構(gòu)的對象指針。

type:
該字段有2個值可選擇蹈丸,即NGX_HASH_SMALL和NGX_HASH_LARGE成黄。用來指明將要建立的hash表的類型,如果是NGX_HASH_SMALL白华,則有比較小的桶的個數(shù)和數(shù)組元素大小慨默。NGX_HASH_LARGE則相反。

ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key,
void *value, ngx_uint_t flags);

一般是循環(huán)調(diào)用這個函數(shù)弧腥,把一組鍵值對加入到這個結(jié)構(gòu)體中厦取。返回NGX_OK是加入成功。返回NGX_BUSY意味著key值重復(fù)管搪。

ha:
該結(jié)構(gòu)的對象指針虾攻。

key:
參數(shù)名自解釋了。

value:
參數(shù)名自解釋了更鲁。

flags:
有兩個標(biāo)志位可以設(shè)置霎箍,NGX_HASH_WILDCARD_KEY和NGX_HASH_READONLY_KEY。同時要設(shè)置的使用邏輯與操作符就可以了澡为。NGX_HASH_READONLY_KEY被設(shè)置的時候漂坏,在計算hash值的時候,key的值不會被轉(zhuǎn)成小寫字符,否則會顶别。NGX_HASH_WILDCARD_KEY被設(shè)置的時候谷徙,說明key里面可能含有通配符,會進(jìn)行相應(yīng)的處理驯绎。如果兩個標(biāo)志位都不設(shè)置完慧,傳0。

有關(guān)于這個數(shù)據(jù)結(jié)構(gòu)的使用剩失,可以參考src/http/ngx_http.c中的ngx_http_server_names函數(shù)屈尼。

ngx_chain_t

nginx的filter模塊在處理從別的filter模塊或者是handler模塊傳遞過來的數(shù)據(jù)(實際上就是需要發(fā)送給客戶端的http response)。這個傳遞過來的數(shù)據(jù)是以一個鏈表的形式(ngx_chain_t)拴孤。而且數(shù)據(jù)可能被分多次傳遞過來脾歧。也就是多次調(diào)用filter的處理函數(shù),以不同的ngx_chain_t演熟。

該結(jié)構(gòu)被定義在src/core/ngx_buf.h|c涨椒。下面我們來看一下ngx_chain_t的定義。

typedef struct ngx_chain_s ngx_chain_t;
struct ngx_chain_s { ngx_buf_t *buf;
ngx_chain_t *next;};

就2個字段绽媒,next指向這個鏈表的下個節(jié)點。buf指向?qū)嶋H的數(shù)據(jù)免猾。所以在這個鏈表上追加節(jié)點也是非常容易是辕,只要把末尾元素的next指針指向新的節(jié)點,把新節(jié)點的next賦值為NULL即可猎提。

ngx_chain_t *ngx_alloc_chain_link(ngx_pool_t *pool);

該函數(shù)創(chuàng)建一個ngx_chain_t的對象获三,并返回指向?qū)ο蟮闹羔槪》祷豊ULL锨苏。

#define ngx_free_chain(pool, cl) 
 \ cl->next = pool->chain; 
\pool->chain = cl

該宏釋放一個ngx_chain_t類型的對象疙教。如果要釋放整個chain,則迭代此鏈表伞租,對每個節(jié)點使用此宏即可贞谓。
注意: 對ngx_chaint_t類型的釋放,并不是真的釋放了內(nèi)存葵诈,而僅僅是把這個對象掛在了這個pool對象的一個叫做chain的字段對應(yīng)的chain上裸弦,以供下次從這個pool上分配ngx_chain_t類型對象的時候,快速的從這個pool->chain上取下鏈?zhǔn)自鼐头祷亓俗鞔?dāng)然理疙,如果這個鏈?zhǔn)强盏模艜娴脑谶@個pool上使用ngx_palloc函數(shù)進(jìn)行分配泞坦。

ngx_buf_t

這個ngx_buf_t就是這個ngx_chain_t鏈表的每個節(jié)點的實際數(shù)據(jù)窖贤。該結(jié)構(gòu)實際上是一種抽象的數(shù)據(jù)結(jié)構(gòu),它代表某種具體的數(shù)據(jù)。這個數(shù)據(jù)可能是指向內(nèi)存中的某個緩沖區(qū)赃梧,也可能指向一個文件的某一部分滤蝠,也可能是一些純元數(shù)據(jù)(元數(shù)據(jù)的作用在于指示這個鏈表的讀取者對讀取的數(shù)據(jù)進(jìn)行不同的處理)。

該數(shù)據(jù)結(jié)構(gòu)位于src/core/ngx_buf.h|c文件中槽奕。我們來看一下它的定義几睛。

struct ngx_buf_s { 
u_char *pos; 
u_char *last;
off_t file_pos; 
off_t file_last; 
u_char *start; /* start of buffer */
u_char *end; /* end of buffer */ 
ngx_buf_tag_t tag; 
ngx_file_t *file; 
ngx_buf_t *shadow;

 /* the buf's content could be changed */ 

unsigned temporary:1; 

/* 
* the buf's content is in a memory cache or in a read only memory 
* and must not be changed 
*/ 

unsigned memory:1;
 /* the buf's content is mmap()ed and must not be changed */ 
unsigned mmap:1; 
unsigned recycled:1; 
unsigned in_file:1; 
unsigned flush:1; 
unsigned sync:1; 
unsigned last_buf:1; 
unsigned last_in_chain:1; 
unsigned last_shadow:1; 
unsigned temp_file:1; 

/* STUB */ int num;};

pos:
當(dāng)buf所指向的數(shù)據(jù)在內(nèi)存里的時候,pos指向的是這段數(shù)據(jù)開始的位置粤攒。

last:
當(dāng)buf所指向的數(shù)據(jù)在內(nèi)存里的時候所森,last指向的是這段數(shù)據(jù)結(jié)束的位置。

file_pos:
當(dāng)buf所指向的數(shù)據(jù)是在文件里的時候夯接,file_pos指向的是這段數(shù)據(jù)的開始位置在文件中的偏移量焕济。

file_last:
當(dāng)buf所指向的數(shù)據(jù)是在文件里的時候,file_last指向的是這段數(shù)據(jù)的結(jié)束位置在文件中的偏移量盔几。

start:
當(dāng)buf所指向的數(shù)據(jù)在內(nèi)存里的時候晴弃,這一整塊內(nèi)存包含的內(nèi)容可能被包含在多個buf中(比如在某段數(shù)據(jù)中間插入了其他的數(shù)據(jù),這一塊數(shù)據(jù)就需要被拆分開)逊拍。那么這些buf中的start和end都指向這一塊內(nèi)存的開始地址和結(jié)束地址上鞠。而pos和last指向本buf所實際包含的數(shù)據(jù)的開始和結(jié)尾。

end:
解釋參見start芯丧。

tag:
實際上是一個void*類型的指針芍阎,使用者可以關(guān)聯(lián)任意的對象上去,只要對使用者有意義缨恒。

file:
當(dāng)buf所包含的內(nèi)容在文件中時谴咸,file字段指向?qū)?yīng)的文件對象。

shadow:
當(dāng)這個buf完整copy了另外一個buf的所有字段的時候骗露,那么這兩個buf指向的實際上是同一塊內(nèi)存岭佳,或者是同一個文件的同一部分,此時這兩個buf的shadow字段都是指向?qū)Ψ降南麸薄D敲磳τ谶@樣的兩個buf珊随,在釋放的時候,就需要使用者特別小心驹暑,具體是由哪里釋放玫恳,要提前考慮好,如果造成資源的多次釋放优俘,可能會造成程序崩潰京办!

temporary:
為1時表示該buf所包含的內(nèi)容是在一個用戶創(chuàng)建的內(nèi)存塊中,并且可以被在filter處理的過程中進(jìn)行變更帆焕,而不會造成問題惭婿。

memory:
為1時表示該buf所包含的內(nèi)容是在內(nèi)存中不恭,但是這些內(nèi)容卻不能被進(jìn)行處理的filter進(jìn)行變更。

mmap:
為1時表示該buf所包含的內(nèi)容是在內(nèi)存中, 是通過mmap使用內(nèi)存映射從文件中映射到內(nèi)存中的财饥,這些內(nèi)容卻不能被進(jìn)行處理的filter進(jìn)行變更换吧。

recycled:
可以回收的。也就是這個buf是可以被釋放的钥星。這個字段通常是配合shadow字段一起使用的沾瓦,對于使用ngx_create_temp_buf 函數(shù)創(chuàng)建的buf,并且是另外一個buf的shadow谦炒,那么可以使用這個字段來標(biāo)示這個buf是可以被釋放的贯莺。

in_file:
為1時表示該buf所包含的內(nèi)容是在文件中。

flush:
遇到有flush字段被設(shè)置為1的的buf的chain宁改,則該chain的數(shù)據(jù)即便不是最后結(jié)束的數(shù)據(jù)(last_buf被設(shè)置缕探,標(biāo)志所有要輸出的內(nèi)容都完了),也會進(jìn)行輸出还蹲,不會受postpone_output配置的限制爹耗,但是會受到發(fā)送速率等其他條件的限制。

sync:

last_buf:
數(shù)據(jù)被以多個chain傳遞給了過濾器谜喊,此字段為1表明這是最后一個buf潭兽。

last_in_chain:
在當(dāng)前的chain里面,此buf是最后一個斗遏。特別要注意的是last_in_chain的buf不一定是last_buf讼溺,但是last_buf的buf一定是last_in_chain的。這是因為數(shù)據(jù)會被以多個chain傳遞給某個filter模塊最易。

last_shadow:
在創(chuàng)建一個buf的shadow的時候,通常將新創(chuàng)建的一個buf的last_shadow置為1炫狱。

temp_file:
由于受到內(nèi)存使用的限制藻懒,有時候一些buf的內(nèi)容需要被寫到磁盤上的臨時文件中去,那么這時视译,就設(shè)置此標(biāo)志 嬉荆。

對于此對象的創(chuàng)建,可以直接在某個ngx_pool_t上分配酷含,然后根據(jù)需要鄙早,給對應(yīng)的字段賦值。也可以使用定義好的2個宏:

#define ngx_alloc_buf(pool) ngx_palloc(pool, sizeof(ngx_buf_t))
#define ngx_calloc_buf(pool) ngx_pcalloc(pool, sizeof(ngx_buf_t))

這兩個宏使用類似函數(shù)椅亚,也是不說自明的限番。
對于創(chuàng)建temporary字段為1的buf(就是其內(nèi)容可以被后續(xù)的filter模塊進(jìn)行修改),可以直接使用函數(shù)ngx_create_temp_buf進(jìn)行創(chuàng)建呀舔。

ngx_buf_t *ngx_create_temp_buf(ngx_pool_t *pool, size_t size);

該函數(shù)創(chuàng)建一個ngx_but_t類型的對象弥虐,并返回指向這個對象的指針,創(chuàng)建失敗返回NULL。
對于創(chuàng)建的這個對象霜瘪,它的start和end指向新分配內(nèi)存開始和結(jié)束的地方珠插。pos和last都指向這塊新分配內(nèi)存的開始處,這樣颖对,后續(xù)的操作可以在這塊新分配的內(nèi)存上存入數(shù)據(jù)捻撑。

pool:
分配該buf和buf使用的內(nèi)存所使用的pool。

size:
該buf使用的內(nèi)存的大小缤底。

為了配合對ngx_buf_t的使用顾患,nginx定義了以下的宏方便操作。

#define ngx_buf_in_memory(b) (b->temporary || b->memory || b->mmap)

返回這個buf里面的內(nèi)容是否在內(nèi)存里训堆。

#define ngx_buf_in_memory_only(b) (ngx_buf_in_memory(b) && !b->in_file)

返回這個buf里面的內(nèi)容是否僅僅在內(nèi)存里描验,并且沒有在文件里。

#define ngx_buf_special(b) \ 
((b->flush || b->last_buf || b->sync) \ 
&& !ngx_buf_in_memory(b) && !b->in_file)

返回該buf是否是一個特殊的buf坑鱼,只含有特殊的標(biāo)志和沒有包含真正的數(shù)據(jù)膘流。

#define ngx_buf_sync_only(b) \ 
(b->sync \ 
&& !ngx_buf_in_memory(b) && !b->in_file && !b->flush && !b->last_buf)

返回該buf是否是一個只包含sync標(biāo)志而不包含真正數(shù)據(jù)的特殊buf。

#define ngx_buf_size(b) \ 
(ngx_buf_in_memory(b) ? (off_t) (b->last - b->pos): \ 
(b->file_last - b->file_pos))

返回該buf所含數(shù)據(jù)的大小鲁沥,不管這個數(shù)據(jù)是在文件里還是在內(nèi)存里呼股。

ngx_list_t

ngx_list_t顧名思義,看起來好像是一個list的數(shù)據(jù)結(jié)構(gòu)画恰。這樣的說法彭谁,算對也不算對。因為它符合list類型數(shù)據(jù)結(jié)構(gòu)的一些特點允扇,比如可以添加元素缠局,實現(xiàn)自增長,不會像數(shù)組類型的數(shù)據(jù)結(jié)構(gòu)考润,受到初始設(shè)定的數(shù)組容量的限制狭园,并且它跟我們常見的list型數(shù)據(jù)結(jié)構(gòu)也是一樣的,內(nèi)部實現(xiàn)使用了一個鏈表糊治。

那么它跟我們常見的鏈表實現(xiàn)的list有什么不同呢唱矛?不同點就在于它的節(jié)點,它的節(jié)點不像我們常見的list的節(jié)點井辜,只能存放一個元素绎谦,ngx_list_t的節(jié)點實際上是一個固定大小的數(shù)組。

在初始化的時候粥脚,我們需要設(shè)定元素需要占用的空間大小窃肠,每個節(jié)點數(shù)組的容量大小。在添加元素到這個list里面的時候刷允,會在最尾部的節(jié)點里的數(shù)組上添加元素铭拧,如果這個節(jié)點的數(shù)組存滿了赃蛛,就再增加一個新的節(jié)點到這個list里面去。

好了搀菩,看到這里呕臂,大家應(yīng)該基本上明白這個list結(jié)構(gòu)了吧?還不明白也沒有關(guān)系肪跋,下面我們來具體看一下它的定義歧蒋,這些定義和相關(guān)的操作函數(shù)定義在src/core/ngx_list.h|c文件中。

typedef struct { 
ngx_list_part_t *last; 
ngx_list_part_t part; 
size_t size; 
ngx_uint_t nalloc; 
ngx_pool_t *pool;
} ngx_list_t;

last:
指向該鏈表的最后一個節(jié)點州既。

part:
該鏈表的首個存放具體元素的節(jié)點谜洽。

size:
鏈表中存放的具體元素所需內(nèi)存大小。

nalloc:
每個節(jié)點所含的固定大小的數(shù)組的容量吴叶。

pool:
該list使用的分配內(nèi)存的pool阐虚。

好,我們在看一下每個節(jié)點的定義蚌卤。

typedef struct ngx_list_part_s ngx_list_part_t;
struct ngx_list_part_s { 
void *elts; 
ngx_uint_t nelts; 
ngx_list_part_t *next;
};

elts:
節(jié)點中存放具體元素的內(nèi)存的開始地址实束。

nelts:
節(jié)點中已有元素個數(shù)。這個值是不能大于鏈表頭節(jié)點ngx_list_t類型中的nalloc字段的逊彭。

next:
指向下一個節(jié)點咸灿。

我們來看一下提供的一個操作的函數(shù)。

ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);

該函數(shù)創(chuàng)建一個ngx_list_t類型的對象,并對該list的第一個節(jié)點分配存放元素的內(nèi)存空間侮叮。

pool:
分配內(nèi)存使用的pool避矢。

n:
每個節(jié)點固定長度的數(shù)組的長度。

size:
存放的具體元素的個數(shù)囊榜。

返回值:
成功返回指向創(chuàng)建的ngx_list_t對象的指針审胸,失敗返回NULL。

void *ngx_list_push(ngx_list_t *list);

該函數(shù)在給定的list的尾部追加一個元素卸勺,并返回指向新元素存放空間的指針歹嘹。如果追加失敗,則返回NULL孔庭。

static ngx_inline ngx_int_t
ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size);

該函數(shù)是用于ngx_list_t類型的對象已經(jīng)存在,但是其第一個節(jié)點存放元素的內(nèi)存空間還未分配的情況下材蛛,可以調(diào)用此函數(shù)來給這個list的首節(jié)點來分配存放元素的內(nèi)存空間圆到。

那么什么時候會出現(xiàn)已經(jīng)有了ngx_list_t類型的對象,而其首節(jié)點存放元素的內(nèi)存尚未分配的情況呢卑吭?那就是這個ngx_list_t類型的變量并不是通過調(diào)用ngx_list_create函數(shù)創(chuàng)建的芽淡。例如:如果某個結(jié)構(gòu)體的一個成員變量是ngx_list_t類型的,那么當(dāng)這個結(jié)構(gòu)體類型的對象被創(chuàng)建出來的時候豆赏,這個成員變量也被創(chuàng)建出來了挣菲,但是它的首節(jié)點的存放元素的內(nèi)存并未被分配富稻。

總之,如果這個ngx_list_t類型的變量白胀,如果不是你通過調(diào)用函數(shù)ngx_list_create創(chuàng)建的椭赋,那么就必須調(diào)用此函數(shù)去初始化,否則或杠,你往這個list里追加元素就可能引發(fā)不可預(yù)知的行為哪怔,亦或程序會崩潰!

ngx_queue_t

ngx_queue_t是nginx中的雙向鏈表,在nginx源碼目錄src/core下面的ngx_queue.h|c里面向抢。它的原型如下:

typedef struct ngx_queue_s ngx_queue_t;

struct ngx_queue_s { 
ngx_queue_t *prev; 
ngx_queue_t *next;
};

不同于教科書中將鏈表節(jié)點的數(shù)據(jù)成員聲明在鏈表節(jié)點的結(jié)構(gòu)體中认境,ngx_queue_t只是聲明了前向和后向指針。在使用的時候挟鸠,我們首先需要定義一個哨兵節(jié)點(對于后續(xù)具體存放數(shù)據(jù)的節(jié)點叉信,我們稱之為數(shù)據(jù)節(jié)點),比如:

ngx_queue_t free;

接下來需要進(jìn)行初始化艘希,通過宏ngx_queue_init()來實現(xiàn):

ngx_queue_init(&free);

ngx_queue_init()的宏定義如下:

#define ngx_queue_init(q) \ 
(q)->prev = q; \
 (q)->next = q;

可見初始的時候哨兵節(jié)點的 prev 和 next 都指向自己硼身,因此其實是一個空鏈表。ngx_queue_empty()可以用來判斷一個鏈表是否為空枢冤,其實現(xiàn)也很簡單鸠姨,就是:

#define ngx_queue_empty(h) \
(h == (h)->prev)

那么如何聲明一個具有數(shù)據(jù)元素的鏈表節(jié)點呢?只要在相應(yīng)的結(jié)構(gòu)體中加上一個 ngx_queue_t 的成員就行了淹真。比如ngx_http_upstream_keepalive_module中的ngx_http_upstream_keepalive_cache_t:

typedef struct { 
ngx_http_upstream_keepalive_srv_conf_t *conf; 
ngx_queue_t queue; 
ngx_connection_t *connection; 
socklen_t socklen; 
u_char sockaddr[NGX_SOCKADDRLEN];
} ngx_http_upstream_keepalive_cache_t;

對于每一個這樣的數(shù)據(jù)節(jié)點讶迁,可以通過ngx_queue_insert_head()來添加到鏈表中,第一個參數(shù)是哨兵節(jié)點核蘸,第二個參數(shù)是數(shù)據(jù)節(jié)點巍糯,比如:

ngx_http_upstream_keepalive_cache_t cache;
ngx_queue_insert_head(&free, &cache.queue);

相應(yīng)的幾個宏定義如下:

#define ngx_queue_insert_head(h, x) \ 
(x)->next = (h)->next; \ 
(x)->next->prev = x; \ 
(x)->prev = h; \ 
(h)->next = x

#define ngx_queue_insert_after ngx_queue_insert_head

#define ngx_queue_insert_tail(h, x) \ 
(x)->prev = (h)->prev; \ 
(x)->prev->next = x; \ 
(x)->next = h; \ 
(h)->prev = x

ngx_queue_insert_head()和ngx_queue_insert_after()都是往頭部添加節(jié)點,ngx_queue_insert_tail()是往尾部添加節(jié)點客扎。從代碼可以看出哨兵節(jié)點的 prev 指向鏈表的尾數(shù)據(jù)節(jié)點祟峦,next 指向鏈表的頭數(shù)據(jù)節(jié)點。另外ngx_queue_head()和ngx_queue_last()這兩個宏分別可以得到頭節(jié)點和尾節(jié)點徙鱼。

那假如現(xiàn)在有一個ngx_queue_t *q 指向的是鏈表中的數(shù)據(jù)節(jié)點的queue成員宅楞,如何得到ngx_http_upstream_keepalive_cache_t的數(shù)據(jù)呢? nginx提供了ngx_queue_data()宏來得到ngx_http_upstream_keepalive_cache_t的指針袱吆,例如:

ngx_http_upstream_keepalive_cache_t *cache = ngx_queue_data(q, 
ngx_http_upstream_keepalive_cache_t, 
queue);

也許您已經(jīng)可以猜到ngx_queue_data是通過地址相減來得到的:

#define ngx_queue_data(q, type, link) \ 
(type *) ((u_char *) q - offsetof(type, link))

另外nginx也提供了ngx_queue_remove()宏來從鏈表中刪除一個數(shù)據(jù)節(jié)點厌衙,以及ngx_queue_add()用來將一個鏈表添加到另一個鏈表。

第八章 負(fù)載均衡簡單配置實戰(zhàn)

準(zhǔn)備三臺虛擬機(jī)來做這個實驗:

192.168.232.132 web服務(wù)器
192.168.232.133 web服務(wù)器
192.168.232.134 負(fù)載均衡服務(wù)器

預(yù)裝nginx軟件

  1. 導(dǎo)入外部軟件庫
rpm -Uvh http://dl.iuscommunity.org/pub/ius/stable/Redhat/6/i386/epel-release-6-5.noarch.rpm  
rpm -Uvh http://dl.iuscommunity.org/pub/ius/stable/Redhat/6/i386/ius-release-1.0-10.ius.el6.noarch.rpm  
rpm -Uvh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm  

以下添加注釋

mirrorlist=http://dmirr.iuscommunity.org/mirrorlist?repo=ius-el6&arch=$basearch  

以下刪除注釋

#baseurl=http://dl.iuscommunity.org/pub/ius/stable/Redhat/5/$basearch  
  1. yum安裝nginx
yum install nginx  ```
3. 啟動nginx

chkconfig nginx on
service nginx start

##向web服務(wù)器中放入測試文件

<html>
<head>
<title>Welcome to nginx!</title>
</head>
<body bgcolor="white" text="black">
<center><h1>Welcome to nginx! 192.168.232.132</h1></center>
</body>
</html>

##配置負(fù)載均衡服務(wù)器

vi /etc/nginx/nginx.conf

內(nèi)容如下:

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '  
                  '$status $body_bytes_sent "$http_referer" '  
                  '"$http_user_agent" "$http_x_forwarded_for"';  

access_log  /var/log/nginx/access.log  main;  

sendfile        on;  
#tcp_nopush     on;  

keepalive_timeout  65;  

#gzip  on;  
upstream test.miaohr.com {  
server 192.168.232.132:80;  
server 192.168.232.133:80;  
}  
  

server {     
    listen       80;     
    server_name  test.miaohr.com;     
    charset utf-8;     
    location / {     
        root   html;     
        index  index.html index.htm;     
        proxy_pass        http://test.miaohr.com;     
        proxy_set_header  X-Real-IP  $remote_addr;     
        client_max_body_size  100m;  
    }     


    location ~ ^/(WEB-INF)/ {      
    deny all;      
    }      

    error_page   500 502 503 504  /50x.html;     
    location = /50x.html {     
        root   /var/www/html/;     
    }     
}     

}

下面瀏覽器打開:192.168.232.134绞绒,如果132婶希、133交替顯示則表明試驗成功。
##拓展:
1. 輪詢(默認(rèn))
每個請求按時間順序逐一分配到不同的后端服務(wù)器蓬衡,如果后端服務(wù)器down掉喻杈,能自動剔除彤枢。
2. weight
指定輪詢幾率,weight和訪問比率成正比筒饰,用于后端服務(wù)器性能不均的情況缴啡。
例如:

upstream bakend {
server 192.168.159.10 weight=10;
server 192.168.159.11 weight=10;
}

3. ip_hash
每個請求按訪問ip的hash結(jié)果分配,這樣每個訪客固定訪問一個后端服務(wù)器龄砰,可以解決session的問題盟猖。
例如:

upstream resinserver{
ip_hash;
server 192.168.159.10:8080;
server 192.168.159.11:8080;
}

4. air(第三方)
按后端服務(wù)器的響應(yīng)時間來分配請求,響應(yīng)時間短的優(yōu)先分配换棚。

upstream resinserver{
server server1;
server server2;
fair;
}

5. url_hash(第三方)
按訪問url的hash結(jié)果來分配請求式镐,使每個url定向到同一個后端服務(wù)器,后端服務(wù)器為緩存時比較有效固蚤。
例:在upstream中加入hash語句娘汞,server語句中不能寫入weight等其他的參數(shù)眼溶,hash_method是使用的hash算法

upstream resinserver{
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}


tips:

upstream resinserver{#定義負(fù)載均衡設(shè)備的Ip及設(shè)備狀態(tài)
ip_hash;
server 127.0.0.1:8000 down;
server 127.0.0.1:8080 weight=2;
server 127.0.0.1:6801;
server 127.0.0.1:6802 backup;
}

在需要使用負(fù)載均衡的server中增加

proxy_pass http://resinserver/;


每個設(shè)備的狀態(tài)設(shè)置為:
+ down 表示單前的server暫時不參與負(fù)載
+ weight 默認(rèn)為1.weight越大漱竖,負(fù)載的權(quán)重就越大哈打。
+ max_fails :允許請求失敗的次數(shù)默認(rèn)為1.當(dāng)超過最大次數(shù)時敬锐,返回proxy_next_upstream 模塊定義的錯誤
+ fail_timeout:max_fails次失敗后,暫停的時間喇勋。
+ backup: 其它所有的非backup機(jī)器down或者忙的時候酗钞,請求backup機(jī)器岸裙。所以這臺機(jī)器壓力會最輕揩页。

nginx支持同時設(shè)置多組的負(fù)載均衡旷偿,用來給不用的server來使用。

+ client_body_in_file_only 設(shè)置為On 可以講client post過來的數(shù)據(jù)記錄到文件中用來做debug
+ client_body_temp_path 設(shè)置記錄文件的目錄 可以設(shè)置最多3層目錄
+ location 對URL進(jìn)行匹配.可以進(jìn)行重定向或者進(jìn)行新的代理 負(fù)載均衡

#第九章    反向代理同一域名的不同端口
##實戰(zhàn)場景
網(wǎng)站上線爆侣,要保證在不影響原正式網(wǎng)站的前提下萍程,部署一套網(wǎng)站的測試環(huán)境,供客戶測試兔仰。
##網(wǎng)站域名
www.xxxxx.com
##備選方案
1. 通過域名+指定路徑的方式部署茫负,即:www.xxxxx.com/test,客戶可以直接通過域名訪問測試環(huán)境乎赴;
2. 通過內(nèi)網(wǎng)IP方式部署忍法,即:選擇一臺nginx服務(wù)器,對測試環(huán)境做反向代理榕吼,客戶只能通過VPN+內(nèi)網(wǎng)IP的方式訪問測試環(huán)境饿序;
3. nginx代理網(wǎng)站子域名,映射到測試環(huán)境友题。即:不同的域名映射到不同的應(yīng)用環(huán)境,這種方式需要申請子域名戴质。
4. nginx代理網(wǎng)站域名的8080端口度宦,映射到測試環(huán)境踢匣。即:同一域名的不同端口映射到不同的應(yīng)用環(huán)境,這種方式需要域名服務(wù)商開放8080端口戈抄。

由于還需代理其他應(yīng)用离唬,所以第一種方式面臨的問題很多,結(jié)果我們選擇的第二種方式第二天被客戶pass划鸽,最終選擇了第四種方式输莺。
##最終配置

user nobody;
worker_processes 8;

error_log logs/error.log;

error_log logs/error.log notice;

error_log logs/error.log info;

pid logs/nginx.pid;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
client_max_body_size 100m;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log  logs/access.log  main;  

sendfile        on;  
#tcp_nopush     on;  

#keepalive_timeout  0;  
keepalive_timeout  65;  

gzip  on;  
gzip_comp_level  6;    # 壓縮比例,比例越大裸诽,壓縮時間越長嫂用。默認(rèn)是1  
gzip_types    text/xml text/plain text/css application/javascript application/x-javascript application/rss+xml;      # 哪些文件可以被壓縮  
gzip_disable    "MSIE [1-6]\.";     # IE6無效  

# 網(wǎng)站服務(wù)器列表  
upstream uni-web {   
 server  xx.x.x.109:8080;   
}   

# 網(wǎng)站英文版      
upstream uni-web-en {  
server xx.x.x.106:8080;  
}  

# pms服務(wù)器列表  
upstream pms {  
  server  xx.x.x.106:8090;  
  server  xx.x.x.109:8090;  
}   

# 運(yùn)營平臺服務(wù)器列表  
upstream control {  
  server  xx.x.x.105:8080;  
}  

#測試環(huán)境     
server {  

    listen 8080;  
    server_name 你的域名;   

    #charset koi8-r;  

    access_log  logs/host.8080.access.log  main;  

    # 轉(zhuǎn)發(fā)所有請求  
    location / {  
        proxy_pass         http://xx.x.x.107;  
        proxy_set_header   Host             $host;  
        proxy_set_header   X-Real-IP        $remote_addr;  
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;  
    }  

}  
  

server {  
    listen 80;  
    server_name  你的域名;  

    #charset koi8-r;  

    access_log  logs/host.access.log  main;  

    # 網(wǎng)站  
    location / {   
        proxy_pass         http://uni-web;   
        proxy_set_header   Host             $host;   
        proxy_set_header   X-Real-IP        $remote_addr;   
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;   
    }  

    # 英文網(wǎng)站  
    location /en {  
        proxy_pass         http://uni-web-en;  
        proxy_set_header   Host             $host;  
        proxy_set_header   X-Real-IP        $remote_addr;  
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;  
    }  

    # pms  
    location /pms {  
        proxy_pass         http://pms;  
        proxy_set_header   Host             $host;  
        proxy_set_header   X-Real-IP        $remote_addr;  
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;  
    }  
    # 運(yùn)營平臺  
    location /cms {  
        proxy_pass         http://control;  
        proxy_set_header   Host             $host;  
        proxy_set_header   X-Real-IP        $remote_addr;  
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;  
    }  

    # jeecms 后臺管理網(wǎng)站  
    location /jeeadmin/ {  
        proxy_pass          http://xx.x.x.107;  
        proxy_set_header   Host             $host;  
        proxy_set_header   X-Real-IP        $remote_addr;  
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;  

rewrite ^(/jeeadmin/)$ /jeeadmin/jeecms/login.do break;

    }  
  

    #location  /apfel150.html {  
    #       rewrite ^/(apfel150.html)$ /study/$1 last;  
    #}  

    #error_page  404              /404.html;  

    # redirect server error pages to the static page /50x.html  
    #  
    error_page   500 502 503 504  /50x.html;  
    location = /50x.html {  
        root   html;  
    }  
     
    location = /baidu_verify_CXOKsFqzpJ.html {  
    root    html;  
}  

    location = /baidusilian.txt {  
    root html;  
}  

    location = /robots.txt {  
    root html;  
    }  

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80  
    #  
    #location ~ \.php$ {  
    #    proxy_pass   http://127.0.0.1;  
    #}  

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000  
    #  
    #location ~ \.php$ {  
    #    root           html;  
    #    fastcgi_pass   127.0.0.1:9000;  
    #    fastcgi_index  index.php;  
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;  
    #    include        fastcgi_params;  
    #}  

    # deny access to .htaccess files, if Apache's document root  
    # concurs with nginx's one  
    #  
    #location ~ /\.ht {  
    #    deny  all;  
    #}  

    #設(shè)定查看Nginx狀態(tài)的地址 ,在安裝時要加上--with-http_stub_status_module參數(shù)   
    location /NginxStatus {   
         stub_status on;   
         access_log on;   
         auth_basic "NginxStatus";   
         auth_basic_user_file conf/htpasswd;     #設(shè)置訪問密碼丈冬,htpasswd -bc filename username password   
    }  
}  

# another virtual host using mix of IP-, name-, and port-based configuration  
#  
#server {  
#    listen       8000;  
#    listen       somename:8080;  
#    server_name  somename  alias  another.alias;  

#    location / {  
#        root   html;  
#        index  index.html index.htm;  
#    }  
#}  


# HTTPS server  
#  
#server {  
#    listen       443 ssl;  
#    server_name  localhost;  

#    ssl_certificate      cert.pem;  
#    ssl_certificate_key  cert.key;  

#    ssl_session_cache    shared:SSL:1m;  
#    ssl_session_timeout  5m;  

#    ssl_ciphers  HIGH:!aNULL:!MD5;  
#    ssl_prefer_server_ciphers  on;  

#    location / {  
#        root   html;  
#        index  index.html index.htm;  
#    }  
#}  

  

# 設(shè)置只允許通過域名訪問站點     
server {  
listen 80 default_server;  
server_name _;  
return 403;  

}

}


#附錄
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嘱函,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子埂蕊,更是在濱河造成了極大的恐慌往弓,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蓄氧,死亡現(xiàn)場離奇詭異函似,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)喉童,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門撇寞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泄朴,你說我怎么就攤上這事重抖。” “怎么了祖灰?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵钟沛,是天一觀的道長。 經(jīng)常有香客問我局扶,道長恨统,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任三妈,我火速辦了婚禮畜埋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘畴蒲。我一直安慰自己悠鞍,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布模燥。 她就那樣靜靜地躺著咖祭,像睡著了一般掩宜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上么翰,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天牺汤,我揣著相機(jī)與錄音,去河邊找鬼浩嫌。 笑死檐迟,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的码耐。 我是一名探鬼主播追迟,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伐坏!你這毒婦竟也來了怔匣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桦沉,失蹤者是張志新(化名)和其女友劉穎每瞒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纯露,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡剿骨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了埠褪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浓利。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钞速,靈堂內(nèi)的尸體忽然破棺而出贷掖,到底是詐尸還是另有隱情,我是刑警寧澤渴语,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布苹威,位于F島的核電站,受9級特大地震影響驾凶,放射性物質(zhì)發(fā)生泄漏牙甫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一调违、第九天 我趴在偏房一處隱蔽的房頂上張望窟哺。 院中可真熱鬧,春花似錦技肩、人聲如沸且轨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旋奢。三九已至阿蝶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間黄绩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工玷过, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留爽丹,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓辛蚊,卻偏偏與公主長得像粤蝎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子袋马,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容