RESP(REdis Serialization Protocol)是 Redis 1.2 引入、并在 Redis 2.0 正式使用的通信协议。它是一种直观的文本协议,最大的优点是 简单、易解析、可读性强 。
RESP 协议通过不同的前缀字符来区分数据类型:
前缀 | 类型 | 示例 |
|---|---|---|
+ | 简单字符串(Simple String) | +OK\r\n |
- | 错误(Error) | -ERR unknown command\r\n |
: | 整数(Integer) | :1000\r\n |
$ | 批量字符串(Bulk String) | $5\r\nhello\r\n |
* | 数组(Array) | *2\r\n$3\r\nget\r\n$4\r\nname\r\n |
客户端发送命令时,使用 数组 格式。以 SET name 小明 为例:
*3
$3
SET
$4
name
$6
小明逐行解读:
- *3 —— 数组有 3 个元素
- $3 —— 第 1 个元素长度 3 字节
- SET —— 第 1 个元素的值
- $4 —— 第 2 个元素长度 4 字节
- name —— 第 2 个元素的值
- $6 —— 第 3 个元素长度 6 字节(中文"小明"在 UTF-8 下占 6 字节)
- 小明 —— 第 3 个元素的值
注意 :每行以 \r\n 结尾,长度是字节数而非字符数,这也是为什么代码中要计算 GetBytes(c).Length 。
实现一个简易 Redis 客户端,核心只需三步:
建立TCP连接 → 按RESP协议拼接命令字节流 → 发送并接收响应
Redis 默认监听 6379 端口,建立 TCP 连接后即可通信。如果设置了密码,连接后需先发送 AUTH 命令完成认证。
将用户输入的命令字符串(如 SET name 小明 )按空格拆分,按 RESP 数组格式拼接成字节流。关键是:
- 按 空格拆分 命令和参数
- 计算每个参数的 UTF-8字节数 (而非字符数)
- 用 *N 声明元素个数, $len 声明每个元素的字节长度
发送字节流后,通过 Socket 接收服务端返回的数据。这里有一个 常见坑点 :
Redis 返回的数据可能分多个 TCP 包到达,一次 Receive 不一定收完。
因此需要 循环接收 ,通过判断缓冲区是否还有可读数据来决定是否继续读取。
package org.open.java;
import java.io.*;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
public class MyRedis {
static Socket socket;
static PrintWriter writer;
static BufferedReader reader;
public static void main(String[] args) {
try {
// 创建连接
socket = new Socket("127.0.0.1", 6379);
// 获取输出流
OutputStream outputStream = socket.getOutputStream();
writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
// 获取输入流
InputStream inputStream = socket.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
sendRedisCode("auth 123123");
sendRedisCode("set name Tom");
sendRedisCode("get name");
sendRedisCode("mget name age addr");
} catch (IOException e) {
System.out.println(e);
}
finally{
try {
// 4.关闭联接
socket.close();
} catch (IOException e) {
System.out.println(e);
}
}
}
private static void handleRedisResponse() throws IOException {
String s = reader.readLine();
System.out.println(s);
if(s.startsWith("$")){
long l = Long.parseLong(s.replace("$", ""));
if(l > 0) {
System.out.println(reader.readLine());
}
}
else if(s.startsWith("*"))
{
long l = Long.parseLong(s.replace("*", ""));
for (long i = 0; i < l; i++) {
handleRedisResponse();
}
}
}
private static void sendRedisCode(String redisCode) throws IOException {
String[] args = redisCode.split(" ");
writer.println("*" + args.length);
for (String arg : args) {
writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
writer.println(arg);
}
writer.flush();
System.out.println(redisCode);
// 3.接收并解析响应
handleRedisResponse();
System.out.println("");
}
}using System.Net.Sockets;
using System.Text;
// 创建Socket
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
// 建立连接
socket.Connect("127.0.0.1", 6379);
sendCode("auth 123123");
sendCode("set name Tom");
sendCode("get name");
sendCode("mget name age addr");
// 关闭Socket
socket.Close();
string sendCode(string code)
{
// 推送命令
socket.Send(Encoding.UTF8.GetBytes(getCode(code)));
// 接收返馈(循环接收,避免数据不全)
var buffer = new byte[8192];
var sb = new StringBuilder();
int received;
do
{
received = socket.Receive(buffer);
sb.Append(Encoding.UTF8.GetString(buffer, 0, received));
} while (socket.Available > 0);
// 解析结果
var msg = sb.ToString();
Console.WriteLine(code);
Console.WriteLine(msg);
return msg;
}
string getCode(string code)
{
if (string.IsNullOrWhiteSpace(code)) {
return "";
}
var cs = code.Split(' ');
StringBuilder sb = new StringBuilder();
sb.AppendLine("*" + cs.Length);
foreach (var c in cs)
{
sb.AppendLine($"${Encoding.UTF8.GetBytes(c).Length}");
sb.AppendLine($"{c}");
}
//Console.WriteLine(sb.ToString());
return sb.ToString();
}import socket
import select
# 创建Socket并建立连接
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 6379))
def get_code(code: str) -> bytes:
"""将Redis命令转换为RESP协议格式"""
if not code or not code.strip():
return b""
parts = code.split(" ")
lines = [f"*{len(parts)}"]
for c in parts:
byte_len = len(c.encode("utf-8"))
lines.append(f"${byte_len}")
lines.append(c)
resp = "\r\n".join(lines) + "\r\n"
print(resp, end="")
return resp.encode("utf-8")
def send_code(code: str) -> str:
"""发送Redis命令并接收响应"""
# 推送命令
sock.send(get_code(code))
# 接收返馈(循环接收,避免数据不全)
chunks = []
while True:
readable, _, _ = select.select([sock], [], [], 0.5)
if not readable:
break
data = sock.recv(8192)
if not data:
break
chunks.append(data)
# 解析结果
msg = b"".join(chunks).decode("utf-8")
print(msg)
print()
return msg
# 登录Redis
send_code("auth 123123")
send_code("get name")
# 关闭Socket
sock.close()这个简易客户端只实现了最基本的发送/接收功能,没有做响应解析。如果需要完整解析 RESP 响应,可以根据返回的首字符( + 、 - 、 : 、 $ 、 * )判断类型并逐行解析,那就离一个真正的 Redis 客户端不远了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。