Go WebSocket Chatroom:从 HTTP 登录到实时消息广播
复盘一个 Go WebSocket 学习项目:注册登录、内存会话、连接升级、消息广播、在线人数、最近历史和可选 Ollama 助手如何组成一条本地实时聊天链路。
为什么用聊天室练习 Go 和 WebSocket
聊天室把普通 Web 请求和实时连接放在同一个小项目里:用户先通过 HTTP 注册和登录,再从受保护页面建立 WebSocket 长连接,之后的消息不再依赖一次次刷新或轮询。
这条链路适合用来观察 Go Web 服务的几个基础问题:
- HTTP handler 如何处理表单和 JSON 请求。
- 密码如何哈希后保存。
- Cookie 和内存 Session 如何保护页面与 WebSocket 端点。
- WebSocket 如何从 HTTP 请求升级为长连接。
- 服务端如何维护当前连接并广播消息。
- 断线、历史消息和在线人数如何反馈给前端。
- 可选 AI 能力如何与核心聊天流程解耦。
项目定位始终是学习和本地演示。它没有经过正式并发或压力测试,也没有在线 Demo 或生产部署结果。
从 HTTP 注册到会话状态
注册和登录仍然是普通 HTTP 流程。
注册时,/api/register 接收用户名和密码,先检查用户名格式与密码长度,再使用 bcrypt 生成密码哈希。users.json 保存用户名到 bcrypt 哈希的映射,不保存明文密码。
登录时,/api/login 再用 bcrypt 比较密码。校验成功后,服务端生成随机 Session Token,把 Token 与用户名保存到进程内的 sessionStore,并通过 Cookie 返回浏览器。
Cookie 当前设置了:
HttpOnlySameSite=Lax- 根路径
- 8 小时 Max-Age
浏览器 JavaScript 不能直接读取 HttpOnly Cookie。后续 HTTP 请求和 WebSocket 握手会自动携带 Cookie,服务端再从内存 Session Map 中查找用户名。
这套机制能完成本地演示,但 Session 只在单个 Go 进程内存在。服务重启后全部失效,也没有持久化会话、设备管理、强制下线或分布式 Session。
HTTP 与 WebSocket 分别负责什么
项目没有让所有操作都走 WebSocket,而是按职责拆分:
| HTTP | WebSocket |
|---|---|
| 注册和登录 | 接收实时聊天消息 |
| 创建和清除 Session Cookie | 广播文字、emoji 和表情包 |
/api/me 查询当前用户 | 推送加入、离开和在线人数 |
| 保护聊天室 HTML 页面 | 回放最近消息 |
| 提供静态页面、样式和脚本 | 处理可选 AI 命令 |
访问 / 时,如果 Session 无效,服务端会重定向到 /login。客户端进入聊天室后,先请求 /api/me 获取当前用户名;只有这个请求成功,才继续建立 WebSocket。
WebSocket 的 /ws handler 也会独立检查 Session。即使绕过页面直接请求 /ws,没有有效 Cookie 仍会得到未登录响应。
客户端如何建立 WebSocket
客户端根据当前页面协议选择连接地址:
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
socket = new WebSocket(`${protocol}//${window.location.host}/ws`);
这样本地 HTTP 使用 ws://,未来如果页面由 HTTPS 提供,则会使用 wss://。
连接建立后,客户端启用消息输入、emoji 和发送按钮,并显示连接状态。收到消息时,会解析 JSON、更新在线人数,再根据消息类型渲染普通消息、系统消息、历史消息、AI 消息或表情包。
连接关闭后,前端最多尝试 5 次重连,延迟按指数增加并限制在 30 秒以内。重连期间输入控件会被禁用,避免把消息写入已经关闭的 Socket。
这能展示断线恢复的基本交互,但没有离线消息队列、发送确认、消息重试或去重机制。连接恢复不等于消息可靠送达。
服务端如何维护连接并广播
服务端的 hub 保存三类状态:
clients 当前 WebSocket 连接与用户名
history 最近消息
mu 保护共享状态的 Mutex
WebSocket 握手通过后,服务端会:
- 发送现有历史消息。
- 把新连接加入
clients。 - 广播用户加入系统消息。
- 广播最新在线人数。
- 循环读取该连接发送的 JSON 消息。
每条普通消息会经过类型规范化、空内容检查和 800 字符限制。表情包 URL 只能来自内置白名单,随后消息被写入历史并发送给当前 Hub 中的所有连接。
客户端断开时,defer 清理逻辑会把连接从 Map 中移除,关闭 Socket,再广播离开消息和新的在线人数。
当前广播在单个进程内直接遍历所有连接并同步写入,没有每个连接独立的发送队列、背压控制或跨节点消息总线。它适合观察广播模型,但不代表已经具备高并发聊天服务的实现。
在线人数和历史消息如何处理
在线人数就是当前 clients Map 的长度。新增或移除连接时,服务端重新计算数量,并通过 online 类型消息广播给所有客户端。
历史消息同样只保存在内存中。Hub 会记住最近 20 条:
- 普通文字
- emoji
- 表情包
- AI 消息
系统加入、离开和在线人数消息不会进入最近聊天记录。新连接升级成功后,会收到一条 history 包装消息,其中包含当前进程内的历史快照。
这意味着:
- 页面刷新或短暂重连可以看到最近消息。
- Go 进程重启后历史全部消失。
- 不同实例之间不会共享历史。
- 没有消息 ID、持久化状态、分页或送达确认。
在线人数与最近历史是单进程运行状态,不是持久化聊天记录。
为什么用户数据保存在本地 users.json
这个项目没有引入数据库。userStore 启动时读取 users.json;文件不存在或为空时,会创建一个包含空用户 Map 的 JSON 文件。注册新账号时,服务端在 Mutex 保护下更新 Map,并将完整结构写回文件。
本地文件方案的优点是:
- 不需要安装数据库。
- 可以直接完成注册与重启后的账号保留。
- 适合单机学习 bcrypt、文件权限和存储封装。
写入时文件权限设置为 0600,密码保存为 bcrypt 哈希。但这并不让它变成生产级账号存储。
本地文件没有事务、索引、迁移和备份策略,也不适合多个进程或多个实例同时读写。账号、会话和消息状态分别散落在文件与内存中,无法支持可靠的多实例生产部署。
为什么 users.json 不应提交到 Git
users.json 即使只包含用户名和密码哈希,也仍然属于本地用户数据。密码哈希不是公开示例内容,用户名也可能反映真实使用记录。
仓库整理时确认:
- 本地
users.json存在。 - 文件没有被 Git 跟踪。
.gitignore明确忽略该路径。- Git 历史中没有发现该路径。
- 仓库只保留结构化的
users.example.json演示文件。
因此,本轮文章不会读取或展示本地用户记录,也不会输出任何密码哈希。Docker Compose 使用命名 Volume 保存容器内用户文件,避免把运行数据打进源码仓库。
Ollama 如何作为可选能力接入
Ollama 不是聊天室的核心依赖。服务端通过环境变量读取本地 Ollama 地址和模型,并调用 /api/chat 的非流式接口。
当前支持两类命令:
@AI或/ai:把后面的内容作为问题发送给 Ollama。/summary:把最近文字聊天整理为 Prompt,请 Ollama 生成简短总结。
AI 请求有 60 秒超时,并在单独 goroutine 中执行。普通聊天的读取循环不需要等待 AI 返回。
如果 Ollama 请求失败、返回非成功状态或空内容,服务端会记录错误,并广播固定的“本地 AI 暂时不可用”提示。基础文字、emoji、表情包、在线人数和历史回放仍然可以继续工作。
因此,Ollama 是可选扩展,不是登录、连接或消息广播的前置条件。
当前项目的限制
这个 Demo 已经覆盖基本实时链路,但没有以下生产能力:
- 没有经过正式并发、压力、长连接稳定性或容量测试。
CheckOrigin当前允许所有 Origin,不适合直接暴露到公网。- Session 只保存在内存中,重启即失效。
- 用户数据保存在单个 JSON 文件。
- 消息历史只保留在进程内,最多 20 条。
- 没有消息持久化、确认、重试、去重和顺序保障。
- 没有房间、私聊、成员角色或细粒度权限。
- 没有验证码、登录限流、CSRF 防护、密码找回或账号审计。
- 没有内容审核、敏感词策略、举报、封禁或风控系统。
- 没有跨实例广播、服务发现、共享 Session 或消息队列。
- 没有运行监控、指标、追踪和生产部署验证。
项目不具备完整的权限、风控、内容审核和消息可靠性机制,只适合学习和本地演示。
当前没有自动化测试文件
仓库整理阶段执行了:
gofmt -d .go mod tidy -diffgo test ./...go vet ./...go build ./...docker compose config
这些检查全部通过,但 go test ./... 明确显示 [no test files]。它验证了包可以编译,不代表注册登录、Session、WebSocket 广播、重连或 Ollama 降级已经有行为测试覆盖。
报告中也明确记录:没有运行手动多用户、浏览器重连或 Ollama 检查,没有执行正式并发与压力测试。Docker 镜像本身没有构建,只验证了 Compose 配置。
从学习 Demo 到真实聊天系统还缺什么
更完整的聊天系统需要重新设计多层能力:
- 数据库中的用户、会话、房间、成员和消息模型。
- 可撤销、可过期、可跨实例共享的认证会话。
- 严格 Origin、CSRF、登录限流和账号安全策略。
- 房间权限、成员角色、封禁和管理操作。
- 消息 ID、持久化、确认、重试、去重与顺序策略。
- 每连接发送队列、背压和慢客户端处理。
- Redis、消息队列或其他跨实例广播机制。
- 内容审核、举报、风控和审计日志。
- 连接指标、错误监控、追踪和容量规划。
- 自动化单元、集成、浏览器和负载测试。
- 容器镜像、反向代理、TLS 和真实部署验证。
真实聊天系统的难点不只是“把消息广播出去”,而是用户身份、状态一致性、消息可靠性和持续运行。
本轮仓库整理修正了什么
本轮整理没有修改 Go 源码,主要收紧了文档与安全边界:
- 将 Go 版本要求校正为与
go.mod和 Dockerfile 一致的 Go 1.25+。 - 使用
go mod download和go run .作为更准确的安装、运行命令。 - 把只读检查与会生成可执行文件的
go build ./...分开说明。 - 为 Windows 和其他平台生成的根目录可执行文件增加忽略规则。
- 明确
users.json是本地用户数据,不能进入 Git。 - 补齐中英文 README 的测试、构建、截图和 MIT License 说明。
- 保留 Ollama 的可选定位,不把 AI 助手写成聊天室核心依赖。
- 明确项目没有自动化测试文件,也没有生产级安全或部署结论。
下一步改进顺序
比继续增加聊天功能更优先的顺序是:
- 为用户存储、Session 和消息限制增加 Go 单元测试。
- 为注册、登录、WebSocket 握手和基础广播增加集成测试。
- 收紧 WebSocket Origin 校验,并增加登录限流和 CSRF 防护。
- 将 Session 与消息历史迁移到可持久化、可共享的存储。
- 为每个连接增加发送队列和慢客户端处理。
- 再进行可重复的多连接和压力测试,记录明确环境与结果。
- 最后评估房间、私聊、内容审核和多实例广播。
Ollama 可以继续保持可选模块。只有基础聊天的身份、连接和消息可靠性边界稳定后,AI 总结或助手能力才值得继续扩展。
项目详情和源码可以从项目作品页继续查看。