websocket 是怎麼連接的

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近項目新增了一個 websocket 服務,用 nginx 做了一個簡單的端口轉發,然後調用的時候發現報錯:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"error: Unexpected server response: 426"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"搜索引擎走一波,找到幾篇相關文章:"}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"link","attrs":{"href":"https://nginx.org/en/docs/http/websocket.html","title":""},"content":[{"type":"text","text":"https://nginx.org/en/docs/http/websocket.html"}]}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.nginx.com/blog/websocket-nginx","title":""},"content":[{"type":"text","text":"https://www.nginx.com/blog/websocket-nginx"}]}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決方式也很簡單,根據第一篇文章的說明,只要增加轉發響應頭的配置:"}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"proxy_set_header Upgrade $http_upgrade;"}]}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"proxy_set_header Connection 'upgrade';"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲 websocket 的連接建立是基於 HTTP/1.1 的,所以有必要制定 http 協議:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"proxy_http_version: 1.1"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然這不是必須的,如果服務默認是 HTTP/1.1 的話"}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體配置如下:"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"http {\n map $http_upgrade $connection_upgrade {\n default upgrade;\n '' close;\n }\n\n server {\n ...\n\n location /chat/ {\n proxy_pass http://backend;\n proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection $connection_upgrade;\n }\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但多年踩坑經驗告訴我,如果不瞭解某項技術的細節,早晚還要掉進下一個坑。所以打算好好研究下這裏面的執行邏輯。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Websocket 建立連接的過程分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖是 websocket "},{"type":"text","marks":[{"type":"strong"}],"text":"建立連接-數據傳輸-斷開連接"},{"type":"text","text":" 的整個過程"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cdba741ea4c5416440613bf8d0855565.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Http1.1 支持協議轉換機制,具體說明參考 "},{"type":"link","attrs":{"href":"https://tools.ietf.org/html/rfc2616#section-14.42","title":""},"content":[{"type":"text","text":"https://tools.ietf.org/html/rfc2616#section-14.42"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"首先客戶端發起 GET 請求"}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d7/d7593a0a9e59f23f957a22d0a7d4be1e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏主要關注 Connection 和 Upgrade 兩個請求頭:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置 Connection 頭的值爲 Upgrade 來指示這是一個升級請求"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Upgrade 頭制定要升級到的協議"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"服務端響應"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果服務端決定升級這次連接,就會返回 101 Switching Protocols 響應狀態"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d3/d3bdd2deaaf08a12cb9a45e4a53f39c9.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過以上過程,Websocket 的連接就建立成功了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"但爲什麼nginx 代理需要明確配置 Connection 和 Upgrade 請求頭呢?"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原來,http 協議將消息頭分爲了兩類:end-to-end 和 hop-by-hop,具體說明參考:"},{"type":"link","attrs":{"href":"https://tools.ietf.org/html/rfc2616#section-13.5.1","title":""},"content":[{"type":"text","text":"https://tools.ietf.org/html/rfc2616#section-13.5.1"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他們的特性如下:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"end-to-end"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該類型的消息頭會被代理轉發"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該類型的消息頭會被緩存"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hop-to-hop: "}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該類型的消息頭不會被代理轉發"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該類型的消息頭不會被緩存"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣就不難理解,nginx 配置代理轉發後,默認並不能轉發 Connection 和 Upgrade 消息頭,這樣轉發後的請求到達 Websockt 服務後就沒有這兩個頭,因此就不能由 http 協議升級爲 websocket 協議。因此需要在 nginx 中進行手動配置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下爲 HTTP/1.1 中 hop-to-hop 類型的消息頭:Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailers, Transfer-Encoding, Upgrade"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"連接爲什麼斷開了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面的抓包可以看到,最終 websocket 建立的連接被自動斷開了,文檔中也做出瞭解釋,這是 nginx 的機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"默認情況下,如果被代理的服務 60 秒內沒有進行數據傳輸,連接就會被斷開。這個超時時間可以通過 proxy_read_timeout 指令設置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果要保持建立的連接不斷開,就要通過心跳包等手段進行維持。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"注意"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"該升級機制只是 HTTP/1.1 有效,HTTP/2 已不支持該機制"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"nginx 會斷開長時間沒有數據傳輸的連接"}]}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章