一、主要目的
用Python语言根据Redis的通信协议,实现简单的set和get命令的驱动库。
二、主要代码
1、工具类
util.py文件,实现缓冲读取类,提供读满数组read_full()和读取一行readline()的函数。
python
import socket
import threading
# 封装读取缓冲类
class BuffReader:
# 构造函数
# sock是TCP的套接字
def __init__(self, sock: socket.socket):
# TCP套接字
self.__sock = sock
# 缓冲字节序列
self.__buff = bytearray()
# 可重入的排他锁
self.__lock = threading.RLock()
# 填充数据
def __fill(self):
# 读取底层套接字的8000个字节
data = self.__sock.recv(8000)
# 如果为空字节,返回0
if data == b"":
return 0
# 加入缓冲中
self.__buff.extend(data)
return len(data)
# 获取缓冲长度
# 返回值:代表缓冲字节个数的正整数
def get_buff_len(self):
return len(self.__buff)
# 判断缓冲是否为空
# 返回值:如果为空返回True,否则返回False
def empty(self):
return len(self.__buff) == 0
def __getmin(self, v1, v2):
if v1 < v2:
return v1
return v2
# 读取字节序列
# size是最大的字节个数
# 返回值:读取的字节序列;如果是空字节序列,代表关闭;
def read(self, size=1):
# 加锁
self.__lock.acquire()
# 缓冲不够就读取
if self.empty():
k = self.__fill()
if k == 0:
return b""
# 获得最小的个数
min_01 = self.__getmin(self.get_buff_len(), size)
# 切片复制
data = self.__buff[0:min_01]
# 切片删除字节序列
self.__buff[0:min_01] = b""
# 解锁
self.__lock.release()
return data
# 读取一行
def readline(self):
# 加锁
self.__lock.acquire()
line = b""
while True:
b = self.read(1)
if b == b"":
break
elif b == b'\r':
self.read(1)
break
else:
line = line + b
# 释放锁
self.__lock.release()
return line
# 读取指定个数的字节序列
def read_full(self, size):
buff = b""
# 记录个数
k = 0
# 循环读取
while k < size:
# 希望本次读取的字节数
target = 0
# 如果剩余数大于4024
if size - k > 4024:
target = 4024
else:
target = size - k
data = self.read(target)
# 如果结束,跳出循环
if data == b"":
raise IOError("sock closed")
buff = buff + data
k = k + len(data)
return buff
# 关闭
def close(self):
# 清空字节
self.__buff.clear()
# 关闭
self.__sock.close()
2、驱动类
python
import util
import socket
# 自定义的Redis驱动类
class HRedis:
def __init__(self, host="localhost", port=6379):
self.sock = socket.create_connection((host, port))
self.reader = util.BuffReader(self.sock)
self.encoding = "UTF-8"
# 编码参数
def __encode_arg(self, arg: str = ""):
# $ + 参数字节序列长度 + \r\n + 命令字符串 + \r\n
return "$" + str(len(arg.encode(self.encoding))) + "\r\n" + arg + "\r\n"
# 设置键
# name是键名;value是值
def set(self, name: str, value: str):
cmd = "*3\r\n$3\r\nSET\r\n" + self.__encode_arg(name) + self.__encode_arg(value)
# 发送命令的字节序列
self.sock.sendall(cmd.encode(self.encoding))
# 读取一行
line = self.reader.readline()
# 如果是字节序列 +OK,代表执行成功
if line == b"+OK":
return True
else:
raise Exception(line.decode(self.encoding))
# 获取键的值
def get(self, name: str):
cmd = "*2\r\n$3\r\nGET\r\n" + self.__encode_arg(name)
self.sock.sendall(cmd.encode(self.encoding))
# 读取一行
line = self.reader.readline()
line = line.decode(self.encoding)
# 如果是$开头的字符串
if line.startswith("$"):
# 截取值的字节个数
size = int(line[1:])
# 读取指定个数的字节序列
data = self.reader.read_full(size)
# 读取\r\n
self.reader.readline()
# 返回解码的字符串结果
return data.decode(self.encoding)
else:
raise Exception(line[1:])
if __name__ == "__main__":
con = HRedis()
result = con.set("v123", "小明")
print("设置结果:", result)
print("键的值:", con.get("v123"))
代码图如下:
三、执行结果
注意:需要在本地开启Redis服务端,监听6379端口。

四、Redis的协议格式
Redis采用TCP传输层协议,默认服务器监听6379端口。
(一)客户端发给服务端的命令的数据包格式
1、主要格式
* + 参数个数的十进制整数值 + 参数集合
参数集合由每个参数单元接在一起,连续传输。
2、每个参数单位格式如下:
$ + 参数字节序列长度的十进制整数值 + \r\n + 参数值字节串 + \r\n
3、案例:
设置键vname的值为"Peter",格式如下:
*3\r\n3\\r\\nSET\\r\\n5\r\nvname\r\n$5\r\nPeter\r\n
说明:
"*3\r\n"代表是数组类型,后面有3个元素。
"$3\r\n"代表是多行字符串,有3个字节;"SET\r\n"是指定的命令,占3个字节,用"\r\n"结尾。
后面"vname"占5个字节,"Peter"占5个字节。
(二)结果格式
1、普通文本的结果格式
适合set等不需要具体结果的命令,只返回成功或者失败。
格式:
第1个字符是"+"。
+说明文本\r\n
案例:
+OK\r\n
2、整数
用":"开头。
格式:
: 整数字符串 \r\n
案例:
:2000\r\n
3、错误
格式:
用字符"-"开头。
-错误字符串\r\n
案例:
-ERR wrong number of arguments for 'set' command\r\n
4、多行文本
格式:
$字节长度的十进制整数值\r\n
字节序列\r\n
案例:
"hello world"有11个字节,所以前缀是"$11\r\n"。
参数的字节序列再用"\r\n"结尾。
$11\r\nhello world\r\n
五、功能扩展
添加execute()和read_result()两个函数,更通用,适合所有的命令。
python
import util
import socket
# 自定义的Redis驱动类
class HRedis:
def __init__(self, host="localhost", port=6379):
self.sock = socket.create_connection((host, port))
self.reader = util.BuffReader(self.sock)
self.encoding = "UTF-8"
# 编码参数
def __encode_arg(self, arg: str = ""):
# $ + 参数字节序列长度 + \r\n + 命令字符串 + \r\n
return "$" + str(len(arg.encode(self.encoding))) + "\r\n" + arg + "\r\n"
# 设置键
# name是键名;value是值
def set(self, name: str, value: str):
cmd = "*3\r\n$3\r\nSET\r\n" + self.__encode_arg(name) + self.__encode_arg(value)
# 发送命令的字节序列
self.sock.sendall(cmd.encode(self.encoding))
# 读取一行
line = self.reader.readline()
# 如果是字节序列 +OK,代表执行成功
if line == b"+OK":
return True
else:
raise Exception(line.decode(self.encoding))
# 获取键的值
def get(self, name: str):
cmd = "*2\r\n$3\r\nGET\r\n" + self.__encode_arg(name)
self.sock.sendall(cmd.encode(self.encoding))
# 读取一行
line = self.reader.readline()
line = line.decode(self.encoding)
# 如果是$开头的字符串
if line.startswith("$"):
# 截取值的字节个数
size = int(line[1:])
# 读取指定个数的字节序列
data = self.reader.read_full(size)
# 读取\r\n
self.reader.readline()
# 返回解码的字符串结果
return data.decode(self.encoding)
else:
raise Exception(line[1:])
# 执行命令,把命令分割为参数设置
def execute(self, *args):
if len(args) > 0:
cmd = "*" + str(len(args)) + "\r\n"
for v in args:
cmd = cmd + self.__encode_arg(v)
self.sock.sendall(cmd.encode(self.encoding))
else:
raise Exception("command is empty.")
# 读取结果
def read_result(self):
line = self.reader.readline().decode(self.encoding)
if line:
if line[0] == "+" or line[0] == ":":
return line[1:]
elif line[0] == "$":
data = self.reader.read_full(int(line[1:]))
self.reader.readline()
return data.decode(self.encoding)
elif line[0] == "*":
count = int(line[1:])
data = []
for k in range(0, count, 1):
data.append(self.read_result())
return data
else:
raise Exception(line[1:])
else:
raise IOError("sock error")
if __name__ == "__main__":
con = HRedis()
con.execute("set", "v123", "小明")
print("设置结果:", con.read_result())
con.execute("get", "v123")
print("键v123的值:", con.read_result())
con.execute("ping")
print(con.read_result())
con.execute("dbsize")
print("个数:", con.read_result())
执行结果:

六、总结
1、基本能实现set和get命令。
2、读者可以自己实现其他的keys、setnx、strlen、rename、lpush、lpop、rpush、rpop、hset、hget等命令。
