扩展ExtendedWebSocket
ts
// 扩展WebSocket连接以包含房间和用户ID
export interface ExtendedWebSocket extends WebSocket {
userId?: string // 可选属性,表示用户的唯一标识符。这对于追踪用户的行为和管理用户状态非常重要。
room?: string // 可选属性,表示用户当前所在的房间ID。如果用户当前不在任何房间中,则此属性可以不存在或为 undefined。
rooms?: Set<string> // 可选属性,一个字符串集合,包含用户加入的所有房间的ID。使用 Set 是因为它自动确保所有元素都是唯一的,避免了重复的房间ID。
connectedAt?: Date // 可选属性,记录了 WebSocket 连接建立的时间。这可以用于计算连接持续时间或用于日志记录。
lastHeartbeatSent?: Date // 可选属性,记录了最后一次从服务器发送心跳消息的时间。心跳机制通常用于维持连接的活性,防止因为超时而被自动关闭。
lastHeartbeatReceived?: Date // 可选属性,记录了最后一次接收到客户端心跳的时间。这同样是为了监控连接的健康状况。
}
ExtendedWebSocket
中新增的这几个属性就是我们今天的主题:心跳机制
在实时通信系统中,维持客户端与服务器之间的连接是至关重要的。WebSocket作为一种在单个TCP连接上提供全双工通信通道的技术,广泛应用于需要实时数据传输的场景,例如在线游戏、聊天应用和金融市场数据更新等。然而,WebSocket连接可能因为网络波动、客户端休眠、服务器重启等原因而中断。为了解决这些问题,心跳机制和连接状态的维护显得尤为重要。
心跳机制
心跳机制是一种通过定期发送小数据包来检测和维持连接活性的方法。在ExtendedWebSocket
中,lastHeartbeatSent
和lastHeartbeatReceived
属性分别记录了最后一次发送和接收心跳的时间。这些时间戳可以用来判断连接是否仍然活跃。
服务器端可以定期向客户端发送心跳消息,如果在一定时间内没有收到客户端的响应,则认为客户端已经断开连接,服务器可以主动关闭该连接。同理,客户端也可以定期发送心跳消息给服务器,以证明自己的活跃状态。
状态维护
在多用户实时交互应用中,跟踪用户的状态、所在房间等信息是非常必要的。ExtendedWebSocket
通过引入userId
、room
和rooms
属性来实现这一功能。
userId
是用户的唯一标识符,用于区分不同的用户。room
表示用户当前所在的房间ID。这对于实现如聊天室这样的应用场景非常有用,服务器可以根据房间ID将消息广播给同一房间内的所有用户。rooms
是一个包含用户加入的所有房间ID的集合。这对于支持用户同时加入多个房间的应用场景非常重要。
connectedAt
属性记录了WebSocket连接建立的时间,这对于分析用户行为、计算连接持续时间等有一定的用途。
心跳方案设计
在基于ExtendedWebSocket
接口的实时通信系统中,设计一个有效的心跳方案是保持连接稳定性的关键。心跳机制不仅能够维持连接的活性,避免因超时而断开,还可以作为监测连接健康状况的手段。以下是一个心跳方案的设计思路,包括服务器端和客户端的实现策略。
服务器端心跳方案设计
服务器端的心跳方案主要负责定期向客户端发送心跳消息,并监测客户端对心跳消息的响应。
- 定期发送心跳消息:服务器应定期(例如每10秒)向所有已连接的客户端发送心跳消息。这可以通过设置一个定时器实现。
- 监测心跳响应 :服务器需要跟踪最后一次向客户端发送心跳消息的时间(
lastHeartbeatSent
)和最后一次收到客户端心跳响应的时间(lastHeartbeatReceived
)。如果在设定的超时时间内(例如30秒)没有收到客户端的心跳响应,则认为该连接已经不活跃,服务器应主动关闭该连接。 - 处理客户端心跳响应 :当服务器收到客户端的心跳响应时,应更新
lastHeartbeatReceived
时间戳,以标记该连接仍然活跃。
客户端心跳方案设计
客户端的心跳方案主要负责响应服务器的心跳消息,并可主动向服务器发送心跳消息。
- 响应服务器心跳消息:客户端在收到服务器的心跳消息后,应立即回复一个心跳响应消息。这确保了服务器能够正确判断客户端的活跃状态。
- 主动发送心跳消息:除了响应服务器的心跳消息,客户端也可以主动定期(例如每10秒)发送心跳消息到服务器。这有助于客户端证明自己的活跃状态,尤其是在网络条件不稳定时。
服务器端心跳方案实现
ts
// 心跳间隔时间设置为10秒
const HEARTBEAT_INTERVAL = 10000
/**
* 定期向所有客户端发送心跳消息。
*/
const sendHeartbeat = () => {
const now = new Date()
const heartbeatMessage = JSON.stringify({
type: 'heartbeat',
message: 'ping'
})
Object.values(rooms).forEach((room) => {
room.forEach((client) => {
// 检查是否收到过心跳响应,并且自上次发送心跳以来是否已超过两倍心跳间隔时间
if (
client.lastHeartbeatReceived &&
client.lastHeartbeatSent &&
client.lastHeartbeatSent.getTime() -
client.lastHeartbeatReceived.getTime() >
HEARTBEAT_INTERVAL * 2
) {
logger.warn(`${client.userId} is not ready. Close connection.`)
// 关闭死连接
client.close()
// 从房间中移除
room.delete(client)
} else if (client.readyState === WebSocket.OPEN) {
client.lastHeartbeatSent = now
// 对于活跃的连接,发送心跳消息
client.send(heartbeatMessage)
}
})
})
}
// 使用 setInterval 设置定时任务,定期执行 sendHeartbeat 函数发送心跳
setInterval(sendHeartbeat, HEARTBEAT_INTERVAL)
// 处理客户端心跳响应
export const handleHeartbeat = (socket: ExtendedWebSocket) => {
socket.lastHeartbeatReceived = new Date() // 更新收到心跳的时间
logger.info(`Heartbeat received from ${socket.userId} in room ${socket.room}`)
}
这段代码展示了在基于WebSocket的实时通信系统中,如何实现心跳机制以维持连接的活性和监控连接的健康状态。心跳机制通过定期发送小数据包(心跳消息)来实现。代码分为两个主要部分:定期发送心跳消息的sendHeartbeat
函数和处理客户端心跳响应的handleHeartbeat
函数。
定期发送心跳消息 (sendHeartbeat
)
- 心跳间隔设置 :心跳消息的发送间隔被设置为10秒(
HEARTBEAT_INTERVAL = 10000
),这意味着服务器每10秒向所有客户端发送一次心跳消息。 - 遍历客户端 :通过遍历
rooms
对象中的所有房间及其内的客户端,服务器能够遍历到所有当前连接的客户端。rooms
对象看起来是以房间ID为键、客户端集合(Set
)为值的结构。 - 检查心跳响应:对于每个客户端,代码首先检查自上次发送心跳消息以来是否已经超过了两倍的心跳间隔时间,并且没有收到客户端的心跳响应。如果是这样,服务器将认为这是一个"死连接",随即关闭该连接,并从其所在房间中移除。
- 发送心跳消息 :对于状态为
OPEN
的WebSocket连接(活跃连接),服务器更新lastHeartbeatSent
为当前时间,并发送心跳消息给客户端。心跳消息是一个JSON字符串,包含类型type
为heartbeat
和消息message
为ping
的对象。
处理客户端心跳响应 (handleHeartbeat
)
当客户端收到心跳消息并响应后,服务器端的handleHeartbeat
函数被调用。
- 更新心跳响应时间 :此函数更新
socket.lastHeartbeatReceived
为当前时间,表示服务器最后一次成功接收到该客户端的心跳响应的时间。 - 日志记录 :记录一条日志信息,指出从哪个用户(
userId
)以及哪个房间(room
)接收到了心跳响应。这有助于监控系统的运行状况和用户活跃度。
写在最后
到目前为止,我们通过实现心跳机制,确保了WebSocket连接的活性和健康状态,同时也为系统引入了周期性的负载。虽然这种方法在一定程度上增加了服务器的负载(类似于自我实施的每10秒一次的DDoS攻击),但它是维持实时通信系统稳定性和可靠性的关键手段。通过定期清理不活跃的连接,我们不仅优化了资源的使用[手动狗头],还提高了系统的整体性能和响应能力。
确实有点问题,我们在下一章修复它