1、什么是WebSocket
?
概念
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的网络传输协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
特点
TCP连接,于HTTP协议兼容
双向通信,主动推送(服务器端向客户端)
无同源限制,协议标识符是ws(加密是wss)
通信方式:
单工通信
半双工通信
全双工通信
对比分析
Http:
无法监听连续变化
效率低下
浪费资源
Websocket
长连接形式,节省服务器资源和带宽,可以更好的进行实时通信
问题分析
问:长连接是否消耗服务器资源?
答:不会,Websocket设计便是为了解决服务器资源的问题,相对于Http只是建立连接,并未在逻辑上进行任何处理或者是查询数据库,所以不会造成系统资源的浪费,同时网络资源上,由于进行通信所以不会占用网络资源,也就是所说的带宽,保持长连接的状态同样不会造成网络资源的浪费,在没有发送消息的时候,整个信道处于空的状态,并不占用带宽。
相关内容
Http:超文本传输协议,是互联网上应用最为广泛的一种网络协议,是一个客户端和服
务器端请求和应答的标准(TCP),用于从 WWW 服务器传输超文本到本地浏览器的传
输协议,它可以使浏览器更加高效,使网络传输减少
ajax轮询:
方式1:设定一个定时器,无论有无结果返回,时间一到就会继续发起请求,这种轮询耗费资源,也不一定能得到想要的数据,这样的轮询是不推荐的。
方式2:
轮询就是在第一次请求的时候,如果返回数据了那么就在成功的回调里面再次发起这个请求,就像递归一样,调用本方法。
如果时间太久,失败了,同样的再次调用这个请求,也就是本函数。当然,长轮询也需要后台配合,没有数据改变的时候就不用返回,或者约定好逻辑。
2、使用
ws常用前端库
ws(实现原生协议,特点:通用、性能高、定制性强)
socket.io(向下兼容,特点:适配性强、性能一般)
学习地址
第一个websocket应用
前置:应用分客户端(client---浏览器端)和服务端(server---node作为服务端)
服务端(node)
空目录 npm init -y npm install ws
服务端 index.js const WebSocket = require('ws') const wss = new WebSocket.Server({ port: 3000 }) wss.on('connection', function connection (ws) { console.log('一个客户以连接'); })
客户端 index.html
客户端
var ws = new WebSocket('ws://localhost:3000') ws.onopen = function () { console.log(ws.readyState); ws.send('client:hello') }
注意事项:
当服务器接收到来自客户端消息的时候,出现编码问题,显示为GBK编码格式,例如:
<Buffer 63 6c 69 65 6e 74 20 3a 20 68 65 6c 6c 6f>
,这里安装依赖进行解码npm i iconv-lite
解码 console.log(msg); 解码前:<Buffer 63 6c 69 65 6e 74 20 3a 20 68 65 6c 6c 6f> console.log(iconv.decode(msg, 'gbk')); 解码后:client : hello
3、极简聊天室
心跳检测
判断客户端和服务端的连接是否牢靠、是否正常,二者可以互相判断,是一个双向的过程,服务端定时的向客户端发送消息,并且客户端及时的做出回应,保证连接正常 ,确保聊天室的在线人数实时封更新
服务端
给当前用户一个初始化变量,维护当前用户是否登录,判断是否开始心跳检测,同时初始化一个是否处于聊天室的变量-作为在线状态(给定初始值0,登录后改为1,发送心跳检测定义为404,接收到用户的反馈后修改为2,表示当前用户连接状态良好)
开启定时器,判断用户当前状态为false时主动断开用户连接,更新数据
接收用户反馈,更新当前用户状态
客户端
定义初始化函数方便重连
定义维护需要变量
定义定时器,用户检测服务端是否再规定时间内发起心跳检测
捕捉到异常,开启重连
代码
服务端(index.js)
const WebSocket = require('ws'); const iconv = require('iconv-lite'); //解析来自用户的消息 const wss = new WebSocket.Server({ port: 3000 }) let group = {} const timeInterval = 1000 //发送心跳请求间隔 wss.on('connection', function (ws) { console.log("一个客户已连接"); ws.isActive = 0 //初始连接状态 ws.isLogin = false ws.on('message', function (msg) { const msgObj = JSON.parse(iconv.decode(msg, 'gbk')) if (msgObj.event == 'enter') { ws.name = msgObj.name ws.roomId = msgObj.roomId console.log(msgObj); if (typeof group[ws.roomId] === 'undefined') { group[ws.roomId] = 1 } else { group[ws.roomId]++ } ws.isActive = 1 ws.isLogin = true } if (msgObj.event == 'heartbeat' && msgObj.message === 'pong') { ws.isActive = 1 return } //实现广播 wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && client.roomId === ws.roomId) { msgObj.name = ws.name msgObj.num = group[ws.roomId] client.send(JSON.stringify(msgObj)) } }) }) ws.on('close', function () { if (ws.name) { group[ws.roomId]-- } let msgObj = {} wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && client.roomId === ws.roomId) { msgObj.name = ws.name msgObj.num = group[ws.roomId] msgObj.event = 'exit' client.send(JSON.stringify(msgObj)) } }) }) }) setInterval(() => { wss.clients.forEach((ws) => { // 主动发送心跳检测请求 // 当客户端返回消息之后,主动设置flag为在线 if (ws.isLogin == true) { // console.log(ws.isActive); if (ws.isActive == '404') { group[ws.roomId]-- ws.isLogin = false return ws.terminate() } // console.log('---'); ws.isActive = '404' ws.send(JSON.stringify({ event: 'heartbeat', message: 'ping', num: group[ws.roomId] })) } }) }, timeInterval)
客户端 (index.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> input { outline-style: none; border: 1px solid #ccc; border-radius: 3px; padding: 6px; width: 300px; font-size: 14px; font-family: "Microsoft soft"; } input:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6) } #login { display: block; } #chatRoom { display: none } </style> </head> <body> <div id='login'> <h1>进入聊天室</h1> <span>用户名:</span><input type="text" class='msg'> <span>房间号:</span><input type="text" class='room'> <button class='btn' id="loginBtn">登录</button> </div> <div id="chatRoom"> <h1>聊天室</h1> <p>在线人数:<span id="nums">0</span></p> <input type="text" class='msg'> <button class='submitBtn' >发送消息</button> <button onclick="clsoeFun()">关闭连接</button> <ul id="content"> </ul> </div> <script> var MyName, MyRoom //数据存储,重连时使用 var isLogin = false //判断是否为登录状态 var isReload = false //当监听到连接断开时,更新为true连接成功后恢复false var content = document.getElementById('content') var nums = document.getElementById('nums') var handle //作为定时器监听服务器状态,服务器断开开始尝试重连 webScoketInit() //初始化 function webScoketInit () { var ws = new WebSocket('ws://localhost:3000') ws.onopen = function () { console.log(ws.readyState); if (isReload) { // 如果重新连接服务器,将原本数据重新传入 let obj = { event: 'enter', name:MyName, roomId:MyRoom, } ws.send(JSON.stringify(obj)) isReload = false } } ws.onmessage = (event) => { if (!isLogin) return; let obj = JSON.parse(event.data) showFun(obj) } ws.onclose = function () { console.log("close" + ws.readyState); } ws.onerror = function () { console.log('error' + ws.readyState); // 连接失败后一秒进行短线重连 setTimeout(function () { isReload = true webScoketInit() }, 1000) } function showFun (obj) { let str = '' switch (obj.event) { case 'enter': str = `欢迎${obj.name}进入聊天室` nums.innerHTML = obj.num break case 'exit': str = `${obj.name}离开进入聊天室` nums.innerHTML = obj.num break case 'auth': return case 'heartbeat': checkServer() //timeInterval+ t ws.send(JSON.stringify({ event: 'heartbeat', message: 'pong' })) return default: str = `${obj.name}:${obj.message}` } content.innerHTML += ` <li>${str}</li> ` } let submitBtn = document.querySelector(".submitBtn") console.log(submitBtn); submitBtn.addEventListener("click",submitFun) function submitFun () { let input = document.getElementsByClassName('msg')[1] let value = input.value let obj = { event: 'message', message: value } ws.send(JSON.stringify(obj)) value.value = '' } let loginBtn = document.getElementById('loginBtn') loginBtn.addEventListener('click', login) function login () { let inputName = document.getElementsByClassName('msg')[0] let inputRoomId = document.getElementsByClassName('room')[0] let roomId = inputRoomId.value let name = inputName.value MyName = name MyRoom = roomId if (name.trim() == '') { alert('请输入名称') } else { let obj = { event: 'enter', name, roomId } ws.send(JSON.stringify(obj)) inputName.value = '' inputRoomId.value = '' document.getElementById('login').style.display = 'none' document.getElementById('chatRoom').style.display = 'block' isLogin = true } } function clsoeFun () { ws.close() } /* 接收到服务器发送的心跳检测, 回复的同时开始开启检测(考虑延时t)如果服务端未再规定时间内再次发送请求, 理解为服务端异常,触发重连 */ function checkServer () { clearTimeout(handle) handle = setTimeout(function () { isReload = true webScoketInit() }, 1000 + 500) } } </script> </body> </html>
4、
socket.io
特点:
易用性:socket.io封装了服务端和客户端,使用起来非常简单方便。
内置心跳检测:短线重连
广播自动过滤当前发消息用户
跨平台:socket.io支持跨平台,这就意味着你有了更多的选择,可以在自己喜欢的平台下开发实时应用。
自适应:它会自动根据浏览器从WebSocket、AJAX长轮询、Iframe流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达IE5.5。
初步使用
服务端
空目录 npm init -y npm i socket-io npm i express
//server.js const app = require('express')(); const http = require('http').createServer(app); const io = require('socket.io')(http) app.get('/', function (req, res) { res.sendFile(__dirname + '/index.html') //创建默认打开文件 }) io.on('connection', function (socket) { console.log('a socket is connected'); socket.on('chatEvent', function (msg) { //监听客户端注册的charEvent事件,接收发送过来的消息 console.log(msg); // socket.send('server 收到') //广播,向聊天室其他成员发送消息,本人并不会接收,同时也无需接收 socket.broadcast.emit('SreverMsg', msg) //服务器注册ServerMsg事件,向所有用户广播,广播消息,实现群聊功能,自动过滤当前用户 //单独向当前用户发送消息 socket.send(msg) }) }) http.listen(3000, function () { console.log("3000端口已开启"); })
客户端
//index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> input { outline-style: none; border: 1px solid #ccc; border-radius: 3px; padding: 6px; width: 300px; font-size: 14px; font-family: "Microsoft soft"; } input:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6) } </style> </head> <input type="text" id='msg'> <button id='btn'>发送消息</button> <body> <!-- <script src="/socket.io/socket.io.js"></script> --> <script src="https://cdn.bootcdn.net/ajax/libs/socket.io/4.5.2/socket.io.js"></script> <script> var socket = io() //初始化 document.getElementById('btn').addEventListener('click', function (e) { var value = document.getElementById('msg').value socket.emit('chatEvent', value) //注册发送消息事件,向服务器发送消息 document.getElementById('msg').value = '' }) //监听服务器单独发送的消息 socket.on('message', function (msg) { console.log(msg); }) //监听服务器注册的广播事件,接收消息 socket.on('SreverMsg', function (msg) { console.log(msg); }) </script> </body> </html>
socket.js引入方式
<script src="/socket.io/socket.io.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/socket.io/4.5.2/socket.io.js"></script>