文章目录
一、 WebSocket简介
WebSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接,这使得Web应用程序可以更加实时地传递数据。WebSocket协议最初由W3C开发,并于2011年成为标准
总结共3点
- WebSocket可以在浏览器里使用
- 支持双向通信
- 使用很简单
- 单工:简单的说就是一方只能发信息,另一方则只能收信息,通信是单向的,类似传呼机。
- 半双工:比单工先进一点,就是双方都能发信息,但同一时间则只能一方发信息,类似对讲机。
- 全双工:比半双工再先进一点,就是双方不仅都能发信息,而且能够同时发送,类似电话。
1.为什么需要 WebSocket
因为 HTTP传统的 HTTP 协议是基于请求-响应模型的,客户端发送请求,服务器处理请求后再发送响应。通信只能由客户端发起。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。
例如:
聊天室。
在线游戏:需要实时互动的多人游戏。
实时通知和更新:如股票行情、体育比分、新闻推送, 实时监控系统等。
协作应用:如文档共同编辑、实时数据分析工具。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,WebSocket 就是这样发明的。
WebSocket 通过在客户端和服务器之间保持一个长连接,使得服务器可以在有新数据时立即推送给客户端,无需客户端频繁地发起请求。
2.和http的区别
-
HTTP是单向的,在长连接中,每一次请求和每一次响应都会去携带大量的请求头
-
WebSocket是双向的,在第一次通过HTTP协议建立了TCP全双工通信协议后,每次服务器和浏览器之间交互就不用去携带请求头进行交互了,减少了网络开销和提高了运行效率
-
WebSocket是需要浏览器和服务器握手进行链接
-
HTTP是浏览器像服务器发起的链接,服务器没有预先准备
HTTP和WebSocket在数据格式上存在一些区别。
1.HTTP格式- 请求格式:HTTP请求由请求行、请求头部和请求体组成。请求行包含请求方法、URI和HTTP协议版本;请求头部包含各种头部字段,如Host、User-Agent、Content-Type等;请求体包含实际的请求数据。
- 响应格式:HTTP响应由状态行、响应头部和响应体组成。状态行包含状态码和状态消息;响应头部包含各种头部字段,如Content-Type、Content-Length等;响应体包含实际的响应数据。
2.WebSocket格式 - 握手格式:WebSocket在握手阶段使用HTTP协议进行协商,因此握手阶段的数据格式与HTTP请求和响应相同。
- 数据帧格式:在建立WebSocket连接后,双方之间传输的数据使用数据帧进行封装。数据帧包含了控制帧和数据帧两种类型。
- 控制帧:用于控制连接的开启、关闭和错误处理等,如握手确认、连接关闭等。
- 数据帧:用于实际的数据传输,可以是文本帧或二进制帧。数据帧中包含了有效载荷(Payload),即实际传输的数据。
总结起来,HTTP和WebSocket在握手阶段的数据格式相同,都是基于HTTP的请求和响应格式。而在建立连接后,WebSocket使用数据帧进行数据的传输,其中控制帧用于控制连接,数据帧用于实际的数据传输。相比之下,HTTP在每次请求和响应时都需要发送完整的头部信息,而WebSocket通过建立持久化连接,可以在多个数据帧中传输数据,减少了头部信息的重复发送,提高了效率。
-
相同点:
都是基于tcp的,都是可靠性传输协议
都是应用层协议 -
不同点:
WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
HTTP是单向的
WebSocket是需要浏览器和服务器握手进行建立连接的
而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接,并且该连接是无状态的。 -
联系:
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的
3.有哪些优点
优点:
- 支持双向通信,实时性更强。
- 更好的二进制支持。
- 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
- 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)如per message-deflate 扩展
- 兼容HTTP协议:端口复用 ws 是80端口,wss 是443 端口
- Http 协议头部存放元数据,websocket 传输的应用层存放元数据
- 是基于帧而不是基于流(HTTP,TCP)每一帧要么承载字符数据要么承载二进制数据
- 基于浏览器的同源策略模型(非浏览器无效)可以使用Access-Control-Allow-Origin 等头部
- 基于URI、子协议支持同主机同端口上的多个服务、
- 维持长链接:websocket 是通过ping,pong 发心跳维持长链接而.http1.1 是通过keeplive
概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。
二、websocket的使用
- 通过WebSocket构造函数创建一个webScoket实例对象
// 假设当前的服务端的接口为ws://192.168.0.15:8080 const ws=new WebSocket('ws://192.168.0.15:8080') // 此时打印ws会出现下现结构的一个信息 conlose.log(ws) // 打印结果 WebSocket { binaryType: "blob" bufferedAmount: 0 extensions: "" onclose: null onerror: null onmessage: null onopen: null protocol: "" readyState: 0 url: "ws://192.168.0.15:8080" }
- 创建了一个webScoket实例对象常用属性和方法的介绍
属性 | 说明 |
---|---|
url | 当前连接的webScoket接口地址 |
readyState | 当前连接的状态: 0:正在链接中 1:已经链接并且可以通讯 2:连接正在关闭 3;连接已关闭或者没有链接成功 |
onopen | 连接成功的回调函数 |
onerror | 连接失败的回调函数 |
onmessage | 从服务端接受到信息的回调函数 |
onclose | 连接关闭的回调函数 |
binaryType | 使用二进制的数据类型连接 |
protocol | 服务器选择的下属协议 |
bufferedAmount | 未发送至服务器的二进制字节数 |
close() | 关闭当前连接 |
send(data) | 发送消息到服务器 |
- 创建了一个webScoket实例对象常用属性和方法的介绍
const ws = new WebSocket('ws://192.168.0.15:8081/') ws.onopen = function () { console.log('我们连接成功啦...') // webSocket.send('Hello!') } ws.onerror = function () { console.log('连接失败了...') } ws.onmessage = function (e) { console.log('服务端传来数据啦...' + e.data) } ws.onclose = function () { console.log('连接关闭了...') }
- onopen
如果要指定多个回调函数,可以使用addEventListener方法。ws.addEventListener('open', function (event) { ws.send('Hello Server!'); });
- onclose
实例对象的onclose属性,用于指定连接关闭后的回调函数。ws.onclose = function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event }; ws.addEventListener("close", function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event });
- onmessage 用于指定收到服务器数据后的回调函数。
注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。ws.onmessage = function(event) { var data = event.data; // 处理数据 }; ws.addEventListener("message", function(event) { var data = event.data; // 处理数据 });
除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。ws.onmessage = function(event){ if(typeof event.data === String) { console.log("Received data string"); } if(event.data instanceof ArrayBuffer){ var buffer = event.data; console.log("Received arraybuffer"); } }
// 收到的是 blob 数据 ws.binaryType = "blob"; ws.onmessage = function(e) { console.log(e.data.size); }; // 收到的是 ArrayBuffer 数据 ws.binaryType = "arraybuffer"; ws.onmessage = function(e) { console.log(e.data.byteLength); };
- send 法用于向服务器发送数据。
发送文本的例子。
发送 Blob 对象的例子。ws.send('your message')
发送 ArrayBuffer 对象的例子。var file = document .querySelector('input[type="file"]') .files[0]; ws.send(file);
// Sending canvas ImageData as ArrayBuffer var img = canvas_context.getImageData(0, 0, 400, 320); var binary = new Uint8Array(img.data.length); for (var i = 0; i < img.data.length; i++) { binary[i] = img.data[i]; } ws.send(binary.buffer);
- bufferedAmount表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000); socket.send(data); if (socket.bufferedAmount === 0) { // 发送完毕 } else { // 发送还没结束 }
- 创建了一个webScoket实例对象常用属性和方法的介绍
在使用WebScoket的时候,难免会出现WebScoket的断联,这时,需要在onclose的回调函数中,重新链接即可ws.onclose = function (e) { // 1每10秒重连一次,直到连接成功 setTimeout(function () { // 重新调用连接webSocket事件 ws = new WebSocket('ws://192.168.0.15:8081/') ws.onopen = function () { console.log('连接成功...') } }, 10000); };
- WebScoket的心跳保活
- websocket长连接有默认的超时时间(1分钟),也就是说,超过一定的时间客户端和服务器之间没有发生任何消息传输,连接会自动断开;除此之外,服务器或防火墙一般也会在一段时间不活动并超时之后终止外部的长连接。因此,若需要使客户端一直保持连接,就需要设置心跳保活机制了。
- 实现原理:我们可以每次在 每次保持心跳保活的时候,向WebScoket发送一条消息,证明我们还"活着"
var timeout = 1000 * 30 //心跳间隔 var timeoutTimer=null //保持心跳的定时器 timeoutTimer = setTimeout(function () { ws.send('HeartBeat') }, timeout)
- 客户端主动断开连接
// 客户端主动断开连接 ws = new WebSocket('ws://192.168.31.19:8081/') ws.close();
- WebSocket的生命周期
WebSocket 生命周期描述了 WebSocket 连接从创建到关闭的过程。一个 WebSocket 连接包含以下四个主要阶段:
- 连接建立阶段(Connection Establishment): 在这个阶段,客户端和服务器之间的 WebSocket 连接被建立。客户端发送一个 WebSocket 握手请求,服务器响应一个握手响应,然后连接就被建立了。
- 连接开放阶段(Connection Open): 在这个阶段,WebSocket 连接已经建立并开放,客户端和服务器可以在连接上互相发送数据。
- 连接关闭阶段(Connection Closing): 在这个阶段,一个 WebSocket 连接即将被关闭。它可以被客户端或服务器发起,通过发送一个关闭帧来关闭连接。
- 连接关闭完成阶段(Connection Closed): 在这个阶段,WebSocket 连接已经完全关闭。客户端和服务器之间的任何交互都将无效。
需要注意的是,WebSocket 连接在任何时候都可能关闭,例如网络故障、服务器崩溃等情况都可能导致连接关闭。因此,需要及时处理 WebSocket 连接关闭的事件,以确保应用程序的可靠性和稳定性。
- WebSocket的生命周期
WebSocket 的消息格式与 HTTP 请求和响应的消息格式有所不同。
WebSocket 的消息格式可以是文本或二进制数据,并且 WebSocket 消息的传输是在一个已经建立的连接上进行的,因此不需要再进行 HTTP 请求和响应的握手操作。
WebSocket 消息格式由两个部分组成:消息头和消息体。
消息头包含以下信息:
- FIN: 表示这是一条完整的消息,一般情况下都是1。
- RSV1、RSV2、RSV3: 暂时没有使用,一般都是0。
- Opcode: 表示消息的类型,包括文本消息、二进制消息等。
- Mask: 表示消息是否加密。
- Payload length: 表示消息体的长度。
- Masking key: 仅在消息需要加密时出现,用于对消息进行解密。
消息体就是实际传输的数据,可以是文本或二进制数据。
三、websocket携带参数支持两种方式
1.在url后面拼接,类似以get请求
var socket = new WebSocket('ws://example.com/socket?token=' + YOUR_TOKEN);
2.使用send发送参数
var ws = new WebSocket("ws://" + url );
ws.onopen=function(){
ws.send(token)
}
3.基于协议头
websocket请求头中可以包含Sec-WebSocket-Protocol这个属性,该属性是一个自定义的子协议。它从客户端发送到服务器并返回从服务器到客户端确认子协议。可以利用这个属性添加token。
var webSocket = new WebSocket(url,[token]);
后台获取
package cn.bool.business.framework.websocket.core.security;
import cn.bool.business.framework.security.core.LoginUser;
import cn.bool.business.framework.security.core.filter.TokenAuthenticationFilter;
import cn.bool.business.framework.security.core.util.SecurityFrameworkUtils;
import cn.bool.business.framework.websocket.core.util.WebSocketFrameworkUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* 登录用户的 {@link HandshakeInterceptor} 实现类
*
* 流程如下:
* 1. 前端连接 websocket 时,会通过Sec-WebSocket-Protocol协议传输token
*/
public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) { // HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest();
// String header = request.getHeader(WS_LOGIN_USER_HEADER); // 不强转也可以使用 HttpHeaders headers = request.getHeaders();
List<String> list = headers.get("Sec-WebSocket-Protocol");
if (CollectionUtil.isNotEmpty(list)) {
String token = list.get(0);
} // 自己项目中登录校验逻辑 TODO
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) { // 需要将前端自定义协议头Sec-WebSocket-Protocol原封不动返回回去,否则会报错
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
HttpServletResponse httpResponse = ((ServletServerHttpResponse) response).getServletResponse();
if (StringUtils.isNotEmpty(httpRequest.getHeader("Sec-WebSocket-Protocol"))) {
httpResponse.addHeader("Sec-WebSocket-Protocol", httpRequest.getHeader("Sec-WebSocket-Protocol"));
}
}
}