## 8.2. 示例: 併發的Clock服務

網絡編程是併發大顯身手的一個領域，由於服務器是最典型的需要同時處理很多連接的程序，這些連接一般來自遠彼此獨立的客戶端。在本小節中，我們會講解go語言的net包，這個包提供編寫一個網絡客戶端或者服務器程序的基本組件，無論兩者間通信是使用TCP，UDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包里的方法，也算是net包的一部分。

我們的第一個例子是一個順序執行的時鐘服務器，它會每隔一秒鐘將當前時間寫到客戶端：

```go
gopl.io/ch8/clock1
// Clock1 is a TCP server that periodically writes the time.
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) // e.g., connection aborted
            continue
        }
        handleConn(conn) // handle one connection at a time
    }
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
        if err != nil {
            return // e.g., client disconnected
        }
        time.Sleep(1 * time.Second)
    }
}

```

Listen函數創建了一個net.Listener的對象，這個對象會監聽一個網絡端口上到來的連接，在這個例子里我們用的是TCP的localhost:8000端口。listener對象的Accept方法會直接阻塞，直到一個新的連接被創建，然後會返迴一個net.Conn對象來表示這個連接。

handleConn函數會處理一個完整的客戶端連接。在一個for死循環中，將當前的時候用time.Now()函數得到，然後寫到客戶端。由於net.Conn實現了io.Writer接口，我們可以直接向其寫入內容。這個死循環會一直執行，直到寫入失敗。最可能的原因是客戶端主動斷開連接。這種情況下handleConn函數會用defer調用關閉服務器側的連接，然後返迴到主函數，繼續等待下一個連接請求。

time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間，而這個格式化模闆限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾，月份，一個月的第幾天，等等)。可以以任意的形式來組合前面這個模闆；出現在模闆中的部分會作爲參考來對時間格式進行輸出。在上面的例子中我們隻用到了小時、分鐘和秒。time包里定義了很多標準時間格式，比如time.RFC1123。在進行格式化的逆向操作time.Parse時，也會用到同樣的策略。(譯註：這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700，而不像其它語言那樣Y-m-d H:i:s一樣，當然了這里可以用1234567的方式來記憶，倒是也不麻煩)

爲了連接例子里的服務器，我們需要一個客戶端程序，比如netcat這個工具(nc命令)，這個工具可以用來執行網絡連接操作。

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

客戶端將服務器發來的時間顯示了出來，我們用Control+C來中斷客戶端的執行，在Unix繫統上，你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具，你可以用telnet來實現同樣的效果，或者也可以用我們下面的這個用go寫的簡單的telnet程序，用net.Dial就可以簡單地創建一個TCP連接：

```go
gopl.io/ch8/netcat1
// Netcat1 is a read-only TCP client.
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)
    }
}
```

這個程序會從連接中讀取數據，併將讀到的內容寫到標準輸出中，直到遇到end of file的條件或者發生錯誤。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關鍵字，讓每一次handleConn的調用都進入一個獨立的goroutine。

```go
gopl.io/ch8/clock2
for {
    conn, err := listener.Accept()
    if err != nil {
        log.Print(err) // e.g., connection aborted
        continue
    }
    go handleConn(conn) // handle connections concurrently
}

```

現在多個客戶端可以同時接收到時間了：

```
$ 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
```

練習8.1: 脩改clock2來支持傳入參數作爲端口號，然後寫一個clockwall的程序，這個程序可以同時與多個clock服務器通信，從多服務器中讀取時間，併且在一個表格中一次顯示所有服務傳迴的結果，類似於你在某些辦公室里看到的時鐘牆。如果你有地理學上分布式的服務器可以用的話，讓這些服務器跑在不同的機器上面；或者在同一台機器上跑多個不同的實例，這些實例監聽不同的端口，假裝自己在不同的時區。像下面這樣：

```
$ TZ=US/Eastern    ./clock2 -port 8010 &
$ TZ=Asia/Tokyo    ./clock2 -port 8020 &
$ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
```

練習8.2: 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令，比如cd命令來切換目録，ls來列出目録內文件，get和send來傳輸文件，close來關閉連接。你可以用標準的ftp命令來作爲客戶端，或者也可以自己實現一個。
