WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议。意为:经过一次 TCP 握手就可以直接创建持久性连接,进而可实现服务端和客户端双向数据传输。websocket 的协议标识是 ws 和 wss
websocket 的应用场景:
在 websocket 没有出现之前,为了让 http 能够实现即时通信,前辈们也做了一些研究,常用的有三种方法:
HTTP 轮询(polling):在固定的时间间隔,由浏览器向服务器发起 http 请求,无论服务器中的数据有没有更新,都会给客户端作出响应。
但如果知道信息交付的精确间隔,那么轮询也是一个好的方案,但对于一些实时的数据是不能预测的,所有就会导致发出一些不必要的请求。
长轮询( long polling):客户端向服务端请求信息,并在设定的时间段内打开一个连接。服务器如果没有任何信息,会保持请求打开,直到有客户端可用的信息,或者直到指定的超时时间用完为止。
长轮询中客户端必须频繁地重连到服务器以读取服务端的信息,会增大服务端到压力。
客户端向服务端发起一个长连接请求,服务端收到请求后响应它并不断更新连接状态,以确保连接在客户端与服务端之间一直有效。服务端可以通过这个连接将数据主动推送到客户端。
但存在一个问题:每当服务器有需要交付给客户端的信息时,它就会更新响应,但是服务器从不发出完成 http 响应,从而导致连接一直打开,在这种情况下,代理和防火墙可能会缓存一个响应,就会导致信息交付的延迟增加。
以上三种方法都实现了近乎实时的通信,但都涉及 HTTP 请求和响应,当然也包含了许多附加和不必要的延迟,此外,在每一种情况下,客户端必须主动给服务器发送消息,且客户端都必须等待请求返回,才能发出后续的请求,再一次增加了延迟。
默认端口是 80 和 443, 并且握手阶段采用 HTTP 协议,因此握手的时候不容易屏蔽,能通过各种的 HTTP 代理。
以七牛 webrtc demo 为例:https://demo-rtc.qnsdk.com/
详解:
每个 WebSocket 连接都开始于一个 http 请求,这个请求和其他请求类似,但是 websocket 连接请求中包含一个特殊的首标,Upgrade:websocket,意为:客户端想将 HTTP 协议升级为 websocket 协议。如果服务端同意,则响应 Connection:Upgrade,同时 101 Switching Protocols 也表示协议切换成功,这个过程叫做初始握手。
但为了成功地完成握手,websocket 服务器必须根据客户端请求消息中的 Sec-WebSocket-Key,响应 SHA-1 的信息摘要,即:Sec-WebSocket-Accept 。其中:Sec-WebSocket-Key 是一个随机字符串,服务端接收到 Key 之后,会对其进行加密,并进行 base-64 编码,然后将结果响应给客户端;客户端将 Key 使用同样的加密算法进行加密并进行 base-64 编码,当得到的值与服务端响应的值保持一致时,表示真正的握手成功。
至此,HTTP 已经完成了它所有的工作,接下来就是完全按照 Websocket 协议进行通信。
在 webrtc 中 websocket 充当信令服务器,那何为信令服务器?信令可理解为信息的传递或者命令的执行,主要是传输用户的一些信息。在 webrtc 中如果没有信令服务器,webrtc 之间是不能够通信的。
蓝色区域表示发送端(Caller)和接收端(Callee),如果两者想要传递媒体数据,那么有两个信息必须经过信令服务器交换;
1)媒体信息:通过 SDP 协议进行交换,SDP 是一个描述多媒体连接内容的协议,其中包含了分辨率、编解码方式、格式、是否支持音频、视频等。例如,Caller 想给 Callee 发一个 H264 的视频,需要先问一下 Callee 能不能解 H264 的视频,如果可以解码,则可以通信;如果 Callee 只能解 H265 的视频,则不可通信。
2)网络信息:通常指的是 ip 地址、端口、以及数据存放地址,我们称之为 ICE,这是一个基于 offer/answer 模式解决 NAT 穿越的协议集合。在 ICE 中主要包含 STUN+TURN 主要协议。当 Callee 想要接收数据时,需要将所有的网络相关的信息传到信令服务器,信令服务器再转发给 Caller,Caller 拿到信息之后,发现处于同一个局域网,则可通信,如果不在同一个局域网,则通过 TURN 协议进行 NAT 穿越,再利用 Relay 转发,两者即可通信。
因为 TCP 的超时时间为 60s,如果要保持长连接的话,最好加一个 ping/pong 的心跳检测,就是服务端给客户端发一个 ping 的消息(绿色),客户端再给服务端发送一个 pong 的消息(红色),就是在 server 端加一个定时调用函数setInterval,即可实现
setInterval(() => {
connect.send('ping');
}, 3000);
第一步:实现服务器
安装第三方依赖库:nodejs-websocket
具体实现如下
const ws = require('nodejs-websocket')
const PORT = 3003
const TYPE_ENTER = 0
const TYPE_LEAVE = 1
const TYPE_MSG = 2
//1. 记录当前连接上来的总的用户数量
let count = 0
//2.conn每个连接到服务器的用户,都会有一个conn
const server = ws.createServer(conn => {
console.log('有用户进来')
count++
conn.userName = `用户${count}`
broadcast({
type:TYPE_ENTER,
msg:`${conn.userName}进入了聊天室`,
time:new Date().toLocaleTimeString()
})
//每当接收到用户传递过来的数据,这个text事件会被触发
conn.on('text',data =>{
console.log('接受到用户的数据',data)
broadcast({
type:TYPE_MSG,
msg:data,
time:new Date().toLocaleTimeString()
})
})
conn.on('close',() => {
console.log('连接断开了')
count--
broadcast({
type:TYPE_LEAVE,
msg:`${conn.userName}离开了聊天室`,
time:new Date().toLocaleTimeString()
})
})
conn.on('error',() => {
console.log('用户连接异常')
})
})
// 通过广播,给所有的用户发送消息
function broadcast(msg){
//server.connections:表示所有用户
server.connections.forEach(item => {
item.send(JSON.stringify(msg))
})
}
server.listen(PORT,() => {
console.log('websocket服务启动成功了,监听了端口' + PORT)
})
第二步:实现客户端
<html>
<head>
<title>在线聊天室</title>
</head>
<body>
<input type="text" placeholder="输入内容">
<button>发送请求</button>
<!--接收消息-->
<div></div>
<script>
var input = document.querySelector('input');
var button = document.querySelector('button');
var div = document.querySelector('div');
const TYPE_ENTER = 0
const TYPE_LEAVE = 1
const TYPE_MSG = 2
//创建websocket对象
var socket = new WebSocket('ws://localhost:3003');
socket.addEventListener('open',function(){
div.innerHTML = '连接服务器成功'
})
//主动给websocket服务发送消息
button.addEventListener('click',function(){
var value = input.value
socket.send(value)
input.value = ''
})
socket.addEventListener('message',function(e){
var data = JSON.parse(e.data)
var dv = document.createElement('div')
dv.innerText = data.msg +'------'+data.time
if(data.typy === TYPE_ENTER){
dv.style.color = 'green'
}else if(data.typy === TYPE_LEAVE){
dv.style.color = 'red'
}else{
dv.style.color = 'blue'
}
div.appendChild(dv)
})
socket.addEventListener('close',function(){
div.innerHTML = '服务断开链接'
})
</script>
</body>
</html>
demo 展示: