摘要:主要涉及到的協議以及的處理流程。并且中必須建立在協議之上。所以對協議的服務發起請求時,一般瀏覽器會建立條連接,并行的去請求不同的資源。表明該字段是否使用了編碼。
運營研發 張仕華
本文通過一個小例子串一遍nginx處理http2的流程。主要涉及到http2的協議以及nginx的處理流程。
http2簡介http2比較http1.1主要有如下五個方面的不同:
二進制協議http1.1請求行和請求頭部都是純文本編碼,即可以直接按ascii字符解釋,而http2是有自己的編碼格式。并且nginx中http2必須建立在ssl協議之上。
頭部壓縮舉個例子,HTTP1.1傳一個header
http1.1一個連接上只能傳輸一個請求,當一個請求結束之后才能傳輸下一個請求。所以對http1.1協議的服務發起請求時,一般瀏覽器會建立6條連接,并行的去請求不同的資源。而http2的二進制協議中有一個frame的概念,每個frame有自己的id,所以一個連接上可以同時多路復用傳輸多個不同id的frame
主動pushhttp1.1是請求-響應模型,而http2可以主動給客戶端推送資源
優先級既然多路復用,所有數據跑在了一條通道上,必然會有優先級的需求
本文的例子主要通過解析報文說明頭三個特性
配置環境NGINX配置如下:
server { listen 8443 ssl http2; access_log logs/host_server2.access.log main; ssl_certificate /home/xiaoju/nginx-2/nginx-selfsigned.crt; ssl_certificate_key /home/xiaoju/nginx-2/nginx-selfsigned.key; ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; location / { root html; index index.html index.htm /abc.html; access_log logs/host_location3.access.log main; http2_push /favicon.ico; http2_push /nginx.png; } }
客戶端按如下方式發起請求:
curl -k -I -L https://IP:8443 HTTP/2 200 //可以看到,返回是http/2 server: nginx/1.14.0 date: Tue, 11 Dec 2018 09:20:33 GMT content-type: text/html content-length: 664 last-modified: Tue, 11 Dec 2018 04:19:32 GMT etag: "5c0f3ad4-298" accept-ranges: bytes請求解析 客戶端請求問題
先思考一個問題,上文配置中使用curl發送請求時,為何直接返回的是http/2,而不是http/1.1(雖然服務端配置了使用http2,但萬一客戶端未支持http2協議,直接返回http2客戶端會解析不了)
因為nginx中http2必須在ssl之上,所以我們首先通過在nginx代碼中的ssl握手部分打斷點gdb跟一下.
(gdb) b ngx_ssl_handshake_handler //ssl握手函數 Breakpoint 1 at 0x47ddb5: file src/event/ngx_event_openssl.c, line 1373. (gdb) c Continuing. Breakpoint 1, ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1373 1373 { 1390 c->ssl->handler(c); //實際處理邏輯位于ngx_http_ssl_handshake_handler (gdb) s ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:782 782 { (gdb) n 805 if (hc->addr_conf->http2) { //配置http2后hc->addr_conf->http2標志位為1 (gdb) n 808 SSL_get0_alpn_selected(c->ssl->connection, &data, &len);//從ssl協議中取出alpn (gdb) n 820 if (len == 2 && data[0] == "h" && data[1] == "2") { //如果為h2,說明客戶端支持升級到http2協議 (gdb) n 821 ngx_http_v2_init(c->read);//開始進入http2的初始化階段
簡單說就是通過ssl協議握手階段獲取一個alpn相關的配置,如果是h2,就進入http2的處理流程。我們通過wireshark抓包可以更直觀的看出這個流程
如上圖,在ssl握手中的Client Hello 階段有一個協議擴展alpn
http2報文格式http2 以一個preface開頭,接著是一個個的frame,其中每個frame都有一個header,如下:
其中length代表frame內容的長度,type表明frame的類型,flag給frame做一些特殊的標記,sid代表的就是frame的id.
其中 frame有如下10種類型
#define NGX_HTTP_V2_DATA_FRAME 0x0 //body數據 #define NGX_HTTP_V2_HEADERS_FRAME 0x1 //header數據 #define NGX_HTTP_V2_PRIORITY_FRAME 0x2 //優先級設置 #define NGX_HTTP_V2_RST_STREAM_FRAME 0x3 //重置一個stream #define NGX_HTTP_V2_SETTINGS_FRAME 0x4 //其他設置項,例如是否開啟push,同時能夠處理的stream數量等 #define NGX_HTTP_V2_PUSH_PROMISE_FRAME 0x5 //push #define NGX_HTTP_V2_PING_FRAME 0x6 //ping #define NGX_HTTP_V2_GOAWAY_FRAME 0x7 //goaway.發送此frame后會重新建立連接 #define NGX_HTTP_V2_WINDOW_UPDATE_FRAME 0x8 //窗口更新 流控使用 #define NGX_HTTP_V2_CONTINUATION_FRAME 0x9 //當一個frame發送不完數據時,可以按continuation格式繼續發送
frame ID在客戶端按奇數遞增,例如1,3,5,偶數型id留給服務端推送push時使用,設置連接屬性相關的frame id都為0
flags有如下定義:
#define NGX_HTTP_V2_NO_FLAG 0x00 //未設置 #define NGX_HTTP_V2_ACK_FLAG 0x01 //ack flag #define NGX_HTTP_V2_END_STREAM_FLAG 0x01 //結束stream #define NGX_HTTP_V2_END_HEADERS_FLAG 0x04 //結束headers #define NGX_HTTP_V2_PADDED_FLAG 0x08 //填充flag #define NGX_HTTP_V2_PRIORITY_FLAG 0x20 //優先級設置flag
如下是一個http頭類型frame具體的內容格式:
padded和priority由上文頭部的flag決定是否有這兩字段。接下來占8bit的flag決定header是否需要索引,如果需要,索引號是多少。
huff(1)表明該字段是否使用了huffman編碼。header_value_len(7)和header_value是具體頭字段的value值
如下是一個設置相關的frame
如下是一個窗口更新的frame
下邊我們看一個具體的例子,來更直觀的了解下。
http2報文解析新版本的curl有一個–http2參數,可以直接指明使用http2進行通訊。我們將客戶端命令修改如下:
curl --http2 -k -I -L https://10.96.79.14:8443
通過上邊的gdb跟蹤,我們看到http2初始化入口函數為ngx_http_v2_init,直接在此處打斷點,繼續跟蹤代碼.跟蹤過程不再詳細描述,當把報文讀取進緩存之后,我們直接在gdb中bt查看調用路徑,如下:
#0 ngx_http_v2_state_preface (h2c=0x15a9310, pos=0x164b0b0 "PRI * HTTP/2.0 SM ", end=0x164b11e "") at src/http/v2/ngx_http_v2.c:713 #1 0x00000000004bca20 in ngx_http_v2_read_handler (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:415 #2 0x00000000004bcf8a in ngx_http_v2_init (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:328 #3 0x0000000000490a13 in ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:821 #4 0x000000000047de24 in ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1390 #5 0x0000000000479637 in ngx_epoll_process_events (cycle=0x1597e30, timer=, flags= ) at src/event/modules/ngx_epoll_module.c:902 #6 0x000000000046f9db in ngx_process_events_and_timers (cycle=0x1597e30) at src/event/ngx_event.c:242 #7 0x000000000047761c in ngx_worker_process_cycle (cycle=0x1597e30, data= ) at src/os/unix/ngx_process_cycle.c:750 #8 0x0000000000475c50 in ngx_spawn_process (cycle=0x1597e30, proc=0x477589 , data=0x0, name=0x684922 "worker process", respawn=-3) at src/os/unix/ngx_process.c:199 #9 0x00000000004769aa in ngx_start_worker_processes (cycle=0x1597e30, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359 #10 0x0000000000477cb0 in ngx_master_process_cycle (cycle=0x1597e30) at src/os/unix/ngx_process_cycle.c:131 #11 0x0000000000450ea4 in main (argc= , argv= ) at src/core/nginx.c:382
調用到ngx_http_v2_state_preface這個函數之后,開始處理http2請求,我們將請求內容打印出來看一下:
(gdb) p end-pos $1 = 110 (gdb) p *pos@110 $2 = "PRI * HTTP/2.0 SM