Golang 搭建 WebSocket 应用(八) - 完整代码

本文应该是本系列文章最后一篇了,前面留下的一些坑可能后面会再补充一下,但不在本系列文章中了。

整体架构

再来回顾一下我们的整体架构:

在这里插入图片描述

在我们的 demo 中,包含了以下几种角色:

  1. 客户端:一般是浏览器,用于接收消息;
  2. Hub:消息中心,用于管理所有的客户端连接,以及将消息推送给客户端;
  3. 调用 /send 发送消息的应用:用于将消息发送给 Hub,然后由 Hub 将消息推送给客户端。

然后,每一个 WebSocket 连接都有一个关联的读协程和写协程,
用于读取客户端发送的消息,以及将消息推送给客户端。

目录结构

├── LICENSE  // 协议
├── Makefile // 一些常用的命令
├── README.md
├── authenticator.go      // 认证器
├── authenticator_test.go // 认证器测试
├── bytes.go // 字符串和 []byte 之间转换的辅助方法
├── client.go // WebSocket 客户端
├── go.mod    // 项目依赖
├── go.sum    // 项目依赖
├── hub.go    // 消息中心
├── main.go   // 程序入口
├── message   // 消息记录器
│   ├── db_logger.go
│   ├── db_logger_test.go
│   ├── log.go
│   └── stdout_logger.go
├── server.go // HTTP 服务
└── server_test.go // HTTP 接口的测试

运行

注:需要 Go 1.20 或以上版本

  1. 下载依赖:

可以使用七牛云的代理加速下载。

go mod tidy
  1. 启动 WebSocket 服务端:
go run main.go

Hub 代码

最终,我们的 Hub 代码演进成了下面这样:

// bufferSize 通道缓冲区、map 初始化大小
const bufferSize = 128// Handler 错误处理函数
type Handler func(log message.Log, err error)// Hub 维护了所有的客户端连接
type Hub struct {// 注册请求register chan *Client// 取消注册请求unregister chan *Client// 记录 uid 跟 client 的对应关系userClients map[string]*Client// 互斥锁,保护 userClients 以及 clients 的读写sync.RWMutex// 消息记录器messageLogger message.Logger// 错误处理器errorHandler Handler// 验证器authenticator Authenticator// 等待发送的消息数量pending atomic.Int64
}// 默认的错误处理器
func defaultErrorHandler(log message.Log, err error) {res, _ := json.Marshal(log)fmt.Printf("send message: %s, error: %s\n", string(res), err.Error())
}func newHub() *Hub {return &Hub{register:      make(chan *Client),unregister:    make(chan *Client),userClients:   make(map[string]*Client, bufferSize),RWMutex:       sync.RWMutex{},messageLogger: &message.StdoutMessageLogger{},errorHandler:  defaultErrorHandler,authenticator: &JWTAuthenticator{},}
}// 注册、取消注册请求处理
func (h *Hub) run() {for {select {case client := <-h.register:h.Lock()h.userClients[client.uid] = clienth.Unlock()case client := <-h.unregister:h.Lock()close(client.send)delete(h.userClients, client.uid)h.Unlock()}}
}// 返回 Hub 的当前的关键指标
func metrics(hub *Hub, w http.ResponseWriter) {pending := hub.pending.Load()connections := len(hub.userClients)_, _ = w.Write([]byte(fmt.Sprintf("# HELP connections 连接数\n# TYPE connections gauge\nconnections %d\n", connections)))_, _ = w.Write([]byte(fmt.Sprintf("# HELP pending 等待发送的消息数量\n# TYPE pending gauge\npending %d\n", pending)))
}

其中:

  • Hub 中的 registerunregister 通道用于处理客户端的注册和取消注册请求;
  • Hub 中的 userClients 用于记录 uidClient 的对应关系;
  • Hub 中的 messageLogger 用于记录消息;
  • Hub 中的 errorHandler 用于处理错误;
  • Hub 中的 authenticator 用于验证客户端的身份;
  • Hub 中的 pending 用于记录等待发送的消息数量。

目前实现存在的问题:

  • registerunregister 通道被消费的时候需要加锁,这样会导致 registerunregister 变成串行的,性能不好;
  • userClients 也是需要加锁的,这样会导致 userClients 的读写也是串行的,性能不好;

对于这两个问题,前面我们讨论过,一种可行的办法分段 map,然后对每一个 map 都有一个对应的 sync.Mutex 互斥锁来保证其读写的安全。

Client 代码

Client 比较关键的方法是:

  • writePump:负责将消息推送给客户端。
  • serveWs:处理 WebSocket 连接请求。
  • send:处理消息发送请求。

writePump

这个方法会从 send 通道中获取消息,然后推送给客户端。
推送失败会调用 errorHandler 处理错误。
推送成功会将 pending 减一。

// writePump 负责推送消息给 WebSocket 客户端
//
// 该方法在一个独立的协程中运行,我们保证了每个连接只有一个 writer。
// Client 会从 send 请求中获取消息,然后在这个方法中推送给客户端。
func (c *Client) writePump() {defer func() {_ = c.conn.Close()}()// 从 send 通道中获取消息,然后推送给客户端for {messageLog, ok := <-c.send// 设置写超时时间_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))// c.send 这个通道已经被关闭了if !ok {c.hub.pending.Add(int64(-1 * len(c.send)))return}if err := c.conn.WriteMessage(websocket.TextMessage, StringToBytes(messageLog.Message)); err != nil {c.hub.errorHandler(messageLog, err)c.hub.pending.Add(int64(-1 * len(c.send)))return}c.hub.pending.Add(int64(-1))}
}

serveWs

serveWs 方法会处理 WebSocket 连接请求,然后将其注册到 Hub 中。
在连接的时候会对客户端进行认证,认证失败会断开连接。
最后会启动读写协程。

// serveWs 处理 WebSocket 连接请求
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {// 升级为 WebSocket 连接conn, err := upgrader.Upgrade(w, r, nil)if err != nil {w.WriteHeader(http.StatusBadRequest)_, _ = w.Write([]byte(fmt.Sprintf("upgrade error: %s", err.Error())))return}// 认证失败的时候,返回错误信息,并断开连接uid, err := hub.authenticator.Authenticate(r)if err != nil {_ = conn.SetWriteDeadline(time.Now().Add(time.Second))_ = conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("authenticate error: %s", err.Error())))_ = conn.Close()return}// 注册 Clientclient := &Client{hub: hub, conn: conn, send: make(chan message.Log, bufferSize), uid: uid}client.conn.SetCloseHandler(closeHandler)// register 无缓冲,下面这一行会阻塞,直到 hub.run 中的 <-h.register 语句执行// 这样可以保证 register 成功之后才会启动读写协程client.hub.register <- client// 启动读写协程go client.writePump()go client.readPump()
}

send

send 是一个 http 接口,用于处理消息发送请求。
它会从 Hub 中获取 uid 对应的 Client,然后将消息发送给客户端。

// send 处理消息发送请求
func send(hub *Hub, w http.ResponseWriter, r *http.Request) {uid := r.FormValue("uid")if uid == "" {w.WriteHeader(http.StatusBadRequest)_, _ = w.Write([]byte("uid is required"))return}// 从 hub 中获取 uid 关联的 clienthub.RLock()client, ok := hub.userClients[uid]hub.RUnlock()if !ok {w.WriteHeader(http.StatusBadRequest)_, _ = w.Write([]byte(fmt.Sprintf("client not found: %s", uid)))return}// 记录消息messageLog := message.Log{Uid: uid, Message: r.FormValue("message")}_ = hub.messageLogger.Log(messageLog)// 发送消息client.send <- messageLog// 增加等待发送的消息数量hub.pending.Add(int64(1))
}

github

完整代码可以在 github 上进行查看:https://github.com/eleven26/go-pusher

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/637234.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图论:最短路(dijkstra算法、bellman算法、spfa算法、floyd算法)详细版

终于是学完了&#xff0c;这个最短路我学了好几天&#xff0c;当然也学了别的算法啦&#xff0c;也是非常的累啊。 话不多说下面看看最短路问题吧。 最短路问题是有向图&#xff0c;要求的是图中一个点到起点的距离&#xff0c;其中我们要输入点和点之间的距离&#xff0c;来求…

day01.基础知识

目录 一.函数与语句 1.1进入C 1.1.1main( )头函数 1.1.2 注释 1.1.3头文件 1.1.4预处理 1.1.5命名空间 1.1.6输入与输出 1.1.7格式化 1.2语句 1.2.1声明语句与变量 1.2.2赋值语句 1.3函数 1.3.1使用有返回值的函数 一.函数与语句 1.1进入C 1.1.1main( )头函数 …

rk1126, 实现 yolov8 目标检测

基于 RKNN 1126 实现 yolov8 目标检测 Ⓜ️ RKNN 模型转换 ONNX yolo export model./weights/yolov8s.pt formatonnx导出 RKNN 这里选择输出 concat 输入两个节点 onnx::Concat_425 和 onnx::Concat_426 from rknn.api import RKNNONNX_MODEL ./weights/yolov8s.onnxRKNN_MOD…

MySQL 索引(下)

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL-进阶篇 &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现…

推荐新版AI智能聊天系统网站源码ChatGPT NineAi

Nine AI.ChatGPT是基于ChatGPT开发的一个人工智能技术驱动的自然语言处理工具&#xff0c;它能够通过学习和理解人类的语言来进行对话&#xff0c;还能根据聊天的上下文进行互动&#xff0c;真正像人类一样来聊天交流&#xff0c;甚至能完成撰写邮件、视频脚本、文案、翻译、代…

GoZero微服务个人探究之路(七)添加中间件、自定义中间件

说在前面 官方已经自己实现了很多中间件&#xff0c;我们可以方便的直接使用&#xff0c;不用重复造轮子了 开启方式可以看官方文档 中间件 | go-zero Documentation 实现自定义的中间件 在业务逻辑中&#xff0c;我们需要实现自定义功能的中间件 ------这里我们以实现跨源…

Spring+SprinMVC+MyBatis配置方式简易模板

SpringSprinMVCMyBatis配置方式简易模板代码Demo GitHub访问 ssm-tpl-cfg 一、SQL数据准备 创建数据库test&#xff0c;执行下方SQL创建表ssm-tpl-cfg /*Navicat Premium Data TransferSource Server : 127.0.0.1Source Server Type : MySQLSource Server Versio…

Linux ---- 小玩具

目录 一、安装&#xff1a; 1、佛祖保佑&#xff0c;永不宕机&#xff0c;永无bug 2、小火车 3、艺术字和其它 天气预报 艺术字 4、会说话的小牦牛 5、其他趣味图片 我爱你 腻害 英雄联盟 帅 忍 龙 你是猪 福 好运连连 欢迎 加油 想你 忘不了你 我错了 你…

细说JavaScript事件处理(JavaScript事件处理详解)

js语言的一个特色和就是它的动态性&#xff0c;即一时间驱动的方式对用户输入作出反应而不需要依赖服务器端程序。事件是指人机交互的结果&#xff0c;如鼠标移动、点击按钮、在表单中输入数据或载入新的Web洁面等。 一、什么是事件 1、事件类型 1.1、事件源 1.2、事件处理…

Node.js Stream.pipeline() Method

Why Stream.pipeline 通过流我们可以将一大块数据拆分为一小部分一点一点的流动起来&#xff0c;而无需一次性全部读入&#xff0c;在 Linux 下我们可以通过 | 符号实现&#xff0c;类似的在 Nodejs 的 Stream 模块中同样也为我们提供了 pipe() 方法来实现。 未使用 Stream p…

Pyro —— Understanding how pyro works

目录 Simulation fields Inside the Pyro Solver Colliders Pressure projection Simulation fields Pyro是纯体积流体解算器&#xff0c;表示流体状态的数据存于各种标量场和矢量场&#xff1b;Smoke Object会创建这些场&#xff0c;且能可视化这些场&#xff1b; vel场&…

【JavaEE】网络原理:网络中的一些基本概念

目录 1. 网络通信基础 1.1 IP地址 1.2 端口号 1.3 认识协议 1.4 五元组 1.5 协议分层 什么是协议分层 分层的作用 OSI七层模型 TCP/IP五层&#xff08;或四层&#xff09;模型 网络设备所在分层 网络分层对应 封装和分用 1. 网络通信基础 1.1 IP地址 概念:IP地址…

C语言/c++指针详细讲解【超详细】【由浅入深】

指针用法简单介绍 指针&#xff0c;是内存单元的编号。 内存条分好多好多小单元&#xff0c;一个小单元有 8 位&#xff0c;可以存放 8 个 0 或 1&#xff1b;也就是说&#xff0c;内存的编号不是以位算的&#xff0c;而是以字节算的&#xff0c;不是一个 0 或 1 是一个编号&…

立体视觉几何(一)

1.什么是立体视觉几何 立体视觉对应重建&#xff1a; • 对应&#xff1a;给定一幅图像中的点pl&#xff0c;找到另一幅图像中的对应点pr。 • 重建&#xff1a;给定对应关系(pl, pr)&#xff0c;计算空间中相应点的3D 坐标P。 立体视觉&#xff1a;从图像中的投影恢复场景中点…

list下

文章目录 注意&#xff1a;const迭代器怎么写&#xff1f;运用场合&#xff1f; inserterase析构函数赋值和拷贝构造区别&#xff1f;拷贝构造不能写那个swap,为什么&#xff1f;拷贝构造代码 面试问题什么是迭代器失效&#xff1f;vector、list的区别&#xff1f; 完整代码 注…

qt学习:QT对话框+颜色+文件+字体+输入

目录 概述 继承图 QColorDialog 颜色对话框 QFileDialog 文件对话框 保存文件对话框 QFontDialog 字体对话框 QInputDialog 输入对话框 概述 对于对话框的功能&#xff0c;在GUI图形界面开发过程&#xff0c;使用是非常多&#xff0c;那么Qt也提供了丰富的对话框类QDia…

网络:FTP

1. FTP 文件传输协议&#xff0c;FTP是用来传输文件的协议。使用FTP实现远程文件传输的同时&#xff0c;还可以保证数据传输的可靠性和高效性。 2. 特点 明文传输。 作用&#xff1a;可以从服务器上下载文件&#xff0c;或将本地文件上传到服务器。 3. FTP原理 FTP有控制层面…

坦克大战游戏代码

坦克大战游戏 主函数战场面板开始界面坦克父类敌方坦克我方坦克子弹爆炸效果数据存盘及恢复图片 主函数 package cn.wenxiao.release9;import java.awt.event.ActionEvent; import java.awt.event.ActionListener;import javax.swing.JFrame; import javax.swing.JMenu; impor…

RS-485通讯

RS-485通讯协议简介 与CAN类似&#xff0c;RS-485是一种工业控制环境中常用的通讯协议&#xff0c;它具有抗干扰能力强、传输距离远的特点。RS-485通讯协议由RS-232协议改进而来&#xff0c;协议层不变&#xff0c;只是改进了物理层&#xff0c;因而保留了串口通讯协议应用简单…

【HarmonyOS】掌握布局组件,提升应用体验

从今天开始&#xff0c;博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”&#xff0c;对于刚接触这项技术的小伙伴在学习鸿蒙开发之前&#xff0c;有必要先了解一下鸿蒙&#xff0c;从你的角度来讲&#xff0c;你认为什么是鸿蒙呢&#xff1f;它出现的意义又是…