由于 TCP
协议是面向有连接的,只有在确认通信接收端存在的情况下才会发送数据,并且在连接断开时也会确认另一方是否还需要发送数据,这也是 TCP
协议的 三次握手 和 四次挥手
建立TCP连接
通过三次握手,客户端与服务器利用 SYN
报文段交换彼此的初始序列号,互相维持一个具有连接特性的状态
关闭TCP连接
通过四次挥手,确保数据传输的完整性,当被动方的数据全部传输给主动方后进行连接的关闭
性能分析
客户端和服务端进行数据交互,只需要使用 2
次网络传输,而建立和断开连接却使用了 7
次网络传输,如果发送端和接收端之间频繁发送小的数据包,则可能会出现建立和断开连接的开销比发送数据的开销还要大,比如:消息通知
除此之外,每次在发送数据前,都需要等待连接建立,也会增加数据的发送等待时间
所以针对客户端和服务端需要频繁传输数据的场景,可以使用 长连接的方式 来提升服务性能,即发送方和接收方建立连接后,保持连接不断开,下次发送数据时可以直接使用已经建立的 TCP
连接,而无需再等待
HTTP1.1协议的长连接
客户端在 HTTP
请求的 header
中添加 Connection: Keep-Alive
来告知服务器发送完响应数据后要维持这个 TCP
连接不断开,服务端会通过 Keep-Alive: timeout=30, max=100
来告知客户端,维持这个长连接的时间和当前维持连接可发送的请求数
如果客户端不再发送数据,服务端可以通过 Connection: close
断开连接
性能分析:在接收响应报文时,必须依赖请求顺序接收;如果前一个请求遇到了阻塞,后面的请求即使已经处理完毕了,仍然需要等待阻塞的请求处理完,也叫 队头阻塞
HTTP2协议的长连接
使用 TCP
的长连接,还升级为复用此长连接,解决了 队头阻塞 的问题
WebSocket协议的长连接
-
什么是WekSocket
是一种在单个
TCP
连接上进行全双工通讯的协议,服务端可以主动向客户端推送信息,客户端也可以主动向服务端发送信息 -
WebSocket特点
可以发送文本,也可以发送二进制数据
没有同源策略的限制,客户端可以与任意服务端通信
协议标识是ws(如果加密,则为wss)
无法自动重连
建立WebSocket的重连机制
-
为什么需要重连
在使用过程当中,如果出现网络断开连接的情况,服务端并不会断开连接
所以服务端会继续向客户端发送数据,就会导致数据丢失
-
心跳机制
每隔一段时间向服务端发送一个数据包,告诉服务端自己还活着
如果服务端回传一个数据包,说明服务端也活着,否则有可能是网络断开连接了,就需要重连
-
代码示例
const heartbeat = Object.assign(Object.create(null), { interval: 30000, // 每隔 30 秒发送心跳 timeout: 50000, // 服务端响应超时时间 timer: null, serverTimer: null, // 重置心跳 reset(ws) { clearTimeout(this.timer) clearTimeout(this.serverTimer) this.start(ws) }, // 开启心跳 start(ws) { this.timer = setTimeout(() => { ws.send(JSON.stringify({ userId: 5, msg: 'ping' })) this.serverTimer = setTimeout(() => { ws.close() }, this.timeout) }, this.interval) } })
当连接成功时,开启心跳;在收到消息时,重置心跳并开启下一轮检测
所以我们只需要在
onopen
和onmessage
中加入心跳检测 -
属性readyState表示的连接状态
0
对应常量CONNECTING
,表示正在建立连接,还没有完成1
对应常量OPEN
,表示连接成功建立,可以进行通信2
对应常量CLOSING
,表示连接正在进行关闭握手,即将关闭3
对应常量CLOSED
,表示连接已经关闭或者根本没有建立
消息通知
- 实现WebSocket的封装(worker/lib/socket.js)
class Socket { constructor(user_id, callback) { Object.assign(this, { interval: 30000, timeout: 50000, retryTimer: null, user_id, callback }) this.init(user_id) } init(user_id) { const { host } = location // 区分测试环境还是生产环境 const socket_url = `${host.startsWith('test') ? 'ws' : 'wss'}://${host}/ws/message/${user_id}` const ws = new WebSocket(socket_url) // 在连接成功时,开启心跳检测 ws.onopen = event => { ws.send(JSON.stringify({ userId: 5, msg: 'ping' })) this.heartbeat() } // 在收到消息时,重置心跳并开启下一轮新的检测 ws.onmessage = event => { callback(event.data) this.heartblock() } ws.onerror = event => { this.reset(ws, callback) } Object.assign(this, { ws }) } // 开启心跳 heartbeat() { const { interval, timeout, user_id, callback, ws } = this this.timer = setTimeout(() => { if (ws.readyState == 1) { ws.send(JSON.stringify({ userId, msg: 'ping' })) } this.serverTimer = setTimeout(() => { this.reset(ws, callback) }, timeout) }, interval) } // 重置心跳,并开启一个新的心跳检测 heartblock() { clearTimeout(this.timer) clearTimeout(this.serverTimer) this.heartbeat() } // 重置连接,并创建一个新的连接 reset(ws) { if (ws.readyState == 3) { const { user_id, callback } = this this.timer && clearTimeout(this.timer) this.serverTimer && clearTimeout(this.serverTimer) this.retryTimer && clearTimeout(this.retryTimer) // 由于 pending 时间无法具体估算,经调试给 100 秒,防止出现多个连接 this.retryTimer = setTimeout(() => { ws && ws.close() new Socket(user_id, callback) }, 100000) } } }
- 主线程环境(worker/share.index.js)
const workerSocket = (data = {}, callback) => { const sharedWorker = new SharedWorker(`${location.origin}/worker/socket.worker.js`, { name: 'socket_worker' }) const worker = sharedWorker.port worker.postMessage(data) worker.onmessage = ({ data: message }) => callback(message) } export default workerSocket
- 工作线程环境(worker/socket.worker.js)
importScripts(`${location.origin}/worker/lib/socket.js`) self.addEventListener('connect', ({ ports }) => { const client = ports[0] client.onmessage = ({ data }) => { new self.Socket(data, message => client.postMessage(message)) } })