hello,咱们好,我是张张,「架构精进之路」公号作者。

关于谈天服务,想必咱们都不会生疏,由于在咱们的生活中经常会用到。

咱们用 Go 并发来实现一个谈天服务器,这个程序能够让一些用户经过服务器向其它一切用户播送文本音讯。

这个程序中有四种 goroutine。
main 和 broadcaster 各自是一个 goroutine 实例,每一个客户端的衔接都会有一个handleConn 和 clientWriter 的 goroutine。
broadcaster 是 select 用法的不错的样例,由于它需要处理三种不同类型的音讯。

下面咱们来演示的 main goroutine 的工作,是 listen 和 accept (网络编程里的概念)从客户端过来的衔接。对每一个衔接,程序都会树立一个新的 handleConn 的 goroutine。

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的“资历”信息。

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来告诉客户端的脱离并封闭衔接。

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,但是这个程序却并不需要显式的锁。clients这个map被约束在了一个独立的goroutine中,broadcaster,所以它不能被并发地拜访。

多个goroutine同享的变量只有这些channel和net.Conn的实例,两个东西都是并发安全的。

尝试用Go goroutine实现一个简单的聊天服务

– END –

Thanks for reading!

作者:架构精进之路,十年研制风雨路,大厂架构师,CSDN 博客专家,专心架构技术沉积学习及共享,职业与认知升级,坚持共享接地气儿的干货文章,等待与你一起生长。
重视并私信我回复“01”,送你一份程序员生长进阶大礼包,欢迎勾搭。