2018年5月18日 星期五

ASP.NET Core 2.0 使用 WebSocket 讓設備和網頁可以即時通訊

目前有一個專案,其中一項功能為
"接收設備端傳送過來的數據並顯示在頁面上"
例如空調系統設備傳送當前溫度

後端使用 ASP.NET Core 2.0 webapi 開發
以前有在 .Net Framework 上使用過 ASP.NET SignalR
查資料得知目前(寫這篇筆記的時候) .Net Core 版本的 SingalR 還在開發階段
2.1 版本才會發行正式版本,有些文章建議不適合在正式產品中使用開發版本
於是就查了如何在 .Net Core 使用 WebSocket

#1 [ASP.NET Core: Building chat room using WebSocket]
http://gunnarpeipman.com/aspnet/aspnet-core-websocket-chat/
上面這篇文章使用網頁發送和接收訊息

但是目前專案需求是要由設備發送訊息
#2 [Real-time chart using ASP.NET Core and WebSocket]
http://gunnarpeipman.com/aspnet/aspnet-core-websocket-chart/
上面這篇文章使用api當端點給設備端呼叫,符合專案的需求

以上兩種發送訊息的方式都想要使用,於是整合兩篇文章
使用 #1 的 SocketMiddleware 和 # 2 的 SocketManager
#2 的 SocketManager 不需修改, #1 產生 socketId 和保存當前 socket 的程式改使用 # 2 的 SocketManaer

public class ACSystemSocketMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ACSystemSocketManager _socketManager;
 
    public ChatWebSocketMiddleware(
        RequestDelegate next, 
        ACSystemSocketManager socketManager)
    {
        _next = next;
        _socketManager = socketManager;
    }
 
    public async Task Invoke(HttpContext context)
    {
        if (!context.WebSockets.IsWebSocketRequest)
        {
            await _next.Invoke(context);
            return;
        }
 
        CancellationToken ct = context.RequestAborted;
        WebSocket currentSocket = await context.WebSockets.AcceptWebSocketAsync();
 
        var socketId = _socketManager.AddSocket(currentSocket);
 
        while (true)
        {
            if (ct.IsCancellationRequested)
            {
                break;
            }
 
            var response = await ReceiveStringAsync(currentSocket, ct);
            if (string.IsNullOrEmpty(response))
            {
                if (currentSocket.State != WebSocketState.Open)
                {
                    break;
                }
 
                continue;
            }
 
            foreach (var socket in _socketManager.GetAll())
            {
                if (socket.Value.State != WebSocketState.Open)
                {
                    continue;
                }
 
                await SendStringAsync(socket.Value, response, ct);
            }
        }
 
        await _socketManager.RemoveSocket(socketId);
 
        await currentSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct);
        currentSocket.Dispose();
    }
 
    private static Task SendStringAsync(
        WebSocket socket, 
        string data, 
        CancellationToken ct = default(CancellationToken))
    {
        var buffer = Encoding.UTF8.GetBytes(data);
        var segment = new ArraySegment<byte>(buffer);
        return socket.SendAsync(segment, WebSocketMessageType.Text, true, ct);
    }
 
    private static async Task<string> ReceiveStringAsync(
        WebSocket socket, 
        CancellationToken ct = default(CancellationToken))
    {
        var buffer = new ArraySegment<byte>(new byte[8192]);
        using (var ms = new MemoryStream())
        {
            WebSocketReceiveResult result;
            do
            {
                ct.ThrowIfCancellationRequested();
 
                result = await socket.ReceiveAsync(buffer, ct);
                ms.Write(buffer.Array, buffer.Offset, result.Count);
            }
            while (!result.EndOfMessage);
 
            ms.Seek(0SeekOrigin.Begin);
            if (result.MessageType != WebSocketMessageType.Text)
            {
                return null;
            }
 
            using (var reader = new StreamReader(ms, Encoding.UTF8))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }
}

後續 :
整合過的程式執行起來正常,但是接收到的中文字串變問號,檢查後發現問題出在 #2 的 SocketManager,發送訊息(SendMessageAsync)使用的 Encoding 是ASCII,改為UTF8或是Default以後可以正常顯示中文,但是序列化成JSON格式的字串卻少了最後一個大括號,調整成下面這段程式碼以後就正常了
private async Task SendMessageAsync(WebSocket socket, string message)
{
    if (socket.State != WebSocketState.Open)
        return;
 
    var buffer = Encoding.UTF8.GetBytes(message);
    var segment = new ArraySegment<byte>(buffer);
    await socket.SendAsync(
        segment, 
        WebSocketMessageType.Text, 
        trueCancellationToken.None);
}




沒有留言: