服務器開發- Asp.Net Core中的websocket,並封裝一個簡單的中間件

先拉開MSDN的文檔,大致讀一遍 (https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets)

 WebSocket 是一個協議,支持通過 TCP 連接建立持久的雙向信道。 它可用於聊天、股票報價和遊戲等應用程序,以及 Web 應用程序中需要實時功能的任何情景。 

  

使用方法  

  • 安裝 Microsoft.AspNetCore.WebSockets 包。
  • 配置中間件。
  • 接受 WebSocket 請求。
  • 發送和接收消息。

如果是創建的asp.net core項目,默認會有一個all的包,裏面默認帶了websocket的包。所以,添加的時候,注意看一下

然後就是配置websocket的中間件

app.UseWebSockets();

  如果需要更細緻的配置websocket,MSDN文檔上也提供了一種配置緩衝區大小和ping的option

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),  //向客戶端發送“ping”幀的頻率,以確保代理保持連接處於打開狀態
    ReceiveBufferSize = 4 * 1024   //用於接收數據的緩衝區的大小。 只有高級用戶才需要對其進行更改,以便根據數據大小調整性能。
};
app.UseWebSockets(webSocketOptions);

接受 WebSocket 請求

在請求生命週期後期(例如在 Configure 方法或 MVC 操作的後期),檢查它是否是 WebSocket 請求並接受 WebSocket 請求。

該示例來自 Configure 方法的後期。

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(context, webSocket);
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
    else
    {
        await next();
    }

});

WebSocket 請求可以來自任何 URL,但此示例代碼只接受 /ws 的請求

(比如要測試websocket的連接,地址必須寫上:ws://ip:端口/ws) 最後這個路徑的ws是可以自己定義的,可以理解爲MVC的路由,或者url地址,websocket第一次連接的時候,可以使用url傳遞參數

發送和接收消息

AcceptWebSocketAsync 方法將 TCP 連接升級到 WebSocket 連接,並提供 WebSocket 對象。 使用 WebSocket 對象發送和接收消息。

之前顯示的接受 WebSocket 請求的代碼將 WebSocket 對象傳遞給 Echo 方法;此處爲 Echo 方法。 代碼接收消息並立即發回相同的消息。 一直在循環中執行此操作,直到客戶端關閉連接

private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

如果在開始此循環之前接受 WebSocket,中間件管道會結束。 關閉套接字後,管道展開。 也就是說,如果接受 WebSocket ,請求會在管道中停止前進,就像點擊 MVC 操作一樣。 但是完成此循環並關閉套接字時,請求將在管道中後退。

如果要測試是否連上,那麼可以自己寫ws的客戶端程序,當然也可以使用一些現成的工具辣

封裝一個簡單的中間件:

什麼是中間件?MSDN對此的解釋是:

中間件是一種裝配到應用程序管道以處理請求和響應的軟件。 每個組件:

  • 選擇是否將請求傳遞到管道中的下一個組件。
  • 可在調用管道中的下一個組件前後執行工作。

請求委託用於生成請求管道。 請求委託處理每個 HTTP 請求。

使用 RunMap 和 Use 擴展方法來配置請求委託。 可將一個單獨的請求委託並行指定爲匿名方法(稱爲並行中間件),或在可重用的類中對其進行定義。 這些可重用的類和並行匿名方法即爲中間件或中間件組件。 請求管道中的每個中間件組件負責調用管道中的下一個組件,或在適當情況下使鏈發生短路。

新建一個WebSocketExtensions.cs的類

 public static class WebSocketExtensions
    {
        public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app,PathString path,WebSocketHandler handler)
        {
            return app.Map(path, (_app) => _app.UseMiddleware<WebSocketManagerMiddleware>(handler));
        }
        public static IServiceCollection AddWebSocketManager(this IServiceCollection services)
        {
            services.AddTransient<WebSocketConnectionManager>();

            foreach (var type in Assembly.GetEntryAssembly().ExportedTypes)
            {
                if (type.GetTypeInfo().BaseType == typeof(WebSocketHandler))
                {
                    services.AddSingleton(type);
                }
            }

            return services;
        }
    }

AddWebSocketManager這個方法主要是處理依賴注入的問題。通過反射把實現WebSocketHandler的類,統統注入

管理websocket連接

public class WebSocketConnectionManager
    {
        private ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket>();

        public int GetCount()
        {
            return _sockets.Count;
        }

        public WebSocket GetSocketById(string id)
        {
            return _sockets.FirstOrDefault(p => p.Key == id).Value;
        }

        public ConcurrentDictionary<string, WebSocket> GetAll()
        {
            return _sockets;
        }
        public WebSocket GetWebSocket(string key)
        {
            WebSocket _socket;
            _sockets.TryGetValue(key, out _socket);
            return _socket;

        }

        public string GetId(WebSocket socket)
        {
            return _sockets.FirstOrDefault(p => p.Value == socket).Key;
        }
        public void AddSocket(WebSocket socket,string key)
        {
            if (GetWebSocket(key)!=null)
            {
                _sockets.TryRemove(key, out WebSocket destoryWebsocket);
            }
            _sockets.TryAdd(key, socket);
            //string sId = CreateConnectionId();
            //while (!_sockets.TryAdd(sId, socket))
            //{
            //    sId = CreateConnectionId();
            //}



        }

        public async Task RemoveSocket(string id)
        {
            try
            {
                WebSocket socket;

                _sockets.TryRemove(id, out socket);


                await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);


            }
            catch (Exception)
            {

            }

        }

        public async Task CloseSocket(WebSocket socket)
        {
            await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
        }

        private string CreateConnectionId()
        {
            return Guid.NewGuid().ToString();
        }
    }

 

這裏我把客戶的連接的管理都封裝到一個連接類裏面,我的思路是

  • 我使用webapi來驗證身份,走http協議的接口
  • 驗證成功後,服務器給客戶端返回一個token
  • 客戶端通過websocket連接服務器的時候,需要帶上上次返回的token,這樣我就可以在連接字典裏面判斷出重複的socket(因爲我試過在.Net core裏面如果多次連接,服務器不會走斷開的事件,而是不斷的出現多個socket對象)

WebSocketManagerMiddleware類的封裝

public class WebSocketManagerMiddleware
    {
        private readonly RequestDelegate _next;
        private WebSocketHandler _webSocketHandler { get; set; }

        public WebSocketManagerMiddleware(RequestDelegate next,
                                          WebSocketHandler webSocketHandler)
        {
            _next = next;
            _webSocketHandler = webSocketHandler;
        }

        public async Task Invoke(HttpContext context)
        {
            if (!context.WebSockets.IsWebSocketRequest)
                return;

            var socket = await context.WebSockets.AcceptWebSocketAsync();
            string Key = context.Request.Query["Key"];
            Console.WriteLine("連接人:"+Key);

            _webSocketHandler.OnConnected(socket,Key);

            await Receive(socket, async (result, buffer) =>
            {
                if (result.MessageType == WebSocketMessageType.Text)
                {
                    await _webSocketHandler.ReceiveAsync(socket, result, buffer);
                    return;
                }

                else if (result.MessageType == WebSocketMessageType.Close)
                {
                    await _webSocketHandler.OnDisconnected(socket);
                    return;
                }

            });

            //TODO - investigate the Kestrel exception thrown when this is the last middleware
            //await _next.Invoke(context);
        }

        private async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
        {
            try
            {
                var buffer = new byte[1024 * 4];

                while (socket.State == WebSocketState.Open)
                {
                    var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte>(buffer),
                                                           cancellationToken: CancellationToken.None);

                    handleMessage(result, buffer);
                }
            }
            catch (Exception ex)
            {
                GsLog.E(ex.StackTrace);
            }

        }
    }

 

 Invoke的時候,傳遞的key參數需要客戶端驗證身份後,傳遞進來:(ws://ip:端口/ws?key=xxx) 這樣的格式來連接websocket

在這個類裏面,我們主要處理三個事情

  • 如果客戶端連接進來,那麼我們把這個連接放到連接管理字典裏面
  • 如果客戶端斷開連接,那麼我們把這個連接衝連接管理字典裏面移除
  • 如果是發送數據過來,那麼我們就調用ReceiveAsync方法,並把連接對象和數據傳遞進去

WebSocketHandler類的封裝

這個類主要關聯遊戲邏輯模塊和websocket的一個紐帶,我們封裝的中間件,通過websockethandler把數據傳遞給實現這個類的子類

 public abstract class WebSocketHandler
    {
        public WebSocketConnectionManager WebSocketConnectionManager { get; set; }

        public WebSocketHandler(WebSocketConnectionManager webSocketConnectionManager)
        {
            WebSocketConnectionManager = webSocketConnectionManager;
        }

        public virtual void OnConnected(WebSocket socket, string key)
        {
            //var ServerSocket = WebSocketConnectionManager.GetWebSocket(key);
            //if (ServerSocket != null)
            //{
            //    WebSocketConnectionManager.AddSocket();
            //    Console.WriteLine("已經存在當前的連接,斷開。。");
            //}
            WebSocketConnectionManager.AddSocket(socket, key);
        }

        public virtual async Task OnDisconnected(WebSocket socket)
        {
            Console.WriteLine("Socket 斷開了");
            await WebSocketConnectionManager.RemoveSocket(WebSocketConnectionManager.GetId(socket));
        }

        public async Task SendMessageAsync(WebSocket socket, string message)
        {
            if (socket.State != WebSocketState.Open)
                return;
            var bytes = Encoding.UTF8.GetBytes(message);
            await socket.SendAsync(buffer: new ArraySegment<byte>(array: bytes, offset: 0, count: bytes.Length), messageType: WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None);
        }

        public async Task SendMessageAsync(string socketId, string message)
        {
            try
            {
                await SendMessageAsync(WebSocketConnectionManager.GetSocketById(socketId), message);
            }
            catch (Exception)
            {

            }

        }

        public async Task SendMessageToAllAsync(string message)
        {
            foreach (var pair in WebSocketConnectionManager.GetAll())
            {
                if (pair.Value.State == WebSocketState.Open)
                    await SendMessageAsync(pair.Value, message);
            }
        }
        /// <summary>
        /// 獲取一些連接
        /// </summary>
        /// <param name="keys"></param>
        /// <returns></returns>
        public IEnumerable<WebSocket> GetSomeWebsocket(string[] keys)
        {
            foreach (var key in keys)
            {
                yield return WebSocketConnectionManager.GetWebSocket(key);
            }
        }

        /// <summary>
        /// 給一堆人發消息
        /// </summary>
        /// <param name="webSockets"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task SendMessageToSome(WebSocket[] webSockets, string message)
        {
            webSockets.ToList().ForEach(async a => { await SendMessageAsync(a, message); });
        }

        public abstract Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer);
    }

需要把一些必須要重寫的方法定義爲abstract  給子類重寫

使用我們寫好的websocket管理中間件

  public void ConfigureServices(IServiceCollection services)
        {
            services.AddWebSocketManager();
        }
 var webSocketOptions = new WebSocketOptions()
            {
                KeepAliveInterval = TimeSpan.FromSeconds(20),
                ReceiveBufferSize = 4 * 1024
            };
            app.UseWebSockets(webSocketOptions);
            app.MapWebSocketManager("/zhajinhua", serviceProvider.GetService<ZjhGame>());

ZjhGame這個類,必須實現 WebSocketHandler,這樣我們就能在ZjhGame這個類,處理遊戲邏輯

好了,大致就是這樣的,畢竟我也沒有開發遊戲的經驗,有錯誤的地方,希望大佬們能指出

 

還有上一篇博客有大佬說加註,但是我沒有收到加註的錢啊,你們到底加不加啊?不加我可要反悔了啊

http://www.cnblogs.com/boxrice/p/8570730.html


 

 

如果要請我喝水,歡迎打賞哈,有錢的捧個錢場,沒錢的捧個人場。打賞點贊吐槽都可以

打賞圖片

 

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