简介:面向前端开发者的Websocket自动回复消息服务端工具,用于后端接口未完成时快速搭建模拟服务,进行接口调试、数据联调与自动化测试,尤其适合前后端分离开发场景。压缩包共17个文件,大小约3.03MB,核心为可直接运行的exe程序,配套dll运行库、config与xml配置文件、SQLite数据库及使用说明PDF,安装部署简单,免去手动搭建服务器的繁琐过程。工具提供可视化界面,可创建、编辑和管理多个服务端配置,基于接口文档快速填充模拟数据,并支持一键启动服务,实现消息即时收发与自动回复;同时生成错误日志,方便定位联调问题。内置SQLite数据库支持数据持久化,日志文件可追踪收发消息;附带的更新说明和PDF文档能帮助新用户快速上手,提升前端开发与测试效率。已有383人学习使用。
1. Websocket自动回复消息服务端工具:先搞清它解决什么问题
做实时链路联调时,最让人烦躁的不是服务端挂掉,而是客户端把消息推过去之后,服务端毫无反应。这个标题描述的就是一个非常具体的工具形态:以 WebSocket 为承载的服务端程序,接收客户端连入,读取消息,再按规则把应答"自动回复"出去。它解决的问题不是服务端主动推送,而是"请求—响应"这一侧的回包,最常见的落地场景是接口 Mock、链路联调、压测前的连通性自检,以及长连接调试。适合后端、测试和运维在本地快速起一个可控回包的服务端,也适合做 WebSocket 测试工具的配套服务。先把消息模型搞清楚,代码才不会写成聊天室。
2. 自动回复服务端的协议基础与消息模型:回什么、何时回、怎么回
2.1 WebSocket 帧类型:自动回复服务端要处理的 4 种帧
WebSocket 的长连接里没有"请求行"和"状态码"的说法,一切交互都是帧。对自动回复服务端来说,最常见的出站与入站都是文本帧,但只处理文本帧不行。一个能长期开着做联调的工具服务端,至少要把操作码分清楚:文本帧负责业务消息,二进制帧留给弹幕、游戏协议这类数据,控制帧里的 Ping/Pong 负责心跳,Close 帧负责体面地断开。
这件事在 gorilla/websocket 里以常量组暴露,写服务端时直接在读写循环里可见:
// WebSocket 操作码,对应 RFC 6455 的 Opcode 字段 const ( TextMessage = 1 // 文本帧,自动回复里最常见的入站/出站类型 BinaryMessage = 2 // 二进制帧,游戏端和流数据的入站类型 CloseMessage = 8 // 服务端主动断开前一定要发它 PingMessage = 9 // 客户端心跳,服务端可自动回 Pong PongMessage = 10 // 对端 Ping 的正常应答 )这里有一个容易踩坑的细节:调用 ReadMessage 读到 PingMessage 时,gorilla/websocket 内部会自动回 Pong,并且不让这帧冒泡到业务层。所以业务代码里不要再单独处理 Ping 回 Pong,否则一个心跳会收到两份 Pong。这个设计对自动回复工具特别重要,因为心跳帧不需要走规则引擎。
四种帧的服务端处理策略可以整理成一张表:
| 帧类型 | 入站时自动回复服务端的动作 | 出站时的用途 |
|---|---|---|
| TextMessage | 走规则匹配,命中选择回包 | 业务回包的主体 |
| BinaryMessage | 按规则记录或原样回传 | 测试二进制协议 |
| PingMessage | gorilla 自动回 Pong,不进业务逻辑 | 不需要手动发 |
| CloseMessage | 读循环读到后应结束连接 | 主动断开前先发,避免 1006 |
注意:控制帧的处理标准各语言实现的细节不一致,用 gorilla/websocket 时不要手写 Pong 回复。
2.2 自动回复 vs 主动推送:两种服务端消息模式别混在一起
很多人提到 WebSocket 服务端,第一反应是"服务端向所有用户推送消息",那是 Spring 里 SimpMessagingTemplate 或游戏服务端活动公告那种场景。自动回复不是这种模型。自动回复的每一次出站都由一条入站消息触发,本质是请求—响应;主动推送则是服务端自己定节奏往外写。
这两种模式的服务端代码结构完全不同。主动推送要维护一个用户连接表,定时遍历写消息;自动回复则是"读一条、回一条",链路串行。写自动回复工具时如果脑子里想的是推送模型,很容易把连接管理和消息匹配写复杂。实际上自动回复工具只需要一个规则和一个死循环。
| 对比维度 | 自动回复(本工具) | 服务端主动推送 |
|---|---|---|
| 触发方 | 客户端发消息 | 服务端定时或事件触发 |
| 回包关系 | 一入一出(可配置延时) | 与入站消息无关 |
| 连接管理 | 简单计数即可 | 需要保存连接表、按组推送 |
| 典型场景 | Mock、联调、链路自检 | 行情推送、公告、游戏活动 |
2.3 让自动回复可配置:内容匹配、延迟回包与作用域
自动回复服务端和普通 echo 服务(收到什么回什么)的区别在于"规则"。常见做法是用一份 JSON 配置把规则外置,这样改规则时不用重新编译。一个够用的配置结构长这样:
{ "listen": ":8080", "reply_rules": [ { "match": "^ping$", "reply": "pong", "delay_ms": 0 }, { "match": "^time$", "reply": "${server_time}", "delay_ms": 100 }, { "match": "^echo:", "reply": "${message}", "delay_ms": 0 } ] }字段含义分别是:match 是匹配入站消息的正则表达式;reply 是回包模板,${message} 表示原样回传收到的消息,${server_time} 是内置变量;delay_ms 模拟慢接口,用于测试客户端的读超时和重连逻辑。设计规则时要注意:多条规则按顺序匹配,第一条命中就返回;没有命中的消息默认保持静默,不回默认错误,更贴近真实服务端的行为。
2.4 服务端关键参数:读超时、写超时、消息大小与心跳间隔
实现之前先确认四个参数,它们决定自动回复服务端在各种异常场景下的表现:
| 参数 | gorilla 默认 | 工具场景建议 | 说明 |
|---|---|---|---|
| ReadDeadline | 无 | 60s 或按规则延迟上限 | 防止半死连接长期占用 |
| WriteDeadline | 无 | 10s | 回包写不出去时尽快失败 |
| ReadBufferSize | 4096 | 4096 或按业务帧上限 | 超过后握手直接失败 |
| PingPeriod | 无 | 30s(由服务端决定) | 服务端也可以主动心跳 |
ReadDeadline 不是"读一条消息必须在这时间内完成"那么简单。它只给每次 ReadMessage 调用设置超时:超时后再次 ReadMessage 会直接返回错误,连接基本不可复用。所以自动回复服务端如果允许配置 delay_ms,ReadDeadline 要设得比最大 delay_ms 大,否则慢回包规则会把连接活活超时掉。
3. 用 gorilla/websocket 实现自动回复消息服务端的最小代码骨架
3.1 选型:gorilla/websocket 是长连接测试工具最常见的选择
自动回复服务端要的是一个协议实现完整、API 足够简单、社区用法多的库。Go 生态里常见的有三个:gorilla/websocket、gobwas/ws、nhooyr.io/websocket。三者都能完成收发消息,上手难度和社区熟悉度差别很大。
| 库 | 特点 | 适合这个工具的场景 |
|---|---|---|
| gorilla/websocket | 经典实现,示例多,gin 项目里最常出现 | 联调工具、内部服务,求快求稳 |
| gobwas/ws | 零分配、底层可定制 | 高并发网关,但 API 偏底层 |
| nhooyr.io/websocket | 新标准、对 context 友好 | 新项目可选,但老问题排查资料少 |
我一般会选 gorilla/websocket,不只是资料多,而是它的 Upgrader 把升级握手、自动 Pong、Close 帧处理都封装好了,自动回复工具的核心逻辑只需要写读循环和规则匹配。这与标题里的"工具"定位很匹配:要的是快速跑起来,而不是研究协议实现。
3.2 最小编程骨架:升级连接、读消息、按规则回消息
先实现一个不带配置文件的版本,规则硬编码在内存里,文件不到 80 行:
package main import ( "log" "net/http" "regexp" "time" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, // 作为本地联调工具放开跨域校验即可 CheckOrigin: func(r *http.Request) bool { return true }, } type Rule struct { Match string `json:"match"` Reply string `json:"reply"` DelayMs int64 `json:"delay_ms"` } func main() { rules := []Rule{ {Match: "^ping$", Reply: "pong"}, {Match: "^time$", Reply: time.Now().Format(time.RFC3339)}, {Match: "^echo:", Reply: "${message}"}, } http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("upgrade failed: %v", err) return } handleConn(conn, rules) }) log.Fatal(http.ListenAndServe(":8080", nil)) }这一步是把 HTTP 升级成 WebSocket。ReadBufferSize 和 WriteBufferSize 直接决定底层缓冲容量,1024 足以支撑绝大多数自动回复消息;CheckOrigin 在本地工具场景不校验来源,挂到公网时必须改成按域名校验。升级失败时响应会带 HTTP 错误码,不会建立一个半残的 WebSocket 连接。
接着是自动回复的核心循环:
func handleConn(conn *websocket.Conn, rules []Rule) { defer conn.Close() for { // 读文本帧或二进制帧;ping 帧由库内部自动回 pong _, msg, err := conn.ReadMessage() if err != nil { // 客户端断开、写超时、对端发 close 都会走到这里 log.Printf("read exit: %v", err) return } reply := matchReply(string(msg), rules) if reply == "" { continue // 没有匹配规则:静默等待下一帧 } if err := conn.WriteMessage(websocket.TextMessage, []byte(reply)); err != nil { log.Printf("write failed: %v", err) return } } } func matchReply(input string, rules []Rule) string { // 顺序匹配:第一条命中即返回 for _, rule := range rules { matched, _ := regexp.MatchString(rule.Match, input) if matched { return rule.Reply } } return "" }实际实现里 ${message} 这个模板变量还要做一次字符串替换,用 strings.ReplaceAll 将 rule.Reply 中的 ${message} 换成 input 即可。读写放在同一个 goroutine 的好处,是天然避免 gorilla 的并发写问题。
3.3 在 gin 里挂载同一个自动回复服务端
很多项目的 WebSocket 入口本来就在 gin 里,没必要另起端口。把上面的 handleConn 直接挂到 gin 的 handler 里:
r := gin.Default() r.GET("/ws", func(c *gin.Context) { conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { return } handleConn(conn, rules) }) r.Run(":8080")唯一要注意的地方:gin 的 handler 里不要提前设 Connection/Upgrade 相关的 Header,也不要提前读 c.Request.Body,这些操作会和 Upgrader 内部的升级流程抢数据。常见报错是websocket: request contains non-header data,或者升级后连接被 400 拒绝。
3.4 客户端 1006 与服务端关闭姿势:断线重连场景下服务端怎么配合
客户端日志里最常见的是[websocket] onclose, code: 1006, reason: '', reconnect: true。1006 是规范里的 abnormal closure,含义是连接在没有收到 Close 帧的情况下被切断。服务端进程被 kill -9、网络断、网关层空闲超时、客户端读超时,都会触发 1006。
服务端要配合客户端断线重连,核心不是"如何不触发 1006",而是把主动关闭做得体面:
func gracefulClose(conn *websocket.Conn) { // 先发关闭帧,让对端走到正常的 close 流程 _ = conn.WriteControl( websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "bye"), time.Now().Add(3*time.Second), ) // 发完再关底层连接 _ = conn.Close() }WriteControl 的第三个参数是写超时,超过 3 秒还在阻塞就直接放弃关闭帧。这样客户端收到 close 帧后会走 onclose 的完整流程,断线重连日志也更干净。1006 并不可怕,自动回复工具本来就要配合客户端测重连,服务端记录好 ReadMessage 的退出原因即可。
4. 用 Websocket King 验证自动回复服务端:连接、发包与断线排错
4.1 用 Websocket King 完成一次自动回复冒烟验证
Websocket King 是一个浏览器里的 WebSocket 客户端调试工具,对服务端联调最大的价值是免安装、自定义 Header、支持保存多个连接配置。启动第 3 章的代码后,按这个顺序做一遍冒烟验证:
- 在连接地址栏填 ws://localhost:8080/ws,点 Connect;
- 连接状态变成 Connected 后,在消息输入框发一条 ping;
- 右侧应收到 pong,时间戳几乎为 0ms;
- 再发一条 time,应收到当前服务端时间;
- 发一条不匹配规则的 xxx,应没有任何回包,连接仍然保持。
最后一步其实在验证"规则未命中默认静默"的设计。如果这里收到了一个错误提示,说明 matchReply 的返回值被当成了默认回包,需要回到 3.2 节检查。
4.2 用 curl 验证 HTTP 升级握手
有些环境的浏览器连不上本地服务,这时可以用 curl 只验证握手阶段:
curl -i --http1.1 \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ http://localhost:8080/ws返回101 Switching Protocols表示 HTTP Upgrade 成功。curl 不负责后续的数据帧,它只能证明"路由和握手没问题"。如果这里返回 400,优先排查 CheckOrigin 是否放行、gin 路由是否挂对、Upgrader 有没有被重复执行。
4.3 一个最小 Go 客户端脚本
Websocket King 能验证交互,但自动回复服务端还经常要和自动化测试一起跑。写一个 30 行的 Go 客户端,可以塞进 CI 做连通性检查:
package main import ( "log" "time" "github.com/gorilla/websocket" ) func main() { // 连接服务器;resp 在握手失败时保留 HTTP 状态码 c, resp, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil) if err != nil { log.Fatalf("dial failed, resp=%v, err=%v", resp, err) } defer c.Close() // 发一条自动回复请求 if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil { log.Fatalf("write failed: %v", err) } // 3 秒收不到回包就判定失败 c.SetReadDeadline(time.Now().Add(3 * time.Second)) _, msg, err := c.ReadMessage() if err != nil { log.Fatalf("read failed: %v", err) } log.Printf("reply: %s", msg) }SetReadDeadline 在这里是必须的,否则自动回复服务端如果因为配置错误长期不回包,客户端会一直挂在 ReadMessage 上。Dial 返回的 resp 在握手失败时能直接看到 HTTP 状态码,比看裸错误信息更有效率。
4.4 断线重连场景:1006 超时与 reconnect 数据的服务端日志判断
自动回复服务端配合断线重连测试时,客户端日志会反复出现 onclose 与 reconnect: true,这时要先判断 1006 是服务端主动断开、还是网络层断开。判断依据是服务端这一侧的 ReadMessage 退出日志:
| 客户端现象 | 服务端日志 | 结论 |
|---|---|---|
| onclose 1006,立即重连 | 无任何 read exit 日志 | 网关或防火墙层断开的,服务端甚至感知不到 |
| onclose 1006,重连前有延迟 | read exit: read tcp ... connection reset by peer | 对端(可能是客户端或中间层)主动重置 |
| 先收到关闭帧再 1006 | read exit: websocket: close 1000 (normal) | 已走正常 close 流程,1006 是客户端后续超时 |
注意最后一行说明,1000 之后的客户端日志理论上不应再出现 1006,如果出现了,多半是客户端把 close 帧和 1006 的日志顺序打反了,不是服务端问题。
5. 给自动回复服务端工具加连接数控制、写锁与耗时观测
5.1 连接数控制与并发写保护
第 3.2 节的代码一个连接一个 goroutine,没有连接数上限。联调时几十个连接没问题,但 Websocket King 这类工具开着多个标签页,服务端很快会被连接洪峰打满。常见做法是维护一个带缓冲的 channel 做信号量:
var sem = make(chan struct{}, 100) // 最多 100 个并发连接 func handleConn(conn *websocket.Conn, rules []Rule) { sem <- struct{}{} // 占用一个连接槽 defer func() { <-sem }() // 结束时释放 // ...原有读循环 }如果后面的规则允许异步回包,比如"收到 A 后 5 秒再补一条提醒",就会有两个 goroutine 同时写一个连接。gorilla/websocket 的 WriteMessage 不是并发安全的,需要自己加锁:
type replyWriter struct { mu sync.Mutex conn *websocket.Conn } func (w *replyWriter) write(msg []byte) error { w.mu.Lock() defer w.mu.Unlock() _ = w.conn.SetWriteDeadline(time.Now().Add(3 * time.Second)) return w.conn.WriteMessage(websocket.TextMessage, msg) }即使暂时不用异步回包,这个锁也应该预留。"慢回包延迟 + 客户端超时后重连"这两个场景叠加时,老连接和新连接可能同时处于服务端同一时间窗口,回包会被两个逻辑同时发起。
5.2 回包耗时与规则命中观测
工具服务端最容易忽视的就是可观测性。自动回复服务端的日志不需要很复杂,每条回包记四个值就够了:规则名、匹配输入、延迟、耗时。实现上在 matchReply 里顺手带回规则名:
start := time.Now() reply, ruleName := matchReply(string(msg), rules) elapsed := time.Since(start) log.Printf("rule=%s input=%q delay=%dms elapsed=%dus", ruleName, msg, rule.DelayMs, elapsed.Microseconds())日志输出格式直接用键值对,方便被日志采集系统抓走。不管是用 Websocket King 做人工测试,还是 CI 里的自动化检查,服务端的实际回包路径都一眼可查。配置规则之外,能控制的变量都收在这里。
本文还有配套的精品资源,点击获取