Redis是一个开源的内存键值数据存储,最常用作主数据库、缓存、消息代理和队列。Redis提供了亚毫秒的响应时间,在游戏、金融科技、广告技术、社交媒体、医疗保健和物联网等行业实现了快速而强大的实时应用。
Redis连续五年成为开发人员最喜爱的数据库。开发人员喜欢Redis,因为它的易用性、性能和可扩展性。Redis客户端可用于各种流行的现代编程语言。再加上性能优势,Redis成为缓存、会话管理、游戏、欺诈检测、排行榜、实时分析、地理空间索引、拼车、社交媒体和流媒体应用程序最受欢迎的选择。
使用CLI探索Redis
外部程序使用TCP套接字和Redis特定的协议与Redis进行通信。该协议在Redis客户端库中实现,用于不同的编程语言。然而,为了简化Redis的黑客攻击,Redis提供了一个命令行实用程序,可以用来向Redis发送命令。这个程序叫做redis-cli。
这里我们先看看redis服务器是否开启。
要检查Redis是否正常工作,首先要做的是使用reds-cli发送一个PING命令:
运行reds-cli,后跟一个命令名及其参数,将此命令发送到本地主机6379端口上运行的redis实例。可以更改reds-cli使用的主机和端口,只需尝试–help选项即可检查使用情况信息。
另一种运行redis-cli的方法是不带参数:程序将以交互模式启动。您可以键入不同的命令并查看它们的答复。
保护好你的Redis
默认情况下,Redis绑定到所有接口,并且根本没有身份验证。如果你在一个非常可控的环境中使用Redis,与外部互联网分离,通常与攻击者分离,那没关系。
然而,如果一个未经保护的Redis暴露在互联网上,这将是一个巨大的安全问题。如果您不能100%确定您的环境是否正确安全,请检查以下步骤以使Redis更安全,这些步骤是为了提高安全性而登记的。
1.确保Redis用于监听连接的端口(默认情况下为6379,如果您在集群模式下运行Redis,则为16379,Sentinel为26379)已防火墙,因此无法从外部联系Redis。
2.使用设置了bind指令的配置文件,以确保Redis只侦听您正在使用的网络接口。例如,如果您只是从同一台计算机本地访问Redis,则仅使用环回接口(127.0.0.1),依此类推。
3.使用requirepass选项可以添加额外的安全层,以便客户端需要使用AUTH命令进行身份验证。
4.如果您的环境需要加密,请使用spiped或其他SSL隧道软件来加密Redis服务器和Redis客户端之间的流量。
请注意,在没有任何安全性的情况下暴露在互联网上的Redis实例很容易被利用,所以请确保您理解以上内容,并至少应用一个防火墙层。防火墙就位后,尝试从外部主机连接reds-cli,以证明该实例实际上是不可访问的。
从应用程序(hiRedis)中使用Redis
当然,仅仅从命令行界面使用Redis是不够的,因为目标是从应用程序中使用它。为了做到这一点,您需要下载并安装适用于您的编程语言的Redis客户端库。
hiRedis使用方法一般顺序为先用 redisConnect 连接数据库,然后用 redisCommand 执行命令,执行完后用 freeReplyObject 来释放redisReply对象,最后用 redisFree 来释放整个连接。
...
int main(int argc, char **argv)
{
/* 连接到redis */
...
redisContext *c = redisConnect(hostname,port);
struct timeval timeout = { 1, 500000 }; // 1.5 seconds
if (isunix)
{
c = redisConnectUnixWithTimeout(hostname, timeout);
}
else
{
c = redisConnectWithTimeout(hostname, port, timeout);
}
if (c == NULL || c->err)
{
if (c)
{
printf("Connection error: %s\n", c->errstr);
redisFree(c);
}
else
{
printf("Connection error: can't allocate redis context\n");
}
exit(1);
}
printf("Connected to redis\n");
// redisFree(c);
/* 2 - PING server */
redisReply *reply; /* 临时答复指针 */
reply = redisCommand(c,"PING");
printf("PING: %s\n", reply->str);
/* 3 - Set a key */
reply = redisCommand(c,"SET %s %s", "foo", "hello world");
printf("SET %s %s \t| %s\n", "foo", "hello world", reply->str);
freeReplyObject(reply); // 释放回复对象
/* 3 - Get a key */
reply = redisCommand(c,"GET %s","foo");
printf("GET %s \t\t| ","foo");
printf("%s\n",reply->str);
freeReplyObject(reply);
/* Set a key using binary safe API */
// reply = redisCommand(c,"SET %b %b", "bar", (size_t) 3, "hello", (size_t) 5);
// printf("SET (binary API): %s\n", reply->str);
// freeReplyObject(reply);
redisFree(c);
return 0;
}
我们也可以使用 nc 命令来替代 redis-cli 命令行:
什么是 RESP?
Redis 的客户端和服务端之间采取了一种独立名为 RESP(REdis Serialization Protocol) 的协议。通过 tcp流式套接字来进行通讯,为了 防止粘包 因此命令或数据均以 \r\n (CRLF) 结尾,然后根据解析规则解析相应信息。
RESP协议可以序列化多种类型,比如Simple Strings(简单字符串),Errors(错误类型),Integers(整形),Bulk Strings(批量串)和Arrays(数组),但此协议只适用于Redis客户端-服务端之间的通信,Redis集群中节点间通信使用的另一种协议。
RESP协议说明
RESP协议是在Redis 1.2中引入的,但它成为了与Redis 2.0中的Redis服务器通信的标准方式。这是所有Redis客户端都要遵循的协议,我们甚至可以基于此协议,开发实现自己的Redis客户端。
RESP在Redis中用作请求-响应协议的方式如下:
1.客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
2.服务器根据命令实现回复一种RESP类型。
3.在RESP中,某些数据的类型取决于第一个字节:
+代表简单字符串(Simple Strings)比如OK,PONG(对应客户端的PING命令)
-代表错误类型(Errors)
:代表整型(Integers)
$代表多行字符串(Bulk Strings)
*代表数组(Arrays)
此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以“\r\n”(CRLF)结束。
RESP抓包验证
我们知道,Redis客户端与server端通信,本身就是基于tcp的一个Request/Response模式。我们不妨用网络抓包工具,拦截客户端与server端传输的数据、一探究竟。抓包使用tcpdump命令,具体参数这里就不多说了,使用的命令是:
tcpdump host 127.0.0.1 and port 6379 -i lo -w redis-packet-test.cap
抓取的结果保存在redis-packet-test.cap,分析工具使用Wireshark,在分析之前,先说下客户端与服务端交互的命令:
1.info,返回redis服务端的相关信息
2.set abc 111,服务端响应OK
3.get abc,返回111
4.lpush abclist 1 2 3,返回 9
5.ee,这是个错误命令,主要看下服务端返回的错误数据格式
接下来我们结合数据包分析下:
TCP三次握手建立连接的
首先发送的命令是info,先看右边部分,可以看到一开始是*1:表示长度为1的数组,后边的··对应左边是0d 0a,其实就是\r\n的16进制表示形式,然后后边$4:代表长度为4的Bulk Strings,也就是info,后边紧跟着info。
info命令返回数据包:$1924:长度为1924的Bulk Strings,后边便是服务器相关信息
set abc 111命令:*3:长度为3的数组,后边是数组里的3个元素:$3··set(长度为3的Bulk Strings)、$3··xfh、$3··111
返回数据:+OK(代表简单字符串‘OK’)简单字符串一般是服务器状态相关,比如’OK’、‘PONG’等;而Bulk Strings可以包含任何内容(比如换行符、控制符)
命令lpush abclist 1 2 3:看到这相信大家都已经明白了,*5代表长度为5的数组,后边紧跟着5个Bulk Strings lpush、xfhlist、1、2、3,而且每个元素的前边都有长度,分别是$5、$7、$1、$1、$1
返回::9(表示整形数据9)
命令ff,这是个错误命令,redis中没有这个命令,应该返回语法错误
返回:发现前缀是-,对应RESP协议中的错误类型,后边紧跟着ERR unknown command ‘ff’
...
int query_parser(const u_char* pkt_data, unsigned int data_len, char **query)
{
...
}
int resp_parser(const u_char* pkt_data, unsigned int data_len, char **resp)
{
...
}
...
void packetHandle(u_char* arg, const struct pcap_pkthdr* header, const u_char* pkt_data)
{
...
if ( !pkt_data )
{
printf ("Didn't grab packet!/n");
exit (1);
}
if (header->caplen < header->len) return;
pehdr = (struct ether_header*)pkt_data;
pkt_data += *linkhdrlen;
piphdr = (struct ip*)pkt_data;
pkt_data += IP_HL(piphdr);
data_len = ntohs(piphdr->ip_len) - IP_HL(piphdr);
switch(piphdr->ip_p)
{
case IPPROTO_TCP:
ptcphdr = (struct tcphdr*)pkt_data;
data_len = data_len - TCP_OFF(ptcphdr);
pkt_data += TCP_OFF(ptcphdr);
strcpy(sip, inet_ntoa(piphdr->ip_src));
strcpy(dip, inet_ntoa(piphdr->ip_dst));
sport = ntohs(ptcphdr->source);
dport = ntohs(ptcphdr->dest);
break;
default:
data_len = 0;
pkt_data = NULL;
break;
}
if (data_len == 0 || pkt_data == NULL ) return;
...
signal(SIGINT, bailout);
signal(SIGTERM, bailout);
signal(SIGQUIT, bailout);
}
...
int main(int argc, char **argv)
{
...
while ((i = getopt(argc, argv, "hi:p:")) != -1)
{
switch(i)
{
case 'h':
Usage();
return -1;
break;
case 'i':
option.device = optarg;
break;
case 'p':
option.port = atoi(optarg);
break;
default:
break;
}
}
sprintf(option.bufstr, "port %d", option.port);
char *d = getenv("jdebug");
if ( d != NULL && !strcmp(d, "true"))
debug = 1;
dbg("debug mode\n");
if((pHandle = init_pcap_t(option.device, option.bufstr)))
{
sniff_loop(pHandle, (pcap_handler)packetHandle);
}
...
}
If you need the complete source code, please add the WeChat number (c17865354792)
总结
Redis 基于 RESP (Redis Serialization Protocal)协议来完成客户端和服务端通讯的。RESP 本质是一种文本协议,实现简单、易于解析。底层采用的是TCP的连接方式,通过tcp进行数据传输,然后根据解析规则解析相应信息。
Welcome to follow WeChat official account【程序猿编码】
参考:https://redis.io/docs/