WebSocket Server in ASP.NET CORE
一:在StartUp.cs中定义WebSocket与拦截WebSocket请求
在app.UseEndpoints()之前增加:
var webSocketOptions = new WebSocketOptions() { KeepAliveInterval = TimeSpan.FromSeconds(120), }; app.UseWebSockets(webSocketOptions); app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/ws")) { if (context.WebSockets.IsWebSocketRequest) { using (var webSocket = await context.WebSockets.AcceptWebSocketAsync()) { await WebSocketRoute.Connect(Configuration, env, context, webSocket); } } else { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } else { await next(); } });
上面的 WebSocketRoute.Connect() 方法是后面定义的WebSocket入口。
二、定义接口、WebSocket根类,WebSocket路由
定义接口 IWebSocket.cs:
public interface IWebSocket { public void Open(); public bool Authentication(); /// <summary> /// 接收消息 /// </summary> public void Message(string Message); public void Close(); public Task Send(string Message); }
这是顶层接口,很简单,表示所有继承的类都必须定义这些方法。
下面是第一个根类WebSocketWrapper.cs:
public class WebSocketWrapper : IWebSocket { public IConfiguration configuration; public IWebHostEnvironment env; public HttpContext context; public System.Net.WebSockets.WebSocket ws; public long SessionId { get; set; } public string ControllerName { get; set; } private static async Task SendMessage(System.Net.WebSockets.WebSocket ws, string Message) { byte[] buffer = new byte[1024 * 4]; byte[] msg = System.Text.Encoding.UTF8.GetBytes(Message); Array.Copy(msg, 0, buffer, 0, msg.Length); await ws.SendAsync(new ArraySegment<byte>(buffer, 0, msg.Length), WebSocketMessageType.Text, true, CancellationToken.None); } public WebSocketWrapper(IConfiguration configuration, IWebHostEnvironment env, HttpContext context, System.Net.WebSockets.WebSocket ws) { this.configuration = configuration; this.env = env; this.context = context; this.ws = ws; } public virtual void Open() { } public virtual bool Authentication() { return true; } /// <summary> /// 接收消息 /// </summary> public virtual void Message(string Message) { } public virtual void Close() { } public virtual async Task Send(string Message) { await SendMessage(ws, Message); } }
下面是WebSocket路由入口WebSocketRoute.cs,使用了反射:
public class WebSocketRoute { public class Connection { public long SessionId; public string ControllerName; public object Instance; public DateTime Datecreated; } public static List<Connection> Connections = new List<Connection>(); public static async Task Connect(IConfiguration configuration, IWebHostEnvironment env, HttpContext context, System.Net.WebSockets.WebSocket ws) { string ControllerName = context.Request.Path.ToString().Substring(3).TrimStart('/'); string TypeName = ControllerName + "WebSocket"; Assembly assembly = Assembly.GetExecutingAssembly(); object instance = assembly.CreateInstance(TypeName, true, BindingFlags.Default, null, new object[] { configuration, env, context, ws }, System.Globalization.CultureInfo.CurrentCulture, null); Type wsType = Type.GetType(TypeName); #region 身份认证 MethodInfo miAuth = wsType.GetMethod("Authentication"); if (miAuth != null) { bool Status = miAuth.Invoke(instance, new object[0]).ToString().ToBoolean(); if (!Status) { await ws.CloseAsync(new WebSocketCloseStatus() { }, "", CancellationToken.None); return; } } #endregion long SessionId; #region 生成sessionid string SessionIdFile = Path.Combine(env.ContentRootPath, "SessionIds.txt"); long Timestamp = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(); byte[] TimeBytes = BitConverter.GetBytes(Timestamp); if (!File.Exists(SessionIdFile)) { SessionId = 1000000; byte[] SidBytes = BitConverter.GetBytes(SessionId); using (FileStream fs = new FileStream(SessionIdFile, FileMode.Append, FileAccess.Write)) { fs.Write(TimeBytes, 0, TimeBytes.Length); fs.Write(SidBytes, 0, SidBytes.Length); } } else { using (FileStream fs = new FileStream(SessionIdFile, FileMode.Open, FileAccess.ReadWrite)) { fs.Position = fs.Length - 8; byte[] buf = new byte[8]; fs.Read(buf, 0, buf.Length); SessionId = BitConverter.ToInt64(buf, 0) + 1; byte[] SidBytes = BitConverter.GetBytes(SessionId); fs.Write(TimeBytes, 0, TimeBytes.Length); fs.Write(SidBytes, 0, SidBytes.Length); } } #endregion PropertyInfo Sid = wsType.GetProperty("SessionId"); if(Sid != null) { Sid.SetValue(instance, SessionId); } PropertyInfo Name = wsType.GetProperty("ControllerName"); if (Name != null) { Name.SetValue(instance, ControllerName); } Connections.Add(new Connection() { SessionId = SessionId, ControllerName = ControllerName, Instance = instance, Datecreated = DateTime.Now }); Console.WriteLine("Sid={0}, Name={1}", Sid.GetValue(instance).ToString(), ControllerName); #region 绑定Open事件 MethodInfo onOpen = wsType.GetMethod("Open"); if (onOpen != null) { onOpen.Invoke(instance, new object[0]); } #endregion var buffer = new byte[1024 * 4]; WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); while (!result.CloseStatus.HasValue) { #region 绑定Message接收消息事件 MethodInfo onMessage = wsType.GetMethod("Message"); if (onMessage != null) { string Txt = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); onMessage.Invoke(instance, new object[] { Txt }); } #endregion //await ws.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); } #region 绑定Close事件 Connection conn = Connections.Where(n => n.SessionId == SessionId).First(); Connections.Remove(conn); await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); MethodInfo onClose = wsType.GetMethod("Close"); if (onClose != null) { onClose.Invoke(instance, new object[0]); } #endregion } }
下面是业务层定义WebSocket控制器,约定如下:
1、控制器文件名必须是 XXXWebSocket.cs,XXX是控制器的名称,类名也必须是 XXXWebSocket,此类必须继承自WebSocketWrapper。
2、按接口IWebSocket的定义,派生类必须定义5个方法:Open, Authentication, Message, Close, Send。身份验证在Authentication方法中实现。不同的控制器有不同的身份验证方法。如果未定义Authentication方法,表示不进行身份验证。
测试 TestWebSocket.cs:
public class TestWebSocket : WebSocketWrapper { public int UserId; public TestWebSocket(IConfiguration configuration, IWebHostEnvironment env, HttpContext context, System.Net.WebSockets.WebSocket ws) :base(configuration, env, context, ws) { } public override void Open() { Console.WriteLine("建立连接=" + this.SessionId); } public override bool Authentication() { int Uid = context.Request.Query["userid"][0].ToInt32(); Console.WriteLine("认证参数=" + Uid.ToString()); this.UserId = Uid; bool Result = UserId > 0; return Result; } /// <summary> /// 接收消息 /// </summary> public override void Message(string Message) { Console.WriteLine("收到消息="+Message+"(Sid:"+this.SessionId + ")"); } public override void Close() { Console.WriteLine("关闭连接=" + this.SessionId); } public override async Task Send(string Message) { ///业务逻辑写在这里 await base.Send(Message); } }
由于在WebSocketRoute中缓存了所有的WebSocket连接,因此找到指定的连接,并向该连接发送消息是可行了。在 ASP.NET CORE的控制器中访问缓存中的连接,就能实现 HTTP2WebSocket了:
public async Task Http2WebSocket() { string Name = Request.Query["name"]; //WebSocket 控制器名称 int UserId = Request.Query["userid"][0].ToInt32(); //该控制器下的指定身份 string Msg = Request.Query["msg"]; //需要向浏览器发送的消息 IEnumerable<ApiCore.WebSocketRoute.Connection> conns = ApiCore.WebSocketRoute.Connections.Where(n => n.ControllerName == Name); List<TestWebSocket> items = new List<TestWebSocket>(); foreach (ApiCore.WebSocketRoute.Connection item in conns) { TestWebSocket t = (TestWebSocket)item.Instance; if (t.UserId != UserId) continue; items.Add(t); } List<Task> listOfTasks = new List<Task>(); foreach (TestWebSocket item in items) { listOfTasks.Add(item.Send(Msg)); } await Task.WhenAll(listOfTasks); }
浏览器中这样连接:
var ws; function WebSocketTest(uid) { if (!"WebSocket" in window) { alert("您的浏览器不支持 WebSocket!"); return; } if (ws != undefined) return; ws = new WebSocket("ws://www.myserver.com:5001/ws/Test?userid=" + uid); ws.onopen = function () { $("#logs").append("已连接<br/>"); }; ws.onmessage = function (evt) { var msg = evt.data; $("#logs").append("接收到:" + msg + "<br />"); }; ws.onclose = function () { // 关闭 websocket $("#logs").append("已断开<br />"); ws = undefined; }; }
2021-06-21
ASP.NET CORE
蔡大卫,广东揭阳人氏,现居深圳,从事互联网行业,专注程序编码工作20年。目前正在创业。
发布评论