kestrel网络编程--开发redis服务器
作者:互联网
1 文章目的
本文讲解基于kestrel开发实现了部分redis命令的redis伪服务器的过程,让读者了解kestrel网络编程的完整步骤,其中redis通讯协议需要读者自行查阅,文章里不做具体解析。
2 开发顺序
- 创建Kestrel的Redis协议处理者
- 配置监听的EndPoint并使用Redis处理者
- 设计交互上下文RedisContext
- 设计Redis命令处理者
- 设计Redis中间件
- 编排Redis中间件构建应用
3. 创建Redis协议处理者
在Kestrel中,末级的中间件是一个没有next的特殊中间件,基表现出来就是一个ConnectionHandler的行为。我们开发redis应用只需要继承ConnectionHandler这个抽象类来,当kestrel接收到新的连接时将连接交给我们来处理,我们处理完成之后,不再有下一个处理者来处理这个连接了。
Copy
/// <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。
Copy
{ "Kestrel": { "Endpoints": { "Redis": { // redis协议服务器,只监听loopback的IP "Url": "http://localhost:5007" } } } }
Copy
{ "Kestrel": { "Endpoints": { "Redis": { // redis协议服务器,监听所有IP "Url": "http://*:5007" } } } }
4.2 在代码中配置Redis处理者
为Redis这个节点关联上RedisConnectionHandler
,当redis客户端连接到5007这个端口之后,OnConnectedAsync()
方法就得到触发且收到连接上下文对象。
Copy
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客户端的什么请求,从而请求命令处理者可以向它响应对应的内容。
Copy
/// <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,做为多个中间件之间的沟通渠道。
Copy
/// <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到多个参数值。
Copy
/// <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的解析:
Copy
/// <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; }