前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >尝试用Go goroutine实现一个简单的聊天服务

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

作者头像
架构精进之路
发布2023-08-18 16:35:01
1370
发布2023-08-18 16:35:01
举报
文章被收录于专栏:架构精进之路架构精进之路

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

对于聊天服务,想必大家都不会陌生,因为在我们的生活中经常会用到。

我们用 Go 并发来实现一个聊天服务器,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。

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

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

代码语言:javascript
复制
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的“资格”信息。

代码语言:javascript
复制
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来通知客户端的离开并关闭连接。

代码语言:javascript
复制
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来聊天:

代码语言:javascript
复制
$ 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的实例,两个东西都是并发安全的。

·END·

代码语言:javascript
复制
相关阅读:服务接口优化的常见方案实战总结聊聊分布式服务下的八种异步实现方式
有没有那么一瞬间,你也曾有过“失业焦虑”?浅析分布式系统中的补偿机制设计问题聊聊分布式日志系统的设计与实践
执行个 DEL 竟然也会阻塞 Redis?深挖一下果然不简单
PHP 中数组是如何灵活支持多数据类型的?
一文带你看通透,MySQL事务ACID四大特性实现原理
通过alter table 来实现重建表,同事大呼开眼界了实习生疑问:为什么要在需要排序的字段上加索引呢?代码多版改造,应用责任链设计模式

专注架构技术研究,一起跨越职业瓶颈!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-06-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构精进之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档