+
14
-

回答

这个项目将包含以下部分:

TCP 服务器:监听一个端口,接收客户端连接。

自定义协议:客户端和服务器之间通过简单的文本协议通信(例如,以换行符 \n 分隔的 SQL 语句)。这比实现完整的 MySQL 二进制协议要简单得多,但能充分展示核心思想。

SQL 解析器(简化版):服务器能解析一小部分 SQL 命令,如 GET key, SET key value, DELETE key。我们使用这个简化的语法来模拟 SQL 查询,避免引入复杂的 SQL 解析库。

内存数据库:服务器将在内存中维护一个简单的键值存储(使用 map),并使用互斥锁(sync.RWMutex)来保证并发安全。

TCP 客户端:一个简单的命令行客户端,可以连接到服务器,发送我们自定义的 "SQL" 命令,并打印结果。

项目结构

为了清晰起见,我们将项目分为两个部分:server 和 client。

go-mysql-example/
├── server/
│   └── main.go
└── client/
    └── main.go

1. 服务器端代码 (server/main.go)

服务器的核心职责是:

监听 TCP 连接。

为每个连接启动一个 goroutine 进行处理。

在 goroutine 中,循环读取、解析和执行客户端发送的命令。

使用读写锁 (RWMutex) 保护共享的内存数据,允许多个客户端同时读取,但写入时独占。

// server/main.go
package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
    "strings"
    "sync"
)

// DB 是我们的内存数据库
type DB struct {
    data map[string]string
    mu   sync.RWMutex
}

// NewDB 创建一个新的数据库实例
func NewDB() *DB {
    return &DB{
        data: make(map[string]string),
    }
}

// processQuery 解析并执行查询
func (db *DB) processQuery(query string) string {
    parts := strings.Fields(query) // 按空格分割命令
    if len(parts) == 0 {
        return "ERROR: Empty query"
    }

    command := strings.ToUpper(parts[0])

    switch command {
    case "SET":
        if len(parts) != 3 {
            return "ERROR: SET syntax is 'SET key value'"
        }
        key, value := parts[1], parts[2]
        db.mu.Lock()
        defer db.mu.Unlock()
        db.data[key] = value
        return "OK"

    case "GET":
        if len(parts) != 2 {
            return "ERROR: GET syntax is 'GET key'"
        }
        key := parts[1]
        db.mu.RLock() // 使用读锁,允许多个GET并发执行
        defer db.mu.RUnlock()
        value, ok := db.data[key]
        if !ok {
            return "NULL" // 类似于SQL中的NULL
        }
        return value

    case "DELETE":
        if len(parts) != 2 {
            return "ERROR: DELETE syntax is 'DELETE key'"
        }
        key := parts[1]
        db.mu.Lock()
        defer db.mu.Unlock()
        delete(db.data, key)
        return "OK"

    default:
        return fmt.Sprintf("ERROR: Unknown command '%s'", command)
    }
}

// handleConnection 处理单个客户端连接
func handleConnection(conn net.Conn, db *DB) {
    remoteAddr := conn.RemoteAddr().String()
    log.Printf("Client connected: %s", remoteAddr)
    defer conn.Close()
    defer log.Printf("Client disconnected: %s", remoteAddr)

    reader := bufio.NewReader(conn)

    for {
        // 读取客户端发送的命令,直到遇到换行符
        query, err := reader.ReadString('\n')
        if err != nil {
            if err != io.EOF {
                log.Printf("Error reading from client %s: %v", remoteAddr, err)
            }
            break // 客户端断开连接或发生错误
        }

        // 去除查询字符串两端的空白字符
        query = strings.TrimSpace(query)
        if query == "" {
            continue
        }
        log.Printf("Received query from %s: %s", remoteAddr, query)

        // 处理查询并获取结果
        result := db.processQuery(query)

        // 将结果发送回客户端,并添加换行符
        _, err = conn.Write([]byte(result + "\n"))
        if err != nil {
            log.Printf("Error writing to client %s: %v", remoteAddr, err)
            break
        }
    }
}

func main() {
    addr := "localhost:3307" // 使用一个不同于MySQL默认的端口
    listener, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatalf("Failed to listen on %s: %v", addr, err)
    }
    defer listener.Close()

    log.Printf("GoSQL server listening on %s", addr)

    // 创建数据库实例
    db := NewDB()

    for {
        // 接受新的客户端连接
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("Failed to accept connection: %v", err)
            continue
        }

        // 为每个连接创建一个新的goroutine来处理
        go handleConnection(conn, db)
    }
}

2. 客户端代码 (client/main.go)

客户端是一个简单的交互式命令行工具:

连接到服务器。

进入一个循环,读取用户在终端的输入。

将用户的输入作为查询发送给服务器。

读取服务器的响应并打印到终端。

// client/main.go
package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "strings"
)

func main() {
    addr := "localhost:3307"
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        log.Fatalf("Failed to connect to server at %s: %v", addr, err)
    }
    defer conn.Close()

    fmt.Printf("Connected to GoSQL server at %s\n", addr)
    fmt.Println("Enter commands (e.g., SET name Alice, GET name, DELETE name, or 'exit' to quit).")

    // 用于读取服务器响应的 reader
    serverReader := bufio.NewReader(conn)
    // 用于读取用户输入的 reader
    stdinReader := bufio.NewReader(os.Stdin)

    for {
        fmt.Print("gosql> ")

        // 读取用户输入
        input, err := stdinReader.ReadString('\n')
        if err != nil {
            log.Printf("Error reading from stdin: %v", err)
            break
        }

        input = strings.TrimSpace(input)
        if input == "" {
            continue
        }

        // 检查退出命令
        if strings.ToLower(input) == "exit" || strings.ToLower(input) == "quit" {
            fmt.Println("Bye!")
            break
        }

        // 将命令发送到服务器 (Fprintln 会自动添加换行符)
        _, err = fmt.Fprintln(conn, input)
        if err != nil {
            log.Printf("Failed to send command to server: %v", err)
            break
        }

        // 读取服务器的响应
        response, err := serverReader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                log.Println("Server closed the connection.")
            } else {
                log.Printf("Error reading from server: %v", err)
            }
            break
        }

        // 打印响应
        fmt.Print(response) // response 已经包含了换行符
    }
}

3. 如何运行

打开第一个终端,启动服务器:

# 导航到 server 目录
cd go-mysql-example/server

# 运行服务器
go run main.go

你应该会看到输出:

2023/10/27 10:30:00 GoSQL server listening on localhost:3307

打开第二个终端,启动客户端进行交互:

# 导航到 client 目录
cd go-mysql-example/client

# 运行客户端
go run main.go

你将看到客户端的欢迎信息和提示符:

Connected to GoSQL server at localhost:3307
Enter commands (e.g., SET name Alice, GET name, DELETE name, or 'exit' to quit).
gosql>

4. 交互示例

现在你可以在客户端终端中输入命令了:

gosql> SET user:1 Alice
OK
gosql> SET user:2 Bob
OK
gosql> GET user:1
Alice
gosql> GET user:2
Bob
gosql> GET user:3
NULL
gosql> SET user:1 AliceSmith
OK
gosql> GET user:1
AliceSmith
gosql> DELETE user:2
OK
gosql> GET user:2
NULL
gosql> UNKNOWN_COMMAND
ERROR: Unknown command 'UNKNOWN_COMMAND'
gosql> SET key_with_no_value
ERROR: SET syntax is 'SET key value'
gosql> exit
Bye!

在你与客户端交互时,服务器终端会打印出相应的日志,显示连接和接收到的查询:

2023/10/27 10:30:00 GoSQL server listening on localhost:3307
2023/10/27 10:31:15 Client connected: 127.0.0.1:54321
2023/10/27 10:31:20 Received query from 127.0.0.1:54321: SET user:1 Alice
2023/10/27 10:31:25 Received query from 127.0.0.1:54321: SET user:2 Bob
2023/10/27 10:31:30 Received query from 127.0.0.1:54321: GET user:1
...
2023/10/27 10:32:00 Client disconnected: 127.0.0.1:54321

总结与展望

这个例子完美地展示了使用 Go 构建一个支持自定义协议的网络服务的基本模式。它虽然简单,但包含了网络编程、并发处理、数据同步等关键概念。

可以扩展的方向:

持久化:将内存中的 map 定期写入文件,或在服务器启动时从文件加载,实现数据持久化。

更复杂的 SQL:引入一个真正的 SQL 解析库(如 vitess.io/vitess/go/vt/sqlparser),支持更复杂的 SELECT ... WHERE ...、INSERT INTO ... 等语句。

数据结构:用 map[string]map[string]string 来模拟 database -> table -> row 的结构。

事务支持:实现 BEGIN, COMMIT, ROLLBACK 命令。

认证:在客户端连接时要求提供用户名和密码。

网友回复

我知道答案,我要回答