範例:並行時鐘伺服器

2020-07-16 10:05:04
網路是一個自然使用並行的領域,因為伺服器通常一次處理很多來自用戶端的連線,每一個用戶端通常和其他用戶端保持獨立。本節介紹 net 包,它提供構建用戶端和伺服器程式的元件,這些程式通過 TCP、UDP 或者 UNIX 通訊端進行通訊。net/http 包就是在 net 包基礎上構建的。

【範例】順序時鐘伺服器,它以每秒鐘一次的頻率向用戶端傳送當前時間,程式碼如下所示:
// clock1 是一個定期報告時間的 TCP 伺服器
package main
import (
    "io"
    "log"
    "net"
    "time"
)
func main() {
    listener, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err) //例如,連線中止
            continue
        }
        handleConn(conn)   // 一次處理一個連線
    }
}
func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("15:04:05n"))
        if err != nil {
            return //例如,連線斷開
        }
        time.Sleep(1 * time.Second)
    }
}
Listen 函數建立一個 net.Listener 物件,它在一個網路埠上監聽進來的連線,這裡是 TCP 埠 localhost:8000。監聽器的 Accept 方法被阻塞,直到有連線請求進來,然後返回 net.Conn 物件來代表一個連線。

handleConn 函數處理一個完整的客戶連線。在迴圈裡,它將 time.Now() 獲取的當前時間傳送給用戶端。因為 net.Conn 滿足 io.Writer 介面,所以可以直接向它進行寫入。

當寫入失敗時迴圈結束,很多時候是用戶端斷開連線,這時 handleconn 函數使用延遲的 Close 呼叫關閉自己這邊的連線,然後繼續等待下一個連線請求。

time.Time.Format 方法提供了格式化日期和時間資訊的方式。它的引數是一個模板,指示如何格式化一個參考時間,具體如 Mon Jan 2 03:04:05PM 2006 UTC-0700 這樣的形式。參考時間有 8 個部分(本週第幾天、月、本月第幾天,等等)。

它們可以以任意的組合和對應數目的格式化字元出現在格式化模板中,所選擇的日期和時間將通過所選擇的格式進行顯示。這裡只使用時間的小時、分鐘和秒部分。time 包定義了許多標準時間格式的模板,如 time.RFC1123。相反,當解析一個代表時間的字串的時候使用相同的機制。

為了連線到伺服器,需要一個像 nc("netcat") 這樣的程式,以及一個用來操作網路連線的標準工具:

$ go build gopl.io/ch8/clockl
$ ./clock1 &
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C

用戶端顯示每秒從伺服器傳送的時間,直到使用 Control+C 快捷鍵中斷它,UNIX 系統 shell 上面回顯為 ^C。如果系統上沒有安裝 nc 或 netcat,可以使用 telnet 或者一個使用 net.Dial 實現的 Go 版的 netcat 來連線 TCP 伺服器:
// netcat1是一個唯讀的 TCP 用戶端程式
package main
import (
    "io"
    "log"
    "net"
    "os"
)
func main() {
    conn, err : = net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    mustCopy(os.Stdout, conn)
}
func mustCopy(dst io.Writer, src io.Reader) {
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}
這個程式從網路連線中讀取,然後寫到標準輸出,直到到達 EOF 或者岀錯。mustCopy 函數是這一節的多個例子中使用的一個實用程式。在不同的終端上同時執行兩個用戶端,一個顯示在左邊,一個在右邊:

$ go build gopl.io/ch8/netcat1
$ ./netcat1                 
13:58:54                        $ ./netcat1
13:58:55                    
13:58:56                    
^C                          
                                    13:58:57
                                    13:58:58
                                    13:58:59
                                    ^C

$ killall clock1

killall 命令是 UNIX 的一個實用程式,用來終止所有指定名字的進程。

第二個用戶端必須等到第一個結束才能正常工作,因為伺服器是順序的,一次只能處理一個客戶請求。讓伺服器支援並行只需要一個很小的改變:在呼叫 handleconn 的地方新增一個 go 關鍵字,使它在自己的 goroutine 內執行。
for {
    conn, err := listener.Accept()
    if err != nil {
        log.Print(err) //例如,連線中止
        continue
    }
    go handleConn(conn) // 並行處理連線
}
現在,多個用戶端可以同時接收到時間:

$ go build gopl.io/ch8/clock2
$ ./clock2 &
$ go build gopl.io/ch8/netcat1
$ ./netcat1
14:02:54                       $ ./netcat1
14:02:55                       14:02:55
14:02:56                       14:02:56
14:02:57                       ^C
14:02:58                  
14:02:59                       $ ./netcat1
14:03:00                       14:03:00
14:03:01                       14:03:01
^C                                14:03:02
                                     ^C
$ killall clock2