WebSocket 浅实现一个简易聊天室

WebSocket 浅实现一个简易聊天室

01 WebSocket 特点

WebSocket 协议,是全双工协议。它的特点:

  • 控制开销

  • 实时通讯

  • ws(wss)://

  • 支持文本,二进制传输

  • 基于 TCP 之下,服务端实现比较简单

02 实现想法

image-20220402175739368

03 客户端的关键实现细节

前端浏览器,本身自带有 WebSocket 对象 ,所以通过 new 的形式,就可以得到 ws 实例

  • 比如,用户第一次进入聊天室
1
2
3
4
5
6
7
8
9
10
this.ws.onopen = () => {
console.log('onopen', this.ws.readyState)
this.roomOpen = true
this.ws.send(JSON.stringify({
userId: this.userName,
userName: this.nickname,
roomId: item.roomId,
roomName: item.name,
event: 'login',
}))
  • 比如,用户界面接收其他用户的发送的消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
this.ws.onmessage = (message) => {
const data = JSON.parse(message.data)
this.onlineNum = data.num
if (data.event === 'login') {
this.msgList.push({
content: `欢迎${data.userName}进入${data.roomName}房间~`,
})
} else if (data.event === 'logout') {
console.log('logout', data)
this.msgList.push({
content: `${data.userName}离开房间`,
})
} else {
const self = this.userId === data.userId
if (self) return
this.msgList.push({
name: data.userName,
self: false,
content: data.content,
})
}
}
  • 当前用户退出界面
1
2
3
4
5
6
7
this.ws.onclose = () => {
this.removeAllTimeJob()
Toast('您已离开房间')
this.roomOpen = false
this.msgList = []
this.onlineNum = 0
}

04 服务端的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
const Websocket = require('ws')

const wss = new Websocket.Server({ port: 9001 })

// 存放所有的房间信息
const group = {}

// 心跳检测有效时间
const heartBeatTime = 50000

wss.on('connection', function (ws) {
ws.isAlive = true
ws.heartBeatTimeIntervalObj = -1
ws.heartBeatTimeoutObj = -1

// 监听心跳定时器,如果长时间没有心跳,自动断开连接
const setHeartBeatTimeout = () => {
if (ws.heartBeatTimeoutObj !== -1) {
clearTimeout(ws.heartBeatTimeoutObj)
ws.heartBeatTimeoutObj = -1
}
ws.heartBeatTimeoutObj = setTimeout(() => {
ws.close()
ws.isAlive = false
}, heartBeatTime)
}
// 重置心跳定时器
setHeartBeatTimeout()

ws.on('message', function (message) {
console.log('server receive message: ', message.toString())
const data = JSON.parse(message.toString())
// 重置心跳定时器
setHeartBeatTimeout()
// 用户初次进入聊天室
if (data.event === 'login') {
ws.enterInfo = data
}

// 接收到客户端发来的心跳消息,则重置心跳定时器
if (data.event === 'heartBeat' && data.content === 'ping') {
ws.isAlive = true

if (ws.heartBeatTimeoutObj === -1) {
clearTimeout(ws.heartBeatTimeoutObj)
ws.heartBeatTimeoutObj = -1
}
// 重置心跳定时器
setHeartBeatTimeout()

ws.send(
JSON.stringify({
event: 'heartBeat',
content: 'pong',
})
)
return
}

// 计算聊天室人数
if (typeof ws.roomId === 'undefined' && data.roomId) {
ws.roomId = data.roomId
if (typeof group[ws.roomId] === 'undefined') {
group[ws.roomId] = 1
} else {
group[ws.roomId]++
}
}

data.num = group[ws.roomId]

// 判断当前收到消息,是否为用户所在的聊天室
wss.clients.forEach((client) => {
if (client.readyState === Websocket.OPEN && client.roomId === ws.roomId) {
client.send(JSON.stringify(data))
}
})
})

// 用户离开房间
ws.on('close', function (message) {
group[ws.roomId]--
console.log('server close: ', message)

// 判断用户离开的哪个聊天室,发送退出房间的消息
wss.clients.forEach(function each(client) {
if (
client !== ws &&
client.readyState === Websocket.OPEN &&
client.roomId === ws.roomId
) {
client.send(
JSON.stringify({
...ws.enterInfo,
event: 'logout',
num: group[ws.roomId],
})
)
}
})
})
})

04 心跳机制

引入心跳机制,是因为于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态,所以需要发送一段很小的报文告诉对方“我还活着”。

image-20220402181042899

其他目的

服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线;
客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 监听心跳定时器,如果长时间没有心跳,自动断开连接
const setHeartBeatTimeout = () => {
if (ws.heartBeatTimeoutObj !== -1) {
clearTimeout(ws.heartBeatTimeoutObj)
ws.heartBeatTimeoutObj = -1
}
ws.heartBeatTimeoutObj = setTimeout(() => {
ws.close()
ws.isAlive = false
}, heartBeatTime)
}
// 重置心跳定时器
setHeartBeatTimeout()

仓库地址“黄华峰 / WebSocket-chatroom · GitLab (kkgroup.cn)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!