并发 TCP 服务器

在这节,您将学习使用 goroutines 开放一个并发 TCP 服务器。TCP 服务器将给每个接入的连接开启一个新的 goroutines 来处理请求。一个并发 TCP 服务器可以接收更多请求,能同时为多个客户端提供服务。

这个 TCP 并发服务器的工作是接收一个正整数并从斐波纳切序列返回一个自然数。如果输入发送错误,则返回值为-1。由于斐波纳切序列数的计算慢,我们将使用曾首次在第11章出现的一个算法,代码测试,优化和分析都包含在 benchmarkMe.go 文件中。另外,这次用到的算法将会详细的讲解。

我们把程序命名为 fiboTCP.go,并把代码分成五部分。由于把 web 服务的端口号定义为命令行参数被认为是良好的实现方式,这次 fiboTCP.go 将完全按此来做。

fiboTCP.go 的第一部分如下:

  1. package main
  2. import(
  3. "bufio"
  4. "fmt"
  5. "net"
  6. "os"
  7. "strconv"
  8. "strings"
  9. "time"
  10. )

fiboTCP.go 的第二部分如下:

  1. func f(n int) int {
  2. fn := make(map[int]int)
  3. for i := 0; i <= n; i++ {
  4. var f int
  5. if i <= 2 {
  6. f = 1
  7. } else {
  8. f = fn[i-1] + fn[i-2]
  9. }
  10. fn[i] = f
  11. }
  12. return fn[n]
  13. }

上面这段代码,您能看到 f() 函数实现了斐波纳切序列自然数的生成。一开始看这个算法很难理解,但它非常有效且运行速度也很快。首先,f() 函数使用了一个被命名为 fn 的字典,这在计算斐波纳切序列数时很不寻常。第二,f() 函数使用了一个 for 循环,这也相当不寻常。最后,f() 函数没有使用递归,这也是它执行快的主要原因。

f() 函数中用到的算法思想使用了动态规划技巧,每当一个斐波纳切数被计算后,就把它放入 fn 字典中,这样它就不会再被计算了。这个简单的想法节省了很多时间,特别是需要计算较大斐波纳切数时,因为您不必对相同的斐波纳切数计算多次。

fiboTCP.go 的第三部分如下:

  1. func handleConnection(c net.Conn) {
  2. for {
  3. netData, err := bufio.NewReader(c).ReadString('\n')
  4. if err != nil {
  5. fmt.Println(err)
  6. os.Exit(100)
  7. }
  8. temp := strings.TrimSpace(string(netData))
  9. if temp == "STOP" {
  10. break
  11. }
  12. fibo := "-1\n"
  13. n, err := strconv.Atoi(temp)
  14. if err == nil {
  15. fibo = strconv.Itoa(f(n)) + "\n"
  16. }
  17. c.Write([]byte(string(fibo)))
  18. }
  19. time.Sleep(5 * time.Second)
  20. c.Close()
  21. }

handleConnection() 函数处理并发 TCP 服务器的每个客户端。

fiboTCP.go的第四部分如下:

  1. func main() {
  2. arguments := os.Args
  3. if len(arguments) == 1 {
  4. fmt.Println("Please provide a port number!")
  5. return
  6. }
  7. PORT := ":" + arguments[1]
  8. l, err := net.Listen("tcp4", PORT)
  9. if err != nil {
  10. fmt.Println(err)
  11. return
  12. }
  13. defer l.Close()

fiboTCP.go的剩余代码如下:

  1. for {
  2. c, err := l.Accept()
  3. if err != nil {
  4. fmt.Println(err)
  5. return
  6. }
  7. go handleConnection(c)
  8. }
  9. }

go handleConnection(c) 声明实现了程序的并发性,每次连接一个新 TCP 客户端时它就开启一个新的 goroutine。goroutine 被并发执行,使服务器有机会服务更多的客户端。

执行 fiboTCP.go 并使用 netcat(l)TCPclient.go 在俩个不同的终端窗口和它交互,输出如下:

  1. $ go run fiboTCP.go 9000
  2. n: 10
  3. fibo: 55
  4. n: 0
  5. fibo: 1
  6. n: -1
  7. fibo: 0
  8. n: 100
  9. fibo: 3736710778780434371
  10. n: 12
  11. fibo: 144
  12. n: 12
  13. fibo: 144

TCPclient.go 这边的输出如下:

  1. $ go run TCPclient.go localhost:9000
  2. >> 12
  3. ->: 144
  4. >> a
  5. ->: -1
  6. >> STOP
  7. ->: TCP client exiting...

netcat(l) 这边的输出如下:

  1. $ nc localhost 9000
  2. 10
  3. 55
  4. 0
  5. 1
  6. -1
  7. 0
  8. 100
  9. 3736710778780434371
  10. ads
  11. -1
  12. STOP

当您发送 STOP 字符串给服务进程时。为指定 TCP 客户端服务的 goroutine 将终止,这将引起连接关闭。

最后,值得关注的是俩个客户端被同时提供服务,这可以通过下面的命令输出来验证:

  1. $ netstat -anp TCP | grep 9000
  2. tcp4 0 0 127.0.0.1 9000 127.0.0.1.57309 ESTABLISHED
  3. tcp4 0 0 127.0.0.1.57309 127.0.0.1.9000 ESTABLISHED
  4. tcp4 0 0 127.0.0.1 9000 127.0.0.1.57305 ESTABLISHED
  5. tcp4 0 0 127.0.0.1 57305 127.0.0.1.9000 ESTABLISHED
  6. tcp4 0 0 *.9000 *.* LISTEN

上面命令的最后一行输出告诉我们有一个进程在监听 9000 端口,这意味着您仍能连接 9000 端口。输出的头俩行显示有个客户端使用 57309 端口与服务进程通信。第三和第四行证明有另一个客户端连接到监听 9000 端口的服务上。这个客户端使用的 TCP 端口是 57035