Goroutines和Channels(二)
網絡編程是並發大顯身手的一個領域,由於服務器是最典型的需要同時處理很多連接的程序,這些連接一般來自於彼此獨立的客戶端。
本小節,我們會講解go語言的net包,這個包提供編寫一個網絡客戶端或者服務器程序的基本組件,通信可以是使用TCP,UDP或者Unix domain sockets。
我們的第一個例子是一個順序執行的時鐘服務器,它會每隔一秒鐘將當前時間寫到客戶端:
// 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連接:
// 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。
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
Goroutines和Channels(二)