ASP.NET Core 中的實時框架 SingalR

SignalR 是什麼?

ASP.NET Core SignalR 是一個開源的實時框架,它簡化了嚮應用中添加實時 Web 功能的過程。
實時 Web 功能是服務器端可以即時的將數據推送到客戶端,而無需讓服務器等待客戶端請求後才返回數據。git

SignalR 主要適用於:github

  • 從服務器獲取數據並高頻更新的應用。好比股票,GPS應用等。
  • 儀表板和監視應用。好比狀態實時更新等。
  • 須要通知的應用。好比即時聊天工具,以及社交網絡裏面的通知等。
  • 協做應用。好比團體會議軟件。

SignalR 支持下面幾種底層傳輸技術:web

  • Web Socket 是不一樣於HTTP的另外一種TCP協議。它是全雙工的通訊協議,瀏覽器和服務器之間能夠相互通訊。它會保持長鏈接狀態只到被主動關閉。它支持文本和二進制的消息傳輸,也支持流媒體。其實正常的HTTP請求也是使用TCP Socket. Web Socket標準使用了握手機制把用於HTTP的Socket升級爲使用WS協議的 WebSocket socket.
  • 服務器發送事件 (Server Sent Events) 服務器能夠在任什麼時候間把數據發送到瀏覽器,而瀏覽器則會監聽進來的信息,並使用一個叫作EventSource的對象用來處理傳過來的信息。這個鏈接一直保持開放,直到服務器主動關閉它。它是單向通訊,只能發生文本信息,並且不少瀏覽器都有最大併發鏈接數的限制。
  • 長輪詢(Long Polling) 客戶端會按期的向服務器發送HTTP請求,若是服務器沒有新數據的話,那麼服務器會繼續保持鏈接,直到有新的數據產生, 服務器才把新的數據返回給客戶端。若是請求發出後一段時間內沒有響應, 那麼請求就會超時。這時,客戶端會再次發出請求。

SignalR 封裝了這些底層傳輸技術,會從服務器和客戶端支持的功能中自動選擇最佳傳輸方法,讓咱們只關注業務問題而不是底層傳輸技術問題.redis

能夠只使用WebSocket,具體參考WebSockets support in ASP.NET Core瀏覽器

在 ASP.NET Core 中使用 SignalR

使用 SignalR 會涉及到服務端和客戶端.服務器

  • Hub 是SignalR服務端最關鍵的組件, 它做爲通訊中心, 接受從客戶端發來的消息, 也能把消息發送給客戶端. 它是服務器端的一個類, 本身建立的Hub類須要繼承於基類Hub.
  • 客戶端 微軟目前官方支持JavaScript, .NET 和 Java客戶端. 具體參考ASP.NET Core SignalR 支持的平臺.

作一個小例子演練一下:websocket

  1. 建立一個空白的Web項目, 而後添加 Hub 類網絡

    public class ChatHub : Hub
    {
        public override async Task OnConnectedAsync()
        {
            await Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId} joined");
        }
    
        public override async Task OnDisconnectedAsync(Exception ex)
        {
            await Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId} left");
        }
    
        public Task Send(string message)
        {
            return Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
        }
    
        public Task SendAllExceptMe(string message)
        {
            return Clients.AllExcept(Context.ConnectionId).SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
        }
    
        public Task SendToGroup(string groupName, string message)
        {
            return Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId}@{groupName}: {message}");
        }
    
        public async Task JoinGroup(string groupName)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
    
            await Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId} joined {groupName}");
        }
    
        public async Task LeaveGroup(string groupName)
        {
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
    
            await Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId} left {groupName}");
        }
    
        public Task Echo(string message)
        {
            return Clients.Client(Context.ConnectionId).SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
        }
    }
  2. 添加配置代碼併發

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSignalR();
        }
    
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStaticFiles();
            app.UseSignalR(routes =>
            {
                routes.MapHub<ChatHub>("/chatHub");
            });
        }
    }
  3. 添加客戶端
    在wwwroot目錄下建立一個名爲chat.html的Html靜態文件,內容以下:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <h1 id="head1"></h1>
        <div>
            <input type="button" id="connect" value="Connect" />
            <input type="button" id="disconnect" value="Disconnect" />
        </div>
    
    
        <h4>To Everybody</h4>
        <form class="form-inline">
            <div class="input-append">
                <input type="text" id="message-text" placeholder="Type a message" />
                <input type="button" id="broadcast" class="btn" value="Broadcast" />
                <input type="button" id="broadcast-exceptme" class="btn" value="Broadcast (All Except Me)" />
            </div>
        </form>
    
        <h4>To Me</h4>
        <form class="form-inline">
            <div class="input-append">
                <input type="text" id="me-message-text" placeholder="Type a message" />
                <input type="button" id="sendtome" class="btn" value="Send to me" />
            </div>
        </form>
    
        <h4>Group</h4>
        <form class="form-inline">
            <div class="input-append">
                <input type="text" id="group-text" placeholder="Type a group name" />
                <input type="button" id="join-group" class="btn" value="Join Group" />
                <input type="button" id="leave-group" class="btn" value="Leave Group" />
            </div>
        </form>
    
        <h4>Private Message</h4>
        <form class="form-inline">
            <div class="input-prepend input-append">
                <input type="text" id="group-message-text" placeholder="Type a message" />
                <input type="text" id="group-name" placeholder="Type the group name" />
    
                <input type="button" id="sendgroupmsg" class="btn" value="Send to group" />
            </div>
        </form>
    
        <ul id="message-list"></ul>
    </body>
    </html>
    <script src="signalr.js"></script>
    <script>
        let connectButton = document.getElementById('connect');
        let disconnectButton = document.getElementById('disconnect');
        disconnectButton.disabled = true;
        var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
    
        document.getElementById("connect").addEventListener("click", function (event) {
    
            connectButton.disabled = true;
            disconnectButton.disabled = false;
    
            connection.on('ReceiveMessage', msg => {
                addLine(msg);
            });
    
            connection.onClosed = e => {
                if (e) {
                    addLine('Connection closed with error: ' + e, 'red');
                }
                else {
                    addLine('Disconnected', 'green');
                }
            }
    
            connection.start()
                .then(() => {
                    addLine('Connected successfully', 'green');
                })
                .catch(err => {
                    addLine(err, 'red');
                });
    
            event.preventDefault();
        });
    
        document.getElementById("disconnect").addEventListener("click", function (event) {
    
            connectButton.disabled = false;
            disconnectButton.disabled = true;
    
            connection.stop();
    
            event.preventDefault();
        });
    
        document.getElementById("broadcast").addEventListener("click", function (event) {
    
            var message = document.getElementById('message-text').value;
            connection.invoke("Send", message).catch(function (err) {
                addLine(err, 'red');
            });
    
            event.preventDefault();
        });
    
        document.getElementById("broadcast-exceptme").addEventListener("click", function (event) {
    
            var message = document.getElementById('message-text').value;
            connection.invoke("SendAllExceptMe", message).catch(function (err) {
                addLine(err, 'red');
            });
    
            event.preventDefault();
        });
    
        document.getElementById("sendtome").addEventListener("click", function (event) {
    
            var message = document.getElementById('me-message-text').value;
            connection.invoke("Echo", message).catch(function (err) {
                addLine(err, 'red');
            });
    
            event.preventDefault();
        });
    
        document.getElementById("join-group").addEventListener("click", function (event) {
    
            var groupName = document.getElementById('group-text').value;
            connection.invoke("JoinGroup", groupName).catch(function (err) {
                addLine(err, 'red');
            });
    
            event.preventDefault();
        });
    
        document.getElementById("leave-group").addEventListener("click", function (event) {
    
            var groupName = document.getElementById('group-text').value;
            connection.invoke("LeaveGroup", groupName).catch(function (err) {
                addLine(err, 'red');
            });
    
            event.preventDefault();
        });
    
        document.getElementById("sendgroupmsg").addEventListener("click", function (event) {
            var groupName = document.getElementById('group-name').value;
            var message = document.getElementById('group-message-text').value;
            connection.invoke("SendToGroup", groupName, message).catch(function (err) {
                addLine(err, 'red');
            });
    
            event.preventDefault();
        });
    
        function addLine(line, color) {
            var child = document.createElement('li');
            if (color) {
                child.style.color = color;
            }
            child.innerText = line;
            document.getElementById('message-list').appendChild(child);
        }
    </script>
  4. 編譯並運行 http://localhost:port/chat.html 測試.

權限驗證

SignalR 能夠採用 ASP.NET Core 配置好的認證和受權體系, 好比 Cookie 認證, Bearer token 認證, Authorize受權特性和 Policy 受權策略等.

  • Cookie 認證基本上不須要額外配置, 但僅限於瀏覽器客戶端.
  • Bearer token 認證適用於全部客戶端. 能夠參考上篇文章 ASP.NET Core WebAPI中使用JWT Bearer認證和受權 進行Token的分發和驗證. 在 SignalR 中使用的時候須要注意兩點:
    • 在 WebAPI 中, bearer token 是經過 HTTP header 傳輸的, 但當 SignalR 使用 WebSockets 和 Server-Sent Events 傳輸協議的時候, 因爲不支持 header, Token是經過 query string 傳輸的, 相似於ws://localhost:56202/chatHub?id=2fyJlq1T5vBOwAsITQaW8Q&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, 因此須要在服務端增長額外的配置以下:

      services.AddAuthentication(options =>
      {
          options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
          options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      
      }).AddJwtBearer(configureOptions =>
      {
          // Configure JWT Bearer Auth to expect our security key
      
          // We have to hook the OnMessageReceived event in order to
          // allow the JWT authentication handler to read the access
          // token from the query string when a WebSocket or 
          // Server-Sent Events request comes in.
          configureOptions.Events = new JwtBearerEvents
          {
              OnMessageReceived = context =>
              {
                  var accessToken = context.Request.Query["access_token"];
      
                  if (!string.IsNullOrEmpty(accessToken) && (context.HttpContext.Request.Path.StartsWithSegments("/chatHub")))
                  {
                      context.Token = accessToken;
                  }
                  return Task.CompletedTask;
              }
          };
      });

      同時, 給 Hub 添加 Authorize 特性.

      [Authorize]
      public class ChatHub: Hub
      {
      }
    • JS 客戶端使用 accessTokenFactory 建立帶 Token 的鏈接.

      this.connection = new signalR.HubConnectionBuilder()
          .withUrl("/chatHub", { accessTokenFactory: () => this.loginToken })
          .build();
    • 若是服務端認證經過, 可使用 Context.User 獲取用戶信息, 它是一個 ClaimsPrinciple 對象.

橫向擴展

Hub 服務器能夠支持的 TCP 併發鏈接數是有限的. 同時因爲 SignalR 鏈接是持久的, 甚至當客戶端進入空閒狀態時,SignalR 鏈接依然保持着打開狀態。因此當鏈接數比較多時, 經過增長服務器來實現橫向擴展是頗有必要的.

但相比於 WebAPI的單向通訊(只存在客戶端請求,服務端響應的情景), SignalR 中可能使用雙向通訊協議(客戶端能夠請求服務端的數據, 服務端也能夠向客戶端推送數據), 此時服務端水平擴展的時候, 一臺服務器是不知道其餘服務器上鍊接了哪些客戶端. 當在一臺服務器想要將消息發送到全部客戶端時,消息只是發送到鏈接到該服務器的客戶端. 爲了可以把消息發送給全部服務器都鏈接的客戶端, 微軟提供了下面兩種方案:

  • Azure SignalR 服務 是一個代理。當客戶端啓動鏈接到服務器時,會重定向鏈接到 Azure SignalR 服務。

    azure-signalr-service-multiple-connections

  • Redis 底板 當服務器想要將消息發送到全部客戶端時,它將先發送到 Redis 底板, 而後使用 Redis 的發佈訂閱功能轉發給其餘全部服務器從而發送給全部客戶端.

    redis-backplane

    添加 NuGet 包, ASP.NET Core 2.2 及更高版本中使用 Microsoft.AspNetCore.SignalR.StackExchangeRedis, 以前版本使用Microsoft.AspNetCore.SignalR.Redis.
    而後在Startup.ConfigureServices方法中, 添加 AddStackExchangeRedis services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");

源代碼

Github

參考

  • https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction
  • https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/
  • https://www.cnblogs.com/cgzl/p/9515516.html
  • https://www.cnblogs.com/maxzhang1985/p/7118426.html
相關文章
相關標籤/搜索