kestrel網絡編程--開發redis服務器

1 文章目的

本文講解基於kestrel開發實現了部分redis命令的redis僞服務器的過程,讓讀者瞭解kestrel網絡編程的完整步驟,其中redis通訊協議需要讀者自行查閱,文章裏不做具體解析。

2 開發順序

  1. 創建Kestrel的Redis協議處理者
  2. 配置監聽的EndPoint並使用Redis處理者
  3. 設計交互上下文RedisContext
  4. 設計Redis命令處理者
  5. 設計Redis中間件
  6. 編排Redis中間件構建應用

3. 創建Redis協議處理者

在Kestrel中,末級的中間件是一個沒有next的特殊中間件,基表現出來就是一個ConnectionHandler的行爲。我們開發redis應用只需要繼承ConnectionHandler這個抽象類來,當kestrel接收到新的連接時將連接交給我們來處理,我們處理完成之後,不再有下一個處理者來處理這個連接了。

/// <summary>
/// 表示Redis連接處理者
/// </summary>
sealed class RedisConnectionHandler : ConnectionHandler
{
    /// <summary>
    /// 處理Redis連接
    /// </summary>
    /// <param name="context">redis連接上下文</param>
    /// <returns></returns>
    public async override Task OnConnectedAsync(ConnectionContext context)
    {
        // 開始處理這個redis連接
        ...
        // 直到redis連接斷開後結束
    }
}

4. 配置監聽的EndPoint

4.1 json配置文件

我們在配置文件裏指定監聽本機的5007端口來做服務器,當然你可以指定本機具體的某個IP或任意IP。

{
  "Kestrel": {
    "Endpoints": {      
      "Redis": { // redis協議服務器,只監聽loopback的IP
        "Url": "http://localhost:5007"
      }
    }
  }
}
{
  "Kestrel": {
    "Endpoints": {      
      "Redis": { // redis協議服務器,監聽所有IP
        "Url": "http://*:5007"
      }
    }
  }
}

4.2 在代碼中配置Redis處理者

爲Redis這個節點關聯上RedisConnectionHandler,當redis客戶端連接到5007這個端口之後,OnConnectedAsync()方法就得到觸發且收到連接上下文對象。

builder.WebHost.ConfigureKestrel((context, kestrel) =>
{
    var section = context.Configuration.GetSection("Kestrel");
    kestrel.Configure(section).Endpoint("Redis", endpoint =>
    {
        endpoint.ListenOptions.UseConnectionHandler<RedisConnectionHandler>();
    });
});

5 設計RedisContext

在asp.netcore裏,我們知道應用層每次http請求都創建一個HttpContext對象,裏面就塞着各種與本次請求有關的對象。對於Redis的請求,我們也可以這麼抄襲asp.netcore來設計Redis。

5.1 RedisContext

Redis請求上下文,包含Client、Request、Response和Features對象,我們要知道是收到了哪個Redis客戶端的什麼請求,從而請求命令處理者可以向它響應對應的內容。

/// <summary>
/// 表示redis上下文
/// </summary>
sealed class RedisContext : ApplicationContext
{
    /// <summary>
    /// 獲取redis客戶端
    /// </summary>
    public RedisClient Client { get; }

    /// <summary>
    /// 獲取redis請求
    /// </summary>
    public RedisRequest Reqeust { get; }

    /// <summary>
    /// 獲取redis響應
    /// </summary>
    public RedisResponse Response { get; }

    /// <summary>
    /// redis上下文
    /// </summary>
    /// <param name="client"></param>
    /// <param name="request"></param>
    /// <param name="response"></param>
    /// <param name="features"></param> 
    public RedisContext(RedisClient client, RedisRequest request, RedisResponse response, IFeatureCollection features)
        : base(features)
    {
        this.Client = client;
        this.Reqeust = request;
        this.Response = response;
    }

    public override string ToString()
    {
        return $"{this.Client} {this.Reqeust}";
    }
}

5.2 ApplicationContext

這是抽象的應用層上下文,它強調Features,做爲多箇中間件之間的溝通渠道。

/// <summary>
/// 表示應用程序請求上下文
/// </summary>
public abstract class ApplicationContext
{
    /// <summary>
    /// 獲取特徵集合
    /// </summary>
    public IFeatureCollection Features { get; }

    /// <summary>
    /// 應用程序請求上下文
    /// </summary>
    /// <param name="features"></param>
    public ApplicationContext(IFeatureCollection features)
    {
        this.Features = new FeatureCollection(features);
    }
}

5.3 RedisRequest

一個redis請求包含請求的命令和0到多個參數值。

/// <summary>
/// 表示Redis請求
/// </summary>
sealed class RedisRequest
{
    private readonly List<RedisValue> values = new();

    /// <summary>
    /// 獲取命令名稱
    /// </summary>
    public RedisCmd Cmd { get; private set; }

    /// <summary>
    /// 獲取參數數量
    /// </summary>
    public int ArgumentCount => this.values.Count - 1;

    /// <summary>
    /// 獲取參數
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public RedisValue Argument(int index)
    {
        return this.values[index + 1];
    }
}

RedisRequest的解析:

/// <summary>
/// 從內存中解析
/// </summary>
/// <param name="memory"></param>
/// <param name="request"></param>
/// <exception cref="RedisProtocolException"></exception>
/// <returns></returns>
private static bool TryParse(ReadOnlyMemory<byte> memory, [MaybeNullWhen(false)] out RedisRequest request)
{
    request = default;
    if (memory.IsEmpty == true)
    {
        return false;
    }

    var span = memory.Span;
    if (span[0] != '*')
    {
        throw new RedisProtocolException();
    }

    if (span.Length < 4)
    {
        return false;
    }

    var lineLength = span.IndexOf((byte)'\n') + 1;
    if (lineLength < 4)
    {
        throw new RedisProtocolException();
    }

    var lineCountSpan = span.Slice(1, lineLength - 3);
    var lineCountString = Encoding.ASCII.GetString(lineCountSpan);
    if (int.TryParse(lineCountString, out var lineCount) == false || lineCount < 0)
    {
        throw new RedisProtocolException();
    }

    request = new RedisRequest();
    span = span.Slice(lineLength);
    for (var i = 0; i < lineCount; i++)
    {
        if (span[0] != '$')
        {
            throw new RedisProtocolException();
        }

        lineLength = span.IndexOf((byte)'\n') + 1;
        if (lineLength < 4)
        {
            throw new RedisProtocolException();
        }

        var lineContentLengthSpan = span.Slice(1, lineLength - 3);
        var lineContentLengthString = Encoding.ASCII.GetString(lineContentLengthSpan);
        if (int.TryParse(lineContentLengthString, out var lineContentLength) == false)
        {
            throw new RedisProtocolException();
        }

        span = span.Slice(lineLength);
        if (span.Length < lineContentLength + 2)
        {
            return false;
        }

        var lineContentBytes = span.Slice(0, lineContentLength).ToArray();
        var value = new RedisValue(lineContentBytes);
        request.values.Add(value);

        span = span.Slice(lineContentLength + 2);
    }

    request.Size = memory.Span.Length - span.Length;
    Enum.TryParse<RedisCmd>(request.values[0].ToString(), ignoreCase: true, out var name);
    request.Cmd = name;

    return true;
}

5.4 RedisResponse

/// <summary>
/// 表示redis回覆
/// </summary>
sealed class RedisResponse
{
    private readonly PipeWriter writer;

    public RedisResponse(PipeWriter writer)
    {
        this.writer = writer;
    }

    /// <summary>
    /// 寫入\r\n
    /// </summary>
    /// <returns></returns>
    public RedisResponse WriteLine()
    {
        this.writer.WriteCRLF();
        return this;
    }

    public RedisResponse Write(char value)
    {
        this.writer.Write((byte)value);
        return this;
    }

    public RedisResponse Write(ReadOnlySpan<char> value)
    {
        this.writer.Write(value, Encoding.UTF8);
        return this;
    }

    public RedisResponse Write(ReadOnlyMemory<byte> value)
    {
        this.writer.Write(value.Span);
        return this;
    }


    public ValueTask<FlushResult> FlushAsync()
    {
        return this.writer.FlushAsync();
    }

    public ValueTask<FlushResult> WriteAsync(ResponseContent content)
    { 
        return this.writer.WriteAsync(content.ToMemory());
    }
}

5.5 RedisClient

Redis是有狀態的長連接協議,所以在服務端,我把連接接收到的連接包裝爲RedisClient的概念,方便我們業務理解。對於連接級生命週期的對象屬性,我們都應該放到RedisClient上,比如是否已認證授權等。

/// <summary>
/// 表示Redis客戶端
/// </summary>
sealed class RedisClient
{
    private readonly ConnectionContext context;

    /// <summary>
    /// 獲取或設置是否已授權
    /// </summary>
    public bool? IsAuthed { get; set; }

    /// <summary>
    /// 獲取遠程終結點
    /// </summary>
    public EndPoint? RemoteEndPoint => context.RemoteEndPoint;

    /// <summary>
    /// Redis客戶端
    /// </summary>
    /// <param name="context"></param> 
    public RedisClient(ConnectionContext context)
    {
        this.context = context;
    }
    
    /// <summary>
    /// 關閉連接
    /// </summary>
    public void Close()
    {
        this.context.Abort();
    }

    /// <summary>
    /// 轉換爲字符串
    /// </summary>
    /// <returns></returns>
    public override string? ToString()
    {
        return this.RemoteEndPoint?.ToString();
    }
}

6. 設計Redis命令處理者

redis命令非常多,我們希望有一一對應的cmdHandler來對應處理,來各盡其責。所以我們要設計cmdHandler的接口,然後每個命令增加一個實現類型,最後使用一箇中間件來聚合這些cmdHandler。

6.1 IRedisCmdHanler接口

/// <summary>
/// 定義redis請求處理者
/// </summary>
interface IRedisCmdHanler
{
    /// <summary>
    /// 獲取能處理的請求命令
    /// </summary>
    RedisCmd Cmd { get; }

    /// <summary>
    /// 處理請求
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    ValueTask HandleAsync(RedisContext context);
}

6.2 IRedisCmdHanler實現

由於實現類型特別多,這裏只舉個例子

/// <summary>
/// Ping處理者
/// </summary>
sealed class PingHandler : IRedisCmdHanler
{
    public RedisCmd Cmd => RedisCmd.Ping;

    /// <summary>
    /// 處理請求
    /// </summary>
    /// <param name="context"></param> 
    /// <returns></returns>
    public async ValueTask HandleAsync(RedisContext context)
    {
        await context.Response.WriteAsync(ResponseContent.Pong);
    }
}

7.設計Redis中間件

對於Redis服務器應用而言,我們處理一個請求需要經過多個大的步驟:

  1. 如果服務器要求Auth的話,驗證連接是否已Auth
  2. 如果Auth驗證通過之後,則查找與請求對應的IRedisCmdHanler來處理請求
  3. 如果沒有IRedisCmdHanler來處理,則告訴客戶端命令不支持。

7.1 中間件接口

/// <summary>
/// redis中間件
/// </summary>
interface IRedisMiddleware : IApplicationMiddleware<RedisContext>
{
}
/// <summary>
/// 應用程序中間件的接口
/// </summary>
/// <typeparam name="TContext"></typeparam>
public interface IApplicationMiddleware<TContext>
{
    /// <summary>
    /// 執行中間件
    /// </summary>
    /// <param name="next">下一個中間件</param>
    /// <param name="context">上下文</param>
    /// <returns></returns>
    Task InvokeAsync(ApplicationDelegate<TContext> next, TContext context);
}

7.2 命令處理者中間件

這裏只拿重要的命令處理者中間件來做代碼說明,其它中間件也是一樣處理方式。

/// <summary>
/// 命令處理中間件
/// </summary>
sealed class CmdMiddleware : IRedisMiddleware
{
    private readonly Dictionary<RedisCmd, IRedisCmdHanler> cmdHandlers;

    public CmdMiddleware(IEnumerable<IRedisCmdHanler> cmdHanlers)
    {
        this.cmdHandlers = cmdHanlers.ToDictionary(item => item.Cmd, item => item);
    }

    public async Task InvokeAsync(ApplicationDelegate<RedisContext> next, RedisContext context)
    {
        if (this.cmdHandlers.TryGetValue(context.Reqeust.Cmd, out var hanler))
        {
            // 這裏是本中間件要乾的活
            await hanler.HandleAsync(context);
        }
        else
        {
            // 本中間件幹不了,留給下一個中間件來幹
            await next(context);
        }
    }
}

8 編排Redis中間件

回到RedisConnectionHandler,我們需要實現它,實現邏輯是編排Redis中間件並創建可以處理應用請求的委託application,再將收到的redis請求創建RedisContext對象的實例,最後使用application來執行RedisContext實例即可。

8.1 構建application委託

sealed class RedisConnectionHandler : ConnectionHandler
{
    private readonly ILogger<RedisConnectionHandler> logger;
    private readonly ApplicationDelegate<RedisContext> application;

    /// <summary>
    /// Redis連接處理者
    /// </summary> 
    /// <param name="appServices"></param> 
    /// <param name="logger"></param>
    public RedisConnectionHandler(
        IServiceProvider appServices,
        ILogger<RedisConnectionHandler> logger)
    {
        this.logger = logger;
        this.application = new ApplicationBuilder<RedisContext>(appServices)
            .Use<AuthMiddleware>()
            .Use<CmdMiddleware>()
            .Use<FallbackMiddlware>()
            .Build();
    }
}

8.2 使用application委託處理請求

sealed class RedisConnectionHandler : ConnectionHandler
{
    /// <summary>
    /// 處理Redis連接
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async override Task OnConnectedAsync(ConnectionContext context)
    {
        try
        {
            await this.HandleRequestsAsync(context);
        }
        catch (Exception ex)
        {
            this.logger.LogDebug(ex.Message);
        }
        finally
        {
            await context.DisposeAsync();
        }
    }

    /// <summary>
    /// 處理redis請求
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private async Task HandleRequestsAsync(ConnectionContext context)
    {
        var input = context.Transport.Input;
        var client = new RedisClient(context);
        var response = new RedisResponse(context.Transport.Output);

        while (context.ConnectionClosed.IsCancellationRequested == false)
        {
            var result = await input.ReadAsync();
            if (result.IsCanceled)
            {
                break;
            }

            var requests = RedisRequest.Parse(result.Buffer, out var consumed);
            if (requests.Count > 0)
            {
                foreach (var request in requests)
                {
                    var redisContext = new RedisContext(client, request, response, context.Features);
                    await this.application.Invoke(redisContext);
                }
                input.AdvanceTo(consumed);
            }
            else
            {
                input.AdvanceTo(result.Buffer.Start, result.Buffer.End);
            }

            if (result.IsCompleted)
            {
                break;
            }
        }
    }
}

9 文章總結

在還沒有進入閱讀本文章之前,您可能會覺得我會大量講解Socket知識內容,例如Socket BindSocket AcceptSocket SendSocket Receive等。但實際上沒完全沒有任何涉及,因爲終結點的監聽、連接的接收、緩衝區的處理、數據接收與發送等這些基礎而複雜的網絡底層kestrel已經幫我處理好,我們關注是我們的應用協議層的解析、還有應用本身功能的開發兩個本質問題。

您可能發也現了,本文章的RedisRequest解析,也沒有多少行代碼!反而文章中都是抽象的中間件、處理者、上下文等概念。實際上這不但不會帶來項目複雜度,反而讓項目更好的解耦,比如要增加一個新的指令的支持,只需要增加一個xxxRedisCmdHanler的文件,其它地方都不用任何修改。

本文章是KestrelApp項目裏面的一個demo的講解,希望對您有用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章