.NET-Core Series
- Server in ASP.NET-Core
- DI in ASP.NET-Core
- Routing in ASP.NET-Core
- Error Handling in ASP.NET-Core
- WebSocket in ASP.NET-Core(一)
- WebSocket in ASP.NET-Core(二)
- To Be Continue...
Introduce
上篇博文中,介紹了WebSocket
的基本原理,以及一個簡單的Demo
用來對其有一個大致的認識。這篇博文講的是我們平常在網站上可能會經常遇到的——實時聊天,本文就是來講在.NET-Core
使用WebSocket
來實現一個“乞丐版”的在線實時聊天Demo。
關鍵詞:Middleware,Real-Time,WebSocket
Before You Read.
這個和我們上一篇博文中Demo
有何不同的呢?有何共同之處呢?
相同的點是,都是網頁作為客戶端,使用JavaScript
來發送和接收請求,.NET-Core
服務端接收到請求,發送該請求給客戶端。
不同的地方呢,就像我下面這張圖這樣:
一次同時有多個客戶端在,所以應該很清楚,我們只要在上面例子的基礎上,對當前的已存在的Socket進行輪詢,發送回應即可達到我們想要的效果。
Create WebSocket Middleware
在上個Demo
的例子中,我們直接在Startup
的Configure
中直接寫接受WebSocket
請求。這次我們換成Middleware形式來處理。在寫Middleware之前,在之前的介紹中,我們知道,需要有多個WebSocket
,那么肯定需要一些對WebSocket
的Get/Set
處理。我簡單的寫了一個下面WebScoket Manger Class
//WebSocketManager.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WebSocketManage
{
public class WSConnectionManager
{
private static ConcurrentDictionary<string, WebSocket> _socketConcurrentDictionary = new ConcurrentDictionary<string, WebSocket>();
public void AddSocket(WebSocket socket)
{
_socketConcurrentDictionary.TryAdd(CreateGuid(), socket);
}
public async Task RemoveSocket(WebSocket socket)
{
_socketConcurrentDictionary.TryRemove(GetSocketId(socket), out WebSocket aSocket);
await aSocket.CloseAsync(
closeStatus: WebSocketCloseStatus.NormalClosure,
statusDescription: "Close by User",
cancellationToken: CancellationToken.None).ConfigureAwait(false);
}
public string GetSocketId(WebSocket socket)
{
return _socketConcurrentDictionary.FirstOrDefault(k => k.Value == socket).Key;
}
public ConcurrentDictionary<string, WebSocket> GetAll()
{
return _socketConcurrentDictionary;
}
public string CreateGuid()
{
return Guid.NewGuid().ToString();
}
}
}
上面主要是對 WebSocket
進行了簡單的存取操作進行了封裝。下面也把WebSocket
的Send
和Recieve
操作進行了封裝。
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WebSocketManage
{
public class WSHandler
{
protected WSConnectionManager _wsConnectionManager;
public WSHandler(WSConnectionManager wSConnectionManager)
{
_wsConnectionManager = wSConnectionManager;
}
public async Task SendMessageAsync(
WebSocket socket,
string message,
CancellationToken cancellationToken = default(CancellationToken))
{
var buffer = Encoding.UTF8.GetBytes(message);
var segment = new ArraySegment<byte>(buffer);
await socket.SendAsync(segment, WebSocketMessageType.Text, true, cancellationToken);
}
public async Task<string> RecieveAsync(WebSocket webSocket, CancellationToken cancellationToken)
{
var buffer = new ArraySegment<byte>(new byte[1024 * 8]);
using (var ms = new MemoryStream())
{
WebSocketReceiveResult result;
do
{
cancellationToken.ThrowIfCancellationRequested();
result = await webSocket.ReceiveAsync(buffer, cancellationToken);
ms.Write(buffer.Array, buffer.Offset, result.Count);
}
while (!result.EndOfMessage);
ms.Seek(0, SeekOrigin.Begin);
if (result.MessageType != WebSocketMessageType.Text)
{
return null;
}
using (var reader = new StreamReader(ms, Encoding.UTF8))
{
return await reader.ReadToEndAsync();
}
}
}
}
}
有了上面兩個輔助類之后,接下來就可以寫我們自己的RealTimeWebSocketMiddlerware
了,
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WebSocketManage;
namespace Robert.Middleware.WebSockets
{
public class RealTimeWSMiddleware
{
private readonly RequestDelegate _next;
private WSConnectionManager _wSConnectionManager { get; set; }
private WSHandler _wsHanlder { get; set; }
public RealTimeWSMiddleware(
RequestDelegate next,
WSConnectionManager wSConnectionManager,
WSHandler wsHandler)
{
_next = next;
_wSConnectionManager = wSConnectionManager;
_wsHanlder = wsHandler;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.WebSockets.IsWebSocketRequest)
{
var cancellationToken = httpContext.RequestAborted;
var currentWebSocket = await httpContext.WebSockets.AcceptWebSocketAsync();
_wSConnectionManager.AddSocket(currentWebSocket);
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
var response = await _wsHanlder.ReceiveAsync(currentWebSocket, cancellationToken);
if (string.IsNullOrEmpty(response) && currentWebSocket.State != WebSocketState.Open) break;
foreach (var item in _wSConnectionManager.GetAll())
{
if (item.Value.State == WebSocketState.Open)
{
await _wsHanlder.SendMessageAsync(item.Value, response, cancellationToken);
}
continue;
}
}
await _wSConnectionManager.RemoveSocket(currentWebSocket);
}
else
{
await _next(httpContext);
}
}
}
}
其實到這里,核心部分已經講完了,接下來就是頁面顯示,發現信息,交互的問題了。在客戶端還是像上篇文章中的一樣,直接使用 JavaScript
發送WebScoket
請求。
下面主要演示一下效果,在上篇博文的基礎上,加上了用戶名。
<script>
$(function () {
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
var Uri = protocol + "//" + window.location.host + "/ws";
var socket = new WebSocket(Uri);
socket.onopen = e => {
console.log("socket opened", e);
};
socket.onclose = function (e) {
console.log("socket closed", e);
};
//function to receive from server.
socket.onmessage = function (e) {
console.log("Message:" + e.data);
$('#msgs').append(e.data + '<br />');
};
socket.onerror = function (e) {
console.error(e.data);
};
});
</script>
當寫好了頁面文件后,運行站點。最終運行的效果圖,界面以實用為主,不求美觀。 😄😄
At End
這里演示的其實只是簡簡單單的信息接受操作,實際環境中肯定不會這樣用的,比如對用戶的身份驗證,可以建一個Auth 站點,還需要對信息傳輸加密。還會有 1對1 的聊天等等。這里只介紹了乞丐版,在參考鏈接中,一個GitHub
上的大哥封裝的很好的一個WebSocke
t 中間件,如果感興趣的可以去fork
下來了解了解。當然等SingleR 2.0
出來后,這些肯定都是可以換掉的。
在上述的中間件輔助的兩個類中,還可以對一些內容進行封裝,比如開始建立連接和斷開連接等,還有群發信息等操作,都是可以封裝的。
內容中如果陳述錯誤處,請多多指正。