这个项目将分为两部分:
Go 服务端 (GoRedis):一个用 Go 语言编写的、并发的、基于 TCP 的键值存储服务器。它会监听一个端口,接收并处理类似 Redis 的命令(如 SET, GET, DEL)。为了简单起见,我们将实现一个简单的文本协议,而不是 Redis 复杂的 RESP 协议。
Python 客户端:一个用 Python 编写的客户端。重要提示:由于我们的 Go 服务器使用自定义的简单文本协议,标准的 redis-py 库将无法直接连接。redis-py 库是为与遵循 RESP 协议的 Redis 服务器通信而设计的。因此,我将提供一个使用 Python socket 库的自定义客户端来与我们的 Go 服务器交互,并解释为什么 redis-py 不能用,以及如果想让它能用需要做什么。
第 1 部分:Go 内存 NoSQL 服务器 (GoRedis)
这个服务器将具备以下特点:
并发处理:为每个客户端连接创建一个新的 Goroutine。
线程安全:使用 sync.RWMutex 来保护对内存中数据的并发读写,允许多个读操作同时进行,但写操作是独占的。
支持的命令:SET key value, GET key, DEL key, PING, QUIT。
网络协议:简单的基于文本行的协议(例如,命令和参数用空格隔开,以 \n 结尾)。
代码 (server.go)package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
"strings"
"sync"
)
// dataStore 是我们的内存键值存储
var dataStore = make(map[string]string)
// rwMutex 用于保护对 dataStore 的并发访问
// RWMutex 允许并发读取,但写入是独占的,非常适合我们的场景
var rwMutex = &sync.RWMutex{}
const (
// 定义服务器监听的端口
SERVER_PORT = "6380" // 使用一个不同于默认Redis端口的端口
SERVER_TYPE = "tcp"
)
func main() {
log.Println("GoRedis Server is starting...")
// 监听指定的TCP端口
server, err := net.Listen(SERVER_TYPE, ":"+SERVER_PORT)
if err != nil {
log.Fatalf("Error listening: %v", err)
}
// 在main函数结束时关闭监听器
defer server.Close()
log.Printf("Listening on %s:%s", "localhost", SERVER_PORT)
// 无限循环,等待并接受新的客户端连接
for {
conn, err := server.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}
// 为每个连接创建一个新的Goroutine来处理
go handleConnection(conn)
}
}
// handleConnection 处理单个客户端连接
func handleConnection(conn net.Conn) {
// 在函数结束时确保连接被关闭
defer conn.Close()
log.Printf("Client connected: %s", conn.RemoteAddr().String())
// 使用bufio.Reader来方便地读取一行数据
reader := bufio.NewReader(conn)
for {
// 读取直到遇到换行符 '\n'
commandLine, err := reader.ReadString('\n')
if err != nil {
if err != io.EOF {
log.Printf("Error reading from client %s: %v", conn.RemoteAddr().String(), err)
} else {
log.Printf("Client %s disconnected", conn.RemoteAddr().String())
}
return // 发生错误或客户端关闭连接时,退出循环
}
// 去除命令行的前后空白字符(包括换行符)
commandLine = strings.TrimSpace(commandLine)
if commandLine == "" {
continue // 忽略空行
}
// 解析命令和参数
parts := strings.Fields(commandLine) // Fields按空白字符分割
command := strings.ToUpper(parts[0])
args := parts[1:]
// 根据命令执行相应的操作
switch command {
case "SET":
handleSet(conn, args)
case "GET":
handleGet(conn, args)
case "DEL":
handleDel(conn, args)
case "PING":
conn.Write([]byte("PONG\n"))
case "QUIT":
conn.Write([]byte("OK\n"))
return // 关闭连接
default:
conn.Write([]byte("ERR unknown command\n"))
}
}
}
func handleSet(conn net.Conn, args []string) {
if len(args) != 2 {
conn.Write([]byte("ERR wrong number of arguments for 'set' command\n"))
return
}
key, value := args[0], args[1]
// 加写锁,因为我们要修改map
rwMutex.Lock()
dataStore[key] = value
rwMutex.Unlock()
conn.Write([]byte("OK\n"))
}
func handleGet(conn net.Conn, args []string) {
if len(args) != 1 {
conn.Write([]byte("ERR wrong number of arguments for 'get' command\n"))
return
}
key := args[0]
// 加读锁,允许多个客户端同时读取
rwMutex.RLock()
value, ok := dataStore[key]
rwMutex.RUnlock()
if !ok {
conn.Write([]byte("(nil)\n")) // 模仿Redis,如果键不存在则返回 (nil)
} else {
conn.Write([]byte(fmt.Sprintf("%s\n", value)))
}
}
func handleDel(conn net.Conn, args []string) {
if len(args) < 1 {
conn.Write([]byte("ERR wrong number of arguments for 'del' command\n"))
return
}
deletedCount := 0
// 加写锁,因为我们要修改map
rwMutex.Lock()
for _, key := range args {
if _, ok := dataStore[key]; ok {
delete(dataStore, key)
deletedCount++
}
}
rwMutex.Unlock()
conn.Write([]byte(fmt.Sprintf("(integer) %d\n", deletedCount)))
} 如何运行 Go 服务器将以上代码保存为 server.go。
打开终端,进入文件所在目录。
运行服务器:
go run server.go
你将看到服务器启动并开始监听 6380 端口的日志。
第 2 部分:Python 客户端
如前所述,redis-py 库期望与服务器通过 RESP (REdis Serialization Protocol) 协议通信。我们的服务器使用了一个更简单的 "命令\n" 协议,所以 redis-py 会失败。
下面,我将提供一个使用 Python 标准 socket 库的自定义客户端来与我们的 GoRedis 服务器交互。
代码 (client.py)import socket
class GoRedisClient:
"""
一个简单的客户端,用于连接我们自定义的 GoRedis 服务器。
它使用简单的文本协议进行通信。
"""
def __init__(self, host='127.0.0.1', port=6380):
self._host = host
self._port = port
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._reader = None
def connect(self):
"""连接到服务器"""
self._socket.connect((self._host, self._port))
# 创建一个文件对象以便按行读取
self._reader = self._socket.makefile('r', encoding='utf-8')
print(f"Connected to GoRedis at {self._host}:{self._port}")
def _send_command(self, *args):
"""发送命令并返回响应"""
if not self._reader:
raise ConnectionError("Client is not connected. Call connect() first.")
# 将命令和参数用空格连接,并添加换行符
command = ' '.join(map(str, args)) + '\n'
# 发送命令
self._socket.sendall(command.encode('utf-8'))
# 读取并返回一行响应
response = self._reader.readline().strip()
return response
def ping(self):
"""发送 PING 命令"""
return self._send_command('PING')
def get(self, key):
"""发送 GET 命令"""
response = self._send_command('GET', key)
if response == '(nil)':
return None
return response
def set(self, key, value):
"""发送 SET 命令"""
return self._send_command('SET', key, value)
def delete(self, *keys):
"""发送 DEL 命令"""
response = self._send_command('DEL', *keys)
# 从 '(integer) 1' 中提取数字
if response and response.startswith('(integer)'):
return int(response.split(' ')[1])
return 0
def close(self):
"""关闭连接"""
if self._reader:
try:
# 尝试发送QUIT命令
self._send_command('QUIT')
except (socket.error, BrokenPipeError):
# 如果连接已经中断,忽略错误
pass
finally:
self._reader.close()
self._socket.close()
self._reader = None
print("Connection closed.")
if __name__ == '__main__':
# 确保你的 Go 服务器正在运行
client = GoRedisClient()
try:
client.connect()
# 1. PING 服务器
print(f"PING -> {client.ping()}")
# 2. SET 一个值
print(f"SET name 'GoRedis' -> {client.set('name', 'GoRedis')}")
# 3. GET 这个值
print(f"GET name -> {client.get('name')}")
# 4. GET 一个不存在的值
print(f"GET non_existent_key -> {client.get('non_existent_key')}")
# 5. SET 另一个值
print(f"SET version 1.0 -> {client.set('version', '1.0')}")
# 6. DELETE 一个值
print(f"DEL version -> Deleted {client.delete('version')} key(s)")
# 7. 确认值已被删除
print(f"GET version (after delete) -> {client.get('version')}")
# 8. 删除多个值
client.set('temp1', 'val1')
client.set('temp2', 'val2')
print(f"DEL temp1 temp2 non_existent -> Deleted {client.delete('temp1', 'temp2', 'non_existent')} key(s)")
except ConnectionRefusedError:
print("Connection failed. Is the GoRedis server running?")
except Exception as e:
print(f"An error occurred: {e}")
finally:
client.close() 如何运行 Python 客户端确保你的 Go 服务器 (server.go) 正在运行。
将以上 Python 代码保存为 client.py。
打开一个新的终端,运行客户端:
python client.py预期输出
Python 客户端的输出应该像这样:
Connected to GoRedis at 127.0.0.1:6380 PING -> PONG SET name 'GoRedis' -> OK GET name -> GoRedis GET non_existent_key -> None SET version 1.0 -> OK DEL version -> Deleted 1 key(s) GET version (after delete) -> None DEL temp1 temp2 non_existent -> Deleted 2 key(s) Connection closed.
同时,Go 服务器的终端会显示客户端连接、断开以及可能发生的错误的日志。
总结与扩展
这个项目成功地实现了一个功能虽简但核心思想与 Redis 类似的 Go NoSQL 服务器,并用 Python 客户端进行了操作。
网友回复


