简洁的并发TCP服务器

尽管上节的并发 TCP 服务器运作良好,但是它还不能为实际应用提供服务。因此,在这节您将学到怎样把第四章中的 keyValue.go 文件对于复杂类型的使用转化为一个功能齐全的并发 TCP 应用。

为了能够和网络中的 key-value 存储交互,我们创建自定义的 TCP 协议。您将需要为 key-value 存储的每一个函数定义关键字。为简单起见,每个关键字都跟着相关数据。大多数命令的结果将是成功或失败消息。

设计您自己的 TCP 或 UDP 协议不是一个简单的工作。这意味着设计一个新协议时,您必须特别细致小心。这里的关键是在您开始编写生产代码之前文档化所有内容。

这个主题使用的工具命名为 kvTCP.go,它被分为六个部分。

kvTCP.go 的第一部分如下:

package main

import (
    "bufio"
    "encoding/gob"
    "fmt"
    "net"
    "os"
    "strings"
)

type myElement struct {
    Name string
    Surname string
    Id string
}

const welcome = "Welcome to the Key-value store!\n"

var DATA = make(map[string]myElement)
var DATAFILE = "/tmp/dataFile.gob"

kvTCP.go 的第二部分如下:

func handleConnection(c net.Conn) {
    c.Write([]byte(welcome))
    for {
        netData, err := bufio.NewReader(c).ReadString('\n')
        if err != nil {
            fmt.Println(err)
            return
        }
        command := strings.TrimSpace(string(netData))
        tokens := strings.Fields(command)
        switch len(tokens) {
        case 0:
            continue
        case 1:
            tokens = append(tokens, "")
            tokens = append(tokens, "")
            tokens = append(tokens, "")
            tokens = append(tokens, "")
        case 2:
            tokens = append(tokens, "")
            tokens = append(tokens, "")
            tokens = append(tokens, "")
        case 3:
            tokens = append(tokens, "")
            tokens = append(tokens, "")
        case 4:
            tokens = append(tokens, "")
        }

        switch tokens[0] {
        case "STOP":
            err = save()
            if err != nil {
                fmt.Println(err)
            }
            c.Close()
            return
        case "PRINT":
            PRINT(c)
        case "DELETE":
            if !DELETE(tokens[1]) {
                netData := "Delete operation failed!\n"
                c.Write([]byte(netData))
            }else{
                netData := "Delete operation successful!\n"
                c.Write([]byte(netData))
            }
        case "ADD":
            n := myElement{tokens[2], tokens[3], tokens[4]}
            if !ADD(tokens[1], n) {
                netData := "Add operation failed!\n"
                c.Write([]byte(netData))
            } else {
                netData := "Add operation successful!\n"
                c.Write([]byte(netData))
            }
            err = save()
            if err != nil {
                fmt.Println(err)
            }
        case "LOOKUP":
            n := LOOKUP(tokens[1])
            if n != nil {
                netData := fmt.Sprintf("%v\n", *n)
                c.Write([]byte(netData))
            } else {
                netData := "Did not find key!\n"
                c.Write([]byte(netData))
            }
        case "CHANGE":
            n := myElement{tokens[2], tokens[3], tokens[4]}
            if !CHANGE(tokens[1], n) {
                netData := "Update operation failed!\n"
                c.Write([]byte(netData))
            } else {
                netData := "Update operation successful!\n"
                c.Write([]byte(netData))
            }
            err = save()
            if err != nil {
                fmt.Println(err)
            }
        default:
            netData := "Unknown command - please try again!\n"
            c.Write([]byte(netData))
        }
    }
}

handleConnection() 函数和每个 TCP 客户端交互并解析客户端的输入。

kvTCP.go 的第三部分包含如下代码:

func save() error {
    fmt.Println("Saving", DATAFILE)
    err := os.Remove(DATAFILE)
    if err != nil {
        fmt.Println(err)
    }

    saveTo, err := os.Create(DATAFILE)
    if err != nil {
        fmt.Println("Cannot create", DATAFILE)
        return err
    }
    defer saveTo.Close()

    encoder := gob.NewEncoder(saveTo)
    err = encoder.Encode(DATA)
    if err != nil {
        fmt.Println("Cannot save to", DATAFILE)
        return err
    }
    return nil
}

func load() error {
    fmt.Println("Loading", DATAFILE)
    loadFrom, err := os.Open(DATAFILE)
    defer loadFrom.Close()
    if err != nil {
        fmt.Println("Empty key/value store!")
        return err
    }

    decoder := gob.NewDecoder(loadFrom)
    decoder.Decode(&DATA)
    return nil
}

kvTCP.go 的第四段如下:

func ADD(k string, n myElement) bool {
    if k == "" {
        return false
    }

    if LOOKUP(k) == nil {
        DATA[k] = n
        return true
    }
    return false
}

func DELETE(k string) bool {
    if LOOKUP(k) != nil {
        delete(DATA, k)
        return true
    }
    return false
}

func LOOKUP(k string) *myElement {
    _, ok := DATA[k]
    if ok {
        n := DATA[k]
        return &n
    } else {
        return nil
    }
}

func CHANGE(k string, n myElement) bool {
    DATA[k] = n
    return true
}

上面的这些函数实现与 keyValue.go 一样。它们没有直接和 TCP 客户端交互。

kvTCP.go 的第五部分包含代码如下:

func PRINT(c net.Conn) {
    for k, d := range DATA {
        netData := fmt.Sprintf("key: %s value: %v\n", k, d)
        c.Write([]byte(netData))
    }
}

PRINT() 函数直接发送数据给 TCP 客户端,一次一行。

这个程序的剩余代码如下:

func main() {
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Please provide a port number!")
        return
    }

    PORT := ":" + arguments[1]
    l, err := net.Listen("tcp", PORT)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer l.Close()

    err = load()
    if err != nil {
        fmt.Println(err)
    }

    for {
        c, err := l.Accept()
        if err != nil {
            fmt.Println(err)
            os.Exit(100)
        }
        go handleConnection(c)
    }
}

执行 kvTCP.go 将产生如下输出:

$ go run kvTCP.go 9000
Loading /tmp/dataFile.gob
Empty key/value store!
open /tmp/dataFile.gob: no such file or directory
Saving /tmp/dataFile.gob
remove /tmp/dataFile.gob: no such file or directory
Saving /tmp/dataFile.gob
Saving /tmp/dataFile.gob

为了这节的目的,netcat(l) 工具用来作为 kvTCP.go 的客户端:

$ nc localhost 9000
Welcome to the Key-value store!
PRINT
LOOKUP 1
Did not find key!
ADD 1 2 3 4
Add operation successful!
LOOKUP 1
{2 3 4}
ADD 4 -1 -2 -3
Add operation successful!
PRINT
key: 1 value: {2 3 4}
key: 4 value: {-1 -2 -3}
STOP

kvTCP.go 文件是一个使用 goroutines 的并发应用,它能够同时服务多个 TCP 客户端。然而,所有的 TCP 客户端共享相同的数据!

最后编辑: kuteng  文档更新时间: 2021-03-27 20:14   作者:kuteng