1. 程式人生 > >理解ASP.NET Core 中的WebSocket

理解ASP.NET Core 中的WebSocket

在本文中,我們將詳細介紹RFC 6455 WebSocket規範,並配置一個通用的.NET 5應用程式通過WebSocket連線與SignalR通訊。

我們將深入底層的概念,以理解底層發生了什麼。

關於WebSocket

引入WebSocket是為了實現客戶端和伺服器之間的雙向通訊。HTTP 1.0的一個痛點是每次向伺服器傳送請求時建立和關閉連線。但是,在HTTP 1.1中,通過使用保持連線機制引入了持久連線(RFC 2616)。這樣,連線可以被多個請求重用——這將減少延遲,因為伺服器知道客戶端,它們不需要在每個請求的握手過程中啟動。

WebSocket建立在HTTP 1.1規範之上,因為它允許持久連線。因此,當你第一次建立WebSocket連線時,它本質上是一個HTTP 1.1請求(稍後詳細介紹)。這使得客戶端和伺服器之間能夠進行實時通訊。簡單地說,下圖描述了在發起(握手)、資料傳輸和關閉WS連線期間發生的事情。我們將在後面更深入地研究這些概念。

協議中包含了兩部分:握手和資料傳輸。

握手

讓我們先從握手開始。

簡單地說,WebSocket連線基於單個埠上的HTTP(和作為傳輸的TCP)。下面是這些步驟的總結。

1. 伺服器必須監聽傳入的TCP套接字連線。這可以是你分配的任何埠—通常是80或443。

2. 客戶端通過一個HTTP GET請求發起開始握手(否則伺服器將不知道與誰對話)——這是“WebSockets”中的“Web”部分。在訊息報頭中,客戶端將請求伺服器將連線升級到WebSocket。

3. 伺服器傳送一個握手響應,告訴客戶端它將把協議從HTTP更改為WebSocket。

4. 客戶端和伺服器雙方協商連線細節。任何一方都可以退出。

下面是一個典型的開啟(客戶端)握手請求的樣子。​​​​​​

GET /ws-endpoint HTTP/1.1
Host: example.com:80
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: L4kHN+1Bx7zKbxsDbqgzHw==
Sec-WebSocket-Version: 13


注意客戶端是如何在請求中傳送Connection: Upgrade和Upgrade: websocket報頭的。

並且,伺服器握手響應。​​​​​​

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: CTPN8jCb3BUjBjBtdjwSQCytuBo=


資料傳輸

我們需要理解的下一個關鍵概念是資料傳輸。任何一方都可以在任何給定的時間傳送訊息——因為它是一個全雙工通訊協議。

訊息由一個或多個幀組成。幀的型別可以是文字(UTF-8)、二進位制和控制幀(例如0x8 (Close)、0x9 (Ping)和0xA (Pong))。

安裝

讓我們付諸行動,看看它是如何工作的。

首先建立一個 ASP.NET 5 WebAPI 專案。

dotnet new webapi -n WebSocketsTutorial
dotnet new sln
dotnet sln add WebSocketsTutorial


現在新增SignalR到專案中。

dotnet add WebSocketsTutorial/ package Microsoft.AspNet.SignalR


示例程式碼

我們首先將WebSockets中介軟體新增到我們的WebAPI應用程式中。開啟Startup.cs,向Configure方法新增下面的程式碼。

在本教程中,我喜歡保持簡單。因此,我不打算討論SignalR。它將完全基於WebSocket通訊。你也可以用原始的WebSockets實現同樣的功能,如果你想讓事情變得更簡單,你不需要使用SignalR。

app.UseWebSockets();


接下來,我們將刪除預設的WeatherForecastController,並新增一個名為WebSocketsController的新控制器。注意,我們將只是使用一個控制器action,而不是攔截請求管道。

這個控制器的完整程式碼如下所示。​​​​​​​

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace WebSocketsTutorial.Controllers{
    [ApiController]
    [Route("[controller]")]
    public class WebSocketsController : ControllerBase
    {
        private readonly ILogger<WebSocketsController> _logger;

        public WebSocketsController(ILogger<WebSocketsController> logger)
        {
            _logger = logger;
        }

        [HttpGet("/ws")]
        public async Task Get()
        {
          if (HttpContext.WebSockets.IsWebSocketRequest)
          {
              using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
              _logger.Log(LogLevel.Information, "WebSocket connection established");
              await Echo(webSocket);
          }
          else
          {
              HttpContext.Response.StatusCode = 400;
          }
        }
        
        private async Task Echo(WebSocket webSocket)
        {
            var buffer = new byte[1024 * 4];
            var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
            _logger.Log(LogLevel.Information, "Message received from Client");

            while (!result.CloseStatus.HasValue)
            {
                var serverMsg = Encoding.UTF8.GetBytes($"Server: Hello. You said: {Encoding.UTF8.GetString(buffer)}");
                await webSocket.SendAsync(new ArraySegment<byte>(serverMsg, 0, serverMsg.Length), result.MessageType, result.EndOfMessage, CancellationToken.None);
                _logger.Log(LogLevel.Information, "Message sent to Client");

                result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                _logger.Log(LogLevel.Information, "Message received from Client");
                
            }
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
            _logger.Log(LogLevel.Information, "WebSocket connection closed");
        }
    }
}


這是我們所做的。

1、新增一個名為ws/的新路由。

2、檢查當前請求是否通過WebSockets,否則丟擲400。

3、等待,直到客戶端發起請求。

4、進入一個迴圈,直到客戶端關閉連線。

5、在迴圈中,我們將傳送“Server: Hello. You said: <client’s message>”資訊,並把它發回給客戶端。

6、等待,直到客戶端傳送另一個請求。

注意,在初始握手之後,伺服器不需要等待客戶端傳送請求來將訊息推送到客戶端。讓我們執行應用程式,看看它是否工作。

dotnet run --project WebSocketsTutorial


執行應用程式後,請訪問https://localhost:5001/swagger/index.html,應該看到Swagger UI。

現在我們將看到如何讓客戶端和伺服器彼此通訊。在這個演示中,我將使用Chrome的DevTools(開啟新標籤→檢查或按F12→控制檯標籤)。但是,你可以選擇任何客戶端。

首先,我們將建立一個到伺服器終結點的WebSocket連線。

let webSocket = new WebSocket('wss://localhost:5001/ws');


它所做的是,在客戶端和伺服器之間發起一個連線。wss://是WebSockets安全協議,因為我們的WebAPI應用程式是通過TLS服務的。

然後,可以通過呼叫webSocket.send()方法傳送訊息。你的控制檯應該類似於下面的控制檯。

讓我們仔細看看WebSocket連線

如果轉到Network選項卡,則通過WS選項卡過濾掉請求,並單擊最後一個稱為WS的請求。

單擊Messages選項卡並檢查來回傳遞的訊息。在此期間,如果呼叫以下命令,將能夠看到“This was sent from the Client!”。試試吧!

webSocket.send("Client: Hello");


如你所見,伺服器確實需要等待客戶端傳送響應(即在初始握手之後),並且客戶端可以傳送訊息而不會被阻塞。這是全雙工通訊。我們已經討論了WebSocket通訊的資料傳輸方面。作為練習,你可以執行一個迴圈將訊息推送到客戶機,以檢視它的執行情況。

除此之外,伺服器和客戶端還可以通過ping-pong來檢視客戶端是否還活著。這是WebSockets中的一個實際特性!如果你真的想看看這些資料包,你可以使用像WireShark這樣的工具來了解。

它是如何握手的?好吧,如果你跳轉到Headers選項卡,你將能夠看到我們在這篇文章的第一部分談到的請求-響應標題。

也可以嘗試一下webSocket.close(),這樣我們就可以完全覆蓋open-data-close迴圈了。

結論

如果你對WebSocket的RFC感興趣,請訪問RFC 6455並閱讀。這篇文章只是觸及了WebSocket的表面,還有很多其他的東西我們可以討論,比如安全,負載平衡,代理等等。

原文連結:https://sahansera.dev/understanding-websockets-with-aspnetcore-5/

​​​​​