在 Go 中實現(xiàn)一個支持并發(fā)的 TCP 服務(wù)端
僅用大約 65 行代碼,開發(fā)一個用于生成隨機數(shù)、支持并發(fā)的 TCP 服務(wù)端。
TCP 和 UDP 服務(wù)端隨處可見,它們基于 TCP/IP 協(xié)議棧,通過網(wǎng)絡(luò)為客戶端提供服務(wù)。在這篇文章中,我將介紹如何使用 ??Go 語言?? 開發(fā)一個用于返回隨機數(shù)、支持并發(fā)的 TCP 服務(wù)端。對于每一個來自 TCP 客戶端的連接,它都會啟動一個新的 goroutine(輕量級線程)來處理相應(yīng)的請求。
你可以在 GitHub 上找到本項目的源碼:??concTcp.go??。
處理 TCP 連接
這個程序的主要邏輯在 ??handleConnection()?
? 函數(shù)中,具體實現(xiàn)如下:
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 客戶端發(fā)送了一個 “STOP” 字符串,為它提供服務(wù)的 goroutine 就會終止;否則,TCP 服務(wù)端就會返回一個隨機數(shù)給它。只要客戶端不主動終止,服務(wù)端就會一直提供服務(wù),這是由 ??for?
? 循環(huán)保證的。具體來說,??for?
? 循環(huán)中的代碼使用了 ??bufio.NewReader(c).ReadString('\n')?
? 來逐行讀取客戶端發(fā)來的數(shù)據(jù),并使用 ??c.Write([]byte(string(result)))?
? 來返回數(shù)據(jù)(生成的隨機數(shù))。你可以在 Go 的 net 標(biāo)準(zhǔn)包 ??文檔?? 中了解更多。
支持并發(fā)
在 ??main()?
? 函數(shù)的實現(xiàn)部分,每當(dāng) TCP 服務(wù)端收到 TCP 客戶端的連接請求,它都會啟動一個新的 goroutine 來為這個請求提供服務(wù)。
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()?
? 確保程序至少有一個命令行參數(shù)。注意,現(xiàn)有代碼并沒有檢查這個參數(shù)是否為有效的 TCP 端口號。不過,如果它是一個無效的 TCP 端口號,??net.Listen()?
? 就會調(diào)用失敗,并返回一個錯誤信息,類似下面這樣:
$ 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()?
? 函數(shù)用于告訴 Go 接受網(wǎng)絡(luò)連接,因而承擔(dān)了服務(wù)端的角色。它的返回值類型是 ??net.Conn?
?,后者實現(xiàn)了 ??io.Reader?
? 和 ??io.Writer?
? 接口。此外,??main()?
? 函數(shù)中還調(diào)用了 ??rand.Seed()?
? 函數(shù),用于初始化隨機數(shù)生成器。最后,??for?
? 循環(huán)允許程序一直使用 ??Accept()?
? 函數(shù)來接受 TCP 客戶端的連接請求,并以 goroutine 的方式來運行 ??handleConnection(c)?
? 函數(shù),處理客戶端的后續(xù)請求。
net.Listen() 的第一個參數(shù)
??net.Listen()?
? 函數(shù)的第一個參數(shù)定義了使用的網(wǎng)絡(luò)類型,而第二個參數(shù)定義了服務(wù)端監(jiān)聽的地址和端口號。第一個參數(shù)的有效值為 ??tcp?
?、??tcp4?
?、??tcp6?
?、??udp?
?、??udp4?
?、??udp6?
?、??ip?
?、??ip4?
?、??ip6?
?、??Unix?
?(Unix 套接字)、??Unixgram?
? 和 ??Unixpacket?
?,其中:??tcp4?
?、??udp4?
? 和 ??ip4?
? 只接受 IPv4 地址,而 ??tcp6?
?、??udp6?
? 和 ??ip6?
? 只接受 IPv6 地址。
服務(wù)端并發(fā)測試
??concTCP.go?
? 需要一個命令行參數(shù),來指定監(jiān)聽的端口號。當(dāng)它開始服務(wù) TCP 客戶端時,你會得到類似下面的輸出:
$ go run concTCP.go 8001
Serving 127.0.0.1:62554
Serving 127.0.0.1:62556
??netstat?
? 的輸出可以確認(rèn) ??congTCP.go?
? 正在為多個 TCP 客戶端提供服務(wù),并且仍在繼續(xù)監(jiān)聽建立連接的請求:
$ 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
在上面輸出中,最后一行顯示了有一個進(jìn)程正在監(jiān)聽 8001 端口,這意味著你可以繼續(xù)連接 TCP 的 8001 端口。第一行和第二行顯示了有一個已建立的 TCP 網(wǎng)絡(luò)連接,它占用了 8001 和 62556 端口。相似地,第三行和第四行顯示了有另一個已建立的 TCP 連接,它占用了 8001 和 62554 端口。
下面這張圖片顯示了 ??concTCP.go?
? 在服務(wù)多個 TCP 客戶端時的輸出:
concTCP.go TCP 服務(wù)端測試
類似地,下面這張圖片顯示了兩個 TCP 客戶端的輸出(使用了 ??nc?
? 工具):
是用 nc 工具作為 concTCP.go 的 TCP 客戶端
你可以在 ??維基百科?? 上找到更多關(guān)于 ??nc?
?(即 ??netcat?
?)的信息。
總結(jié)
現(xiàn)在,你學(xué)會了如何用大約 65 行 Go 代碼來開發(fā)一個生成隨機數(shù)、支持并發(fā)的 TCP 服務(wù)端,這真是太棒了!如果你想要讓你的 TCP 服務(wù)端執(zhí)行別的任務(wù),只需要修改 ??handleConnection()?
? 函數(shù)即可。