```go
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 254.
// Chat is a server that lets clients chat with each other.
package main
import (
"bufio"
"fmt"
"log"
"net"
)
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
)
//负责广播的goroutine,抱有疑问,这个函数用一个for死循环嵌套了一个select,通过select去选择性地匹配某一个case,但这些case中好像没有与clientWriter的goroutine通过channel之间进行交流呢,我不太理解,从handleConn中接受的messages是如何通过broadcaster与clientWriter广播写入连接conn中去的呢?
//entering与leaving在整个过程中又起到什么作用呢?如果去掉这两个通道又会如何?
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)
}
}
}
//处理连接的goroutine,每一个客户端与服务器建立的连接都会新开一个goroutine,相互独立。在这个函数中,又开了一个goroutine,并发执行clientWriter,将handleConn的局部变量写入conn中。
func handleConn(conn net.Conn) {
ch := make(chan string) // outgoing client messages
go clientWriter(conn, ch)
who := conn.RemoteAddr().String()
//1.用A客户端建立连接,“You are ‘主机ip’”传入局部通道ch,此时执行clientWriter的goroutine将结束阻塞,将传入通道的“You are ‘主机ip’”写入连接conn,“You are ‘主机ip’”会返回至A客户端终端中显示。
ch <- "You are " + who
//2.“‘主机ip’ has arrived”传入全局通道messages中,“‘主机ip’ has arrived”不会返回至A客户端终端中,如果此时有B客户端与服务器保持连接,那么“‘主机ip’ has arrived”会返回至B客户端,这是为什么?
messages <- who + " has arrived"
//3.ch中的消息传给entering这个通道去,我不是很理解这一句话,ch是一个无缓冲通道,即容量为1,刚才注释第一点说了,clientWriter会将通道ch中的消息接收,这样ch中不就没有值了么,这样enter <- ch不就阻塞了么?
entering <- ch
//4.通过扫描conn中的客户端A写入conn中的信息,将其取出,传入messages通道中,即“‘主机ip’:‘A客户端写入conn的信息’”;最后这个字符串“‘主机ip’:‘A客户端写入conn的信息’”会在A,B客户端的终端显示,这是为什么呢?执行broadcaster()的goroutine并没有将messages的内容发送至某处并将其写入conn的操作啊?
input := bufio.NewScanner(conn)
for input.Scan() {
messages <- who + ": " + input.Text()
}
// NOTE: ignoring potential errors from input.Err()
//5.若客户端A写入连接“control+z”,那么ch会发送至通道leaving,但ch里面不是没有值么,不是会阻塞么?"主机A has left"会发送至通道messages,最后B客户端终端会显示"主机A has left"。
leaving <- ch
messages <- who + " has left"
conn.Close()
}
//负责写入conn的goroutine,将单向in通道ch的消息写入conn中(每条连接是相互独立的goroutine)
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
}
}
//三个goroutine(broadcaster,handleConn和clientWriter)通过channel进行通信。
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)
}
}
```
**总结一下我的问题:**
1.(Line 54)“‘主机ip’ has arrived”传入全局通道messages中,“‘主机ip’ has arrived”不会返回至A客户端终端中,如果此时有B客户端与服务器保持连接,那么“‘主机ip’ has arrived”会返回至B客户端,这是为什么?
2.(Line56-59)通过扫描conn中的客户端A写入conn中的信息,将其取出,传入messages通道中,即“‘主机ip’:‘A客户端写入conn的信息’”;最后这个字符串“‘主机ip’:‘A客户端写入conn的信息’”会在A,B客户端的终端显示,这是为什么呢?执行broadcaster()的goroutine并没有将messages的内容发送至某处并将其写入conn的操作啊?
3.(Line23-40,function broadcaster)负责广播的goroutine,抱有疑问,这个函数用一个for死循环嵌套了一个select,通过select去选择性地匹配某一个case,但这些case中好像没有与clientWriter的goroutine通过channel之间进行交流呢,我不太理解,从handleConn中接受的messages是如何通过broadcaster与clientWriter广播写入连接conn中去的呢?
4.(Line55)ch中的消息传给entering这个通道去,我不是很理解这一句话,ch是一个无缓冲通道,即容量为1,而clientWriter会将通道ch中的消息接收,这样ch中不就没有值了么,这样enter <- ch不就阻塞了么?
5.(Line61)若客户端A写入连接“control+z”,那么ch会发送至通道leaving,但ch里面不是没有值么,不是会阻塞么?”主机A has left”会发送至通道messages,最后B客户端终端会显示”主机A has left”。
6.entering与leaving这两个全局通道在整个过程中又起到什么作用呢?如果去掉这两个通道又会如何?
谢谢,希望有哪位能回答我的疑惑~感激不尽!!
先简单理解下整个程序:
mian:TCP监听8000端口,使用goroutine执行broadcaster,获取客户端连接并使用handleConn处理。
broadcaster:循环select三个通道messages、entering、leaving,其中从messages中取到的数据会遍历clients写入cli(也就是发送给所有客户端),从entering和leaving取到的可以理解为和客户端绑定的通道,entering取到后放入clients也就相当于一个客户端上线,leaving取到后从clients中移除,也就相当于客户端下线。
handleConn:创建通道ch,使用goroutine执行clientWriter(循环从ch中取数据写入conn),从conn获取客户端IP,"You are " + who 写入ch(由于clientWriter的执行,消息会发送至客户端),who + " has arrived"写入messages(由于broadcaster的执行,消息会发送至所有客户端),ch写入entering即客户端的上线注册,然后循环从conn中读取数据写入messages(由于broadcaster的执行,消息会发送至所有客户端),连接断开时leaving <- ch,实现客户端离线,并广播who + " has left"。
#3
更多评论
没看过这本书,这段代码看了几遍,写的真心奇葩。
问题1:
代码中50行开始群发消息,52行才把A客户端加入队列
(52行加入entering队列,34行从entering队列加入clients,32行-32行负责对所有clients里的内容进行广播)
所以这是先发送消息再注册客户端的问题。
问题2:
发送消息在67-69行,不清楚range一个chan的用法的话参考
https://tour.golang.org/concurrency/4
问题3:
同2。你可以理解每一个entering是一个消息队列。通过这个来发送的
问题4.
entering有消费者啊,第33行。
问题5
同问题4,第35行
问题6
起到不同协程之间通信的作用,分配任务和数据。
去掉你就没法调控各个线程了。
#1
简单回答一下:
1.
```
messages <- who + " has arrived"
```
这句是向 messages 传递了一个字符串,而 messages 拿到后会向所有的已经进入 clients 这个 map 的客户端传递,此时,你所谓的 A客户端 还并未被进入 clients,这个进入动作要到 entering <- ch 才会被执行
2.
同上,此时执行的
```
messages <- who + ": " + input.Text()
```
已经是当前的 A客户端 进入 clients 之后了,自然会收到消息了。
3.
需要注意到
```
var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // all incoming client messages
)
```
这是三个全局变量,保证了 broadcaster 与 handleConn 的交流,而 44 行的 ch := make(chan string) 则保证了 handleConn 与 clientWriter 的交流
4.
ch是一个无缓冲通道,即容量为1 这句话错了,无缓冲渠道,容量为 0.
entering 是个以 chan<- string 为传递值得渠道,也就是说 entering 里面存储的是渠道,而不是 string。
33 行 case cli := <-entering: 保证了不会阻塞在 52 行。
enter <- ch 做的事情是把这个 ch 作为一个值传递给 enter 这个渠道,而不是取里面的值。
for msg := range ch 才是真正的取 ch 这个渠道的值。
5.
跟4 一样,ch 是作为一个值被传递给 leaving, 而不是取里面的值。
6.
entering leaving 只是作为 tcp 链接的一个中间状态而已,去掉也可以,这是较为熟悉渠道的人做的补充,而对于不太熟悉渠道的就觉得迷惑了。
#2