作为新一代的 web 标准, html5 为我们提供了很多有用的东西, 比如 canvas, 本地存储 (已经分离出去了), 多媒体编程接口, 当然还有我们的 WebSocket.WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通讯(full-duplex) 的网络技术, 可以传输基于信息的文本和二进制的数据. 它于 2011 年被 IETF 定为标准 RFC 6455, 同时 WebSocket API 也被 W3C 定为标准.
WebSocket 产生的背景
黎明前的黑暗 -- 实时 web 应用的需求
web 应用的信息交互过程我想大家或多或少都知道一些, 通常是客户端通过浏览器发出一个请求, 然后服务器端在接受和审核请求后, 进行处理并将结果返回给客户端, 最后由客户端的浏览器将信息呈现出来. 这种通信机制在信息交互不是特别频繁的情况下并没有太大的问题, 但对于那些实时性要求高, 海量数据并发的应用来说, 就显得捉襟见肘了, 比如现在常见的网页游戏, 证券网站, RSS 订阅推送, 网页实时对话, 打车软件等. 通常当客户端准备呈现一些信息时, 这些信息在服务器端很有可能就已经过时了. 为了满足以上那些场景, 大佬们研究出来了一些折衷方案, 其中最常用的就是普通轮询和 Comet 技术, 而 Comet 技术实际上就是轮询的改进, 细分起来 Comet 有两种实现方式:
长轮询机制
流技术机制
长轮询机制
长轮序是对普通轮询的改进和提高. 普通轮询简单来说, 就是客户端每隔一定的时间就向服务器端发送请求, 从而以频繁请求的方式来保持客户端和服务器端的同步. 这种同步方案的最大问题是, 客户端已固定的频率发送请求时, 很可能服务端的数据没有更新, 产生很多无用的网络传输, 非常低效.
为了减少无效的网络传输, 长轮询对普通轮询进行了改进和提高, 当服务器端没有数据更新时, 链接会保持一段时间的周期, 直到数据或状态发生改变或连接时间过期, 通过这种机制我们就可以减少很多无效的客户端和服务器间的交互. 当然, 如果服务器端的数据变更非常频繁的话, 这种机制并没有有效的提高性能, 和普通轮询没有太大的区别, 且长轮询也会耗费更多的资源, 比如 CPU, 内存, 带宽等.
流技术机制
流技术机制简单来说就是客户端的页面使用一个隐藏的窗口向服务端发出一个长连接的请求. 服务器接到请求后作出回应, 并不断更新状态, 以保证客户端和服务器端的连接不过期. 通过这种机制就可以将服务器端的信息不断传向客户端, 从而保证信息的时效性. 但这种机制对于用户体验并不友好, 需要针对不同的浏览器升级不同的方案来改进用户体验, 同时这种机制如果在并发情况下发生时, 会对服务器的资源造成很大压力.
黎明的到来 --WebSocket
正是出于以上几种解决方案都有着各自的局限性, HTML5 WebSocket 也就应运而生了, 浏览器可以通过 JavaScript 借助现有的 HTTP 协议来向服务器发出 WebSocket 连接的请求, 当连接建立后, 客户端和服务器端就可以直接通过 TCP 连接来直接进行数据交换. 这是由于 websocket 协议本质上就是一个 TCP 连接, 所以在数据传输的稳定性和传输量上有所保证, 且相对于以往的轮询和 Comet 技术在性能方面也有了长足的进步:
有一点需要注意的是虽然 websocket 在通信时需要借助 HTTP, 但它本质上和 HTTP 有着很大的区别:
WebSocket 是一种双向通信协议, 在建立连接之后, WebSocket 服务端和客户端都能主动向对方发送或者接受数据.
WebSocket 需要先连接, 只有再连接后才能进行相互通信.
他们的关系其实就和这张图表现的一样, 虽然有相交的部分, 但依然有着很大的区别:
WebSocket API 的用法
由于每个服务器端的语言都有着自己的 API, 因此首先我们来讨论客户端的 API:
- // 创建一个 socket 实例:
- const socket = new WebSocket(ws://localhost:9093')
- // 打开 socket
- socket.onopen = (event) => {
- // 发送一个初始化消息
- socket.send('Hello Server!')
- // 服务器有响应数据触发
- socket.onmessage = (event) => {
- console.log('Client received a message',event)
- }
- // 出错时触发, 并且会关闭连接. 这时可以根据错误信息进行按需处理
- socket.onerror = (event) => {
- console.log('error')
- }
- // 监听 Socket 的关闭
- socket.onclose = (event) => {
- console.log('Client notified socket has closed',event)
- }
- // 关闭 Socket
- socket.close(1000, 'closing normally')
- }
是不是感觉 HTML5 websocket 所提供的 API 贼鸡儿简单, 没错, 就是这么简单. 但有几点我们需要注意:
在创建 socket 实例的时候, new WebSocket()接受两个参数, 第一个参数是 ws 或 wss, 第二个参数可以选填自定义协议, 如果是多协议, 可以是数组的方式.
在 socket.send(data)发送 data 时, data 可以是各种数据, 但只有在建立连接后才能使用.
在使用 socket.close(code,[reason])关闭连接时, code 和 reason 都是选填的.
ws 和 wss
我们在上面提到过, 创建一个 socket 实例时可以选填 ws 和 wss 来进行通信协议的确定. 他们两个其实很像 HTTP 和 HTTPS 之间的关系. 其中 ws 表示纯文本通信, 而 wss 表示使用加密信道通信(TCP+TLS). 那为啥不直接使用 HTTP 而要自定义通信协议呢? 这就要从 WebSocket 的目的说起来, WebSocket 的主要功能就是为了给浏览器中的应用与服务器端提供优化的, 双向的通信机制, 但这不代表 WebScoket 只能局限于此, 它当然还能够用于其他的场景, 这就需要他可以通过非 HTTP 协议来进行数据交换, 因此 WebSocket 也就采用了自定义 URI 模式, 以确保就算没有 HTTP, 也能进行数据交换.
ws 和 wss:
ws 协议: 普通请求, 占用与 HTTP 相同的 80 端口
wss 协议: 基于 SSL 的安全传输, 占用与 TLS 相同的 443 端口.
注: 有些 HTTP 中间设备有时候可能会不理解 WebSocket, 而导致各种诸如: 盲目连接升级, 乱修改内容等问题. 而 WSS 就很好的解决了这个问题, 它建立了一台哦端到端的安全通道, 这个通道对中间设备模糊了数据, 因此中间设备就不能感知到数据, 也就无法对请求做一些特殊处理了.
WebSocket 协议的规范
以下是一个典型的 WebSocket 发起请求到响应请求的示例:
客户端到服务端:
- GET / HTTP/1.1
- Connection:Upgrade
- Host:127.0.0.1:8088
- Origin:null
- Sec-WebSocket-Extensions:x-webkit-deflate-frame
- Sec-WebSocket-Key:puVOuWb7rel6z2AVZBKnfw==
- Sec-WebSocket-Version:13
- Upgrade:websocket
服务端到客户端:
- HTTP/1.1 101 Switching Protocols
- Connection:Upgrade
- Server:beetle websocket server
- Upgrade:WebSocket
date: Thu, 10 May 2018 07:32:25 GMT
- Access-Control-Allow-Credentials:true
- Access-Control-Allow-Headers:content-type
- Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q=
我们可以看到, WebSocket 协议和 HTTP 协议乍看并没有太大的区别, 但细看下来, 区别还是有些的, 这其实是一个握手的 http 请求, 首先请求和响应的,"Upgrade:WebSocket" 表示请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议. 从客户端到服务器端请求的信息里包含有 "Sec-WebSocket-Extensions","Sec-WebSocket-Key" 这样的头信息. 这是客户端浏览器需要向服务器端提供的握手信息, 服务器端解析这些头信息, 并在握手的过程中依据这些信息生成一个 28 位的安全密钥并返回给客户端, 以表明服务器端获取了客户端的请求, 同意创建 WebSocket 连接.
当握手成功后, 这个时候 TCP 连接就已经建立了, 客户端与服务端就能够直接通过 WebSocket 直接进行数据传递. 不过服务端还需要判断一次数据请求是什么时候开始的和什么时候是请求的结束的. 在 WebSocket 中, 由于浏览端和服务端已经打好招呼, 如我发送的内容为 utf-8 编码, 如果我发送 0x00, 表示包的开始, 如果发送了 0xFF, 就表示包的结束了. 这就解决了黏包的问题.
兼容性情况
浏览器 支持情况
Chrome Supported in version 4+
Firefox Supported in version 4+
Internet Explorer Supported in version 10+
Opera Supported in version 10+
Safari Supported in version 5+
Socket.IO
简单来说 Socket.IO 就是对 WebSocket 的封装, 并且实现了 WebSocket 的服务端代码. Socket.IO 将 WebSocket 和轮询 (Polling) 机制以及其它的实时通信方式封装成了通用的接口, 并且在服务端实现了这些实时机制的相应代码. 也就是说, WebSocket 仅仅是 Socket.IO 实现实时通信的一个子集. Socket.IO 简化了 WebSocket API, 统一了返回传输的 API. 传输种类包括:
- WebSocket
- Flash Socket
- AJAX long-polling
- AJAX multipart streaming
- IFrame
JSONP polling.
我们来看一下服务端的 Socket.IO 基本 API:
- // 引入 socke.io
- const io = require('socket.io')(80)
- // 监听客户端连接, 回调函数会传递本次连接的 socket
- io.on('connection',function(socket))
- // 给所有客户端广播消息
- io.sockets.emit('String',data)
- // 给指定的客户端发送消息
- io.sockets.socket(socketid).emit('String', data)
- // 监听客户端发送的信息
- socket.on('String',function(data))
- // 给该 socket 的客户端发送消息
- socket.emit('String', data)
另外, Socket.IO 还提供了一个 Node.JS API, 它看起来很像客户端 API. 所以我们来看看它的实际应用吧:
- // socket-server.js
- // 需要使用 HTTP 模块来启动服务器和 Socket.IO
- const http= require('http'),
- const io= require('socket.io')
- const server= http.createServer(function(req, res){
- // 发送 HTML 的 headers 和 message
- res.writeHead(200,{ 'Content-Type': 'text/html' })
- res.end('<p>Hello Socket.IO!<p>')
- });
- // 在 8080 端口启动服务器
- server.listen(8080)
- // 创建一个 Socket.IO 实例, 并把它传递给服务器
- const socket= io.listen(server)
- // 添加一个连接监听器
- socket.on('connection', function(client) {
- // 连接成功, 开始监听
- client.on('message',function(event){
- console.log('Received message from client!',event)
- })
- // 连接失败
- client.on('disconnect',function(){
- clearInterval(interval)
- console.log('Server has disconnected')
- })
- })
然后我们就可以启动这个文件了:
node socket-server.js
然后我们就可以创建一个每秒钟发送消息到客户端的发送器了;
- var interval= setInterval(function() {
- client.send('This is a message from the server,hello world' + new Date().getTime());
- },1000);
注: 需要注意的是, 如果我们想在前端使用 socket.IO, 我们需要下载这个:
npm install socket.io-client --save
然后再连接网络:
- import io from 'socket.io-client'
- const socket = io('ws://localhost:8080')
来源: https://juejin.im/post/5af5693451882530646527d1