在 Go 中實現一個支援併發的 TCP 服務端

語言: CN / TW / HK

僅用大約 65 行程式碼,開發一個用於生成隨機數、支援併發的 TCP 服務端。

TCP 和 UDP 服務端隨處可見,它們基於 TCP/IP 協議棧,通過網路為客戶端提供服務。在這篇文章中,我將介紹如何使用 Go 語言 開發一個用於返回隨機數、支援併發的 TCP 服務端。對於每一個來自 TCP 客戶端的連線,它都會啟動一個新的 goroutine(輕量級執行緒)來處理相應的請求。

你可以在 GitHub 上找到本專案的原始碼: concTcp.go

處理 TCP 連線

這個程式的主要邏輯在 handleConnection() 函式中,具體實現如下:

func handleConnection(c net.Conn) {
        fmt.Printf("Serving %s\n", c.RemoteAddr().String())
        for {
                netData, err := bufio.NewReader(c).ReadString('\n')
                if err != nil {
                        fmt.Println(err)
                        return
                }

                temp := strings.TrimSpace(string(netData))
                if temp == "STOP" {
                        break
                }

                result := strconv.Itoa(random()) + "\n"
                c.Write([]byte(string(result)))
        }
        c.Close()
}

如果 TCP 客戶端傳送了一個 “STOP” 字串,為它提供服務的 goroutine 就會終止;否則,TCP 服務端就會返回一個隨機數給它。只要客戶端不主動終止,服務端就會一直提供服務,這是由 for 迴圈保證的。具體來說, for 迴圈中的程式碼使用了  bufio.NewReader(c).ReadString('\n') 來逐行讀取客戶端發來的資料,並使用  c.Write([]byte(string(result))) 來返回資料(生成的隨機數)。你可以在 Go 的 net 標準包  文件 中瞭解更多。

支援併發

main() 函式的實現部分,每當 TCP 服務端收到 TCP 客戶端的連線請求,它都會啟動一個新的 goroutine 來為這個請求提供服務。

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

        PORT := ":" + arguments[1]
        l, err := net.Listen("tcp4", PORT)
        if err != nil {
                fmt.Println(err)
                return
        }
        defer l.Close()
        rand.Seed(time.Now().Unix())

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

首先, main() 確保程式至少有一個命令列引數。注意,現有程式碼並沒有檢查這個引數是否為有效的 TCP 埠號。不過,如果它是一個無效的 TCP 埠號, net.Listen() 就會呼叫失敗,並返回一個錯誤資訊,類似下面這樣:

$ go run concTCP.go 12a
listen tcp4: lookup tcp4/12a: nodename nor servname provided, or not known
$ go run concTCP.go -10
listen tcp4: address -10: invalid port

net.Listen() 函式用於告訴 Go 接受網路連線,因而承擔了服務端的角色。它的返回值型別是  net.Conn ,後者實現了  io.Reader 和  io.Writer 介面。此外, main() 函式中還呼叫了  rand.Seed() 函式,用於初始化隨機數生成器。最後, for 迴圈允許程式一直使用  Accept() 函式來接受 TCP 客戶端的連線請求,並以 goroutine 的方式來執行  handleConnection(c) 函式,處理客戶端的後續請求。

net.Listen() 的第一個引數

net.Listen() 函式的第一個引數定義了使用的網路型別,而第二個引數定義了服務端監聽的地址和埠號。第一個引數的有效值為  tcptcp4tcp6udpudp4udp6ipip4ip6Unix (Unix 套接字)、 Unixgram 和  Unixpacket ,其中: tcp4udp4 和  ip4 只接受 IPv4 地址,而  tcp6udp6 和  ip6 只接受 IPv6 地址。

服務端併發測試

concTCP.go 需要一個命令列引數,來指定監聽的埠號。當它開始服務 TCP 客戶端時,你會得到類似下面的輸出:

$ go run concTCP.go 8001
Serving 127.0.0.1:62554
Serving 127.0.0.1:62556

netstat 的輸出可以確認  congTCP.go 正在為多個 TCP 客戶端提供服務,並且仍在繼續監聽建立連線的請求:

$ netstat -anp TCP | grep 8001
tcp4       0      0  127.0.0.1.8001         127.0.0.1.62556        ESTABLISHED
tcp4       0      0  127.0.0.1.62556        127.0.0.1.8001         ESTABLISHED
tcp4       0      0  127.0.0.1.8001         127.0.0.1.62554        ESTABLISHED
tcp4       0      0  127.0.0.1.62554        127.0.0.1.8001         ESTABLISHED
tcp4       0      0  *.8001                 *.*                    LISTEN

在上面輸出中,最後一行顯示了有一個程序正在監聽 8001 埠,這意味著你可以繼續連線 TCP 的 8001 埠。第一行和第二行顯示了有一個已建立的 TCP 網路連線,它佔用了 8001 和 62556 埠。相似地,第三行和第四行顯示了有另一個已建立的 TCP 連線,它佔用了 8001 和 62554 埠。

下面這張圖片顯示了 concTCP.go 在服務多個 TCP 客戶端時的輸出:

類似地,下面這張圖片顯示了兩個 TCP 客戶端的輸出(使用了 nc 工具):

你可以在 維基百科 上找到更多關於  nc (即  netcat )的資訊。

總結

現在,你學會了如何用大約 65 行 Go 程式碼來開發一個生成隨機數、支援併發的 TCP 服務端,這真是太棒了!如果你想要讓你的 TCP 服務端執行別的任務,只需要修改 handleConnection() 函式即可。

via: https://opensource.com/article/18/5/building-concurrent-tcp-server-go

作者: Mihalis Tsoukalos 選題: lkxed 譯者: lkxed 校對: wxy

本文由 LCTT 原創編譯,Linux中國榮譽推出