在.Net Core中实现一个WebSocket路由



.Net Core中使用WebSocket默认是没有路由系统的,只能通过Request.Path=="/xxx"来判断请求,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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();
    }

});

要使用类似[HttpGet("/xxx")]这种特性标签的路由方式,可以自己写一个简单的Attribute来实现。

1. 实现Attribute特性类

这个Attribute类很简单,只接收一个定义的Path,用来和开头提到的Request.Path对应。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/// <summary>
    /// WebSockets执行函数特性标签
    /// </summary>
[AttributeUsage(AttributeTargets.Method,Inherited = false)]
 public class WebSocketsAttribute:Attribute
 {
     // WebSocket请求的Path
    public string Path;
    public WebSocketsAttribute(string path)
    {
        Path = path;
    }
}

2. 实现一个分发WebSocket请求的帮助类

使用一个帮助类来根据请求的PATH分发给对应的函数。该类中有两个静态方法,实现同样的功能,区别是一个会缓存请求的Path和处理请求函数,另一个是实时反射解析的,实际使用中当然推荐带缓存的,因为反射对性能的损耗也是很大的。

2.1. 不带缓存的分发函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// 分发websockets请求(没有缓存)
/// </summary>
/// <param name="assemblyName">对应处理程序所在的程序集名称</param>
/// <param name="context">HTTP上下文</param>
/// <param name="task">TASK任务</param>
/// <returns></returns>
public static async Task DistributeAsync(string assemblyName, HttpContext context, Func<Task> task)
{
    try
    {
        // 获取在WebSockets下面的websocket处理methods
        var assembly = Assembly.Load(assemblyName);
        // 根据特性标签获取对应的执行函数
        var methods = assembly
            .GetTypes()
            .SelectMany(s => s.GetMethods())
            .First(f => f
                .GetCustomAttributes(typeof(WebSocketsAttribute), false)  // 这一步获取打了WebSocketsAttribute特性标签的函数
                .Any(w => ((WebSocketsAttribute)w).Path == context.Request.Path)); // 过滤找出其中WebSocketsAttribute标签中Path参数对应的执行函数
        if (context.WebSockets.IsWebSocketRequest)
        {
            var webSockt = await context.WebSockets.AcceptWebSocketAsync();
            // 异步调用该方法
            await (Task)methods.Invoke(null, new object[] { context, webSockt });
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
    catch (Exception)
    {
        await task();
    }
}

2.2. 带缓存的分发函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 使用并发安全的字段类型缓存请求Path与处理请求函数
private static ConcurrentDictionary<string, MethodInfo> ExecuteMethods;
/// <summary>
/// 分发websockets请求(缓存执行函数)
/// </summary>
/// <param name="assemblyName">对应处理程序所在的程序集名称</param>
/// <param name="context">HTTP上下文</param>
/// <param name="task">TASK任务</param>
/// <returns></returns>
public static async Task DistributeAsync(string assemblyName, HttpContext context, Func<Task> task)
{
    if (!ExecuteMethods.Any())
    {
        // 获取在WebSockets下面的websocket处理methods
        var assembly = Assembly.Load(assemblyName);
        // 根据特性标签获取对应的执行函数
        var methods = assembly
            .GetTypes()
            .SelectMany(s => s.GetMethods())
            .Select(s => new
            {
                ((WebSocketsAttribute) s.GetCustomAttributes(typeof(WebSocketsAttribute), false).First()).Path,
                s
            }).ToArray();
        ExecuteMethods =
            new ConcurrentDictionary<string, MethodInfo>(methods.ToDictionary(t => t.Path, t => t.s));

        await task();
    }
    else
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            var webSockt = await context.WebSockets.AcceptWebSocketAsync();
            // 从键值对中获取对应的执行函数
            ExecuteMethods.TryGetValue(context.Request.Path, out var method);
            if (method != null)
                // 异步调用该方法
                await (Task) method.Invoke(null, new object[] {context, webSockt});
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
}

3. 在.Net Core中调用分发函数

startup.cs中的Config函数中可以使用我们自己创建的分发函数,需要注意的是我们自定义的函数需要传入程序集的名称,也就是真实的请求处理函数所在的程序集。

由此可见,这种方式使用的一些限制,请求处理函数必须写在一个程序集里面,不过我想一般情况下,没有人会把同一类型的处理函数写在不同的程序集,所以这应该不算是问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/// <summary>
/// startup.cs
/// </summary>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    // 增加websocket
    app.Use(async (context, next) =>
    {
        // 将原本的Echo方法换成我们自定义的分发函数
        await WebSocketsHandler.DistributeAsync("NetCoreFrame.WebSockets", context, next);
    });
    ...
}

4. 使用方式

使用方式很简单,就是一个正常的WebSocket处理函数,只不过标注了我们自己的“路由特性标签”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Test
{
    [WebSockets("/ws")]
    public static async Task Echo(HttpContext context, WebSocket webSocket)
    {
        var buffer = new byte[1024 * 4];
        var 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);
    }
}

5. 参考资料

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-2.2