一、WebSocket 简介
1、什么是 WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的应用层的协议
。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就创建了持久性的连接,两者之间就直接可以进行双向数据传输
。
所有浏览器都支持 WebSocket ,目的是在服务器可以在任意时刻发消息给浏览器, 不需要等待浏览器的请求
。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
(1)WebSocket 特点
服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种
。1)建立在 TCP 协议之上,服务器端的实现比较容易。
2)与 HTTP 协议有着良好的兼容性,握手阶段采用 HTTP 协议
。
3)数据格式比较轻量,性能开销小,通信高效。
4)可以发送文本,也可以发送二进制数据。
5)没有同源限制,客户端可以与任意服务器通信
。
6)协议标识符是 ws(如果加密,则为wss)
,服务器网址就是 URL。
(2)HTTP 和 WebSocket 的区别
(3)AJAX 与 WebSocket 的区别
Ajax 是 Web 应用程序通过频繁的异步的 javaScript 长时间的轮循来更新浏览器页面显示的内容。
WebSocket 是全双工通信,双方都可以向对方推送消息,不需要长轮询。
2、WebSocket 的由来
HTTP 在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”
。要保持客户端程序的在线状态,就需要向服务器轮询,即不断地向服务器发起连接请求
。即使不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。
简单的说就是 TCP 连接是一个长连接,HTTP 是一个短连接。
(1)HTTP 的缺点
1)
不支持长连接
:信息的获取需要通过轮徇的方式, 每隔一段时间发一次请求, 才能得到更新的消息。
2)通信只能由客户端发起
:如果你不主动发请求要查询、查看, 服务器商不主动推送消息给客户。(“请求—响应”的方式:发送请求才建立连接,服务器只能请求之后发消息到客户端)
(2)以往和服务器交互的几种方式
1)轮询
长轮询就是客户端按照一个固定的时间定期向服务器发送请求,通常这个时间间隔的长度受到服务端的更新频率和客户端处理更新数据时间的影响。这种方式缺点很明显,就是浏览器要不断发请求到服务器以获取最新信息,造成服务器压力过大,占用宽带资源。
2)使用 streaming AJAX
streaming ajax是一种通过 ajax 实现的长连接维持机制。主要目的就是在数据传输过程中对返回的数据进行读取,并且不会关闭连接。
3)iframe方式
iframe 可以在页面中嵌套一个子页面,通过将 iframe 的 src 指向一个长连接的请求地址,服务端就能不断往客户端传输数据。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出 HTTP 请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
为了能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,WebSocket 应运而生。
3、WebSocket 的工作流程
(1)WebSocket 连接的建立
1)
客户端申请升级
浏览器发送一个 ‘upgrade’ request,这是一个标准的 HTTP 请求,使用的是 HTTP 协议。请求如下:
GET ws://localhost:3000/ws/chat HTTP/1.1 Host: localhost:8080 Origin: http://127.0.0.1:3000 Connection: Upgrade // 表示要升级协议 Upgrade: websocket // 表示要升级到websocket协议 Sec-WebSocket-Version: 13 // 表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。 Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== // 与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
注:
客户端发起的协议升级请求只支持 GET 方法
;GET 请求的地址不是类似 http:// 开头,而是以 ws:// 开头
的。2)
服务端响应协议升级
:服务端返回内容如下:
HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
过了握手阶段后,就只能采用特定的错误码。
(2)相互通讯
接下来双方就开始使用 WebSocket 协议相互通讯了,不再使用 http 协议了。
一旦 WebSocket 客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。WebSocket 根据 opcode 来区分操作的类型:比如 0x8 表示断开连接,0x0-0x2 表示数据交互。
(3)连接保持 + 心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
二、WebSocket 服务端 API
1、WebSocket 属性
(1)Socket.readyState
1) 0 - 表示连接尚未建立。2) 1 - 表示连接已建立,可以进行通信。
3) 2 - 表示连接正在进行关闭。
4) 3 - 表示连接已经关闭或者连接不能打开
(2)Socket.bufferedAmount
只读属性 bufferedAmount 表示已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
2、WebSocket 事件
3、WebSocket 方法
以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:
三、JS 实现 WebSocket 客户端 API
1、WebSocket 构造函数
// 地址为 WebSocket 服务端的访问地址
var ws = new WebSocket("ws://localhost:8080/websocketserver");
2、webSocket.readyState
readyState 属性返回实例对象的当前状态,共有四种。
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败
例如:
if(ws.readyState ==WebSocket.CONNECTING ){
}
3、webSocket.onopen
用于指定连接成功后的回调函数。
// 方式一
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
// 方式二
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
4、webSocket.onclose
用于指定连接关闭后的回调函数。
// 方式一
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
};
// 方式二
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
});
5、webSocket.onmessage
用于指定收到服务器数据后的回调函数。
// 方式一
ws.onmessage = function(event) {
var data = event.data;
};
// 方式二
ws.addEventListener("message", function(event) {
var data = event.data;
});
服务器数据有可能是文本,也有可能是二进制数据,需要判断
:
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");
}
}
除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型:
// 收到的是 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);
};
6、webSocket.send
send 方法用于向服务器发送数据。
(1)发送文本
ws.send("Hello WebSockets!");
(2)发送 Blob 数据
var file = document
.querySelector('input[type="file"]')
.files[0];
ws.send(file);
(3)发送 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);
7、webSocket.bufferedAmount
bufferedAmount 属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}
8、webSocket.onerror
用于指定报错时的回调函数
// 方式一
ws.onerror = function(event) {
};
// 方式二
es.addEventListener("error", function(event) {
});
四、springboot 使用 WebSocket 实现在线聊天
1、群发消息
(1)添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
(2)WebSocket 配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
注入 ServerEndpointExporter,这个 bean 会自动注册使用了 @ServerEndpoint 注解声明的 Websocket endpoint。 要注意,如果使用独立的 servlet 容器,而不是直接使用 springboot 的内置容器,就不要注入ServerEndpointExporter, 因为它将由容器自己提供和管理。
(3)创建一个服务端
@ServerEndPoint 注解是一个
类层次的注解
,它的功能主要是将目前的类定义成一个 websocket 服务器端
。
注解的值将被用于监听用户连接的终端访问 UR L地址
,客户端可以通过这个 URL 连接到 websocket 服务器端
@ServerEndpoint("/websocket/{name}") // 服务端访问地址
@RestController
public class WebSocketServer {
// 存储客户端的连接对象,每个客户端连接都会产生一个连接对象,即 WebSocketServer 实例。
private static ConcurrentHashMap<String,WebSocketServer> map = new ConcurrentHashMap();
//每个连接都会有自己的会话
private Session session;
private String name;
@OnOpen
public void open(@PathParam("name") String name, Session session){
map.put(name,this); // 存储客户端连接对象
System.out.println(name + "连接服务器成功");
System.out.println("客户端连接个数:" + getConnetNum());
this.session=session;
this.name=name;
}
@OnClose
public void close(){
map.remove(name);
System.out.println(name+"断开了服务器连接");
}
@OnError
public void error(Throwable error){
error.printStackTrace();
System.out.println(name+"出现了异常");
}
@OnMessage
public void getMessage(String message) throws IOException {
System.out.println("收到" + name+":" + message);
System.out.println("客户端连接个数:" + getConnetNum());
Set<Map.Entry<String, WebSocketServer>> entries = map.entrySet();
for (Map.Entry<String, WebSocketServer> entry : entries) {
if(!entry.getKey().equals(name)){//将消息转发到其他非自身客户端
entry.getValue().send(message);
}
}
}
public void send(String message) throws IOException {
if(session.isOpen()){
session.getBasicRemote().sendText(message);
}
}
public int getConnetNum(){
return map.size();
}
}
(4)创建一个客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>在线聊天</title>
</head>
<body>
Welcome<br/>
<input id="login" type="text" />
<button onclick="login()">登录</button><br>
<div id="message1"></div><br><br>
<input id="text" type="text"/>
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr/>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null;
// 输入账户名登录,然后进行 WebSocket 连接
function login() {
// 页面显示登陆信息
document.getElementById('message1').innerHTML += '欢迎' + document.getElementById('login').value + '<br/>';
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
// 获得当前登录的用户名,作为与服务端建立的连接对象的标识
var user = document.getElementById('login').value
websocket = new WebSocket('ws://localhost:8010/websocket/'+user);
}
else {
alert('当前浏览器 Not support websocket')
}
//连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
console.log(event.data)
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
};
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</body>
</html>
(5)效果
用户1:
用户2:
服务器连接信息: