Go+React自托管WebSocket通知服务:零依赖实时推送实现
简介这是一套轻量级、可自托管的实时消息推送服务开源实现面向开发者与运维人员解决私有化部署场景下 WebSocket 实时通知与跨端消息分发的需求。项目采用 Go 语言构建后端服务搭配 React TypeScript 前端 UI支持 REST API 发送消息、WebSocket 实时接收、多用户/客户端/应用管理并提供 Android 客户端及 Docker 部署能力兼顾隐私性、可控性与免费开源特性。资源包共226个文件含113个 Go 源码核心服务逻辑、29个 TypeScript 与27个 TSX 文件Web 管理界面组件、21个 PNG 图标资源、6个 Markdown 文档含说明与贡献指南、以及 Dockerfile、YML 配置、ARM 架构构建脚本等结构完整适配云原生与边缘部署。压缩包仅1.11MB精简高效。目前已有221人学习下载读者可直接获取开箱即用的自托管通知系统源码、前后端分离工程结构、多平台构建配置及 MIT 协议下的二次开发基础。1. 这不是另一个“Hello World” WebSocket 服务它用 Go 轻量启动、React 实时渲染、零依赖推送通知专治「消息发出去像石沉大海」的焦虑你有没有试过改完前端通知逻辑刷新页面却收不到测试消息或者在本地跑通了 WebSocket一上云就卡在WebSocket connection failed这个项目不是教你怎么写ws.onmessage的教学 Demo而是一个开箱即用、连docker-compose up -d都省了的可立即部署的消息中继核——它把 Go 的并发优势压进一个二进制里把 React UI 打包成静态资源嵌进服务内部所有通信走纯 WebSocket不碰 HTTP 轮询、不调第三方推送网关、不上传用户数据到任何云平台。它解决的不是「怎么实现 WebSocket」而是「怎么让通知真正抵达浏览器且你完全知道它走哪条路、卡在哪一秒」。适合正在做 IoT 设备状态看板、CI/CD 构建结果弹窗、内部运维告警面板的开发者也适合对「隐私即配置」有执念、拒绝把通知通道交给 SaaS 厂商的自托管实践者。关键词里的self-hosted不是口号——它连 SQLite 数据库都默认内置首次启动自动建表连config.yaml都给你预置了最小可行参数。2. 从源码结构看设计取舍为什么用 Go 写后端 React 做 UI而不是 Node.js 全栈这个项目没选 Node.js并非否定其生态而是直面三个真实场景痛点第一当你要同时维持 500 个长连接比如监控大屏集群Node.js 的单线程事件循环在 CPU 密集型日志解析或消息广播时容易成为瓶颈第二部署时你得确保目标服务器装了正确版本的 Node 和 npm而 Go 编译出的二进制文件自带运行时./server就能跑第三React UI 打包后是纯静态资源但若用 Express 托管你得额外配express.static路由、处理index.htmlfallback、防 MIME 类型错误——而本项目用 Go 的http.FileServer直接挂载dist/并内建了/api/*和/ws的路由隔离连nginx反向代理层都可省略。2.1 后端核心Go 中的 WebSocket 管理器与消息广播模型项目后端主干在main.go关键结构体是ClientManagertype ClientManager struct { clients map[*Client]bool broadcast chan []byte register chan *Client unregister chan *Client }这不是简单的 map 存 client而是用 channel 实现协程安全的注册/注销。每个 WebSocket 连接由*Client封装含conn *websocket.Conn和send chan []byte。ClientManager在独立 goroutine 中监听register/unregisterchannel动态更新clients映射同时监听broadcastchannel将收到的[]byte广播给所有在线clients。这种设计避免了锁竞争——注册/注销不直接操作 map而是发消息给 manager 的主循环广播也不遍历 map 加锁而是靠 channel 解耦。提示broadcastchannel 是无缓冲的这意味着当没有 client 在读取时发送会阻塞。实际部署中若需高吞吐应改为带缓冲 channel如make(chan []byte, 100)并在Client.writePump中加超时控制否则一个慢 client 可能拖垮整个广播队列。2.2 前端集成React App 如何与 Go 后端 WebSocket 对齐握手协议UI 代码在webapp/下使用 Vite React 构建。关键在于src/services/websocket.ts中的连接初始化const ws new WebSocket(ws://${window.location.host}/ws); ws.onopen () { // 发送认证消息可选 ws.send(JSON.stringify({ type: auth, token: localStorage.getItem(token) || })); }; ws.onmessage (event) { const data JSON.parse(event.data); if (data.type notification) { showNotification(data.payload); // 触发系统通知 } };注意两点第一ws://协议必须与当前页面同源window.location.host否则浏览器会直接拒绝连接——这是硬性安全策略不是 bug第二type字段是本项目约定的协议头后端Client.readPump会解析此字段决定如何分发。例如{ type: ping }会被 manager 忽略仅用于心跳保活{ type: notification, payload: { ... } }才进入广播队列。这种轻量协议比用 Socket.IO 的完整 handshake 减少了 300ms 的握手延迟。2.3 消息持久化SQLite 嵌入式存储为何比 Redis 更适合小规模自托管项目默认启用 SQLite路径为data/gotify.db而非更常见的 Redis。原因很务实Redis 需单独部署、配置密码、管理内存淘汰策略而 SQLite 文件就是普通二进制chmod 600 data/gotify.db即完成权限锁定。表结构极简CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, message TEXT NOT NULL, priority INTEGER DEFAULT 0, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP );所有写入通过 Go 的database/sql接口执行用INSERT INTO messages (title, message, priority) VALUES (?, ?, ?)参数化防止注入。重点在于——它只存历史消息不参与实时广播。WebSocket 广播走内存 mapSQLite 仅作审计与回溯用。当你在 UI 点击「清空历史」它删的是这张表不影响当前连接。这种分离让「实时性」和「可追溯性」互不干扰。3. 三步完成本地验证编译、启动、发一条消息确认链路通别急着看文档先用最短路径跑通端到端。以下步骤在 Linux/macOS 终端执行Windows 用户请用 WSL2 或 Git Bash。3.1 编译后端Go 1.19 环境下 10 秒生成单文件确保已安装 Gogo version输出 ≥ 1.19# 克隆仓库假设已下载解压到 ~/gotify-server cd ~/gotify-server # 安装依赖仅需一次 go mod download # 编译为当前系统二进制无 CGO跨平台友好 CGO_ENABLED0 go build -a -ldflags -extldflags -static -o server . # 验证生成 ls -lh server # 输出类似-rwxr-xr-x 1 user user 12M May 20 10:30 serverCGO_ENABLED0强制静态链接生成的server不依赖系统 glibc扔到任意 Linux 服务器都能跑。-ldflags中的-static进一步剥离动态库依赖。如果你在 macOS 编译想生成 Linux 二进制加GOOSlinux GOARCHamd64前缀。3.2 构建前端Vite 打包输出静态资源到 dist/前端代码在webapp/目录cd webapp # 安装依赖需 Node.js 18 npm install # 构建生产包输出到 webapp/dist/ npm run build # 验证构建结果 ls -R dist/ | head -20 # 应看到 index.html, assets/*.js, assets/*.css构建后dist/目录即为完整静态资源。后端启动时会自动加载此目录无需 Nginx。3.3 启动服务并发送首条消息curl 命令直连 API回到项目根目录启动服务# 创建数据目录SQLite 需要 mkdir -p data # 启动服务默认端口 8080日志输出到终端 ./server # 此时访问 http://localhost:8080 应看到 Web UI # 若端口被占加 -port8081 参数新开终端用 curl 发送通知模拟后端服务调用curl -X POST http://localhost:8080/message \ -H Content-Type: application/json \ -d { title: 构建成功, message: main 分支 CI 流水线通过, priority: 5 }注意/message是 HTTP API 入口它接收 JSON存入 SQLite并通过ClientManager.broadcast推送给所有 WebSocket 连接。返回200 OK表示入库成功但不保证前端已收到——这正是你要验证的下一步。打开浏览器访问http://localhost:8080点击右上角「订阅」按钮首次需允许通知权限然后再次执行上述 curl 命令。你应该在页面右下角看到系统级通知弹窗且 UI 的消息列表中新增一条。此时端到端链路已闭环。4. 避坑指南五个真实翻车现场与血泪修复方案部署不是点一下「运行」就完事。以下是我在某高校实验室部署该服务时踩过的坑按发生频率排序每条都附可复现现象和精准解法。4.1 现象浏览器控制台报WebSocket connection to ws://localhost:8080/ws failed但curl http://localhost:8080返回正常原因服务启动时未绑定0.0.0.0默认只监听127.0.0.1导致 WebSocket 协议因同源策略被浏览器拦截ws://localhost与http://127.0.0.1被视为不同源。解决启动时加-addr0.0.0.0:8080参数或修改config.yaml中server.addr为0.0.0.0:8080。切记localhost和127.0.0.1在 WebSocket 中不等价这是浏览器硬性限制。4.2 现象UI 上显示「已连接」但发消息后无通知server日志无 broadcast 记录原因前端websocket.ts中ws.onopen后未发送任何消息导致后端Client.readPump未激活读取循环Go 的conn.ReadMessage()是阻塞调用。解决在onopen回调中强制发送一条心跳例如ws.send(JSON.stringify({ type: ping }))。后端readPump收到任意消息即开始持续读取否则会挂起。4.3 现象多开两个浏览器标签页A 标签发消息B 标签收不到原因ClientManager.clientsmap 未加sync.RWMutex保护高并发注册时 map 并发写 panicGo runtime 直接 crash。解决在ClientManager结构体中添加mu sync.RWMutex所有对clients的读写操作前加m.mu.Lock()/m.mu.RLock()。原项目未加锁属典型并发缺陷必须补。4.4 现象SQLite 数据库文件被多个进程同时写入报错database is locked原因SQLite 默认 WAL 模式未开启多 goroutine 并发写入时触发锁等待超时。解决在initDB()函数中执行_, _ db.Exec(PRAGMA journal_mode WAL)并确保所有db.Exec调用都带重试逻辑如for i : 0; i 3; i { _, err : db.Exec(...); if err nil { break } time.Sleep(100*time.Millisecond) }。4.5 现象Docker 部署后UI 能打开但 WebSocket 连接 400 错误日志显示websocket: not a websocket handshake原因Nginx 或 Traefik 反向代理未透传 WebSocket 头部缺少Upgrade: websocket和Connection: Upgrade。解决在 Nginx 配置中添加location /ws { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; }若用 Docker Compose确保ports映射正确且容器内server绑定0.0.0.0。5. 生产就绪加固HTTPS、反向代理、消息优先级与离线兜底跑通只是起点。真正在公司内网或公网暴露服务必须处理四类现实问题加密传输、流量调度、消息分级、断网续传。下面给出经某物联网公司产线验证的配置组合。5.1 用 Lets Encrypt 自动签发 HTTPS让 WebSocket 走 wss://wss://不是简单加个 TLS而是要求服务端同时提供 HTTP 和 HTTPS 端口并在 ACME 协议下自动续期。项目本身不内置 ACME但可通过 Caddy 反向代理无缝实现# docker-compose.yml 片段 services: caddy: image: caddy:2 ports: - 80:80 - 443:443 volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config depends_on: - gotify gotify: build: . expose: - 8080Caddyfile内容your-domain.com { reverse_proxy http://gotify:8080 tls your-emailexample.com }Caddy 启动时自动申请证书所有http://请求 301 重定向到https://ws://自动升级为wss://。无需改一行 Go 代码这是反向代理的价值。5.2 消息优先级机制从「全量广播」到「分级触达」原项目priority字段仅存于 SQLite未参与广播逻辑。要实现「高优消息推给所有人低优只推在线用户」需扩展ClientManager.broadcast// 修改 broadcast channel 类型 type BroadcastMsg struct { Data []byte Priority int } // 在 broadcast 处理中 for client : range m.clients { if msg.Priority client.minPriority { // client.minPriority 可由前端连接时协商 select { case client.send - msg.Data: default: // 发送失败关闭连接 close(client.send) delete(m.clients, client) } } }前端连接时发送{ type: subscribe, minPriority: 3 }后端Client记录此值。这样priority: 5的告警消息推给所有 clientpriority: 1的日志消息只推给minPriority 1的 client。这是用 20 行代码实现的轻量分级。5.3 离线消息兜底当 WebSocket 断开时用 Service Worker 缓存最近 10 条React UI 已内置src/service-worker.js但默认未激活。需在main.tsx中注册if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js).then(reg { reg.onupdatefound () { const installingWorker reg.installing; installingWorker.onstatechange () { if (installingWorker.state activated) { // 激活后向 SW 发送最近 10 条消息 navigator.serviceWorker.controller?.postMessage({ type: CACHE_NOTIFICATIONS, data: getRecentNotifications(10) }); } }; }; }); }); }sw.js中监听此消息用caches.open(notifications).put(...)存储。当用户离线再打开页面SW 拦截/api/notifications请求返回缓存消息。这解决了「手机锁屏 5 分钟后打开错过关键告警」的体验断层。5.4 Docker 镜像瘦身从 1.2GB 到 12MB 的多阶段构建原始Dockerfile直接COPY . /app包含node_modules和go.mod镜像臃肿。优化为多阶段# 构建阶段 FROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 go build -a -ldflags -extldflags -static -o server . # 运行阶段 FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --frombuilder /app/server . COPY --frombuilder /app/webapp/dist ./dist EXPOSE 8080 CMD [./server]alpine基础镜像仅 5MBserver二进制 12MB总镜像大小 17MB比 Ubuntu 基础镜像小 98%。apk add ca-certificates是必须的——否则 HTTPS 请求会因证书链缺失失败。从那以后我每次部署自托管服务都强制走一遍「curl API → 查看 WebSocket 控制台 → 检查 SQLite 是否写入 → 模拟断网重连」四步验证。不是 paranoid而是发现 80% 的线上问题其实在本地./server启动时就有蛛丝马迹比如日志里出现write tcp: broken pipe说明某个 client 异常断开未清理比如 SQLite 报disk I/O error八成是data/目录权限不对。这些细节不会写在 README 里但它们才是让服务真正「稳如磐石」的颗粒度。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

MySQL实时增量同步方案:Canal+Kafka+Spark Streaming实战详解

MySQL实时增量同步方案:Canal+Kafka+Spark Streaming实战详解

写这篇东西的起因,是我去年接手了一个实时数据监测需求。业务方希望在订单数据写入MySQL后,几百毫秒内就能同步到下游分析系统,让运营侧实时看到各省份的销量和库存压力。当时我第一反应是,这不就是典型的“数据库CDC 消息队列 …

2026/10/18 6:37:53 阅读更多 →
.NET 10 Blazor WASM缓存自动清除机制详解与迁移指南

.NET 10 Blazor WASM缓存自动清除机制详解与迁移指南

做 Blazor WASM 项目这些年,我对官方的每次改版都是又期待又忐忑。期待是因为 C# 写前端的体验确实独一份,忐忑是因为每次发版之后,总有几个用户跑来反馈“还是旧数据”“页面白屏”“功能点了没反应”。排查到最后,十有八九都是浏…

2026/10/18 6:36:53 阅读更多 →
大数据安全体系建设实战:从权限管控到审计监控

大数据安全体系建设实战:从权限管控到审计监控

1. 从一次内部审计说起:为什么大数据安全越来越难做我做数据平台这一行也有十年了,早年大家谈数据安全,多半是“买套防火墙”、“装个杀毒软件”就完事。可到了现在,尤其是数据量真正跑起来之后,你会发现情况完全变了—…

2026/10/18 6:36:53 阅读更多 →

最新新闻

基于STM32H750与SOEM的EtherCAT主站开发实战解析

基于STM32H750与SOEM的EtherCAT主站开发实战解析

1. 为什么我最终选了"H750SOEM"这套组合做EtherCAT主站开发,绕不开一个问题:用哪颗芯片、跑哪套协议栈。市面上主流的方案分两派,一派走ASIC专用芯片,另一派走"MCU软件协议栈"。我这次选的是后者,…

2026/10/18 7:29:04 阅读更多 →
AI辅助学术论文写作书籍(2026.10.17)

AI辅助学术论文写作书籍(2026.10.17)

1、秒懂AI写作:让你轻松成为写作高手(2023.06) 2、玩转ChatGPT:秒变AI写作高手(2024.01) 3、挑战疑难病:SCI论文背后的故事.第二辑(2024.04) 4、AI辅助论文写作——DeepS…

2026/10/18 7:29:04 阅读更多 →
BLDC方波控制SIL测试实战:Simulink模型到代码回环验证

BLDC方波控制SIL测试实战:Simulink模型到代码回环验证

1. 这个项目到底在做什么:BLDC方波控制与SIL的定位先说一个我最近做完的项目,链路是完整的:控制对象是无刷直流电机(BLDC),控制算法是带霍尔传感器的方波控制(也叫六步换相、梯形波控制&#xf…

2026/10/18 7:29:04 阅读更多 →
hom_mat2d_rotate 的 Px 和 Py 参数

hom_mat2d_rotate 的 Px 和 Py 参数

hom_mat2d_translate 里也有 Tx, Ty,那里的 T 是 Translation(平移),表示"平移量";而 P 表示"一个点的位置"。hom_mat2d_rotate 的 Px 和 Py 参数,代表的是旋转中心(也称作…

2026/10/18 7:29:04 阅读更多 →
Vue 3 基础:模板语法、响应式、指令

Vue 3 基础:模板语法、响应式、指令

前言上一篇讲了 JavaScript、DOM 和 ES6,这一篇进入 Vue 3。Vue 是目前最主流的前端框架之一,它把"数据驱动视图"这个理念做到了极致:你只管改数据,页面自动更新。这篇覆盖 Vue 3 的基础三件套:模板语法、响…

2026/10/18 7:29:04 阅读更多 →
全球电商数据接口一键接入:技术要点与选型指南

全球电商数据接口一键接入:技术要点与选型指南

上个季度我帮一个做跨境电商的朋友整理运营流程,他手上有5个平台账号,每天早晨雷打不动要花两小时导出各后台的订单和库存,再做一轮手工汇总。有一回因为某个平台库存同步晚了半天,一单爆款直接超卖,赔了运费还被买家差…

2026/10/18 7:28:03 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →