+
34
-

回答

这个项目将分为两部分:

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 客户端进行了操作。

网友回复

我知道答案,我要回答