NGINX HTTP2 處理流程

本文經過一個小例子串一遍nginx處理http2的流程。主要涉及到http2的協議以及nginx的處理流程。html

http2簡介

http2比較http1.1主要有以下五個方面的不一樣:nginx

  • 二進制協議 http1.1請求行和請求頭部都是純文本編碼,便可以直接按ascii字符解釋,而http2是有本身的編碼格式。而且nginx中http2必須創建在ssl協議之上。
  • 頭部壓縮 舉個例子,HTTP1.1傳一個header <method: GET>,須要11個字符.http2中有一個靜態索引表,客戶端傳索引鍵,例如1,nginx經過查表能知道1表明method: GET.nginx中除了該靜態表,還會有一個動態表,保存例如host這種變化的頭部
  • 多路複用 http1.1一個鏈接上只能傳輸一個請求,當一個請求結束以後才能傳輸下一個請求。因此對http1.1協議的服務發起請求時,通常瀏覽器會創建6條鏈接,並行的去請求不一樣的資源。而http2的二進制協議中有一個frame的概念,每一個frame有本身的id,因此一個鏈接上能夠同時多路複用傳輸多個不一樣id的frame
  • 主動push http1.1是請求-響應模型,而http2能夠主動給客戶端推送資源
  • 優先級 既然多路複用,全部數據跑在了一條通道上,必然會有優先級的需求

本文的例子主要經過解析報文說明頭三個特性promise

配置環境

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客戶端會解析不了)curl

由於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抓包能夠更直觀的看出這個流程ui

圖片描述
如上圖,在ssl握手中的Client Hello 階段有一個協議擴展alpn編碼

http2報文格式

http2 以一個preface開頭,接着是一個個的frame,其中每一個frame都有一個header,以下:url

圖片描述
其中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\r\n\r\nSM\r\n\r\n", 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=<optimized out>, flags=<optimized out>)
    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=<optimized out>) at src/os/unix/ngx_process_cycle.c:750
#8  0x0000000000475c50 in ngx_spawn_process (cycle=0x1597e30, proc=0x477589 <ngx_worker_process_cycle>, 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=<optimized out>, argv=<optimized out>) 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\r\n\r\nSM\r\n\r\n\000\000\022\004\000\000\000\000\000\000\003\000\000\000d\000\004@\000\000\000\000\002\000\000\000\000\000\000\004\b\000\000\000\000\000?\377\000\001\000\000%\001\005\000\000\000\001B\004HEAD\204\207A\214\b\027}\305\335}p\265q\346\232gz\210%\266Pë\266\322\340S\003*/*"

nginx接下來開始處理http2請求,處理方法能夠按上述方法繼續跟蹤,咱們直接按http2協議將上述報文解析一下,以下所示:

注意gdb打印出來的是八進制格式

圖片描述
圖片描述

http push抓包

注意上文nginx配置中配置了兩條http2_push指令,即服務端會在請求index.html時主動將favicon.ico和nginx.png兩個圖片push下去。

wireshark中抓包以下:

圖片描述
服務端首先發送一個push_promise報文,報文中會包括push的文件路徑和frame id.第二個和第三個紅框即開始push具體的信息,frame id分別爲2和4

咱們從瀏覽器端看一下push的請求:

圖片描述
不主動push請求以下:
圖片描述
瀏覽器必須首先將index.html加載以後纔會知道接着去請求哪些資源,因而favicon.ico和nginx.png就會延遲加載。

Q&A

  • HTTP2若是在服務端動態索引header,會使http變成有狀態的服務,集羣之間如何解決header頭緩存的問題?
  • 靜態資源文件首次請求後會在瀏覽器端緩存,push如何保證只推送一次(即只有首次請求時才push)?

參考資料

1.https://www.nginx.com/blog/ht...

2.https://httpwg.org/specs/rfc7540

相關文章
相關標籤/搜索