Golang 搭建 WebSocket 应用(二) - 基本群聊 demo

上一篇文章中,我们已经了解了 gorilla/websocket 的一些基本概念和简单的用法。
接下来,我们通过一个再复杂一点的例子来了解它的实际用法。

功能

这个例子来自源码里面的 examples/chat,它包含了以下功能:

  1. 用户访问群聊页面的时候,可以发送消息给所有其他在聊天室内的用户(也就是同样打开群聊页面的用户)
  2. 所有的用户发送的消息,群聊中的所有用户都能收到(包括自己)

其基本效果如下:

在这里插入图片描述

为了更好地理解 gorilla/websocket 的使用方式,下文在讲解的时候会去掉一些出于健壮性考虑而写的代码。

基本架构

这个 demo 的基本组件如下图:

在这里插入图片描述

  1. Client:也就是连接到了服务端的客户端,可以有多个
  2. Hub:所有的客户端会保存到 Hub 中,同时所有的消息也会经过 Hub 来进行广播(也就是将消息发给所有连接到 Hub 的客户端)

在这里插入图片描述

工作原理

Hub

Hub 的源码如下:

type Hub struct {
    // 保存所有客户端
	clients map[*Client]bool
    // 需要广播的消息
	broadcast chan []byte
    // 等待连接的客户端
	register chan *Client
    // 等待断开的客户端
	unregister chan *Client
}

Hub 的核心方法如下:

func (h *Hub) run() {
	for {
		select {
		case client := <-h.register:
            // 从等待连接的客户端 chan 取一项,设置到 clients 中
			h.clients[client] = true
		case client := <-h.unregister:
            // 断开连接:
            // 1. 从 clients 移除
            // 2. 关闭发送消息的 chan
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
		case message := <-h.broadcast:
            // 发送广播消息给每一个客户端
			for client := range h.clients {
				select {
                    // 成功写入消息到客户端的 send 通道
				case client.send <- message:
				default:
                    // 发送失败则剔除这个客户端
					close(client.send)
					delete(h.clients, client)
				}
			}
		}
	}
}

这个例子中使用了 chan 来做同步,这可以提高 Hub 的并发处理速度,因为不需要等待 Hubrun 方法中其他 chan 的处理。

简单来说,Hub 做了如下操作:

  1. 维护所有的客户端连接:客户端连接、断开连接等
  2. 发送广播消息

Client

Client 的源码如下:

type Client struct {
    // Hub 单例
	hub *Hub
    // 底层的 websocket 连接
	conn *websocket.Conn
    // 等待发送给客户端的消息
	send chan []byte
}

它包含了如下字段:

  1. Hub 单例(我们的 demo 中只有一个聊天室)
  2. conn 底层的 WebSocket 连接
  3. send 通道,这里保存了等待发送给这个客户端的数据

Client 中,是通过 readPump 这个方法来从客户端接收消息的:

func (c *Client) readPump() {
	defer func() {
        // 连接断开、出错等:
        // 会关闭连接,从 hub 移除这个连接
		c.hub.unregister <- c
		c.conn.Close()
	}()
	// ... 
	for {
        // 接收消息
		_, message, err := c.conn.ReadMessage()
		if err != nil {
			// ... 错误处理
			break
		}
        // 消息处理,最终放入 broadcast,准备发给所有其他在线的客户端
		message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
		c.hub.broadcast <- message
	}
}

readPump 方法做的事情很简单,它就是接收消息,然后通过 Hubbroadcast 来发给所有在线的客户端。

而发送消息会稍微复杂一点,我们来看看 writePump 的源码:

func (c *Client) writePump() {
	defer func() {
        // 连接断开、出错:关闭 WebSocket 连接
		c.conn.Close()
	}()
	for {
		select {
		case message, ok := <-c.send:
            // 控制写超时时间
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				// 连接已经被 hub 关闭了
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}

            // 获取用以发送消息的 Writer
			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
            // 发送消息
			w.Write(message)

			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
                // 将接收到的信息发送出去
				w.Write(<-c.send)
			}

            // 调用 Close 的时候,消息会被发送出去
			if err := w.Close(); err != nil {
				return
			}
		}
	}
}

虽然比读操作复杂了一点,但是也还是很好理解,它做的东西也不多:

  1. 获取用以发送消息的 Writer
  2. 获取从 hub 中接收到的其他客户端的消息,发送给当前这个客户端

具体是如何工作起来的?

  1. main 函数中创建 hub 实例
  2. 通过下面这个 serveWs 来将建立 WebSocket 连接:
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
    // 将 HTTP 连接转换为 WebSocket 连接
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
    // 客户端
	client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
    // 注册到 hub
	client.hub.register <- client

	// 发送数据到客户端的协程
	go client.writePump()
    // 从客户端接收数据的协程
	go client.readPump()
}

serveWs 中,我们在跟客户端建立起连接后,创建了两个协程,一个是从客户端接收数据的,另一个是发送消息到客户端的。

这个 demo 的作用

这个 demo 是一个比较简单的 demo,不过也包含了我们构建 WebSocket 应用的一些关键处理逻辑,比如:

  • 使用 Hub 来维持一个低层次的连接信息
  • Client 中区分读和写的协程
  • 以及一些边界情况的处理:比如连接断开、超时等

在后续的文章中,我们会基于这些已有知识去构建一个更加完善的 WebSocket 应用,今天就到此为止了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于JavaSocket重写Dubbo网络传输层

前言 我们知道&#xff0c;位于 Serialize 层上面的是负责网络传输的 Transport 层&#xff0c;它负责调用编解码器 Codec2 把要传输的对象编码后传输、再对接收到的字节序列解码。 站在客户端的角度&#xff0c;一次 RPC 调用的流程大概是这样的&#xff1a; Invoker 发起 …

JMeter请求参数Parameters,带中文或特殊字符(+/=)时,例如登录密码或者token等,需要勾选编码

以前的登录接口密码参数不包含特殊字符&#xff0c;为了安全&#xff0c;产品今天修改了需求&#xff0c;密码必须由数字&#xff0c;字母和特殊字符构成&#xff0c;之前利用JMeter接口编写的脚本报错了&#xff0c;调整了一下&#xff0c;里面踩了一点坑&#xff0c;记录下来…

AM5-DB低压备自投装置在河北冠益荣信科技公司洞庭变电站工程中的应用

摘 要&#xff1a;随着电力需求的不断增加&#xff0c;电力系统供电可靠性要求越来越高&#xff0c;许多供电系统已具备两回或多回供电线路。备用电源自动投入装置可以有效提高供电的可靠性&#xff0c;该类装置能够在工作电源因故障断开后&#xff0c;自动且迅速地将备用电源投…

SpringMVC JSON数据处理见解6

6.JSON数据处理 6.1.添加json依赖 springmvc 默认使用jackson作为json类库,不需要修改applicationContext-servlet.xml任何配置&#xff0c;只需引入以下类库springmvc就可以处理json数据&#xff1a; <!--spring-json依赖--> <dependency><groupId>com.f…

react umi/max 封装页签组件

1. models/tabs // 全局共享数据示例 import { useState } from react;const useUser () > {const [items, setItems] useState<any[]>([]); // 页签的全局Item数据const [key, setKey] useState<string>(/home); // 页签的高亮Keyreturn {items,setItems…

Alinx ZYNQ 7020 LED调试--in RAM

设置拨码开关为JTAG方式 烧写LED bit stream a. 点击“Program device”烧录程序到FPGA中&#xff08;重新上电程序就丢失了&#xff09; b. /01_led/led.runs/impl_1/led.bit 程序烧录到Flash中 ZYNQ与以往的直接烧录Flash不同&#xff0c;首先必须PS&#xff0c;然后烧…

C语言总结十二:文件操作详细总结

在操作系统中&#xff0c;为了统一对各种硬件的操作&#xff0c;简化接口&#xff0c;不同的硬件设备也都被看成一个文件。对这些文件的操作&#xff0c;等同于对磁盘上普通文件的操作。我们不去探讨硬件设备是如何被映射成文件的&#xff0c;把任意 I/O 设备&#xff0c;转换成…

边缘计算AI智能分析网关V4客流统计算法的概述

客流量统计AI算法是一种基于人工智能技术的数据分析方法&#xff0c;通过机器学习、深度学习等算法&#xff0c;实现对客流量的实时监测和统计。该算法主要基于机器学习和计算机视觉技术&#xff0c;其基本流程包括图像采集、图像预处理、目标检测、目标跟踪和客流量统计等步骤…

HTML快速上手

前腰&#xff1a;本文只是概括重要的 html 标签&#xff0c;这些标签的使用频率较高&#xff0c;更多标签相关的资源您可以跳转 Mmdn 进行深入的学习。 1.HTML 基础 就其核心而言&#xff0c;HTML 是一种相当简单的、由不同 元素 组成的标记语言&#xff0c;它可以被应用于文本…

一款基于Frida的Android- SO动态库逆向命令行工具

前言 YJ是一款基于Frida框架的款Native层逆向分析的交互式工具&#xff0c;就像在GUN-LINUX上使用GDB工具一样&#xff0c;设计YJ的灵感来自GNU-GDB调试工具&#xff0c;它通过交互命令模式轻松地向展示你想要窥探的内存数据 Frida是一个底层hook工具及框架。提供了hook工具的…

防火墙如何处理nat(私网用户访问Internet场景)

目录 私网用户访问Internet场景源NAT的两种转换方式NAT No-PAT NAPT配置思路规划 NAPT配置命令配置接口IP地址并将接口加入相应安全区域配置安全策略配置NAT地址池配置源NAT策略配置缺省路由配置黑洞路由 私网用户访问Internet场景 多个用户共享少量公网地址访问Internet的时候…

CAN记录仪在矿卡中的应用

CAN数据记录仪在矿卡中主要用于记录和监控车辆的运行数据&#xff0c;以保障安全和提高运营效率。那么就需要记录整车数据来进行车辆诊断分析&#xff0c;查找问题解决问题。 CAN数据记录仪可以记录矿卡的各种运行参数&#xff0c;如发动机转速、车速、制动状态、转向状态、油…

首届PolarDB开发者大会在京举办,阿里云李飞飞:云数据库加速迈向智能化

1月17日&#xff0c;阿里云PolarDB开发者大会在京举办&#xff0c;中国首款自研云原生数据库PolarDB发布“三层分离”新版本&#xff0c;基于智能决策实现查询性能10倍提升、节省50%成本。此外&#xff0c;阿里云全新推出数据库场景体验馆、训练营等系列新举措&#xff0c;广大…

Git项目分支管理规范

一、分支管理 创建项目时&#xff0c;会针对不同环境创建两个常设分支(也可以算主分支&#xff0c;永久不会删除) master&#xff1a;生产环境的稳定分支&#xff0c;生产环境基于该分支构建。仅用来发布新版本&#xff0c;除了从release测试分支或 hotfix-*Bug修复分支进行m…

redis数据安全(四)复制

关系数据库通常会使用一个主服务器向多个从服务器发送更新&#xff0c;并使用从服务器来处理所有读请求&#xff0c;Redis也采用了同样的方法来实现自己的复制特性&#xff0c;并将其用做扩展性能的一种手段。 一、特点&#xff1a; 1、异步复制&#xff1a;Redis默认使用的是…

Mysql 数据库DML 数据操作语言—— 对数据库表中的数据进行增删改

DML&#xff1a;数据操作语言&#xff0c;用来对数据库表中的数据进行增删改 前提&#xff0c;数据库里面有一张表&#xff0c;具体如何创建&#xff0c;请看上篇文章 1、增添数据 1.1、给指定字段增添数据 insert into tt4 (name,age) values (张三,18); 1.2、给全部字段添…

使用Markdown编辑器

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

k8s集群环境搭建以及插件安装

前置条件 终端工具MobaXterm很好用。 1、虚拟机三台&#xff08;ip按自己的网络环境相应配置&#xff09;(master/node) 节点ipk8s-master192.168.200.150k8s-node1192.168.200.151k8s-node2192.168.200.152 2、关闭防火墙(master/node) systemctl stop firewalld systemc…

【Linux 命令】tree 对目录进行树形展示

目录 1、tree 命令功能展示 2、tree 命令安装 3、tree 命令语法及其参数功能 4、终止 tree 展开树命令 1、tree 命令功能展示 在 Linux 中&#xff0c;我们使用 ll 命令对目录的展示并不太方便我们查看&#xff0c;不太清晰明了&#xff0c;所以我们可以使用 tree 命令以…

Dubbo核心功能解析

Dubbo核心功能讲解 Dubbo是一个精耕服务治理领域的框架&#xff0c;秉承了阿里一贯的大而全风格&#xff0c;和Eureka相比复杂度有不小的提高&#xff0c;这一节我们选了Registry和Remoting两个核心模块&#xff0c;从功能层面做个简单的了解(后面的章节会深入介绍底层原理) …