掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本節(jié)將帶領(lǐng)大家結(jié)合咱們前面所學(xué)的知識(shí)開(kāi)發(fā)一個(gè)聊天的示例程序,它可以在幾個(gè)用戶之間相互廣播文本消息。

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

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流