“ 这一篇文章前面部分我们会先介绍WebSocket协议的基本知识,在最后我们会用Spring Boot来集成WebSocket实现一个简单的在线聊天功能,我们也可以跨过前面的介绍直接看集成部分,后续在慢慢研究WebSocket。”
通常情况下,浏览器和服务器之间的消息通讯一般会使用Http协议,但是如果我们想服务器返回数据,必须先由浏览器发送请求给服务器,服务器才能响应这个请求。一般情况下Http协议基本能够满足我们需求,但是如果我们想打造一个网站在线聊天平台,这个时候我们发送一条消息,其他用户的浏览器该如何接受这条消息呢?
必须使用Http协议的情况下,我们可以使用轮询的方式让浏览器发送请求到服务器,查询是否有消息返回,这种方式能基本满足需求。但是在网站的后期会有一些问题衍生出来,比如实时性不够高,而且频繁的请求会给服务器带来极大的压力。
实时性的问题我们可以利用Http的Comet方式保持长链接,但是Comet本质上也是轮询,在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。这个机制暂时地解决了实时性问题,但是它带来了新的问题:以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。另外,一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发一些ping数据表示连接“正常工作”。
基于上面的请求,HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。
WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。但是WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。
我们先看下面WebSocket请求的格式
General:
Request URL: ws://127.0.0.1:8080/websocket/%E6%AF%8F%E5%A4%A9%E5%AD%A6Java
Request Method: GET
Status Code: 101
Response Header:
Connection: upgrade
Date: Sat, 30 Mar 2019 02:53:11 GMT
Sec-WebSocket-Accept: 4yMemfpbrCzWxZ1vpNqCI2IR+YI=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Upgrade: websocket
Resquest Header:
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: lD0B2FWxinrTPcNDMsO3hQ==
Sec-WebSocket-Version: 13
对比下面普通的Http请求
General:
Request URL: http://127.0.0.1:8080/index
Request Method: GET
Status Code: 200
Remote Address: 127.0.0.1:8080
Referrer Policy: no-referrer-when-downgrade
Request Header:
我们会发现这些不同:
首先WebSocket的GET请求是以ws://开头的地址;
其次WebSocket的Status Code是101;
且WebSocket的请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
在Resquest Header中:Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据,Sec-WebSocket-Version指定了WebSocket的协议版本。
101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
Http为什么不能实现全双工通信呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。
而WebSocket连接建立以后,没有Http协议的限制,进而可以进行互相进行通讯
安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。
支持WebSocket的主流浏览器如下:
对于低版本不支持WebSocket浏览器,可以参考以下的解决方案
我们先看以下实现的效果
这里我们使用Spring Boot来集成WebSocket
目录结构
当应用加载完之后...
第一步是要创立websocket endpoint
在Spring Boot中我们通过注入ServerEndpointExporter,来创建websocket endpoint
package com.example.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
//这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
第二步是写websocket的具体实现类
实现类中注解@ServerEndpoint(value = "/websocket/{user}") 对应前端的请求方式为: new WebSocket("ws://127.0.0.1:8080/websocket/2");
而前端想通过 new WebSocket("ws://127.0.0.1:18080/testWebsocket?id=23&name=Lebron")来传递参数,我们就需要使用WebSocketHandler来实现websocket
CopyOnWriteArrayList小伙伴私下也可以研究下,应用与一些并发场景中来保证线程安全。
package com.example.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint(value = "/websocket/{user}")
@Component
public class MyWebSocket {
// 通过类似GET请求方式传递参数的方法(服务端采用第二种方法"WebSocketHandler"实现)
// websocket = new WebSocket("ws://127.0.0.1:18080/testWebsocket?id=23&name=Lebron");
/**
* 在线人数
*/
public static AtomicInteger onlineNumber = new AtomicInteger(0);
/**
* 所有的对象,每次连接建立,都会将我们自己定义的MyWebSocket存放到List中,
*/
public static List<MyWebSocket> webSockets = new CopyOnWriteArrayList<MyWebSocket>();
/**
* 会话,与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 每个会话的用户
*/
private String user;
/**
* 建立连接
*
* @param session
*/
@OnOpen
public void onOpen(Session session, @PathParam("user") String user) {
if (user == null || "".equals(user)) {
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
onlineNumber.incrementAndGet();
for (MyWebSocket myWebSocket : webSockets) {
if (user.equals(myWebSocket.user)) {
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
}
this.session = session;
this.user = user;
webSockets.add(this);
System.out.println("有新连接加入! 当前在线人数" + onlineNumber.get());
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
onlineNumber.decrementAndGet();
webSockets.remove(this);
System.out.println("有连接关闭! 当前在线人数" + onlineNumber.get());
}
/**
* 收到客户端的消息
*
* @param message 消息
* @param session 会话
*/
@OnMessage
public void onMessage(String message, Session session, @PathParam("user") String user) {
System.out.println("来自" + user + "消息:" + message);
pushMessage(user, message, null);
}
/**
* 发送消息
*
* @param message 消息
*/
public void sendMessage(String message) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 消息推送
*
* @param message
* @param uuid uuid为空则推送全部人员
*/
public static void pushMessage(String user, String message, String uuid) {
if (uuid == null || "".equals(uuid)) {
for (MyWebSocket myWebSocket : webSockets) {
myWebSocket.sendMessage(user + ":" + message);
}
} else {
for (MyWebSocket myWebSocket : webSockets) {
if (uuid.equals(myWebSocket.user)) {
myWebSocket.sendMessage(message);
}
}
}
}
}
第三步写服务器主动推送
package com.example.websocket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
@RequestMapping(value = "/index")
public void index() {
MyWebSocket.pushMessage("群主", "你们好", null);
}
}
前端
<html>
<head>
<meta charset="UTF-8">
<title>Web sockets test</title>
<script type="text/javascript">
var ws;
function login() {
if (!ws) {
var user = document.getElementById("name").value;
try {
ws = new WebSocket("ws://127.0.0.1:8080/websocket/" + user);//连接服务器
ws.onopen = function (event) {
console.log("已经与服务器建立了连接...");
alert("登陆成功,可以开始聊天了")
};
ws.onmessage = function (event) {
console.log("接收到服务器发送的数据..." + event.data);
document.getElementById("info").innerHTML += event.data + "<br>";
};
ws.onclose = function (event) {
console.log("已经与服务器断开连接...");
};
ws.onerror = function (event) {
console.log("WebSocket异常!");
};
} catch (ex) {
alert(ex.message);
}
document.getElementById("login").innerHTML = "退出";
} else {
ws.close();
ws = null;
}
}
function SendData() {
var data = document.getElementById("data").value;
try {
ws.send(data);
} catch (ex) {
alert(ex.message);
}
};
</script>
</head>
<body>
<input id="name" value="" placeholder="用户名">
<button id="login" type="button" onclick="login()" value="">登陆</button>
<br/><br/>
<input id="data">
<button type="button" onclick='SendData();'>发送消息</button>
<br/><br/>
<div id="info">
</div>
</body>
</html>
到这里我们就实现了简单的聊天效果,对于WebSocket还不了解的小伙伴可以去试一试效果!