昨日的文章沒指出websocket請求協商切換的精髓,刪除重發。
前文相關:
??.NET WebSockets 核心原理初體驗[1]
??SignalR 從開發到生產部署避坑指南[2]
tag:瀏覽器--->nginx--> server
其中提到nginx默認不會為客戶端轉發Upgrade
、Connection
標頭[3], 因為為了讓被代理的后端服務器知道客戶端要升級協議,故要在nginx上顯式轉發標頭:
#?以下為?/realtime/路徑請求添加??Connection、Upgrade標頭location?/realtime/?{?????proxy_pass?http://backend;proxy_http_version?1.1;proxy_set_header?Upgrade?$http_upgrade;proxy_set_header?Connection?"upgrade";
}
事情本該就就這么簡單, 但devops總會有各種奇怪的姿勢。
小動作引起的頭腦風暴
但是運維在給nginx配置的時候,給/
根路徑配置了webcoket協議升級標頭。
按照字面理解,導致所有的客戶端轉發請求都在要求切換到websocket協議,但是除了/realtime路徑, 服務器其他http路徑并沒有做websocket協議的邏輯,那其他http請求是不是都該報錯了。

是實際看,所有的請求(websocket、http)都沒有報錯,都按照指定預期返回。
刨一下
利用asp.netcore默認腳手架項目:
已知http://localhost:5000/WeatherForecast
是http請求,返回一大坨json數據;
在WeatherForecast
添加斷言日志:

模擬ops的錯配效果,我們給這個請求添加websocket協議升級標頭。
第一次:curl 'http://localhost:5000/WeatherForecast' -H 'Upgrade: websocket' -H 'Connection: Upgrade' --verbose
?=====> 200ok、大坨json數據。
日志記錄:
該請求是不是webcocket請求:False,headers:[Accept,?*/*],?[Connection,?Upgrade],?[Host,?localhost:5000],?[User-Agent,?curl/7.79.1],?[Upgrade,?websocket]
以上說明,服務端并不認為是websocket請求,但是按照http業務處理返回了200ok+大坨json數據,這演示了ops雖然錯配,但對于常規的http請求沒造成影響。
那服務端到底是怎么認定websocket請求?
從服務端認定websocket請求的源碼[4]看

依次判斷;
??HttpMethod: GET
??Sec-WebSocket-Version標頭==13
??Connection標頭==Upgrade
??Upgrade標頭==websocket
??有效的Sec-WebSocket-Key標頭
這樣我們就明白了,雖然websocket協議基于http,添加了httpConnection
、Upgrade
協商標頭,但是瀏覽器實際會給我們帶上Sec-WebSocket-Key
[5]、Sec-WebSocket-Version
等標頭,以向服務器證明這是一個有效的websocket握手。
于是我們可以使用?curl 'http://localhost:5000/WeatherForecast' -H 'Upgrade: websocket' -H 'Connection: Upgrade' -H 'Sec-WebSocket-Version: 13' -H 'Sec-webSocket-Key: eeZn6lg/rOu8QbKwltqHDA==' --verbose
?仿造客戶端websocket請求。=====> 200ok、 大坨json數據
這里提示:瀏覽器websocket會自動幫我們加上這些標頭;在前端編程 let ss = new WebSocket("ws://localhost:5000/WeatherForecast") 也會自動幫助我們帶上這些標頭。

日志記錄:
該請求是不是webcocket請求:True,headers:[Accept,?*/*],?[Connection,?Upgrade],?[Host,?localhost:5000],?[User-Agent,?curl/7.79.1],?[Upgrade,?websocket],?[Sec-WebSocket-Version,?13],?[Sec-WebSocket-Key,?eeZn6lg/rOu8QbKwltqHDA==]
服務器認可這是websocket請求,服務端處理邏輯沒改,故按原http代碼邏輯返回200ok和JSON數據。
真正要讓服務端按照websocket姿勢, 要使用HttpContext.WebSockets.AcceptWebSocketAsync()
告知客戶端開始切換協議,返回101響應碼[6],并在原tcp上發起全雙工通信。
協商切換
以上行為完美詮釋了協商切換?的理念。
客戶端僅攜帶 Connection、Upgrade標頭,被服務端當成一般的http標頭。
但是若帶上sec-websocket-verison
、sec-websocket-key
,則被認為是有效的websocket請求,既然是“協商”, 服務器依舊可以拒絕切換,用原http協議返回。
就坡下驢,將腳手架項目改成一個同時支持http和websocket協議的Action吧:
//?服務端對于websocket請求,使用服務端單向推送[HttpGet(Name?=?"GetWeatherForecast")]public?async?Task?Get(){_logger.LogInformation("該請求是不是webcocket請求:"+?HttpContext.WebSockets.IsWebSocketRequest+",headers:{0}",?Request.Headers);if?(HttpContext.WebSockets.IsWebSocketRequest){var?webSocket?=?await?HttpContext.WebSockets.AcceptWebSocketAsync();Enumerable.Range(1,?5).ToList().ForEach(async?x?=>{var?wf?=?new?WeatherForecast{Date?=?DateTime.Now.AddDays(x),TemperatureC?=?Random.Shared.Next(-20,?55),Summary?=?Summaries[Random.Shared.Next(Summaries.Length)]};var?ss?=?JsonConvert.SerializeObject(wf);var?serverMsg?=?Encoding.UTF8.GetBytes(ss);//?下面參數3:true 表示結束此次消息await?webSocket.SendAsync(new?ArraySegment<byte>(serverMsg,?0,?serverMsg.Length),?WebSocketMessageType.Text,?true,?CancellationToken.None);});}else{var?arr?=?Enumerable.Range(1,?5).Select(index?=>?new?WeatherForecast{Date?=?DateTime.Now.AddDays(index),TemperatureC?=?Random.Shared.Next(-20,?55),Summary?=?Summaries[Random.Shared.Next(Summaries.Length)]}).ToArray();await??Response.WriteAsJsonAsync(arr);}}
curl 'http://localhost:5000/WeatherForecast' -H 'Upgrade: websocket' -H 'Connection: Upgrade' -H 'Sec-WebSocket-Version: 13' -H 'Sec-webSocket-Key: eeZn6lg/rOu8QbKwltqHDA==' --verbose

curl 'http://localhost:5000/WeatherForecast' -H 'Upgrade: websocket' -H 'Connection: Upgrade' --verbose

回顧
1. 本文記錄了nginx在轉發websocket請求時要添加的配置。
2. 雖然ops錯配了nginx for websocket url:nginx為http請求轉發了
Connection
、Upgrade
標頭, 但是服務器并不認可這是websocket升級協議,僅認為是攜帶了特殊標頭的http請求,走原來的http業務處理邏輯是沒有問題的。3. 在curl指令添加了sec-websocket-version、sec-websocket-key 標頭,從客戶端仿造了真實的websocket請求。
再次提示:瀏覽器對websocket協商切換會自動幫我們加上這些標頭;在前端編程 let ss = new WebSocket("ws://localhost:5000/WeatherForecast") 也會自動幫助我們帶上這些標頭。
4.?websocket是基于http協議為藍本,是一個協商切換協議的行為,既然是協商, 服務端是可以拒絕協議切換,依舊采用原http協議來處理。
本文內容和制圖均為原創,文章永久更新地址請參閱左下角博客園原文;
如公號內容對您有所幫助,請一鍵三連,方便的話置一個星標 ~。。~。
引用鏈接
[1]
?.NET WebSockets 核心原理初體驗:?https://www.cnblogs.com/JulianHuang/p/14681331.html[2]
?SignalR 從開發到生產部署避坑指南:?https://www.cnblogs.com/JulianHuang/p/15434137.html[3]
?nginx默認不會為客戶端轉發Upgrade
、Connection
標頭:?https://nginx.org/en/docs/http/websocket.html[4]
?服務端認定websocket請求的源碼:?https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/WebSockets/src/WebSocketMiddleware.cs#L219[5]
?Sec-WebSocket-Key
:?https://www.rfc-editor.org/rfc/rfc6455#section-11.3.1[6]
?開始切換協議,返回101響應碼:?https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/WebSockets/src/WebSocketMiddleware.cs#L134