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

发布评论