.net core 系列之kestrel


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM