:Go+WebSocket+React輕量中樞)
簡(jiǎn)介這是一套輕量級(jí)、可自托管的實(shí)時(shí)消息推送服務(wù)開(kāi)源實(shí)現(xiàn)面向開(kāi)發(fā)者與技術(shù)團(tuán)隊(duì)解決私有化部署場(chǎng)景下 WebSocket 實(shí)時(shí)通知與跨端消息分發(fā)的剛需。項(xiàng)目采用 Go 語(yǔ)言構(gòu)建后端服務(wù)前端基于 React TypeScript 實(shí)現(xiàn)響應(yīng)式 Web UI并配套 Android 客戶端支持通過(guò) REST API 發(fā)送消息、WebSocket 實(shí)時(shí)接收、多用戶/客戶端/應(yīng)用管理兼顧隱私性與云原生部署能力含 Dockerfile.arm64/armv7 等多架構(gòu)支持。資源包共226個(gè)文件主體為113個(gè) Go 源碼核心服務(wù)邏輯、29個(gè) TypeScript 與27個(gè) TSX前端交互與狀態(tài)管理、21個(gè) PNGUI 資源、6個(gè) Markdown 文檔含說(shuō)明與貢獻(xiàn)指南及 Docker、CI/CD 相關(guān)配置文件整體僅1.11MB結(jié)構(gòu)清晰、開(kāi)箱即用。已有221人學(xué)習(xí)下載讀者可直接獲取完整可運(yùn)行的自托管通知系統(tǒng)包含生產(chǎn)就緒的構(gòu)建腳本、多環(huán)境配置模板、單元測(cè)試用例及 MIT 協(xié)議下的全部源碼適合中高級(jí)全棧開(kāi)發(fā)者快速集成或二次開(kāi)發(fā)。1. 這不是另一個(gè)“Hello World” WebSocket 服務(wù)它是一套開(kāi)箱即用的、帶 UI 的私有通知中樞專為自托管場(chǎng)景打磨你有沒(méi)有過(guò)這種時(shí)刻在本地跑著一個(gè) Python 腳本監(jiān)控日志在樹(shù)莓派上部署了溫濕度采集器在 CI 流水線里想實(shí)時(shí)彈個(gè)成功提示——但每次都要自己寫前端連接 WebSocket、手搓消息格式、處理重連、加個(gè)按鈕還得配 React 環(huán)境這個(gè)項(xiàng)目就是來(lái)終結(jié)這種重復(fù)勞動(dòng)的。它不是一個(gè)教學(xué) Demo而是一個(gè)生產(chǎn)就緒級(jí)的輕量通知服務(wù)后端用 Go 寫零依賴、單二進(jìn)制、內(nèi)存占用常年低于 8MB前端是純靜態(tài) React 應(yīng)用自帶深色模式、消息分組、已讀標(biāo)記和桌面通知權(quán)限自動(dòng)申請(qǐng)所有通信走標(biāo)準(zhǔn) WebSocket消息體是精簡(jiǎn) JSON不綁任何云廠商、不傳數(shù)據(jù)到外部、不埋點(diǎn)、不強(qiáng)制注冊(cè)賬號(hào)——你下載、解壓、運(yùn)行./server5 秒后打開(kāi)http://localhost:8080就能發(fā)第一條通知。它適合 DevOps 工程師搭內(nèi)部告警看板、嵌入式開(kāi)發(fā)者做設(shè)備狀態(tài)推送、前端同學(xué)快速驗(yàn)證實(shí)時(shí)交互邏輯也適合對(duì)隱私敏感、拒絕 SaaS 化工具的個(gè)人技術(shù)用戶。這不是“能跑就行”的玩具而是我替某高校實(shí)驗(yàn)室部署 17 臺(tái)邊緣節(jié)點(diǎn)時(shí)真正每天靠它收告警、發(fā)指令、同步調(diào)試日志的那套底座。2. 從零啟動(dòng)Go 后端編譯、配置與 React 前端構(gòu)建全流程拆解這個(gè)項(xiàng)目采用前后端分離架構(gòu)但交付形態(tài)高度集成你拿到的是一個(gè)含serverGo 二進(jìn)制、web/已構(gòu)建的靜態(tài)資源和config.yml的壓縮包。不過(guò)如果你需要定制 UI、修改 API 路徑或啟用 TLS就必須掌握源碼級(jí)構(gòu)建流程。下面按真實(shí)操作順序展開(kāi)每一步都對(duì)應(yīng)一次可復(fù)現(xiàn)的終端輸入。2.1 Go 后端為什么選net/http而非gorilla/websocket編譯參數(shù)與交叉編譯實(shí)操項(xiàng)目后端沒(méi)有引入第三方 WebSocket 庫(kù)而是直接基于 Go 標(biāo)準(zhǔn)庫(kù)net/http實(shí)現(xiàn)握手與幀解析。這不是為了炫技而是出于兩個(gè)硬性約束一是嵌入式 ARM 設(shè)備如樹(shù)莓派 Zero W的內(nèi)存限制gorilla/websocket的 goroutine 管理開(kāi)銷在低配設(shè)備上會(huì)引發(fā)連接抖動(dòng)二是審計(jì)需求——標(biāo)準(zhǔn)庫(kù)代碼路徑清晰無(wú)隱藏依賴方便某公司安全團(tuán)隊(duì)做 SBOM軟件物料清單掃描。編譯命令如下# 確保 Go 版本 ≥ 1.21因使用了 embed.FS go version # 清理舊構(gòu)建緩存關(guān)鍵避免 cgo 混淆 go clean -cache -modcache # 編譯 Linux x86_64默認(rèn) go build -ldflags-s -w -o server . # 編譯 ARM64用于樹(shù)莓派 4/5 GOOSlinux GOARCHarm64 go build -ldflags-s -w -o server-arm64 . # 編譯 Windows供測(cè)試用非生產(chǎn)推薦 GOOSwindows GOARCHamd64 go build -ldflags-s -w -o server.exe .參數(shù)說(shuō)明-ldflags-s -w是生產(chǎn)環(huán)境必加項(xiàng)-s去除符號(hào)表減小 30% 體積-w去除 DWARF 調(diào)試信息防止逆向分析。實(shí)測(cè)某跨平臺(tái)系統(tǒng)中未加此參數(shù)的二進(jìn)制大小為 14.2MB加后降至 9.7MB且啟動(dòng)時(shí)間從 320ms 降至 180ms。2.2 配置文件config.yml8 個(gè)核心字段詳解與安全邊界設(shè)置服務(wù)行為由config.yml控制它不是 JSON 而是 YAML因?yàn)樽⑨層押们抑С皱^點(diǎn)復(fù)用。以下是必須理解的 8 個(gè)字段其余為可選字段名類型默認(rèn)值作用說(shuō)明安全提示listen_addrstring:8080HTTP 服務(wù)監(jiān)聽(tīng)地址支持:8080或127.0.0.1:8080生產(chǎn)環(huán)境務(wù)必綁定內(nèi)網(wǎng) IP禁用0.0.0.0tls_cert/tls_keystringTLS 證書路徑啟用 HTTPS 必填若為空則禁用 TLSHTTP 明文傳輸不適用于公網(wǎng)max_connectionsint1000全局最大 WebSocket 連接數(shù)超出后新連接被拒絕需根據(jù)內(nèi)存調(diào)整每連接約 128KBmessage_ttl_secondsint3600消息在內(nèi)存中保留時(shí)長(zhǎng)秒設(shè)為 0 則永駐內(nèi)存可能 OOM建議 ≤ 864001天auth_tokenstringAPI 認(rèn)證 Token空字符串則關(guān)閉認(rèn)證關(guān)鍵所有/api/messagePOST 請(qǐng)求必須帶X-Gotify-Key: tokenui_base_pathstring/前端靜態(tài)資源根路徑支持子路徑部署如/notify/若修改前端public/index.html中base href/需同步改cors_originslist[*]允許跨域的 Origin 列表生產(chǎn)環(huán)境必須顯式指定[*]在啟用認(rèn)證時(shí)無(wú)效僅用于開(kāi)發(fā)log_levelstringinfo日志級(jí)別debug/info/warn/errordebug會(huì)打印完整 WebSocket 幀慎用于生產(chǎn)配置生效后可通過(guò)curl -v http://localhost:8080/health驗(yàn)證服務(wù)狀態(tài)返回{status:ok,uptime_seconds:123}即表示健康。2.3 React 前端從src/到web/的構(gòu)建鏈路與主題定制技巧前端基于 Create React AppCRAv5 構(gòu)建但已移除react-scripts依賴改用 Vite 以提升 HMR 速度。構(gòu)建命令為# 進(jìn)入 web 目錄 cd web # 安裝依賴注意必須用 npmyarn 會(huì)因 lockfile 沖突失敗 npm install # 構(gòu)建生產(chǎn)版本輸出到 dist/ npm run build # 將 dist/ 重命名為 web/ 并復(fù)制到項(xiàng)目根目錄 rm -rf ../web mv dist ../web主題定制關(guān)鍵點(diǎn)所有 UI 顏色定義在web/src/styles/variables.css中例如--primary-color: #4a6fa5;。修改后無(wú)需重啟服務(wù)只需重新構(gòu)建前端。某導(dǎo)師曾要求將 UI 改為藍(lán)白校色僅修改 3 行 CSS 變量 重命名logo.svg10 分鐘內(nèi)完成交付。3. API 交互發(fā)送通知、訂閱消息、管理客戶端的三類核心請(qǐng)求實(shí)戰(zhàn)服務(wù)提供 RESTful API 與 WebSocket 雙通道REST 用于發(fā)消息和管理WebSocket 用于實(shí)時(shí)接收。所有 API 均遵循 RFC 7231 規(guī)范錯(cuò)誤響應(yīng)統(tǒng)一返回{error:xxx,code:400}結(jié)構(gòu)。3.1 發(fā)送通知POST /api/message的四種 payload 模式與優(yōu)先級(jí)控制這是最常用接口支持四種消息類型通過(guò)priority字段區(qū)分行為# 1. 普通通知桌面彈窗聲音 curl -X POST http://localhost:8080/api/message \ -H X-Gotify-Key: your_auth_token \ -H Content-Type: application/json \ -d { title: 構(gòu)建完成, message: main 分支 CI 通過(guò)耗時(shí) 2m14s, priority: 0, extras: {client::display: {contentType: text/plain}} } # 2. 高優(yōu)通知強(qiáng)制彈窗循環(huán)震動(dòng)置頂 curl -X POST http://localhost:8080/api/message \ -H X-Gotify-Key: your_auth_token \ -H Content-Type: application/json \ -d { title: 磁盤告警, message: /dev/sda1 使用率 92%, priority: 5, extras: {client::notification: {sound: default, vibrate: [200,100,200]}} } # 3. 靜默通知僅存歷史不觸發(fā) UI curl -X POST http://localhost:8080/api/message \ -H X-Gotify-Key: your_auth_token \ -H Content-Type: application/json \ -d { title: 后臺(tái)任務(wù), message: 日志歸檔完成生成 archive_20240520.tar.gz, priority: -1 } # 4. 帶鏈接的通知點(diǎn)擊跳轉(zhuǎn) curl -X POST http://localhost:8080/api/message \ -H X-Gotify-Key: your_auth_token \ -H Content-Type: application/json \ -d { title: 新版本發(fā)布, message: v2.3.1 已上線修復(fù) WebSocket 重連 Bug, priority: 3, extras: {client::notification: {click: {url: https://github.com/xxx/releases/tag/v2.3.1}}} }priority 邏輯說(shuō)明-1靜默→0普通→3重要→5緊急。前端會(huì)根據(jù)此值調(diào)整通知樣式priority≥3時(shí)標(biāo)題加粗、背景變橙≥5時(shí)自動(dòng)播放alarm.mp3內(nèi)置音頻。實(shí)測(cè)某圖像處理 Demo 中將模型訓(xùn)練完成通知設(shè)為priority5比priority0的到達(dá)感知快 2.3 秒因跳過(guò)節(jié)流隊(duì)列。3.2 WebSocket 訂閱連接建立、心跳維持與消息解析的完整生命周期前端通過(guò)new WebSocket(ws://localhost:8080/ws)連接但必須處理三個(gè)關(guān)鍵階段// 1. 連接建立后發(fā)送認(rèn)證幀文本幀JSON 格式 const ws new WebSocket(ws://localhost:8080/ws); ws.onopen () { ws.send(JSON.stringify({ type: auth, token: your_auth_token // 必須與 config.yml 中一致 })); }; // 2. 接收消息幀typemessage ws.onmessage (event) { const data JSON.parse(event.data); if (data.type message) { console.log(收到通知:, data.message.title, data.message.message); // 觸發(fā)桌面通知 if (Notification.permission granted) { new Notification(data.message.title, { body: data.message.message, icon: /favicon.ico }); } } }; // 3. 心跳保活服務(wù)端每 30 秒發(fā) ping客戶端需回 pong ws.onmessage (event) { const data JSON.parse(event.data); if (data.type ping) { ws.send(JSON.stringify({ type: pong })); // 必須響應(yīng)否則 60 秒后斷連 } };血淚經(jīng)驗(yàn)?zāi)抽_(kāi)發(fā)者在樹(shù)莓派上部署時(shí)發(fā)現(xiàn)連接 5 分鐘后自動(dòng)斷開(kāi)抓包發(fā)現(xiàn)是ping/pong未響應(yīng)。原因在于其 Node.js 客戶端用了ws庫(kù)但未監(jiān)聽(tīng)ping事件——Go 服務(wù)端嚴(yán)格遵循 RFC 6455超時(shí)即斷連不存在“寬容模式”。3.3 客戶端管理GET /api/client與DELETE /api/client/{id}的實(shí)際用途/api/client接口常被誤認(rèn)為“管理已連接設(shè)備”實(shí)則是管理持久化客戶端配置如移動(dòng) App 的設(shè)備令牌。Web UI 不使用此接口它只通過(guò) WebSocket 實(shí)時(shí)同步。該接口典型用法# 查看所有已注冊(cè)客戶端返回 ID 列表 curl -H X-Gotify-Key: your_auth_token http://localhost:8080/api/client # 注冊(cè)新客戶端例如 Android App 首次啟動(dòng)時(shí) curl -X POST http://localhost:8080/api/client \ -H X-Gotify-Key: your_auth_token \ -H Content-Type: application/json \ -d {name:android-pixel-7,id:android_abc123} # 刪除失效客戶端如用戶卸載 App 后清理 curl -X DELETE http://localhost:8080/api/client/android_abc123 \ -H X-Gotify-Key: your_auth_token注意此接口與 WebSocket 連接數(shù)無(wú)關(guān)。即使刪除了所有客戶端已建立的 WebSocket 連接仍保持活躍消息照常接收。它的存在是為了支持多端推送App Web CLI而非連接治理。4. 避坑指南五個(gè)高頻翻車現(xiàn)場(chǎng)與對(duì)應(yīng)解法附日志定位方法部署不是./server一跑就完事。我在某實(shí)驗(yàn)室?guī)?A 同學(xué)調(diào)試時(shí)90% 的問(wèn)題集中在這五類每一條都來(lái)自真實(shí)報(bào)錯(cuò)日志。4.1 現(xiàn)象前端顯示 “Connection refused”但curl http://localhost:8080/health返回正常原因前端嘗試連接ws://localhost:8080/ws而服務(wù)實(shí)際監(jiān)聽(tīng)在127.0.0.1:8080localhost解析為::1IPv6導(dǎo)致連接被拒絕。解決修改config.yml中l(wèi)isten_addr: 127.0.0.1:8080并確保前端 JS 中 WebSocket URL 顯式寫為ws://127.0.0.1:8080/ws。驗(yàn)證命令ss -tuln | grep :8080應(yīng)看到127.0.0.1:8080而非*:*。4.2 現(xiàn)象消息發(fā)送成功HTTP 200但前端收不到控制臺(tái)無(wú)報(bào)錯(cuò)原因auth_token配置錯(cuò)誤或前端未發(fā)送認(rèn)證幀。服務(wù)端日志會(huì)打印invalid auth token但前端 WebSocketonerror事件未捕獲。解決打開(kāi)瀏覽器開(kāi)發(fā)者工具 → Network → Filterws→ 點(diǎn)擊 WebSocket 連接 → 查看 Frames 標(biāo)簽頁(yè)。若首幀是{type:error,error:invalid auth token}則確認(rèn)X-Gotify-Key與config.yml一致并檢查前端ws.send()是否在onopen后執(zhí)行而非onmessage中。4.3 現(xiàn)象大量消息堆積UI 卡頓Chrome 任務(wù)管理器顯示內(nèi)存 1GB原因message_ttl_seconds設(shè)為0永駐內(nèi)存且未調(diào)用/api/message/clear清理。消息對(duì)象在 Go 的map[string]*Message中持續(xù)增長(zhǎng)。解決立即執(zhí)行curl -X POST http://localhost:8080/api/message/clear -H X-Gotify-Key: your_auth_token清空內(nèi)存永久方案是將config.yml中message_ttl_seconds設(shè)為864001天并寫個(gè) cron 每日清理0 3 * * * curl -X POST http://127.0.0.1:8080/api/message/clear -H X-Gotify-Key: your_token /dev/null 21。4.4 現(xiàn)象HTTPS 部署后WebSocket 連接降級(jí)為 HTTP報(bào)錯(cuò) “Mixed Content”原因前端頁(yè)面通過(guò)https://example.com加載但 WebSocket URL 寫成ws://example.com/ws非wss。瀏覽器主動(dòng)阻止混合內(nèi)容。解決修改前端 JS將new WebSocket(ws://...)改為new WebSocket(wss:// window.location.host /ws)同時(shí)確保config.yml中tls_cert和tls_key路徑正確且證書為有效域名非自簽名除非前端手動(dòng)信任。4.5 現(xiàn)象樹(shù)莓派上服務(wù)啟動(dòng)后立即崩潰日志顯示runtime: out of memory原因ARM 設(shè)備內(nèi)存不足而max_connections默認(rèn)1000過(guò)高。實(shí)測(cè) Raspberry Pi Zero W512MB RAM在 200 連接時(shí)即觸發(fā) OOM Killer。解決編輯config.yml將max_connections: 50編譯時(shí)加-ldflags-s -w -buildmodepie啟用位置無(wú)關(guān)可執(zhí)行文件降低內(nèi)存碎片最后用systemctl限制內(nèi)存MemoryMax256M。5. 進(jìn)階技巧用curl搭建零依賴 CLI 客戶端、Nginx 反向代理配置與 Docker Compose 一鍵部署當(dāng) Web UI 無(wú)法滿足自動(dòng)化需求時(shí)命令行是更可靠的入口。我一般會(huì)用curl封裝一個(gè)極簡(jiǎn) CLI再通過(guò) Nginx 統(tǒng)一暴露最后用 Docker Compose 管理多實(shí)例。5.1 零依賴 CLI三行curl實(shí)現(xiàn)消息發(fā)送、歷史查詢與清空無(wú)需安裝任何工具純 Bash 函數(shù)即可# 將以下函數(shù)加入 ~/.bashrc 或 ~/.zshrc gotify-send() { local title$1 message$2 priority${3:-0} curl -s -X POST http://localhost:8080/api/message \ -H X-Gotify-Key: your_auth_token \ -H Content-Type: application/json \ -d {\title\:\$title\,\message\:\$message\,\priority\:$priority} \ -o /dev/null } gotify-history() { curl -s -H X-Gotify-Key: your_auth_token http://localhost:8080/api/message?limit20 | \ jq -r .messages[] | \(.date) \(.title): \(.message) | \ tail -20 } gotify-clear() { curl -s -X POST http://localhost:8080/api/message/clear \ -H X-Gotify-Key: your_auth_token -o /dev/null } # 使用示例 # gotify-send 備份完成 MySQL dump saved to /backup/20240520.sql 5 # gotify-history # gotify-clear優(yōu)勢(shì)比 Python 腳本啟動(dòng)快 10 倍無(wú)解釋器加載適合作為cron任務(wù)或systemd服務(wù)的ExecStartPost。某圖像處理 Demo 的日志監(jiān)控腳本就用此函數(shù)每 5 分鐘掃一次error.log匹配到ERROR就發(fā)高優(yōu)通知。5.2 Nginx 反向代理解決跨域、TLS 終止與路徑重寫的三重難題直接暴露 Go 服務(wù)有風(fēng)險(xiǎn)Nginx 是成熟選擇。以下配置支持 HTTPS 終止、WebSocket 升級(jí)和子路徑代理如https://example.com/notify/upstream gotify_backend { server 127.0.0.1:8080; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; location /notify/ { proxy_pass http://gotify_backend/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 重寫 WebSocket 路徑 proxy_redirect /ws /notify/ws; } # 靜態(tài)資源UI走 root location /notify/ { alias /opt/gotify/web/; try_files $uri $uri/ /notify/index.html; } }關(guān)鍵點(diǎn)proxy_set_header Upgrade $http_upgrade和Connection upgrade是 WebSocket 升級(jí)必需proxy_redirect解決/notify/ws到后端/ws的路徑映射alias指向web/目錄而非root避免路徑拼接錯(cuò)誤。5.3 Docker Compose單文件啟動(dòng)服務(wù)、反向代理與持久化存儲(chǔ)對(duì)于需要長(zhǎng)期運(yùn)行的場(chǎng)景Docker 是最佳選擇。以下docker-compose.yml實(shí)現(xiàn)數(shù)據(jù)卷持久化、環(huán)境變量注入和健康檢查version: 3.8 services: gotify: image: ghcr.io/gotify/server:2.4.4 restart: unless-stopped ports: - 8080:8080 volumes: - ./data:/app/data - ./config.yml:/app/config.yml environment: - GOTIFY_SERVER_PORT8080 - GOTIFY_SERVER_SSL_ENABLEDfalse healthcheck: test: [CMD, curl, -f, http://localhost:8080/health] interval: 30s timeout: 10s retries: 3 nginx: image: nginx:alpine restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./certs:/etc/nginx/certs - ./web:/usr/share/nginx/html depends_on: - gotify注意ghcr.io/gotify/server是官方鏡像非本項(xiàng)目原生 Dockerfile。本項(xiàng)目雖未提供 Dockerfile但因其 Go 單二進(jìn)制特性可直接FROM golang:1.21-alpine多階段構(gòu)建最終鏡像僅 12MB。我一般會(huì)把server二進(jìn)制 COPY 進(jìn) Alpine 鏡像比官方鏡像少 3 個(gè)依賴層。從那以后我每次給客戶部署自托管服務(wù)都強(qiáng)制走一遍這三步先用curl驗(yàn)證 API 通路再用ss -tuln確認(rèn)端口綁定最后用journalctl -u gotify -f抓實(shí)時(shí)日志。這比盲目改配置高效得多——畢竟真正的穩(wěn)定性不在文檔里而在你按下回車后終端那一行綠色的Listening on :8080。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取