当前位置: 首页 > Web前端 > HTML5

【GoWebSocket】单间聊天室

时间:2023-04-05 20:16:01 HTML5

我是公众号线下派对游戏的作者HullQin(欢迎关注公众号,发送加微信,交友),作者HullQin转发前需授权本文。我独立开发了《联机桌游合集》,这是一个网页,在这里你可以轻松地和朋友一起玩网络游戏,五子棋等游戏,不收费,也没有广告。还为GameJam2022开发了《Dice Crush》,喜欢的话可以关注我HullQin哦~有空我会分享制作游戏的相关技术。背景第一篇:《为什么我选用Go重构Python版本的WebSocket服务?》,介绍我的目标。上篇文章讲了《你的第一个Go WebSocket服务: echo server》,今天我们实现一个聊天室。如果您还没有阅读上一篇文章,请务必先阅读它,因为这篇文章比较复杂。如果你不理解上一篇文章,你可能不会理解这篇文章。新建工程和安装依赖请参考《你的第一个Go WebSocket服务: echo server》。新建一个项目文件夹,在命令行执行以下命令,安装GoWebsocket依赖:gogetgithub.com/gorilla/websocket复制聊天代码,复制gorilla/websocket官方demo,我们慢慢分析:github。com/gorilla/websocket/tree/master/examples/chat你需要这4个文件:main.gohub.goclient.goindex.html第一步,看main函数funcmain(){flag.Parse()hub:=newHub()gohub.run()http.HandleFunc("/",serveHome)http.HandleFunc("/ws",func(whttp.ResponseWriter,r*http.Request){serveWs(hub,w,r)})err:=http.ListenAndServe(*addr,nil)iferr!=nil{log.Fatal("ListenAndServe:",err)}}flag和http.HandleFunc在上一篇文章中已经介绍过了,这里是和上一篇完全一样。这里也开了一个goroutine。注意是写在main函数里面,不是写在http.HandleFunc里面。所以无论有多少客户端连接,该服务只启动一个goroutine。新中心()。跑步()。接下来让我们看看hub.go文件中的newHub()。查看注册的两个请求处理函数:serveHome是一个HTTP服务,返回html文件给请求者(浏览器)。对于/ws路由,将调用serveWs。让我们看看serveWs接下来在clent.go文件中做什么。第二步,查看hub.goHub定义和newHub函数定义:make(map[*Client]bool),register:make(chan*Client),unregister:make(chan*Client),broadcast:make(chan[]byte),}}可以看到newHub刚刚创建了一个空白中心。而一个Hub包含4个东西:clients,一个Map,保存了每个client的references(其实这个Map的value并没有用到,key是client的reference,在other中可以看做一个集合语言)。register,用于注册客户端的频道。每当客户端建立websocket连接时,通过注册将客户端保存到客户端引用。注销,用于注销客户端的频道。每当客户端断开websocket连接时,客户端引用就会通过注销从客户端中删除。broadcast,用于发送广播的频道。将消息保存到该通道后,其他goroutine会遍历客户端,将消息发送给所有客户端。goroutine在服务启动时启动:hub.run()func(h*Hub)run(){for{select{caseclient:=<-h.register:h.clients[client]=truecaseclient:=<-h.unregister:如果_,ok:=h.clients[client];ok{delete(h.clients,client)close(client.send)}casemessage:=<-h.broadcast:forclient:=rangeh.clients{选择{caseclient.send<-message:default:close(client.send)delete(h.clients,client)}}}}}无限循环:不断从通道读取数据。读取寄存器时,注册客户端。读取注销时,客户端断开连接并删除引用。读取广播后,遍历客户端,广播消息(通过将消息写入每个客户端的client.sendchannel实现广播),这就是下一步要看的逻辑。接下来,我们看看客户端。第三步看client.goClient的定义typeClientstruct{hub*Hubconn*websocket.Connsendchan[]byte}hub:每个Client保存Hub引用。(虽然目前全球只有一个hub,但是为了扩展性,还是留一个比较好,因为以后还会有更多的hub,我们下一篇再介绍!)conn:是的连接到客户端的websocket。通过这个conn,你可以与Client进行交互(即发送和接收消息)。send:一个channel,第二步已经看到了。广播时,将消息写入每个Client的发送通道。通过从该频道读取消息来向客户端发送消息。mainfunction使用的serveWs函数funcserveWs(hub*Hub,whttp.ResponseWriter,r*http.Request){conn,err:=upgrager.Upgrade(w,r,nil)iferr!=nil{log.Println(err)return}client:=&Client{hub:hub,conn:conn,send:make(chan[]byte,256)}client.hub.register<-client//允许收集调用者引用的内存在//newgoroutines.goclient.writePump()goclient.readPump()}中完成所有工作在集线器中注册。然后启动2个goroutines:client.writePump()和client.readPump(),函数逻辑结束。这两个goroutine分别用来处理写消息和读消息。client.writePumpfunc(c*Client)writePump(){ticker:=time.NewTicker(pingPeriod)deferfunc(){ticker.Stop()c.conn.Close()}()for{select{casemessage,ok:=<-c.send:c.conn.SetWriteDeadline(time.Now().Add(writeWait))if!ok{c.conn.WriteMessage(websocket.CloseMessage,[]byte{})返回}w,err:=c.conn.NextWriter(websocket.TextMessage)iferr!=nil{return}w.Write(message)iferr:=w.Close();错误!=nil{return}case<-ticker.C:c.conn.SetWriteDeadline(time.Now().Add(writeWait))err!=nil{return}}}}首先启动一个ping定时器。Ping消息将定期发送到客户端。这是WebSocket协议要求的,参考《RFC6455》。浏览器抓包是看不到这个Ping消息的。通过这种方式,可以清理无响应的连接。然后,这个goroutine声明了defer执行的逻辑:关闭定时器,关闭连接。最重要的是,这个goroutine有一个无限循环:不断读取client.send通道中的数据。只要hub.broadcast向它发送消息,就会被这个goroutine处理。c.conn.NextWriter和w.Write(message)才是真正发送消息的逻辑。另外,每隔一段时间(定时器设定的时间间隔),服务器会向浏览器发送一个Ping。浏览器会自动回复一个Pong(不需要客户端开发者关注,客户端开发者通常是JS开发者)。client.readPumpfunc(c*Client)readPump(){deferfunc(){c.hub.unregister<-cc.conn.Close()}()c.conn.SetReadLimit(maxMessageSize)c.conn.SetReadDeadline(time.Now().Add(pongWait))c.conn.SetPongHandler(func(string)error{c.conn.SetReadDeadline(time.Now().Add(pongWait));returnnil})for{_,message,err:=c.conn.ReadMessage()iferr!=nil{ifwebsocket.IsUnexpectedCloseError(err,websocket.CloseGoingAway,websocket.CloseAbnormalClosure){log.Printf("error:%v",err)}中断}消息=bytes.TrimSpace(bytes.Replace(message,newline,space,-1))c.hub.broadcast<-message}}readPump是读取消息,收到客户端消息后,借助hub进行广播。播送。另外,这个goroutine还有一个重要的任务:关闭连接后,负责hub.unregister和conn.Close。总结!最重要的图!为了帮助大家理解,我画了这张图:其中,彩色的矩形代表goroutines,彩色的线条是各个channel(从A到B,数据由goroutineA写入,数据由goroutineB读取)。User和Client图中只画了2个,可以继续增加。最后,我是公众号线下派对游戏的作者HullQin(欢迎关注公众号,发送加微信,交友),转载本文需作者HullQin授权。我独立开发了《联机桌游合集》,这是一个网页,在这里你可以轻松地和朋友一起玩网络游戏,五子棋等游戏,不收费,也没有广告。还为GameJam2022开发了《Dice Crush》,喜欢的话可以关注我HullQin哦~有空我会分享制作游戏的相关技术。