【SignalR】簡介及使用

SignalR

SignalR是一個.NET Core/.NET Framework的開源實時框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作爲底層傳輸方式.

SignalR基於這三種技術構建, 抽象於它們之上, 它讓你更好的關注業務問題而不是底層傳輸技術問題.

SignalR這個框架分服務器端和客戶端, 服務器端支持ASP.NET Core 和 ASP.NET; 而客戶端除了支持瀏覽器裏的javascript以外, 也支持其它類型的客戶端, 例如桌面應用.

 

回落機制

SignalR使用的三種底層傳輸技術分別是Web Socket, Server Sent Events 和 Long Polling.

其中Web Socket僅支持比較現代的瀏覽器, Web服務器也不能太老.

而Server Sent Events 情況可能好一點, 但是也存在同樣的問題.

 

所以SignalR採用了回落機制, SignalR有能力去協商支持的傳輸類型.

Web Socket是最好的最有效的傳輸方式, 如果瀏覽器或Web服務器不支持它的話, 就會降級使用SSE, 實在不行就用Long Polling.

 

一旦建立連接, SignalR就會開始發送keep alive消息, 來檢查連接是否還正常. 如果有問題, 就會拋出異常.

因爲SignalR是抽象於三種傳輸方式的上層, 所以無論底層採用的哪種方式, SignalR的用法都是一樣的.

 

SignalR默認採用這種回落機制來進行傳輸和連接.

但是也可以禁用回落機制, 只採用其中一種傳輸方式.

 

RPC

RPC (Remote Procedure Call). 它的優點就是可以像調用本地方法一樣調用遠程服務.

SignalR採用RPC範式來進行客戶端與服務器端之間的通信.

SignalR利用底層傳輸來讓服務器可以調用客戶端的方法, 反之亦然, 這些方法可以帶參數, 參數也可以是複雜對象, SignalR負責序列化和反序列化.

 

Hub

Hub是SignalR的一個組件, 它運行在ASP.NET Core應用裏. 所以它是服務器端的一個類.

Hub使用RPC接受從客戶端發來的消息, 也能把消息發送給客戶端. 所以它就是一個通信用的Hub.

 

在ASP.NET Core裏, 自己創建的Hub類需要繼承於基類Hub.

在Hub類裏面, 我們就可以調用所有客戶端上的方法了. 同樣客戶端也可以調用Hub類裏的方法.

這種Hub+RPC的方式還是非常適合實時場景的.

 

之前說過方法調用的時候可以傳遞複雜參數, SignalR可以將參數序列化和反序列化. 這些參數被序列化的格式叫做Hub 協議, 所以Hub協議就是一種用來序列化和反序列化的格式.

Hub協議的默認協議是JSON, 還支持另外一個協議是MessagePack. MessagePack是二進制格式的, 它比JSON更緊湊, 而且處理起來更簡單快速, 因爲它是二進制的.

此外, SignalR也可以擴展使用其它協議..

 

橫向擴展

隨着系統的運行, 有時您可能需要進行橫向擴展. 就是應用運行在多個服務器上.

這時負載均衡器會保證每個進來的請求按照一定的邏輯分配到可能是不同的服務器上.

在使用Web Socket的時候, 沒什麼問題, 因爲一旦Web Socket的連接建立, 就像在瀏覽器和那個服務器之間打開了隧道一樣, 服務器是不會切換的.

但是如果使用Long Polling, 就可能有問題了, 因爲使用Long Polling的情況下, 每次發送消息都是不同的請求, 而每次請求可能會到達不同的服務器. 不同的服務器可能不知道前一個服務器通信的內容, 這就會造成問題.

針對這個問題, 我們需要使用Sticky Sessions (粘性會話).

 

Sticky Sessions 貌似有很多中實現方式, 但是主要是下面要介紹的這種方式.

作爲第一次請求的響應的一部分, 負載均衡器會在瀏覽器裏面設置一個Cookie, 來表示使用過這個服務器. 在後續的請求裏, 負載均衡器讀取Cookie, 然後把請求分配給同一個服務器. 

 

在ASP.NET Core 中使用SignalR

創建項目,新建SignalRHub:

using Core.Parameters;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace XXXXX.SignalR
{
    public class SignalRHub : Hub
    {
        private readonly DcListService _dcListService;
        public SignalRHub(DcListService dcListService)
        {
            _dcListService = dcListService;
        }
        public void AddMachineCode(string dcCode)
        {
            _dcListService.Add(dcCode, Context.ConnectionId);
        }

        public void Exec(Paramete paramete)
        {
            string connectionId = Context.ConnectionId;
            var toClient = _dcListService.FirstOrDefault(paramete.MachineCode);
            if (toClient == null)
            {
                Clients.Client(Context.ConnectionId).SendAsync("Result", connectionId, false, "系統消息:當前設備不在線!");
            }
            else
            {
                Clients.Client(toClient.ConnectionId).SendAsync("Exec", connectionId, paramete);
            }
        }

        public void Result(string connectionId,bool flag, string data)
        {
            Clients.Client(connectionId).SendAsync("Result", connectionId, flag, data);
        }

        public async override Task OnDisconnectedAsync(Exception exception)
        {
            _dcListService.Remove(Context.ConnectionId);
        }

        public async override Task OnConnectedAsync()
        {
            //string connectionId = Context.ConnectionId;
        }
    }
    public class Paramete
    {
        public string MachineCode { get; set; }
        public string ProcessPath { get; set; }
        public string ExecType { get; set; }
        public string ViewType { get; set; }
        public DateTime? StartDate { get; set; }
        public DateTime? EndDate { get; set; }
    }
}

在Startup裏添加:

services.AddScoped<DcListService>();
services.AddSignalR();
app.UseSignalR(routes =>
{
    routes.MapHub<SignalRHub>("/signalR");
});

Winform客戶端代碼:

using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsSample
{
    public partial class ChatForm : Form
    {
        private HubConnection _connection;

        public ChatForm()
        {
            InitializeComponent();
        }

        private void ChatForm_Load(object sender, EventArgs e)
        {
            addressTextBox.Focus();
        }

        private void addressTextBox_Enter(object sender, EventArgs e)
        {
            AcceptButton = connectButton;
        }

        private async void connectButton_Click(object sender, EventArgs e)
        {
            UpdateState(connected: false);

            _connection = new HubConnectionBuilder()
                .WithUrl(addressTextBox.Text)
                .Build();

            _connection.On<string, Paramete>("Exec", Exec);

            Log(Color.Gray, "Starting connection...");
            try
            {
                await _connection.StartAsync();
                await _connection.InvokeAsync("AddMachineCode", this.dcCodeTextBox.Text);
            }
            catch (Exception ex)
            {
                Log(Color.Red, ex.ToString());
                return;
            }

            Log(Color.Gray, "Connection established.");

            UpdateState(connected: true);

            messageTextBox.Focus();
        }

        private async void disconnectButton_Click(object sender, EventArgs e)
        {
            Log(Color.Gray, "Stopping connection...");
            try
            {
                await _connection.StopAsync();
            }
            catch (Exception ex)
            {
                Log(Color.Red, ex.ToString());
            }

            Log(Color.Gray, "Connection terminated.");

            UpdateState(connected: false);
        }

        private void messageTextBox_Enter(object sender, EventArgs e)
        {
            AcceptButton = sendButton;
        }

        private async void sendButton_Click(object sender, EventArgs e)
        {
            try
            {
                await _connection.InvokeAsync("SendSingle", this.toDcCodeTextBox.Text, messageTextBox.Text);
            }
            catch (Exception ex)
            {
                Log(Color.Red, ex.ToString());
            }
        }

        private void UpdateState(bool connected)
        {
            disconnectButton.Enabled = connected;
            connectButton.Enabled = !connected;
            addressTextBox.Enabled = !connected;

            messageTextBox.Enabled = connected;
            sendButton.Enabled = connected;
        }

        private async void Exec(string connectionId, Paramete paramete)
        {
            Log(Color.Black, "");
            Log(Color.Black, "消息來了");
            Log(Color.Black, $"ConnectionId:{connectionId}");
            Log(Color.Black, $"MachineCode:{paramete.MachineCode}");
            Log(Color.Black, $"ProcessPath:{paramete.ProcessPath}");
            Log(Color.Black, $"ExecType:{paramete.ExecType}");
            Log(Color.Black, $"ViewType:{paramete.ViewType}");
            Log(Color.Black, $"StartDate:{paramete.StartDate}");
            Log(Color.Black, $"EndDate:{paramete.EndDate}");

            await _connection.InvokeAsync("Result", connectionId, true, "客戶端執行成功!");
        }

        private void Log(Color color, string message)
        {
            Action callback = () =>
            {
                messagesList.Items.Add(new LogMessage(color, message));
            };

            Invoke(callback);
        }

        private class LogMessage
        {
            public Color MessageColor { get; }

            public string Content { get; }

            public LogMessage(Color messageColor, string content)
            {
                MessageColor = messageColor;
                Content = content;
            }
        }

        private void messagesList_DrawItem(object sender, DrawItemEventArgs e)
        {
            var message = (LogMessage)messagesList.Items[e.Index];
            e.Graphics.DrawString(
                message.Content,
                messagesList.Font,
                new SolidBrush(message.MessageColor),
                e.Bounds);
        }
    }
    public class Paramete
    {
        public string MachineCode { get; set; }
        public string ProcessPath { get; set; }
        public string ExecType { get; set; }
        public string ViewType { get; set; }
        public DateTime? StartDate { get; set; }
        public DateTime? EndDate { get; set; }
    }
}
using Core.Parameters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace XXXX.SignalR
{
    public class DcListService
    {
        private static List<OnlineDcCode> DcList = new List<OnlineDcCode>();
        public OnlineDcCode FirstOrDefault(string dcCode)
        {
            return DcList.Where(p => p.DcCode == dcCode).FirstOrDefault();
        }

        public void Add(string dcCode, string connectionId)
        {
            if (FirstOrDefault(dcCode) == null)
            {
                DcList.Add(new OnlineDcCode()
                {
                    ConnectionId = connectionId,
                    DcCode = dcCode
                });
            }
            else
            {
                DcList.Where(p => p.DcCode == dcCode).FirstOrDefault().ConnectionId = connectionId;
            }
        }

        public void Remove(string connectionId)
        {
            var dc = DcList.FirstOrDefault(p => p.ConnectionId == connectionId);
            if (dc != null)
            {
                DcList.Remove(dc);
            }
        }
    }
}

參考:https://www.cnblogs.com/cgzl/p/9515516.html

參考:https://github.com/aspnet/SignalR-samples

SignalR in ASP.NET Core behind Nginx

在Nginx中轉發,配置:

server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    add_header Access-Control-Allow-Origin *;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html =404;

        if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin *;
                add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
                return 204;
        }
    }

    location ^~ /api/signalR
    {
        if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin *;
                add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
                return 204;
        }
        proxy_pass http://192.168.1.231:8091;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location ^~ /api/                                                                                                                                                    {
        if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin *;
                add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
                return 204;
        }
        proxy_pass http://192.168.1.231:8091;
    }
    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    # error_page   500 502 503 504  /50x.html;
    # location = /50x.html {
    #   root   /usr/share/nginx/html;
    # }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

 參考:https://stackoverflow.com/questions/48300288/signalr-in-asp-net-core-behind-nginx

斷開後自動重新連接:

//2、徹底斷開 重新連接
_connection.Closed += async (error) =>
{
    Log(Color.Red, $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ff")} 斷開連接: " + error);
    await Task.Delay(new Random().Next(0, 5) * 1000);
    int count = 0;
    while (_connection.State == HubConnectionState.Disconnected)
    {
        try
        {
            //4、開啓會話
            await _connection.StartAsync();
            //5、將 機器碼 和 exe文件路徑 與 當前連接關聯上
            await _connection.InvokeAsync("AddMachineCode", this.dcCodeTextBox.Text);
            Log(Color.Green, $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ff")} ----- 重新連接成功! ------");
        }
        catch (Exception ex)
        {
            Log(Color.Red, ex.ToString());
        }
        Log(Color.Red, $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ff")} ----- 第 {++count} 徹底斷開 嘗試重連 ------");
        await Task.Delay(new Random().Next(0, 5) * 1000);
    }
};

JWT認證:

#region Authentication
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidAudience = Configuration["Jwt:Issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
        };
        options.Events = new JwtBearerEvents()
        {
            OnMessageReceived = context =>
            {
                if (context.Request.Path.ToString().StartsWith("/api/signalR"))
                    context.Token = context.Request.Query["access_token"];
                return Task.CompletedTask;
            },
        };
    });
#endregion

主要是這一段:

options.Events = new JwtBearerEvents()
{
    OnMessageReceived = context =>
    {
        if (context.Request.Path.ToString().StartsWith("/api/signalR"))
            context.Token = context.Request.Query["access_token"];
        return Task.CompletedTask;
    },
};

客戶端配置:

服務器端配置:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章