4zhvml8 发表于 2024-8-22 15:45:16

用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&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1724866407&amp;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&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1724866407&amp;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;">"&lt;b&gt;Connection closed.&lt;/b&gt;"</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 := &amp;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 &lt;- 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> -&gt;广播给其他的客户端</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 := &lt;-h.register:
    h.clients =<span style="color: black;">true</span>
    <span style="color: black;">// 下线channel:从注册表里面删除</span>
    <span style="color: black;">case</span> client := &lt;-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 := &lt;-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 &lt;- 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> -&gt; 加入全局注册表<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 -&gt; client -&gt; hub -&gt; 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 &lt;- 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 := &lt;-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 &lt; n; i++ {
    w.Write(newline)
    w.Write(&lt;-c.send)
    }
    ...
    <span style="color: black;">case</span>&lt;-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>




nykek5i 发表于 2024-10-6 11:33:09

“沙发”(SF,第一个回帖的人)‌

b1gc8v 发表于 2024-11-11 17:09:14

对于这个问题,我有不同的看法...
页: [1]
查看完整版本: 用go+websocket快速实现一个chat