WebSocket
Categories:
如果需要定期给浏览器推送数据,例如股票行情,或者不定期给浏览器推送数据,例如在线聊天,基于HTTP协议实现这类需求,只能依靠浏览器的JavaScript定时轮询,效率很低且实时性不高。
2009年出现的新技术Websocket
允许建立浏览器客户端和服务端之间的双向连接, 并且发送轻量级的数据模型, 服务端可以在需要时直接向客户端推送消息
不同于传统的Http
,Websocket
允许建立通路, 可以直接发送信息而不需像HTTP一样创建连接上下文, 原理如下
在建立TCP连接后, 附带几个请求头
GET /chat HTTP/1.1
Host: www.example.com
Upgrade: websocket
Connection: Upgrade
此后连接升级为长连接
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
收到成功响应后表示WebSocket“握手”成功,这样,代表WebSocket的这个TCP连接将不会被服务器关闭,而是一直保持,服务器可随时向浏览器推送消息,浏览器也可随时向服务器推送消息。双方推送的消息既可以是文本消息,也可以是二进制消息,一般来说,绝大部分应用程序会推送基于JSON的文本消息。
使用 Websocket
开始前阅读 架构说明
建立新模块 sample-ws
<!-- ws -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
启动类
/**
* 描述:程序启动入口
*
* @author hamhuo
* @version 1.0.0
*/@SpringBootApplication
@EnableBinding(Sink.class)
@EnableDiscoveryClient
public class WsApplication {
public static void main(String[] args) {
SpringApplication.run(WsApplication.class, args);
}
}
实现服务端和浏览器的双向通信, 我们要写一个服务器
@Component
@ServerEndpoint("/chat")
public class ChatServer {
/**
* 保存连接对象, 连接池, 放用户session和key
*/
private static final ConcurrentHashMap<String, Session> SESSION_POOL = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session)
@OnMessage
public String onMessage(String text, Session session)
@OnClose
public void onClose(Session session)
@OnError
public void onError(Session session, Throwable throwable)
}
解释一下
-
首先组件注解加上, Spring 会自动装载为组件
-
关于
@ServerEndpoint
WebSocket 是一种基于 TCP 的协议,它是持久化连接,通信过程中不会像 HTTP 请求那样每次都经过请求-响应的流程,因此不能直接通过传统的 Spring MVC 控制器方法来处理。@ServerEndpoint
是 WebSocket 规范中的一种标注方式,用来标识一个 WebSocket 服务器端点。当客户端连接到这个端点时,会自动调用这个类中的相应方法。我们只需要知道服务器与 /server 绑定即可
-
我们在server中定义了四个方法, 启动时调用, 消息时调用, 关闭时调用, 错误时调用
-
我们创建一个链接池, 存放浏览器和服务器的长连接
这个端点注解依赖一个配置类
我们在 ws.config
下新建一个配置类 ,这是写死的, 不用理解
/**
* @program: demo
* @description: 服务端点配置
* @author: hamhuo
**/
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpoint() {
return new ServerEndpointExporter();
}
}
当浏览器首先试图建立与服务器的连接, 这里调用 @OnOpen
注解方法
@OnOpen
public void onOpen(Session session) throws IOException {
//判断客户端对象是否存在
//排除重复的连接对象
if (SESSION_POOL.containsKey(session.getQueryString())) {
CloseReason reason = new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "ID冲突,连接拒绝");
session.getUserProperties().put("reason", reason);
session.close();
return;
}
//将客户端记录到Map中
SESSION_POOL.put(session.getQueryString(), session);
System.out.println("客户端(" + session.getQueryString() + "):开启连接");
}
解释一下, 浏览器会传入一个客户端ID, 服务器根据ID来提供服务
每个ID标识了长连接, 服务器根据ID提供服务
- 传入连接后判断是否重复, 从连接池拿session里的key, 如果重复就拒绝连接
- 没有重复, 将Session保存到连接池(Map)里
- 控制套打印日志, 这里可以用slf4j
注意: 这里的
Session
指的是长连接, 和HTTP Session
不是一个东西
之后长连接建立, 可以双向监听信息, 一旦服务器或者浏览器有信息了就调用 @OnMessage
@OnMessage
public String onMessage(String text, Session session) throws IOException {
//解析消息 => ID::消息内容
String[] msgArr = text.split("::", 2);
//群发消息 ID=all表示群发
if ("all".equalsIgnoreCase(msgArr[0])) {
for (Session one : SESSION_POOL.values()) {
//排除自己
if (session == one) {
continue;
}
//发送消息
one.getBasicRemote().sendText(msgArr[1]);
}
}
//指定发送人, 不是all的情况
else {
Session target = SESSION_POOL.get(msgArr[0]);
if (target != null) {
target.getBasicRemote().sendText(msgArr[1]);
}
}
return session.getQueryString() + ":消息发送成功";
}
消息发送的方式有两种, 点对点和群发
点对点就找出目标Session, 服务器向Session写信息发送就行 群发就需要遍历连接池的每一个Session, 全部写信息
当然, 前提是排除发送者的Session
关闭连接, 也就是浏览器离线需要执行线程池移除线程的操作, 调用 @OnClose
方法
这里注意, webSocket并没有强制关闭连接的能力, 这是通知远程连接自主关闭, 服务器只负责监视是否关闭
@OnClose
public void onClose(Session session) {
//处理拒绝连接session关闭对象, 不从池子里移除
Object obj = session.getUserProperties().get("reason");
if (obj instanceof CloseReason) {
CloseReason reason = (CloseReason) obj;
if (reason.getCloseCode() == CloseReason.CloseCodes.CANNOT_ACCEPT) {
System.out.println("拒绝客户端(" + session.getQueryString() + "):关闭连接");
return;
}
}
//将session对象从map中移除,正常关闭
SESSION_POOL.remove(session.getQueryString());
System.out.println("客户端(" + session.getQueryString() + "):关闭连接");
}
这是比较简单的应用不再考虑连接复用的问题
接下来写浏览器页面
这里我们略过了~
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室客户端</title>
</head>
<body>
<h1>Chat Client</h1>
<div>
<input id="clientId" placeholder="输入ID" value="1">
<input onclick="init()" value="连接服务器" type="button"><br><br>
<input id="receiverId" placeholder="输入接收人ID" value="all"><br><br>
<textarea id="message" style="margin: 0; height: 197px; width: 362px;"
placeholder="发送消息内容"></textarea><br>
<input onclick="send()" value="发送消息" type="button">
<input onclick="closeConnect()" value="关闭连接" type="button">
</div>
<div id="output"></div>
<script type="text/javascript" language="JavaScript">
//屏幕回显输出方法
function writeToScreen(message) {
let pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
document.getElementById("output").appendChild(pre);
}
//初始化websocket
let echo_websocket;
function init() {
let clientId = document.getElementById("clientId").value;
/* 这里端口是写死的 */ let wsUri = "ws://localhost:10800/chat?" + clientId;
writeToScreen("连接到" + wsUri);
//1.创建WebSocket客户端对象
echo_websocket = new WebSocket(wsUri);
//2.开门握手完成回调
echo_websocket.onopen = function (evt) {
console.log(evt);
writeToScreen("连接打开成功 !");
};
//3.监听服务端的消息
echo_websocket.onmessage = function (evt) {
writeToScreen("接收服务端消息:<br> " + evt.data);
};
//4.如果连接中断
echo_websocket.onerror = function (evt) {
writeToScreen('<span style="color: red;">ERROR:'+evt.data+'</span>');
//关闭连接
closeConnect();
};
//5.注册close事件
echo_websocket.onclose = function(evt){
writeToScreen('<span style="color: green;">INFO:关闭连接</span> ');
if(evt.reason){
writeToScreen
(`<span style="color: red;">错误信息:${evt.reason}</span> `);
}
}
}
//6.向服务发送消息
function send() {
let message = document.getElementById("message").value;
let receiver = document.getElementById("receiverId").value;
echo_websocket.send(receiver + "::" + message);
writeToScreen("发送消息: " + message);
}
//7.如果不需要通讯,那么关闭连接
function closeConnect() {
echo_websocket.close();
}
</script>
</body>
</html>
我们需要解释下构建/发送信息的代码
//6.向服务发送消息
function send() {
let message = document.getElementById("message").value;
let receiver = document.getElementById("receiverId").value;
echo_websocket.send(receiver + "::" + message);
writeToScreen("发送消息: " + message);
}
这里拿到表单组件, 取值后通过 ::
拼接为消息, 链接符之前为id, 之后为消息
其中初始化的 echo_websocket
组件是dom库提供的 echo_websocket = new WebSocket(wsUri);
, 除了IE其他浏览器都支持
在前端打开三个浏览器页面, 作为三个客户端
注意id要不同
后台运行服务器, 就可以开始监听了