用go+websocket快速实现一个chat
<h1 style="color: black; text-align: left; margin-bottom: 10px;">前言</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">在 go-zero 开源之后,非常多的用户询问<span style="color: black;">是不是</span><span style="color: black;">能够</span>支持以及什么时候支持 websocket,<span style="color: black;">最终</span>在 v1.1.6 里面<span style="color: black;">咱们</span>从框架层面让 websocket 的支持落地了,下面<span style="color: black;">咱们</span>就以 chat <span style="color: black;">做为</span>一个示例来讲解<span style="color: black;">怎样</span>用 go-zero 来实现一个 websocket 服务。</p>
<h1 style="color: black; text-align: left; margin-bottom: 10px;">整体设计</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">咱们</span>以 zero-example 中的 chat 聊天室为例来一步步一讲解 websocket 的实现,分为如下几个部分:</p>多客户端接入<span style="color: black;">信息</span>广播客户端的<span style="color: black;">即时</span>上线下线全双工通信【客户端本身是发送端,<span style="color: black;">亦</span>是接收端】<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">先放一张图,大致的数据传输:</p>
<div style="color: black; text-align: left; margin-bottom: 10px;"><img src="https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/b2fcc98d1e31471dbeb55abcb9a15360~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1724866407&x-signature=4SX5ZVSs5MqxsgYk6%2FswG%2B%2BScNc%3D" style="width: 50%; margin-bottom: 20px;"></div>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">中间有个 select loop <span style="color: black;">便是</span><span style="color: black;">全部</span> chat 的 engine。<span style="color: black;">首要</span>要支撑双方通信:</p>得有一个交流数据的管道。客户端只管从 管道 读取/<span style="color: black;">传送</span>数据;客户端在线<span style="color: black;">状况</span>。<span style="color: black;">不可</span>说你下线了,还往你那传输数据;<h1 style="color: black; text-align: left; margin-bottom: 10px;">数据流</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">数据流是 engine 的<span style="color: black;">重点</span>功能,先不急着看代码,<span style="color: black;">咱们</span>先想 client 怎么接入并被 engine 感知:</p><span style="color: black;">首要</span>是从前端发 websocket 请求;<span style="color: black;">创立</span>连接;准备接收/发送通道;注册到 engine;<div style="color: black; text-align: left; margin-bottom: 10px;"><img src="https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/3e3cfd128865419aa81f70807be82810~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1724866407&x-signature=hkbKWczPVqsMaUpOzs2I9D0mTxs%3D" style="width: 50%; margin-bottom: 20px;"></div><span style="color: black;">// HTML 操作 {js}</span>
<span style="color: black;">if</span> (window[<span style="color: black;">"WebSocket"</span>]) {
conn = <span style="color: black;">new</span> WebSocket(<span style="color: black;">"ws://"</span> + document.location.host + <span style="color: black;">"/ws"</span>);
conn.onclose = function (evt) {<span style="color: black;">var</span> item = document.createElement(<span style="color: black;">"div"</span>);
item.innerHTML = <span style="color: black;">"<b>Connection closed.</b>"</span>;
appendLog(item);
};
...
}
<span style="color: black;">// 路由</span>engine.AddRoute(rest.Route{
Method: http.MethodGet,
Path:<span style="color: black;">"/ws"</span>,
Handler: <span style="color: black;"><span style="color: black;">func</span><span style="color: black;">(w http.ResponseWriter, r *http.Request)</span></span>{
internal.ServeWs(hub, w, r)
},
})<span style="color: black;">// 接入<span style="color: black;">规律</span></span>
<span style="color: black;"><span style="color: black;">func</span> <span style="color: black;">ServeWs</span><span style="color: black;">(hub *Hub, w http.ResponseWriter, r *http.Request)</span></span> {
<span style="color: black;">// 将http请求升级为websocket</span>
conn, err := upgrader.Upgrade(w, r, <span style="color: black;">nil</span>)
...<span style="color: black;">// 构建client:hub{engine}, con{websocker conn}, send{channel buff}</span>
client := &Client{
hub: hub,
conn: conn,
send: <span style="color: black;">make</span>(<span style="color: black;">chan</span> []<span style="color: black;">byte</span>, bufSize),
}
client.hub.register <- client<span style="color: black;">// <span style="color: black;">起始</span>客户端双工的通信,接收和写入数据</span>
<span style="color: black;">go</span> client.writePump()
<span style="color: black;">go</span> client.readPump()
}
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">这般</span>,新接入的 client 就被加入到 注册 通道中。</p>
<h1 style="color: black; text-align: left; margin-bottom: 10px;">hub engine</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">发出了 注册 的动作,engine 会怎么处理呢?</p><span style="color: black;">type</span> Hub <span style="color: black;">struct</span> {
clients <span style="color: black;">map</span>[*Client]<span style="color: black;">bool</span> <span style="color: black;">// 上线clients</span>
broadcast <span style="color: black;">chan</span> []<span style="color: black;">byte</span> <span style="color: black;">// 客户端发送的<span style="color: black;">信息</span> ->广播给其他的客户端</span>
register <span style="color: black;">chan</span> *Client <span style="color: black;">// 注册channel,接收注册msg</span>
unregister <span style="color: black;">chan</span> *Client <span style="color: black;">// 下线channel</span>
}
<span style="color: black;"><span style="color: black;">func</span> <span style="color: black;">(h *Hub)</span> <span style="color: black;">Run</span><span style="color: black;">()</span></span> {
<span style="color: black;">for</span> {
<span style="color: black;">select</span> {
<span style="color: black;">// 注册channel:存放到注册表中,数据流<span style="color: black;">亦</span>就在这些client中<span style="color: black;">出现</span></span>
<span style="color: black;">case</span>client := <-h.register:
h.clients =<span style="color: black;">true</span>
<span style="color: black;">// 下线channel:从注册表里面删除</span>
<span style="color: black;">case</span> client := <-h.unregister:
<span style="color: black;">if</span> _, ok := h.clients; ok {
<span style="color: black;">delete</span>(h.clients, client)
<span style="color: black;">close</span>(client.send)
}
<span style="color: black;">// 广播<span style="color: black;">信息</span>:发送给注册表中的client中,send接收到并<span style="color: black;">表示</span>到client上</span>
<span style="color: black;">case</span> message := <-h.broadcast:
<span style="color: black;">for</span> client := <span style="color: black;">range</span> h.clients {
<span style="color: black;">select</span> {
<span style="color: black;">case</span> client.send <- message:
<span style="color: black;">default</span>:
<span style="color: black;">close</span>(client.send)<span style="color: black;">delete</span>(h.clients, client)
}
}
}
}
}
接收注册<span style="color: black;">信息</span> -> 加入全局注册表<span style="color: black;">倘若</span> engine.broadcast 接收到,会将 msg 传递给 注册表 的 client.sendChan<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">这般</span>从 <strong style="color: blue;">HTML -> client -> hub -> other client</strong> 的<span style="color: black;">全部</span>数据流就清晰了。</p>
<h1 style="color: black; text-align: left; margin-bottom: 10px;">广播数据</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">上面说到 engine.broadcast 接收到数据,那从页面<span style="color: black;">起始</span>,数据是怎么发送到这?</p><span style="color: black;"><span style="color: black;">func</span> <span style="color: black;">(<span style="color: black;">c</span> *Client)</span></span> readPump() {
...
<span style="color: black;">for</span> {
<span style="color: black;">// 1</span>
<span style="color: black;">_</span>, message, err :=<span style="color: black;">c</span>.conn.<span style="color: black;">ReadMessage</span>()
<span style="color: black;">if</span> err != <span style="color: black;">nil</span> {
<span style="color: black;">if</span> websocket.<span style="color: black;">IsUnexpectedCloseError</span>(err, websocket.<span style="color: black;">CloseGoingAway</span>, websocket.<span style="color: black;">CloseAbnormalClosure</span>) {
log.<span style="color: black;">Printf</span>(<span style="color: black;">"error: %v"</span>, err)
}
<span style="color: black;">break</span>
}
message = bytes.<span style="color: black;">TrimSpace</span>(bytes.<span style="color: black;">Replace</span>(message, newline, space, -<span style="color: black;">1</span>))
<span style="color: black;">// 2.</span>
<span style="color: black;">c</span>.hub.broadcast <- message
}
}
从 conn 中<span style="color: black;">持续</span>读取 msg【页面点击后传递】将 msg 传入 engine.broadcast,从而广播到其他的 client当<span style="color: black;">显现</span>发送<span style="color: black;">反常</span><span style="color: black;">或</span>是超时,<span style="color: black;">反常</span>退出时,会触发下线 client<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">同期</span>要<span style="color: black;">晓得</span>,此时发送<span style="color: black;">信息</span>的 client 不止有一个,可能会有<span style="color: black;">非常多</span>个。那发送到其他client,client 从自己的 send channel 中读取<span style="color: black;">信息</span><span style="color: black;">就可</span>:</p><span style="color: black;"><span style="color: black;">func</span> <span style="color: black;">(c *Client)</span> <span style="color: black;">writePump</span><span style="color: black;">()</span></span> {
<span style="color: black;">// 写超时<span style="color: black;">掌控</span></span>
ticker := time.NewTicker(pingPeriod)
...
<span style="color: black;">for</span> {
<span style="color: black;">select</span> {
<span style="color: black;">case</span> message, ok := <-c.send:
<span style="color: black;">// 当接收<span style="color: black;">信息</span>写入时,延长写超时时间。</span>c.conn.SetWriteDeadline(time.Now().Add(writeWait))
...
w, err := c.conn.NextWriter(websocket.TextMessage)
...
w.Write(message)<span style="color: black;">// 依次读取 send 中<span style="color: black;">信息</span>,并write</span>
n := <span style="color: black;">len</span>(c.send)
<span style="color: black;">for</span> i := <span style="color: black;">0</span>; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
...
<span style="color: black;">case</span><-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
...
}
}
}<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">上面<span style="color: black;">亦</span>说了,send 有来自各自客户端中发送的msg:<span style="color: black;">因此</span>当检测到 send 有数据,就<span style="color: black;">持续</span>接收<span style="color: black;">信息</span>并写入当前 client;<span style="color: black;">同期</span>当写超时,会检测websocket长连接<span style="color: black;">是不是</span>还存活,存活则继续读 send chan,断开则直接返回。</p>
<h1 style="color: black; text-align: left; margin-bottom: 10px;">完整示例代码</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">https://github.com/zeromicro/zero-examples/tree/main/chat</p>
<h1 style="color: black; text-align: left; margin-bottom: 10px;">总结</h1>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">本篇<span style="color: black;">文案</span>从<span style="color: black;">运用</span>上介绍<span style="color: black;">怎样</span>结合 go-zero <span style="color: black;">起始</span>你的 websocket 项目,<span style="color: black;">研发</span>者<span style="color: black;">能够</span><span style="color: black;">根据</span>自己的<span style="color: black;">需要</span>改造。</p>
“沙发”(SF,第一个回帖的人) 对于这个问题,我有不同的看法...
页:
[1]