先拉開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 請求。
使用 Run、Map 和 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
如果要請我喝水,歡迎打賞哈,有錢的捧個錢場,沒錢的捧個人場。打賞點贊吐槽都可以