深入理解kestrel的应用
1 前言
之所以寫(xiě)本文章,是因?yàn)樵谖彝V咕S護(hù)多年前寫(xiě)的NetworkSocket組件兩年多來(lái),還是有一些開(kāi)發(fā)者在關(guān)注這個(gè)項(xiàng)目,我希望有類似需求的開(kāi)發(fā)者明白為什么要停止更新,可以使用什么更好的方式來(lái)替換(其實(shí)很大原因是我把時(shí)間花在開(kāi)發(fā)WebApiClient上面了)。那時(shí).netcore還沒(méi)有生下來(lái),asp.net除了蝸居在iis里處理http,其它什么也不能干,而NetworkSocket是這樣定義的:
NetworkSocket是一個(gè)以中間件(middleware)擴(kuò)展通訊協(xié)議,以插件(plug)擴(kuò)展服務(wù)器功能的支持SSL安全傳輸?shù)耐ㄓ嵖蚣?#xff1b;目前支持http、websocket、fast、flex策略與silverlight策略協(xié)議。
2 Kestrel是什么
談到asp.netcore,人們自然就想到它的默認(rèn)服務(wù)器kestrel,在很多場(chǎng)景中,人們甚至認(rèn)為kestrel等于Web服務(wù)器,或者說(shuō)它只能處理http和http之上的東西。本文先在此下個(gè)定義:Kestrel是一款基于中間件來(lái)處理tcp連接的服務(wù)器,并內(nèi)置了http(包含websocket、SignalR)解析中間件。也就是說(shuō),我們完全可以給kestrel添加其它中間件,用來(lái)處理非http的連接的業(yè)務(wù)場(chǎng)景,讓kestrel使用一個(gè)端口支持多種協(xié)議或多協(xié)議一個(gè)端口一種協(xié)議的要求。
2.1 Kestrel的中間件是什么
在asp.netcore的Startup里,我們使用app.UseXXX的擴(kuò)展方法來(lái)應(yīng)用各種中間件,比如UseRouting、UseStaticFiles等等,它本質(zhì)上還是調(diào)用了IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware),也就說(shuō)Func<RequestDelegate, RequestDelegate>就是一個(gè)中間件。
對(duì)應(yīng)的,在kestrel世界里,也有一個(gè)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的最后一個(gè)中間處理者是http中間件,以上代碼,實(shí)際的kestrel已經(jīng)包含3種處理者(文章后部分有中間件的篇幅,然后就容易理解了),邏輯1、邏輯2和http解析,我們可以簡(jiǎn)單理解為Startup的app對(duì)象,對(duì)應(yīng)kestrel的內(nèi)置的那個(gè)最后中間件。
2.2 Kestrel的ConnectionContext
在kestrel中間件里,最重要的對(duì)象就是ConnectionDelegate,它等同于Func<ConnectionContext,Task>,我們可以理解為它就是一個(gè)Hanlder,傳入連接上下文,剩下就是我們要干的工作了,而中間件是除了這個(gè)Handler之外,我們還能拿到一個(gè)叫next的Handler,我們可以選擇是否調(diào)用它,如果不調(diào)用,流程終止。
ConnectionContext是kestrel的一個(gè)Tcp連接抽象,其核心屬性是Transport,表示雙工傳輸層的操作對(duì)象,另外提供Abort()方法用于服務(wù)端主動(dòng)關(guān)閉連接。基于ConnectionContext,很容易實(shí)現(xiàn)一個(gè)自定義協(xié)議的tcp雙工通訊服務(wù)器,相比從Socket寫(xiě)起,我們可能可以減少100倍代碼量,而得到的是更高性能的服務(wù)。
3 基于Kestrel的SignalR+Redis的推送服務(wù)
本實(shí)戰(zhàn)中,我們使用asp.netcore內(nèi)置的SignalR功能,外加自己實(shí)現(xiàn)的部分Redis協(xié)議(只簡(jiǎn)單實(shí)現(xiàn)發(fā)布訂閱功能),來(lái)做一個(gè)消息從云端推送到客戶端的服務(wù),我們的服務(wù)對(duì)客戶端支持redis協(xié)議訂閱或Signal協(xié)議訂閱,同時(shí)我們提供redis+signalR+http三種協(xié)議接口給云端其它微服務(wù)來(lái)發(fā)布消息,發(fā)布者不用關(guān)心客戶端是什么協(xié)議,只需要選擇自己喜歡的協(xié)議的發(fā)布接口來(lái)調(diào)用發(fā)布。
3.1 協(xié)議與ConnectionContext的關(guān)系
在我們的這個(gè)應(yīng)用里,一個(gè)連接不允許同時(shí)使用SignalR和Redis并存協(xié)議,也就是說(shuō),一個(gè)連接在發(fā)起第一個(gè)請(qǐng)求里,就確定了它整個(gè)生命周期里的協(xié)議。所以,我們需要分析連接讀取到的第一個(gè)數(shù)據(jù)包,確定它是否為Redis協(xié)議,如果不是redis協(xié)議,我們要將ConnectionContext傳達(dá)到下一個(gè)中間件(即http中間件)。
3.2 使用Redis中間件
如下代碼,Use里面就是Redis中間件,里面的個(gè)協(xié)議分析邏輯:
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)} 斷開(kāi)");}else{logger.LogDebug($"{context.RemoteEndPoint} {nameof(ClientType.SignalR)} 連接");await next(context);logger.LogDebug($"{context.RemoteEndPoint} {nameof(ClientType.SignalR)} 斷開(kāi)");}}); });Protocol類
/// <summary> /// 連接的協(xié)議判斷 /// </summary> public static class Protocol {/// <summary>/// 返回連接是否為redis協(xié)議/// </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>/// 返回?cái)?shù)據(jù)是否為redis協(xié)議/// 這里不必嚴(yán)格檢查,只要能區(qū)分是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] == '*';} }3.3 RedisConnectionHandler
在3.2代碼里,有一個(gè)await redis.HandleAsync(context);這個(gè)redis就是RedisConnectionHandler實(shí)例,它的功能是處理一個(gè)redis連接從建立成功之后到斷開(kāi)的所有邏輯。
我們知道,Redis有好幾十個(gè)命令,單單是實(shí)現(xiàn)發(fā)布和訂閱功能,我們也要實(shí)現(xiàn)必要的8個(gè)命令。說(shuō)到這里,我的腦海里又閃現(xiàn)出一個(gè)長(zhǎng)長(zhǎng)的switch(收到的cmd) case xxx的代碼了,我們甚至還需要在switch之前寫(xiě)公共性的代碼,比如打印收到的cmd內(nèi)容,還需要在switch里特別強(qiáng)調(diào)default分支:我們不支持這個(gè)命令。。。
既然kestrel基于連接處理中間件,上層的asp.netcore也是基于請(qǐng)求處理中間件,我們完全也可以也依葫蘆畫(huà)瓢,造一個(gè)Redis命令中間件Builder,最后將所有Redis中間件串起來(lái),Buid得一個(gè)Redis處理委托。
var builder = new PipelineBuilder<RedisContext>(appServices, context => {// 沒(méi)有handler來(lái)處理return context.Client.ResponseAsync(RedisResponse.Error("unsupported cmd")); }) .Use((context, next) => {this.logger.LogDebug(context.ToString());// 驗(yàn)證客戶端是否已授權(quán)return context.Cmd.Name != RedisCmdName.Auth && context.Client.IsAuthed == false? context.Client.ResponseAsync(RedisResponse.Error("need auth password")): next(); });// 添加各個(gè)cmd對(duì)應(yīng)的handler條件分支 appServices.GetServices<IRedisCmdHanler>().ForEach(item => builder.When(item.CanHandle, item.HandleAsync));this.handler = builder.Build();在RedisConnectionHandler,每收一個(gè)Redis命令,將命令包裝為RedisContext,然后使用build出來(lái)的handler對(duì)象來(lái)處理這個(gè)RedisContext就行。剩下的工作,就是我們一個(gè)命令實(shí)現(xiàn)一個(gè)IRedisCmdHanler對(duì)象就行,邏輯完全分開(kāi)。
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); }3.4 統(tǒng)一Redis和Signal客戶端操作接口
在Signal和Redis訂閱之后,我們將他們的連接包裝為統(tǒng)一接口的IClient對(duì)象,IClient提供PublishAsync()方法用于發(fā)布消息。
/// <summary> /// 定義客戶端的接口 /// </summary> public interface IClient {/// <summary>/// 獲取唯一標(biāo)識(shí)/// </summary>string Id { get; }/// <summary>/// 獲取連接時(shí)間/// </summary>DateTime ConnectedTime { get; }/// <summary>/// 獲取客戶端類型/// </summary>[JsonConverter(typeof(JsonStringEnumConverter))]ClientType ClientType { get; }/// <summary>/// 發(fā)送消息/// </summary>/// <param name="message"></param>/// <returns></returns>Task<bool> SendMessageAsync(Message message); }3.5 IClient管理器
我們還需要維護(hù)一份單例的IClient管理器對(duì)象,用于維護(hù)正在訂閱的客戶端,在發(fā)布消息時(shí),從這個(gè)管理器里查找IClient,并調(diào)用SendMessageAsync()方法發(fā)布消息內(nèi)容。
3.6 SignalR部分
由于SignalR的內(nèi)容非常簡(jiǎn)單,官方文檔細(xì)節(jié)齊全,這里將不作任何講解了。
4 總結(jié)
由于要講解的內(nèi)部比較多,篇幅和時(shí)間都有限,本文就只從思路上大概講解Kestrel在多協(xié)議連接的場(chǎng)景的使用方式。一句話,中間件的使用,使得這些場(chǎng)景變得簡(jiǎn)單,那問(wèn)題來(lái)了,什么是中間件,你理解了嗎?
總結(jié)
以上是生活随笔為你收集整理的深入理解kestrel的应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 后端开发都应该了解点接口的压力测试(Ap
- 下一篇: Istio 中的 Sidecar 注入及