最近读到官网的一篇文章 https://redis.io/topics/protocol, 主要是描述了一下redis在通讯协议。忽然觉得豁然开朗:redis作为作为一个内存数据库,其实其本质也是一个服务器而已。监听在6379(默认)接口,然后定义了一套自己的命令方便调用者使用。
其实市面上的主流第三方客户端都会遵守这套协议。只不过实现的语言不一样而已。这里试着分析这套协议,并且写一个简单的“redis客户端”。
内联协议
既然是个tcp服务器,我们就可以telnet上去,类似下图这样:
这些命令看起来和我们平时使用的差不多,不过这些命令被称为“内联命令”,而不是redis真正的通信协议。怎么理解呢?就是这些命令发过去后,redis服务器会先分析你的命令,如果发现是“内联的”,就会自己再解析成标准协议。
对于redis这种高性能服务器来说,花费额外的性能解析这些“内联命令”是有些得不偿失的。这里斗胆坏一下:网上有些第三方的客户端做了这种投机取巧的:)
真正的协议
虽然分为请求和相应两个部分,其实他们都遵循一个协议:
- 对于简单字符串类型,它的第一个byte是"+"
- 对于错误类型,它的第一个byte是"-"
- 对于整数类型, 它的第一个byte是":"
- 对于复杂字符串类型,它的第一个byte是"$"
- 对于数组类型,它的第一个byte是"*"
- 结束字符固定为 "\r\n" (CRLF)。
对于发送端:客户端永远使用数组类型,里面使用复杂字符串类型。
所以举个例子,比如get name, 在底层就变成了
*2\r\n$3\r\nget\r\n$4\r\nname\r\n
这里做下说明: *2表示整个数组长度为2,然后\r\n都是结束字符,可以认为是redis的一种强制分隔符; $3表示get这个字符长度为3;同理, $4表示name字符长度为4。当然每个表达式中间再用CRLF字符分割即可。
这里我写了个简单的拼接程序:
static readonly string CRLF = "\r\n";
static string SocketCommand(string[] command)
{
var _len = command.Length;
var _command = new StringBuilder();
_command.Append($"*{_len}{CRLF}");
for (int i = 0; i < _len; i++)
{
_command.Append($"${command[i].Length}{CRLF}{command[i]}{CRLF}");
}
return _command.ToString();
}
测试结果:
可以看到和telnet模式是一样的操作,但底层协议完全不一样。
不同的模式
除了上面说得这种经典“一问一答”模式之外, redis还支持另外两种操作:
- 管道技术(PIPELINE): 当你有很多命令需要短时间执行时候,这个技术非常有用。其核心就是将多个命令打包成一个命令,减少来回的网络开销。这里有一点说明,管道和事务是有区别的,虽然他们都是合并命令。管道可以认为这些命令是松散的,中间可以插入其他的命令;而事务是相反的,这些命令是紧耦合的,必须全部执行完毕才能执行后续命令。
- 订阅模式:当客户端订阅(subscribe)某个频道后,redis将不再需要等待命令了。可以反过来在pub端发送消息,而sub端会自动获取到这些消息。这项技术在应用在我们游戏的聊天室中,还有一些跨服消息(rpc)之类。