深入理解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