Skip to content

Commit 42c7d26

Browse files
author
ShiJiashuai
committed
feat: 添加可选心跳机制支持,前端定时发送 ping 消息,后端可回应 pong
1 parent 66ea3ac commit 42c7d26

7 files changed

Lines changed: 78 additions & 29 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ WebRTC/
127127

128128
## 路线图
129129

130-
- [ ] 房间成员列表与自动呼叫提示
130+
- [x] 房间成员列表与自动呼叫提示
131131
- [ ] TURN 支持(coturn)
132132
- [ ] 多人通话(Mesh / 引入 SFU 框架)
133133
- [ ] 录制与旁路推流(RTMP/RTC)

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
- [x]`HandleWS` 的循环中,只要出现读/写错误,就调用 `removeClient`,确保房间中不会残留“僵尸连接”。
5555
- [x]`removeClient` 中确认:当房间成员为空时删除该房间。
5656

57-
- [ ] **(可选)简单心跳机制**
57+
- [x] **(可选)简单心跳机制**
5858
- 前端每隔 N 秒发送一个 `type: "ping"` 的消息。
59-
- 后端可以选择忽略或原样返回,主要用于学习“保活”概念,不做复杂超时逻辑。
59+
- 后端可以选择忽略或返回一个 `type: "pong"`,主要用于学习“保活”概念,不做复杂超时逻辑。
6060

6161
### 3.2 前端(web/app.js + web/index.html)
6262

changelog/2025-12-18.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
- 后端:入口统一解析 `WS_ALLOWED_ORIGINS` 并注入 Hub;服务启动改用 `ServeMux` + `http.Server`,避免使用默认全局路由。
1313
- 后端:`writePump` 写失败时主动关闭连接,触发读循环退出并完成资源清理。
1414
- 后端:升级失败日志补充 `path`;连接结束时关闭 `send` 通道以退出写协程;`removeClient` 调整为不关闭通道,避免 `leave` 后同连接再次 `join` 引发 panic。
15+
- 后端:支持可选心跳消息 `ping`,并以 `pong` 非阻塞回应,用于学习“保活”概念。
1516

1617
- 文档:同步更新 `docs/signaling.md` 中的 Hub 结构与 `writePump` 示例代码。
1718
- 文档:更新 `README.md` 配置章节(补充 `ADDR``WS_ALLOWED_ORIGINS="*"` 用法),并同步更新 `ROADMAP.md` 阶段 1 后端任务勾选状态。
1819

1920
- 文档:同步补充 `docs/guide.md` 中 Hub 的 Origin 校验配置说明(`allowedOrigins`/`allowAllOrigins`),强调由入口解析 `WS_ALLOWED_ORIGINS` 并通过 `signal.Options` 注入。
21+
- 前端:WebSocket 信令连接增加简单心跳(定时发送 `ping`,在关闭/离开时清理定时器)。
22+
- 前端:收到 `room_members``Remote ID` 为空时,自动填入第一个非自己的成员作为“可呼叫对象提示”。
23+
- 文档:补充 `ping/pong` 消息类型与处理说明,并在 `ROADMAP.md` 中标记心跳机制已完成。

docs/guide.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ type Message struct {
9797
- `candidate`:ICE 候选,帮助打洞。
9898
- `room_members`:服务器广播的“当前房间成员列表”。
9999

100+
可选的辅助消息:
101+
102+
- `ping` / `pong`:简单心跳(前端定时发送 `ping`,服务端可选回 `pong`),用于学习“保活”概念。
103+
100104
### 2.2 Hub 与 Client
101105

102106
文件:`internal/signal/hub.go`

docs/signaling.md

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ type Message struct {
6060
- `"leave"`:离开房间请求(当前 Demo 中暂时很少手动用到);
6161
- `"offer"` / `"answer"`:SDP 交换;
6262
- `"candidate"`:ICE 候选;
63-
- `"room_members"`:服务端广播的当前房间成员列表(从服务端发给前端)。
63+
- `"room_members"`:服务端广播的当前房间成员列表(从服务端发给前端);
64+
- `"ping"`:可选心跳消息(从前端发给服务端,用于保活/学习);
65+
- `"pong"`:服务端对 `"ping"` 的可选回应(从服务端发给前端)。
6466
- `Room`:房间名,字符串。
6567
- `From`:发送方 ID(前端生成的 `myId`)。
6668
- `To`:接收方 ID,仅点对点消息(`offer/answer/candidate`)需要。
@@ -110,7 +112,7 @@ type Client struct {
110112
- `rooms["room1"]["userB"] = *Client`
111113
- 处理下列操作:
112114
- WebSocket 连接升级与关闭;
113-
- 收到 `join/leave/offer/answer/candidate` 消息并处理;
115+
- 收到 `join/leave/ping/offer/answer/candidate` 消息并处理;
114116
-`offer/answer/candidate``Room + To` 进行转发;
115117
- 在房间成员变化时,广播 `room_members` 消息。
116118

@@ -175,6 +177,11 @@ func (h *Hub) HandleWS(w http.ResponseWriter, r *http.Request) {
175177
h.addClient(client)
176178
case "leave":
177179
h.removeClient(client)
180+
case "ping":
181+
select {
182+
case client.send <- Message{Type: "pong"}:
183+
default:
184+
}
178185
case "offer", "answer", "candidate":
179186
h.forward(msg)
180187
default:
@@ -202,6 +209,7 @@ func (h *Hub) HandleWS(w http.ResponseWriter, r *http.Request) {
202209
- 根据 `msg.Type`
203210
- `join`:设置 `client.id`/`client.room`,并调用 `addClient`
204211
- `leave`:调用 `removeClient`
212+
- `ping`:可选心跳消息,服务端可回写 `pong`(非阻塞发送);
205213
- `offer/answer/candidate`:调用 `forward(msg)`,根据房间和 `To` 转发;
206214
- 其他:打印未知类型日志。
207215

@@ -268,30 +276,33 @@ func (h *Hub) removeClient(c *Client) {
268276
if c.room == "" || c.id == "" {
269277
return
270278
}
271-
if m, ok := h.rooms[c.room]; ok {
272-
if existing, ok2 := m[c.id]; ok2 {
273-
delete(m, c.id)
274-
close(existing.send)
279+
room := c.room
280+
if m, ok := h.rooms[room]; ok {
281+
if _, ok2 := m[c.id]; !ok2 {
282+
c.room = ""
283+
return
275284
}
285+
delete(m, c.id)
286+
c.room = ""
276287
if len(m) == 0 {
277-
delete(h.rooms, c.room)
278-
log.Printf("signal: room %s closed", c.room)
279-
} else {
280-
members := make([]string, 0, len(m))
281-
for id := range m {
282-
members = append(members, id)
283-
}
284-
msg := Message{
285-
Type: "room_members",
286-
Room: c.room,
287-
Members: members,
288-
}
289-
for _, cli := range m {
290-
if cli != nil && cli.conn != nil {
291-
select {
292-
case cli.send <- msg:
293-
default:
294-
}
288+
delete(h.rooms, room)
289+
log.Printf("signal: room %s closed", room)
290+
return
291+
}
292+
members := make([]string, 0, len(m))
293+
for id := range m {
294+
members = append(members, id)
295+
}
296+
msg := Message{
297+
Type: "room_members",
298+
Room: room,
299+
Members: members,
300+
}
301+
for _, cli := range m {
302+
if cli != nil && cli.conn != nil {
303+
select {
304+
case cli.send <- msg:
305+
default:
295306
}
296307
}
297308
}
@@ -302,10 +313,11 @@ func (h *Hub) removeClient(c *Client) {
302313
关键点:
303314

304315
- 同样通过互斥锁保护 `rooms`
305-
- 找到 `rooms[c.room]` 后:
306-
- 删除对应 `id`,并关闭其 `send` 通道;
316+
- 找到 `rooms[room]` 后:
317+
- 删除对应 `id`(这里不关闭 `send` 通道`send``HandleWS``defer` 中统一关闭,以结束 `writePump`
307318
- 若该房间已空,删除房间并打印“room closed”;
308319
- 若仍有其他成员:重新构建成员列表,广播 `room_members` 给房间内剩余成员。
320+
- 离开后将 `c.room` 置空,避免同一连接在 `leave` 后被 `defer` 再次 `removeClient` 时重复处理。
309321

310322
> 注意:
311323
> - 这里使用 `select { case cli.send <- msg: default: }` 非阻塞发送,避免因为某个客户端处理过慢而卡住整个 Hub。

internal/signal/hub.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ func (h *Hub) HandleWS(w http.ResponseWriter, r *http.Request) {
107107
h.addClient(client)
108108
case "leave":
109109
h.removeClient(client)
110+
case "ping":
111+
select {
112+
case client.send <- Message{Type: "pong"}:
113+
default:
114+
}
110115
case "offer", "answer", "candidate":
111116
h.forward(msg)
112117
default:

web/app.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ idEl.textContent = myId
55

66
let ws
77
let manualClose = false
8+
let pingTimer = null
89
let pc
910
let localStream
1011
let roomId
@@ -143,6 +144,22 @@ if (chatInput) {
143144
}
144145

145146
const servers = { iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }] }
147+
const pingIntervalMs = 25000
148+
149+
function startPing() {
150+
stopPing()
151+
pingTimer = setInterval(() => {
152+
if (ws && ws.readyState === WebSocket.OPEN) {
153+
ws.send(JSON.stringify({ type: 'ping', room: roomId, from: myId }))
154+
}
155+
}, pingIntervalMs)
156+
}
157+
158+
function stopPing() {
159+
if (!pingTimer) return
160+
clearInterval(pingTimer)
161+
pingTimer = null
162+
}
146163

147164
async function getMedia() {
148165
if (!localStream) {
@@ -233,6 +250,7 @@ function connectWS() {
233250
setError('')
234251
ws.send(JSON.stringify({ type: 'join', room: roomId, from: myId }))
235252
setState('joined')
253+
startPing()
236254
}
237255
ws.onmessage = async (ev) => {
238256
const msg = JSON.parse(ev.data)
@@ -255,13 +273,18 @@ function connectWS() {
255273
} else if (msg.type === 'room_members') {
256274
const list = msg.members || []
257275
renderMembers(list)
276+
if (state === 'joined' && remoteInput && !remoteInput.value.trim()) {
277+
const peer = list.find(id => id && id !== myId)
278+
if (peer) remoteInput.value = peer
279+
}
258280
}
259281
}
260282
ws.onerror = (e) => {
261283
console.error('ws error', e)
262284
setError('信令服务器连接出错')
263285
}
264286
ws.onclose = () => {
287+
stopPing()
265288
ws = null
266289
closePeerConnection()
267290
roomId = null
@@ -339,6 +362,7 @@ function leaveRoom() {
339362
renderMembers([])
340363
if (remoteInput) remoteInput.value = ''
341364
setError('')
365+
stopPing()
342366
if (ws) {
343367
try {
344368
manualClose = true

0 commit comments

Comments
 (0)