十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊
量身定制 + 運(yùn)營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
本節(jié)將帶領(lǐng)大家結(jié)合咱們前面所學(xué)的知識開發(fā)一個聊天的示例程序,它可以在幾個用戶之間相互廣播文本消息。

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項目有:主機(jī)域名、網(wǎng)站空間、營銷軟件、網(wǎng)站建設(shè)、萊西網(wǎng)站維護(hù)、網(wǎng)站推廣。
服務(wù)端程序中包含 4 個 goroutine,分別是一個主 goroutine 和廣播(broadcaster)goroutine,每一個連接里面又包含一個連接處理(handleConn)goroutine 和一個客戶寫入(clientwriter)goroutine。
廣播器(broadcaster)是用于如何使用 select 的一個規(guī)范說明,因為它需要對三種不同的消息進(jìn)行響應(yīng)。
主 goroutine 的工作是監(jiān)聽端口,接受連接客戶端的網(wǎng)絡(luò)連接,對每一個連接,它將創(chuàng)建一個新的 handleConn goroutine。
完整的示例代碼如下所示:
package main
import (
"bufio"
"fmt"
"log"
"net"
)
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)
}
}
type client chan<- string // 對外發(fā)送消息的通道
var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // 所有連接的客戶端
)
func broadcaster() {
clients := make(map[client]bool)
for {
select {
case msg := <-messages:
// 把所有接收到的消息廣播給所有客戶端
// 發(fā)送消息通道
for cli := range clients {
cli <- msg
}
case cli := <-entering:
clients[cli] = true
case cli := <-leaving:
delete(clients, cli)
close(cli)
}
}
}
func handleConn(conn net.Conn) {
ch := make(chan string) // 對外發(fā)送客戶消息的通道
go clientWriter(conn, ch)
who := conn.RemoteAddr().String()
ch <- "歡迎 " + who
messages <- who + " 上線"
entering <- ch
input := bufio.NewScanner(conn)
for input.Scan() {
messages <- who + ": " + input.Text()
}
// 注意:忽略 input.Err() 中可能的錯誤
leaving <- ch
messages <- who + " 下線"
conn.Close()
}
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg) // 注意:忽略網(wǎng)絡(luò)層面的錯誤
}
}代碼中 main 函數(shù)里面寫的代碼非常簡單,其實服務(wù)器要做的事情總結(jié)一下無非就是獲得 listener 對象,然后不停的獲取鏈接上來的 conn 對象,最后把這些對象丟給處理鏈接函數(shù)去進(jìn)行處理。
在使用 handleConn 方法處理 conn 對象的時候,對不同的鏈接都啟一個 goroutine 去并發(fā)處理每個 conn 這樣則無需等待。
由于要給所有在線的用戶發(fā)送消息,而不同用戶的 conn 對象都在不同的 goroutine 里面,但是Go語言中有 channel 來處理各不同 goroutine 之間的消息傳遞,所以在這里我們選擇使用 channel 在各不同的 goroutine 中傳遞廣播消息。
下面來介紹一下 broadcaster 廣播器,它使用局部變量 clients 來記錄當(dāng)前連接的客戶集合,每個客戶唯一被記錄的信息是其對外發(fā)送消息通道的 ID,下面是細(xì)節(jié):
type client chan<- string // 對外發(fā)送消息的通道
var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // 所有連接的客戶端
)
func broadcaster() {
clients := make(map[client]bool)
for {
select {
case msg := <-messages:
// 把所有接收到的消息廣播給所有客戶端
// 發(fā)送消息通道
for cli := range clients {
cli <- msg
}
case cli := <-entering:
clients[cli] = true
case cli := <-leaving:
delete(clients, cli)
close(cli)
}
}
}在 main 函數(shù)里面使用 goroutine 開啟了一個 broadcaster 函數(shù)來負(fù)責(zé)廣播所有用戶發(fā)送的消息。
這里使用一個字典來保存用戶 clients,字典的 key 是各連接申明的單向并發(fā)隊列。
使用一個 select 開啟一個多路復(fù)用:
下面再來看一下每個客戶自己的 goroutine。
handleConn 函數(shù)創(chuàng)建一個對外發(fā)送消息的新通道,然后通過 entering 通道通知廣播者新客戶到來,接著它讀取客戶發(fā)來的每一行文本,通過全局接收消息通道將每一行發(fā)送給廣播者,發(fā)送時在每條消息前面加上發(fā)送者 ID 作為前綴。一旦從客戶端讀取完畢消息,handleConn 通過 leaving 通道通知客戶離開,然后關(guān)閉連接。
func handleConn(conn net.Conn) {
ch := make(chan string) // 對外發(fā)送客戶消息的通道
go clientWriter(conn, ch)
who := conn.RemoteAddr().String()
ch <- "歡迎 " + who
messages <- who + " 上線"
entering <- ch
input := bufio.NewScanner(conn)
for input.Scan() {
messages <- who + ": " + input.Text()
}
// 注意:忽略 input.Err() 中可能的錯誤
leaving <- ch
messages <- who + " 下線"
conn.Close()
}
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg) // 注意:忽略網(wǎng)絡(luò)層面的錯誤
}
}handleConn 函數(shù)會為每個過來處理的 conn 都創(chuàng)建一個新的 channel,開啟一個新的 goroutine 去把發(fā)送給這個 channel 的消息寫進(jìn) conn。
handleConn 函數(shù)的執(zhí)行過程可以簡單總結(jié)為如下幾個步驟:
前面對服務(wù)端做了簡單的介紹,下面介紹客戶端,這里將其命名為“netcat.go”,完整代碼如下所示:
// netcat 是一個簡單的TCP服務(wù)器讀/寫客戶端
package main
import (
"io"
"log"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // 注意:忽略錯誤
log.Println("done")
done <- struct{}{} // 向主Goroutine發(fā)出信號
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done // 等待后臺goroutine完成
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
} 當(dāng)有 n 個客戶 session 在連接的時候,程序并發(fā)運(yùn)行著2n+2 個相互通信的 goroutine,它不需要隱式的加鎖操作。clients map 限制在廣播器這一個 goroutine 中被訪問,所以不會并發(fā)訪問它。唯一被多個 goroutine 共享的變量是通道以及 net.Conn 的實例,它們又都是并發(fā)安全的。
使用go build 命令編譯服務(wù)端和客戶端,并運(yùn)行生成的可執(zhí)行文件。
下圖中展示了在同一臺計算機(jī)上運(yùn)行的一個服務(wù)端和三個客戶端: