简洁的并发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!
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!
key: 1 value: {2 3 4}
key: 4 value: {-1 -2 -3}
STOP
kvTCP.go
文件是一个使用 goroutines 的并发应用,它能够同时服务多个 TCP 客户端。然而,所有的 TCP 客户端共享相同的数据!