## 8.10. 示例: 聊天服務
我們用一個聊天服務器來終結本章節的內容,這個程序可以讓一些用戶通過服務器向其它所有用戶廣播文本消息。這個程序中有四種goroutine。main和broadcaster各自是一個goroutine實例,每一個客戶端的連接都會有一個handleConn和clientWriter的goroutine。broadcaster是select用法的不錯的樣例,因為它需要處理三種不同類型的消息。
下面演示的main goroutine的工作,是listen和accept(譯注:網絡編程里的概念)從客戶端過來的連接。對每一個連接,程序都會建立一個新的handleConn的goroutine,就像我們在本章開頭的并發的echo服務器里所做的那樣。
<u><i>gopl.io/ch8/chat</i></u>
```go
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
go handleConn(conn)
}
}
```
然后是broadcaster的goroutine。他的內部變量clients會記錄當前建立連接的客戶端集合。其記錄的內容是每一個客戶端的消息發出channel的"資格"信息。
```go
type client chan<- string // an outgoing message channel
var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // all incoming client messages
)
func broadcaster() {
clients := make(map[client]bool) // all connected clients
for {
select {
case msg := <-messages:
// Broadcast incoming message to all
// clients' outgoing message channels.
for cli := range clients {
cli <- msg
}
case cli := <-entering:
clients[cli] = true
case cli := <-leaving:
delete(clients, cli)
close(cli)
}
}
}
```
broadcaster監聽來自全局的entering和leaving的channel來獲知客戶端的到來和離開事件。當其接收到其中的一個事件時,會更新clients集合,當該事件是離開行為時,它會關閉客戶端的消息發送channel。broadcaster也會監聽全局的消息channel,所有的客戶端都會向這個channel中發送消息。當broadcaster接收到什么消息時,就會將其廣播至所有連接到服務端的客戶端。
現在讓我們看看每一個客戶端的goroutine。handleConn函數會為它的客戶端創建一個消息發送channel并通過entering channel來通知客戶端的到來。然后它會讀取客戶端發來的每一行文本,并通過全局的消息channel來將這些文本發送出去,并為每條消息帶上發送者的前綴來標明消息身份。當客戶端發送完畢后,handleConn會通過leaving這個channel來通知客戶端的離開并關閉連接。
```go
func handleConn(conn net.Conn) {
ch := make(chan string) // outgoing client messages
go clientWriter(conn, ch)
who := conn.RemoteAddr().String()
ch <- "You are " + who
messages <- who + " has arrived"
entering <- ch
input := bufio.NewScanner(conn)
for input.Scan() {
messages <- who + ": " + input.Text()
}
// NOTE: ignoring potential errors from input.Err()
leaving <- ch
messages <- who + " has left"
conn.Close()
}
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
}
}
```
另外,handleConn為每一個客戶端創建了一個clientWriter的goroutine,用來接收向客戶端發送消息的channel中的廣播消息,并將它們寫入到客戶端的網絡連接。客戶端的讀取循環會在broadcaster接收到leaving通知并關閉了channel后終止。
下面演示的是當服務器有兩個活動的客戶端連接,并且在兩個窗口中運行的情況,使用netcat來聊天:
```
$ go build gopl.io/ch8/chat
$ go build gopl.io/ch8/netcat3
$ ./chat &
$ ./netcat3
You are 127.0.0.1:64208 $ ./netcat3
127.0.0.1:64211 has arrived You are 127.0.0.1:64211
Hi!
127.0.0.1:64208: Hi! 127.0.0.1:64208: Hi!
Hi yourself.
127.0.0.1:64211: Hi yourself. 127.0.0.1:64211: Hi yourself.
^C
127.0.0.1:64208 has left
$ ./netcat3
You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
Welcome.
127.0.0.1:64211: Welcome. 127.0.0.1:64211: Welcome.
^C
127.0.0.1:64211 has left”
```
當與n個客戶端保持聊天session時,這個程序會有2n+2個并發的goroutine,然而這個程序卻并不需要顯式的鎖(§9.2)。clients這個map被限制在了一個獨立的goroutine中,broadcaster,所以它不能被并發地訪問。多個goroutine共享的變量只有這些channel和net.Conn的實例,兩個東西都是并發安全的。我們會在下一章中更多地講解約束,并發安全以及goroutine中共享變量的含義。
**練習 8.12:** 使broadcaster能夠將arrival事件通知當前所有的客戶端。這需要你在clients集合中,以及entering和leaving的channel中記錄客戶端的名字。
**練習 8.13:** 使聊天服務器能夠斷開空閑的客戶端連接,比如最近五分鐘之后沒有發送任何消息的那些客戶端。提示:可以在其它goroutine中調用conn.Close()來解除Read調用,就像input.Scanner()所做的那樣。
**練習 8.14:** 修改聊天服務器的網絡協議,這樣每一個客戶端就可以在entering時提供他們的名字。將消息前綴由之前的網絡地址改為這個名字。
**練習 8.15:** 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。修改broadcaster來跳過一條消息,而不是等待這個客戶端一直到其準備好讀寫。或者為每一個客戶端的消息發送channel建立緩沖區,這樣大部分的消息便不會被丟掉;broadcaster應該用一個非阻塞的send向這個channel中發消息。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言