Go 語(yǔ)言 示例: 聊天服務(wù)

2023-03-14 16:57 更新

原文鏈接:https://gopl-zh.github.io/ch8/ch8-10.html


8.9. 示例: 聊天服務(wù)

我們用一個(gè)聊天服務(wù)器來(lái)終結(jié)本章節(jié)的內(nèi)容,這個(gè)程序可以讓一些用戶通過(guò)服務(wù)器向其它所有用戶廣播文本消息。這個(gè)程序中有四種goroutine。main和broadcaster各自是一個(gè)goroutine實(shí)例,每一個(gè)客戶端的連接都會(huì)有一個(gè)handleConn和clientWriter的goroutine。broadcaster是select用法的不錯(cuò)的樣例,因?yàn)樗枰幚砣N不同類型的消息。

下面演示的main goroutine的工作,是listen和accept(譯注:網(wǎng)絡(luò)編程里的概念)從客戶端過(guò)來(lái)的連接。對(duì)每一個(gè)連接,程序都會(huì)建立一個(gè)新的handleConn的goroutine,就像我們?cè)诒菊麻_(kāi)頭的并發(fā)的echo服務(wù)器里所做的那樣。

gopl.io/ch8/chat

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。他的內(nèi)部變量clients會(huì)記錄當(dāng)前建立連接的客戶端集合。其記錄的內(nèi)容是每一個(gè)客戶端的消息發(fā)出channel的“資格”信息。

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監(jiān)聽(tīng)來(lái)自全局的entering和leaving的channel來(lái)獲知客戶端的到來(lái)和離開(kāi)事件。當(dāng)其接收到其中的一個(gè)事件時(shí),會(huì)更新clients集合,當(dāng)該事件是離開(kāi)行為時(shí),它會(huì)關(guān)閉客戶端的消息發(fā)送channel。broadcaster也會(huì)監(jiān)聽(tīng)全局的消息channel,所有的客戶端都會(huì)向這個(gè)channel中發(fā)送消息。當(dāng)broadcaster接收到什么消息時(shí),就會(huì)將其廣播至所有連接到服務(wù)端的客戶端。

現(xiàn)在讓我們看看每一個(gè)客戶端的goroutine。handleConn函數(shù)會(huì)為它的客戶端創(chuàng)建一個(gè)消息發(fā)送channel并通過(guò)entering channel來(lái)通知客戶端的到來(lái)。然后它會(huì)讀取客戶端發(fā)來(lái)的每一行文本,并通過(guò)全局的消息channel來(lái)將這些文本發(fā)送出去,并為每條消息帶上發(fā)送者的前綴來(lái)標(biāo)明消息身份。當(dāng)客戶端發(fā)送完畢后,handleConn會(huì)通過(guò)leaving這個(gè)channel來(lái)通知客戶端的離開(kāi)并關(guān)閉連接。

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為每一個(gè)客戶端創(chuàng)建了一個(gè)clientWriter的goroutine,用來(lái)接收向客戶端發(fā)送消息的channel中的廣播消息,并將它們寫(xiě)入到客戶端的網(wǎng)絡(luò)連接??蛻舳说淖x取循環(huán)會(huì)在broadcaster接收到leaving通知并關(guān)閉了channel后終止。

下面演示的是當(dāng)服務(wù)器有兩個(gè)活動(dòng)的客戶端連接,并且在兩個(gè)窗口中運(yùn)行的情況,使用netcat來(lái)聊天:

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

當(dāng)與n個(gè)客戶端保持聊天session時(shí),這個(gè)程序會(huì)有2n+2個(gè)并發(fā)的goroutine,然而這個(gè)程序卻并不需要顯式的鎖(§9.2)。clients這個(gè)map被限制在了一個(gè)獨(dú)立的goroutine中,broadcaster,所以它不能被并發(fā)地訪問(wèn)。多個(gè)goroutine共享的變量只有這些channel和net.Conn的實(shí)例,兩個(gè)東西都是并發(fā)安全的。我們會(huì)在下一章中更多地講解約束,并發(fā)安全以及goroutine中共享變量的含義。

練習(xí) 8.12: 使broadcaster能夠在每個(gè)新的客戶端到來(lái)時(shí)通知它當(dāng)前的客戶端集合。這需要你在clients集合中,以及entering和leaving的channel中記錄客戶端的名字。

練習(xí) 8.13: 使聊天服務(wù)器能夠斷開(kāi)空閑的客戶端連接,比如最近五分鐘之后沒(méi)有發(fā)送任何消息的那些客戶端。提示:可以在其它goroutine中調(diào)用conn.Close()來(lái)解除Read調(diào)用,就像input.Scanner()所做的那樣。

練習(xí) 8.14: 修改聊天服務(wù)器的網(wǎng)絡(luò)協(xié)議,這樣每一個(gè)客戶端就可以在entering時(shí)提供他們的名字。將消息前綴由之前的網(wǎng)絡(luò)地址改為這個(gè)名字。

練習(xí) 8.15: 如果一個(gè)客戶端沒(méi)有及時(shí)地讀取數(shù)據(jù)可能會(huì)導(dǎo)致所有的客戶端被阻塞。修改broadcaster來(lái)跳過(guò)一條消息,而不是等待這個(gè)客戶端一直到其準(zhǔn)備好讀寫(xiě)?;蛘邽槊恳粋€(gè)客戶端的消息發(fā)送channel建立緩沖區(qū),這樣大部分的消息便不會(huì)被丟掉;broadcaster應(yīng)該用一個(gè)非阻塞的send向這個(gè)channel中發(fā)消息。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)