# websocket 的基本使用

websocket 是全双工,可以做到客户端与服务端的实时连接,在直播,即时通讯,等要求实时传输的场景下有广泛的运用。

# 一、基本使用

# 1. 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.前端
let socket = new WebSocket(url) //注意url以ws或者wss开头,而不是http或者https,当调用这个构造函数的时候会连接一下。
socket实例对象上的方法与事件:
onclose:连接关闭事件
onerror:连接失败事件
onopen:连接成功事件
onmessage:从服务器上获取数据时的事件
close:断开连接
send:发送数据
2.后端
npm install ws // 下载ws,一个支持websocket的第三方库
const WebSocket = require('ws') //引入构造函数websocket
const ws = new WebSocket.Server({port:3000}) //调用websocket的server方法并传入port参数确定监听的端口号

# 2. 连接,断连,发送,接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.前端
socket.onopen=()=>{
socket.send('hello,world') //向服务器发送消息
socket.close() //断开连接
} //连接成功后的事件。
socket.onmessage=(data)=>{
console.log(data)
}//当服务器向客户端发送消息后的事件。
2.后端
ws.on('connection',(server)=>{
//后端两种方法接收数据
server.on('message',(data)=>{
console.log(data)
console.log(data.toString('utf8')) //可以使用toString将buffer转化为字符串
}) //这时传来的数据是buffer二进制格式
server.onmessage=(data)=>{
console.log(data)
} //这时传来的数据是json格式
server.on('close',()=>{

})连接断开后的事件。
})// ws为实例对象,其中有connection的事件,每有一次连接就会调用一次,客户端可以有多次连接。同时,回调函数上有一个参数,之后的连接关闭,以及发送,接受事件都在这个参数上而不在实例对象上

# 3. 心跳机制

​ 当客户端与服务端断开连接时,是没有事件回调的,这时如果客户端往服务端发送消息时,会触发 onclose 事件,如果在 onclose 重连,那么之前发送的消息就没了。为了保障客户端与服务端一直维持连接,客户端应该时不时发送消息给服务端,如果某次消息没有回应或者说回应的时间超过了设置超时的时间便发起重新连接直至连接成功。

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
前端
let url = 'ws://localhost:3000'
let socket,heart,localConnection = false
const createWebSocket=(url)=>{
try{
socket = new WebSocket(url)
}catch{
connection(url)
}
}
const connection=(url)=>{
if(heart&&heart.live) return //如果有心脏且在跳动就没有必要连接了
if(localConnection) return //类似节流的写法,一段时间内连接一次,避免重复连接
localConnection = true
createWebSocket(url)
setTimeout(()=>{
localConnection = false
},2000)
}
createWebSocket(url)
class Heart{
constructor(time,timeOut){
this.time = time
this.timeOut = timeOut
this.live = false
this.startTime = null
this.connectionTime = null
}
start(){
let self = this
this.startTime = setTimeout(()=>{
socket.send('ping')
self.connectionTime = setTimeout(()=>{
connection(url)
},self.timeOut) //如果在设置超时时间内有回应便会在onmessage处重置心跳将这个定时器清除,如果这个定时器触发了说明了超时,连接可能失败。这时重新连接
},this.time) //每次接收到消息都会重置心跳重新启动,这里使用setTimeout,而不是setInterval。
}
reset(time1,time2){
let time3 = time1?time1:this.time
let time4 = time2?time2:this.timeOut
clearTimeout(this.connectionTime)
clearTimeout(this.timer)
return new Heart(time3,time4)
} //重置,将定时器清除同时可以更改心跳间隔以及超时时间。
}

socket.onopen=()=>{
heart = new Heart(30000,1000) //每隔30秒跳动一次,超时时间为1秒
heart.start() //开始跳动
}//连接上后开始跳动心脏
socket.onclose=()=>{
console.log('连接断开')
heart.live = false
connection(url)
}//连接断开时重新连接
socket.onmessage=(message)=>{
heart = heart.reset()
heart.start()
heart.live = true
if(message=='pong') return
}//收到消息,说明连接还在,将心脏的live改为true同时重置心脏的跳动,类似防抖的写法。

相关资料

WebSocket 加入心跳包防止自动断开连接 - 简书 (jianshu.com)

ws 中文文档 | ws js 中文教程 | 解析 | npm 中文文档 (npmdoc.org)

# 二、socket.io 的基本使用

​ socket.io 是一个第三方库,集成了 websocket, 如果客户端不支持 websocket 的话,会转为 http。同时 websocket 客户端无法访问使用 socket.io 的服务端,socket.io 的客户端也无法访问 websocket 的服务端,即如果使用 socket.io 的话,服务端和客户端都要使用 socket.io。其中还有 room 和断开重连,在制作直播间以及即时通讯软件的群聊的功能的时候会很方便。重新连接,内置心跳机制。注意,websocket 支持跨域,所以 websocket 并不是一个安全的连接方式,但是 socket.io 不支持跨域,所以在后端使用 socket.io 的时候要使用 cors 解决跨域问题。

# 1. 引用

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
前端

<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> //使用cdn引用,也可以下载到本地直接引用
<script>
主要这里的url是http或者https,如果是ws或者wss则无法连接。如果不传参数,默认请求地址与客户端同域,另外如果没有设置autoConnect为false,调用则会连接。
const socket = io(url,options)
</script>
后端

npm install socket.io //安装最新版本
npm install socket.io@version //安装指定版本
//纯socket写法
const {Server} = require('socket.io')
const io = new Server({port,{option}}) //port端口号,options配置对象
io.on('connection',(socket)=>{

})
//使用express框架写法
const express = require("express");
const { createServer } = require("http");
const { Server } = require("socket.io");

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { /* options */ });

io.on("connection", (socket) => {
// ...
});

httpServer.listen(3000);
//处理跨域
const io = new Server(httpServer,{
cors : url //允许请求的地址

})

# 2. 基本使用

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
前端
const socket = io(url)
console.log(socket.id) //undefined
socket.on('connect',()=>{
console.log(socket.id) //在建立连接之后socket上有一个由20个字符串组成的id值
socket.on('connect',()=>{

})//连接事件
socket.on('disconnect',()=>{
socket.connect()
})//断连事件,如果触发了可以重新连接
socket.emit(eventName,args)//发送,第一个参数是事件名称,后面可以传入多个参数,参数也可以是回调函数
socket.emit('1',(res)=>{
console.log(res) //res为后端调用时传入的参数
})
socket.on('1',(cb)=>{
cb('hello,world')
})//这边前端传了一个函数,后端获取后可以调用
注意这时会在前端打印这个消息,而不是后端,本质上是前端给后端传一个函数,后端把参数传过去,前端接收到后端的参数后调用。
socket.on(eventName,()=>{})//监听,第一个参数是事件名称,后面是回调函数。
socket.io的接收和发送的写法与electron和vue的eventBus一致。
socket.once(eventName,()=>{})//与on不同的是这个调用一次就寄了,
socket.off(eventName,function) //删除某个监听事件,第二个参数可选,如果不传参就删除所有监听事件。
socket.listeners(eventName) //获取某一个事件的回调函数
socket,timeout(time).emit(eventName,()=>{}) //给事件设置一个时间,如果超时则触发后面的回调函数,与心跳机制的超时类似。
socket.close() //断连
socket.connet() //连接
})

# 3. 广播,房间

1
2
3
4
5
6
7
8
9
10
后端
io.on('connection',(socket)=>{
socket.emit(eventName,args) //向本次与服务端相连的客户端发送消息
socket.broadcast.emit() //除了本次连接外的其他连接发送消息。
socket.join(room) //加入房间
socket.leave(room) //离开房间
socket.to(room).emit() //给某个房间广播
socket.to(room1).to(room2) //同时给多个房间发消息
socket.to(socket.id) //每个连接都会生成一个id值,给某个客户端发送消息。实现私人消息。
})

相关资料

Socket.IO

# 多人聊天室

实现功能,用户可以选择一个创建或者加入一个聊天室,聊天室会展示该房间的所有用户。用户可以发送消息以及文件。因为时间紧做的比较粗糙,页面简陋且还有一些功能比如给用户加头像,用户名唯一。同时还有一些 bug 比如图片在接收的时候,拿到的 url 有点问题。

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<style>
#user{
position: absolute;
background-image: linear-gradient(to top, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%);
width: 600px;
height: 300px;
left: 50%;
top: 50%;
margin-left: -300px;
margin-top: -150px;
}
#roomList{
width: 300px;
margin: 40px;
}
#userData{
width: 300px;
margin: 0 auto;
}
#join{
display: block;
width: 200px;
background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
height: 50px;
margin: 40px auto;
}
input{
margin: 4px;
padding: 4px;
}
#ul{
display: flex;
}
ul{

list-style: none;
}
li{
list-style: none;
margin: 0 4px;
}
#chat{
position: relative;
background-color: #a1c4fd;
top: 100px;
left: 200px;
width: 1600px;
height: 800px;
}
#left{
position: absolute;
background-color: #c2e9fb;
width: 60%;
height: 90%;
left: 5%;
top: 5%;
}
#right{
position: absolute;
background-color: #ffd1ff;
width: 25%;
height: 80%;
left: 70%;
top: 10%;
}
#textarea{
position: absolute;
top: 95%;
width: 80%;
left: 12%;
resize: none;
overflow: hidden;
}
#sendFile{
position: absolute;
top: 95%;
width: 10%;
text-align: center;
margin: 10px;
background-color: #ffd1ff;
}
#sendmsg{
position: absolute;
top: 95%;
left: 90%;
width: 10%;
text-align: center;
margin: 10px;
}
#webchat{
position: absolute;
background-color: antiquewhite;
top: 4%;
width: 100%;
height: 90%;
}
</style>
</head>
<body>
<div id="user" >
<div id="roomList">
<ul id="list">房间列表:</ul>
</div>
<div id="userData">
<div >用户名:<input id="userName"/></div>
<div >房间号:<input id="roomName"/></div>
</div>
<div><button id="join">加入</button></div>
</div>
<div id="chat" style="display: none;">
<div id="left">
房间号:<span id="roomNumber"></span>
<div id="webchat"></div>
<div id="sendFile">发送文件</div>
<textarea name="" id="textarea"></textarea>
<div id="sendmsg">发送</div>
</div>
<div id="right">
<ul id="userList">用户列表</ul>
</div>
</div>
<script>
let userName = document.querySelector('#userName')
let roomName = document.querySelector('#roomName')
let join = document.querySelector('#join')
let sendmsg = document.querySelector('#sendmsg')
let webchat = document.querySelector('#webchat')
let name,room
/*
加入后,登录页面消失,聊天页面显示,同时展示房间号,该房间的用户。
*/
function addmsg(value,name){
let div = document.createElement('div')
let user = document.createElement('span')
user.innerHTML = name+':'+value
div.appendChild(user)
webchat.appendChild(div)
}
join.addEventListener('click',()=>{
let user ={
userName:userName.value,
roomName:roomName.value
} //获取用户的名字以及房间号
name = user.userName
room = user.roomName
socket.emit('joinRoom',user) //告知服务器是哪个用户加入了那个房间
document.querySelector('#user').style.display = 'none' //登录页面消失
document.querySelector('#chat').style.display = 'block' //聊天页面显示
let li = document.createElement('li')
let ul = document.querySelector('#userList') //该房间的用户列表
document.querySelector('#roomNumber').innerHTML = roomName.value
li.textContent = userName.value
ul.appendChild(li)
})
sendmsg.addEventListener('click',()=>{
let textarea = document.querySelector('#textarea')
let value = textarea.value
textarea.value = ''
addmsg(value,name)
socket.emit('chatmsg',value,room,name)
})
const socket = io('http://localhost:3000')
socket.on('connect',()=>[
])
socket.on('disconnect',()=>{
socket.connect() //断连后重新连接
})
socket.on('roomUser',(list)=>{
let ul = document.querySelector('#userList')//该房间的用户列表
if(list&&list.length>=2){

console.log(list)
if(document.querySelector('#userList li')){
document.querySelector('#userList li').remove()
}
//说明加入了一个有用户的房间
list.forEach((user)=>{
let li = document.createElement('li')
li.textContent = user
ul.appendChild(li)
})
}

})
socket.on('chat',(msg,name)=>{
addmsg(msg,name)
}) //该房间的用户发送的消息
socket.on('roomList',(roomList)=>{
let roomUl = document.querySelector('#list') //登录页面的房间列表
roomList.forEach((room)=>{
let roomLi = document.createElement('li')
roomLi.textContent = room
roomUl.appendChild(roomLi)
})
})
socket.on('addNewRoom',(room)=>{
let roomUl = document.querySelector('#list') //登录页面的房间列表
let roomLi = document.createElement('li')
roomLi.textContent = room
roomUl.appendChild(roomLi)
})//有用户创建了新的房间
socket.on('roomIn',(msg,user)=>{
//当有其他用户加入房间,便会向该房间广播这个消息
let userLi = document.createElement('li')
let userUl = document.querySelector('#userList') //聊天页面的该房间下的用户列表。
userLi.textContent = user
userUl.appendChild(userLi)
})

/*
文件上传功能实现
*/

let drag = document.querySelector('#sendFile')
drag.ondragover=(e)=>{
e.preventDefault() //阻止事件默认行为
}
drag.ondrop=(e)=>{
e.preventDefault()
let file = e.dataTransfer.files[0]
let read = new FileReader()
let blob = new Blob([file],{type:file.type})
read.readAsArrayBuffer(blob)
read.onload=(e)=>{
let arrayBuffer = e.target.result
socket.emit("upload",arrayBuffer,file.type,room)
}
}
socket.on('sendFile',(file,type)=>{
let div = document.createElement('div')
let blob = new Blob([file],{type})
let read = new FileReader()
div.style.height = 100+'px'
div.style.width = 100+'px'
read.readAsDataURL(blob)
read.onload=(e)=>{
let url = e.target.result
console.log(url)
if(type.indexOf('image')!=-1){
let image = document.createElement('image')
let span = document.createElement('span')
image.src = url
span.innerHTML=name+':'+'文件'
div.appendChild(span)
div.appendChild(image)
}else{
let a = document.createElement('a')
let span = document.createElement('span')
span.innerHTML=name+':'+'文件'
a.appendChild(span)
a.href = url
a.download = true
div.appendChild(a)
}
webchat.appendChild(div)
}
})
</script>
</body>
</html>

后端
const express = require('express')
const { createServer } = require('http')
const {Server} = require('socket.io')

const app = express()
const httpServer = createServer(app)
const io = new Server(httpServer,{
cors:'127.0.0.1:5500'
})

let roomSet = new Set() //储存用户创建的房间
let roomMap = new Map() //键为房间,值为这个房间下所有用户

io.on('connection',(socket)=>{
console.log('连接成功')
//当有用户连接时,将目前已有的房间传过去
socket.emit('roomList',[...roomSet])
/*
当有用户加入房间成功后,会向服务器发送用户以及房间名。
如果是新房间则向所有用户广播新房间同时给这个房间的所有用户发送消息。
*/
socket.on('joinRoom',(data)=>{
const {userName,roomName} = data
if(!roomSet.has(roomName)){
io.emit('addNewRoom',roomName)
}//如果是新的房间,则向所有用户广播。
let userList
if(roomMap.has(roomName)){
userList = roomMap.get(roomName)
let list = roomMap.get(roomName)
list.push(userName)
roomMap.set(roomName,list)
}else{
let list = [userName]
roomMap.set(roomName,list)
}
socket.join(roomName) //让该用户加入此房间
roomSet.add(roomName) //记录用户创建的房间
socket.to(roomName).emit('roomIn',`${userName}进入${roomName}房间`,userName)
socket.emit('roomUser',userList) //用户进入房间后,向用户发送该房间的所有用户
})
socket.on('upload',(file,type,room)=>{
socket.to(room).emit('sendFile',file,type)
}) //收到文件后向该房间的用户广播
socket.on('chatmsg',(msg,room,name)=>{
socket.to(room).emit('chat',msg,name)
}) //有用户发言就给该房间的所有用户发送消息
socket.on('disconnect',()=>{
console.log('断开连接')
}) //本次连接断开连接
})
io.on('disconnect',()=>{
console.log('断开连接')
})
httpServer.listen(3000)