深入理解kestrel
何为kestrel
谈到asp.netcore,人们自然就想到它的默认服务器kestrel,在很多场景中,人们甚至认为kestrel等于Web服务器,或者说它只能处理http和http之上的东西。本文先在此下个定义:Kestrel是一款基于中间件来处理tcp连接的服务器,并内置了http(包含websocket、SignalR)解析中间件。也就是说,我们完全可以给kestrel添加其它中间件,用来处理非http的连接的业务场景,让kestrel使用一个端口支持多种协议或多协议一个端口一种协议的要求。
kestrel的中间件是什么
在asp.netcore的Startup里,我们使用app.UseXXX的扩展方法来应用各种中间件,比如UseRouting、UseStaticFiles等等,它本质上还是调用了IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware)
,也就说Func<RequestDelegate, RequestDelegate>
就是一个中间件。
对应的,在kestrel世界里,也有一个IConnectionBuilder.Use(Func<ConnectionDelegate, ConnectionDelegate> middleware)
,Func<ConnectionDelegate, ConnectionDelegate>
就是kestrel的中间件,我们可以如下安装kestrel的中间件:
kestrel.ListenAnyIP(port: 80, listen => { listen.Use(next => context => { if(true) { // 中间件1的逻辑 }else { return next(context); } }) .Use(next => context => { if(true) { // 中间件2的逻辑 }else { return next(context); } }); });
值得注意的是,kestrel的最后一个中间处理者是http中间件,以上代码,实际的kestrel已经包含3种处理者(文章后部分有中间件的篇幅,然后就容易理解了),逻辑1、逻辑2和http解析,我们可以简单理解为Startup的app对象,对应kestrel的内置的那个最后中间件。
kestrel的ConnectionContext
在kestrel中间件里,最重要的对象就是ConnectionDelegate,它等同于Func<ConnectionContext,Task>
,我们可以理解为它就是一个Hanlder,传入连接上下文,剩下就是我们要干的工作了,而中间件是除了这个Handler之外,我们还能拿到一个叫next的Handler,我们可以选择是否调用它,如果不调用,流程终止。
ConnectionContext是kestrel的一个Tcp连接抽象,其核心属性是Transport,表示双工传输层的操作对象,另外提供Abort()方法用于服务端主动关闭连接。基于ConnectionContext,很容易实现一个自定义协议的tcp双工通讯服务器,相比从Socket写起,我们可能可以减少100倍代码量,而得到的是更高性能的服务。
基于Kestrel的SignalR+Redis的推送服务
本实战中,我们使用asp.netcore内置的SignalR功能,外加自己实现的部分Redis协议(只简单实现发布订阅功能),来做一个消息从云端推送到客户端的服务,我们的服务对客户端支持redis协议订阅或Signal协议订阅,同时我们提供redis+signalR+http三种协议接口给云端其它微服务来发布消息,发布者不用关心客户端是什么协议,只需要选择自己喜欢的协议的发布接口来调用发布。
协议与ConnectionContext的关系
在我们的这个应用里,一个连接不允许同时使用SignalR和Redis并存协议,也就是说,一个连接在发起第一个请求里,就确定了它整个生命周期里的协议。所以,我们需要分析连接读取到的第一个数据包,确定它是否为Redis协议,如果不是redis协议,我们要将ConnectionContext传达到下一个中间件(即http中间件)。
使用Redis中间件
如下代码,Use里面就是Redis中间件,里面的个协议分析逻辑:
kestrel.ListenAnyIP(options.Port, listen => { listen.Use(next => async context => { if (await Protocol.IsRedisAsync(context)) { logger.LogDebug($"{context.RemoteEndPoint} {nameof(ClientType.Redis)} 连接"); await redis.HandleAsync(context); logger.LogDebug($"{context.RemoteEndPoint} {nameof(ClientType.Redis)} 断开"); } else { logger.LogDebug($"{context.RemoteEndPoint} {nameof(ClientType.SignalR)} 连接"); await next(context); logger.LogDebug($"{context.RemoteEndPoint} {nameof(ClientType.SignalR)} 断开"); } }); });
Protocol类
/// <summary> /// 连接的协议判断 /// </summary> public static class Protocol { /// <summary> /// 返回连接是否为redis协议 /// </summary> /// <param name="connection"></param> /// <returns></returns> public static async Task<bool> IsRedisAsync(ConnectionContext connection) { var result = await connection.Transport.Input.ReadAsync(); var state = IsRedis(result); connection.Transport.Input.AdvanceTo(result.Buffer.Start); return state; } /// <summary> /// 返回数据是否为redis协议 /// 这里不必严格检查,只要能区分是http还是redis就行 /// </summary> /// <param name="result"></param> /// <returns></returns> private static bool IsRedis(ReadResult result) { if (result.Buffer.IsEmpty) { return false; } var span = result.Buffer.FirstSpan; return span.Length > 0 && span[0] == '*'; } }
RedisConnectionHandle
在3.2代码里,有一个await redis.HandleAsync(context);这个redis就是RedisConnectionHandler实例,它的功能是处理一个redis连接从建立成功之后到断开的所有逻辑。
我们知道,Redis有好几十个命令,单单是实现发布和订阅功能,我们也要实现必要的8个命令。说到这里,我的脑海里又闪现出一个长长的switch(收到的cmd) case xxx的代码了,我们甚至还需要在switch之前写公共性的代码,比如打印收到的cmd内容,还需要在switch里特别强调default分支:我们不支持这个命令。。。
既然kestrel基于连接处理中间件,上层的asp.netcore也是基于请求处理中间件,我们完全也可以也依葫芦画瓢,造一个Redis命令中间件Builder,最后将所有Redis中间件串起来,Buid得一个Redis处理委托。
var builder = new PipelineBuilder<RedisContext>(appServices, context => { // 没有handler来处理 return context.Client.ResponseAsync(RedisResponse.Error("unsupported cmd")); }) .Use((context, next) => { this.logger.LogDebug(context.ToString()); // 验证客户端是否已授权 return context.Cmd.Name != RedisCmdName.Auth && context.Client.IsAuthed == false ? context.Client.ResponseAsync(RedisResponse.Error("need auth password")) : next(); }); // 添加各个cmd对应的handler条件分支 appServices .GetServices<IRedisCmdHanler>() .ForEach(item => builder.When(item.CanHandle, item.HandleAsync)); this.handler = builder.Build();
在RedisConnectionHandler,每收一个Redis命令,将命令包装为RedisContext,然后使用build出来的handler对象来处理这个RedisContext就行。剩下的工作,就是我们一个命令实现一个IRedisCmdHanler对象就行,逻辑完全分开。
IRedisCmdHanler接口:
/// <summary> /// 定义redis命令处理者 /// </summary> interface IRedisCmdHanler { /// <summary> /// 返回是否可以处理 /// </summary> /// <param name="context"></param> /// <returns></returns> bool CanHandle(RedisContext context); /// <summary> /// 处理 /// </summary> /// <param name="context"></param> /// <returns></returns> Task HandleAsync(RedisContext context); }
统一Redis和Signal客户端操作接口
在Signal和Redis订阅之后,我们将他们的连接包装为统一接口的IClient对象,IClient提供PublishAsync()方法用于发布消息。
/// <summary> /// 定义客户端的接口 /// </summary> public interface IClient { /// <summary> /// 获取唯一标识 /// </summary> string Id { get; } /// <summary> /// 获取连接时间 /// </summary> DateTime ConnectedTime { get; } /// <summary> /// 获取客户端类型 /// </summary> [JsonConverter(typeof(JsonStringEnumConverter))] ClientType ClientType { get; } /// <summary> /// 发送消息 /// </summary> /// <param name="message"></param> /// <returns></returns> Task<bool> SendMessageAsync(Message message); }
IClient管理器
我们还需要维护一份单例的IClient管理器对象,用于维护正在订阅的客户端,在发布消息时,从这个管理器里查找IClient,并调用SendMessageAsync()方法发布消息内容。
SignalR部分
由于SignalR的内容非常简单,官方文档细节齐全,这里将不作任何讲解了。
总结
本文只从思路上大概讲解Kestrel在多协议连接的场景的使用方式。一句话,中间件的使用,使得这些场景变得简单。
链接:https://www.cnblogs.com/kewei/p/12775469.html