第一章 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個包
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é)果如下
第三章 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.com和www.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啟動后,如果我們要操作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長度的確定:
- 對于http1.0協(xié)議來說偿洁,如果響應(yīng)頭中有content-length頭逮诲,則以content-length的長度就可以知道body的長度了秸应,客戶端在接收body時荆针,就可以依照這個長度來接收數(shù)據(jù),接收完后梗肝,就表示這個請求完成了榛瓮。而如果沒有content-length頭,則客戶端會一直接收數(shù)據(jù)巫击,直到服務(wù)端主動斷開連接禀晓,才表示body接收完了。
- 而對于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軟件
- 導(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
- 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;
}
}
#附錄