之前兩次singnalr、 websocket實時推送相關:
tag: 瀏覽器--->nginx--> server
其中提到nginx默認不會爲客戶端轉發Upgrade
、Connection
標頭, 因爲爲了讓被代理的後端服務器知道客戶端要升級協議,故要在nginx上顯式轉發標頭:
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協議,但是除了/chat路徑, 服務器其他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
,正常返回大坨json數據。
日誌記錄:
該請求是不是webcocket請求:False,headers:[Accept, */*], [Connection, Upgrade], [Host, localhost:5000], [User-Agent, curl/7.79.1], [Upgrade, websocket]
以上說明,服務端並不認爲是websocket請求, 這也印證了ops雖然錯配,但對於常規的http請求沒造成影響。
那服務端到底是怎麼認定websocket請求?
依次判斷;
- HttpMethod: GET
- Sec-WebSocket-Version標頭==13
- Connection標頭==Upgrade
- Upgrade標頭==websocket
- 有效的Sec-WebSocket-Key標頭
這樣我們就明白了,雖然websocket協議基於http,添加了httpConnection
、Upgrade
標頭,但是瀏覽器實際會給我們帶上Sec-WebSocket-Key
、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請求。
日誌記錄:
該請求是不是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數據,從這個層面上看,http協議是兼容websocket的。
真正要讓服務端按照websocket姿勢, 要使用HttpContext.WebSockets.AcceptWebSocketAsync()
告知客戶端開始切換協議,並在原tcp上發起全雙工通信。
前後對比, 困惑得解: 雖然nginx爲http請求轉發了Connection
、Upgrade
標頭, 但是服務器並不認可這是websocket升級協議,認爲是攜帶了特殊標頭的http請求,走原來的http業務處理邏輯是沒有問題的。
就坡下驢
將腳手架項目改成一個同時支持http和websocket協議的Action 吧:
[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();
while(true) // 服務端單向推送
{
var wf = Summaries[Random.Shared.Next(Summaries.Length)];
var serverMsg = Encoding.UTF8.GetBytes(wf);
await webSocket.SendAsync(new ArraySegment<byte>(serverMsg, 0, serverMsg.Length), WebSocketMessageType.Text, false, 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);
}
}
}
總結
- 本文記錄了nginx在轉發websocket請求時要添加的配置
- websocket以http協議爲藍本,添加了特定的htp標頭來要求切換協議;爲了與常規http區分,瀏覽器自動增加了Sec-websocket-key等標頭, 讓服務端認爲這是一個有效的websocket請求。