最近读到官网的一篇文章 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还支持另外两种操作:

  1. 管道技术(PIPELINE): 当你有很多命令需要短时间执行时候,这个技术非常有用。其核心就是将多个命令打包成一个命令,减少来回的网络开销。这里有一点说明,管道和事务是有区别的,虽然他们都是合并命令。管道可以认为这些命令是松散的,中间可以插入其他的命令;而事务是相反的,这些命令是紧耦合的,必须全部执行完毕才能执行后续命令。
  2. 订阅模式:当客户端订阅(subscribe)某个频道后,redis将不再需要等待命令了。可以反过来在pub端发送消息,而sub端会自动获取到这些消息。这项技术在应用在我们游戏的聊天室中,还有一些跨服消息(rpc)之类。