WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket 的工作流程是这 样的:浏览器通过 js 向服务端发出建立 WebSocket 连接的请求,在 WebSocket 连接建立成功后,客户端和服务端就可以通过 TCP 连接传输数据。
添加依赖
本文使用 maven
方式,添加在 pom.xml
里面依赖部分。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
在项目里面建立一个单独的 websocket 文件夹,再在配置文件 spring-mvc.xml
里面添加:
<context:component-scan base-package="com.stu.controller" />
<context:component-scan base-package="com.stu.websocket" />
这样 WebSocketConfig 就相当于是 controller 层了
Spring整合WebSocket
WebSocketConfig.java
配置 WebSocket 入口、允许访问的域,注册 Handler、SockJs 支持和拦截器。
用户登录后建立 WebSocket 连接,默认选择 WebSocket 连接,如果浏览器不支持,则使用 Sockjs 注册方法进行模拟连接
package com.stu.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Component
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
/* 当客户端发起 websocket 连接,把 /path 交给对应的 handler 处理,而不实现具体的业务逻辑,
第一个参数用来注册 websocket server 实现类,第二个参数是访问 websocket 的地址 */
registry.addHandler(myHandler(), "/ws").addInterceptors(new HandShake());
registry.addHandler(myHandler(), "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyWebSocketHandler();
}
}
HandShake.java
握手拦截器
此处为 WebSocket 连接的第一步验证,必须要满足定义的条件才能初步建立连接,满足条件后可以再绑定身份信息传给下一步以便于区分用户
package com.stu.websocket;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
public class HandShake implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
String parameter = servletRequest.getServletRequest().getParameter("id");
// 登陆时自己手动绑定的 session,非 websocket session
HttpSession session = servletRequest.getServletRequest().getSession(false);
System.out.println("Websocket:用户[ID:" + parameter + "]握手成功");
System.out.println(request.getURI().toString());
// 若要编写 websocket 客户端进行连接,则再定义一个不需要 session 的判定进行绑定
if (session != null) {
// 使用 userCd 区分 WebSocketHandler,以便定向发送消息
String userCd = session.getAttribute("uid").toString();
attributes.put("uid", userCd);
}
}
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
System.out.println("After Handshake");
}
}
MyWebSocketHandler.java
消息拦截器
提供了客户端连接、关闭、错误、发送等方法,重写这几个方法即可实现自定义业务逻辑
这里通过 Map 绑定 ID 和 WebSocket 的 Session,以便于后期的消息发送,也可以自己定义用户的判定方式
package com.stu.websocket;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.util.*;
@Component
@Service
public class MyWebSocketHandler implements WebSocketHandler {
// 当 MyWebSocketHandler 类被加载时就会创建该 Map
private static final Map<String, WebSocketSession> userSocketSessionMap;
static {
userSocketSessionMap = new HashMap<String, WebSocketSession>();
}
/**
* 连接成功后
*/
@Override
public void afterConnectionEstablished(WebSocketSession session)
throws Exception {
// 此处为 WebSocketSession,uid 是由拦截器已绑定好的,用于区分用户
String uid = session.getAttributes().get("uid").toString();
// 绑定session
if (userSocketSessionMap.get(uid) == null) {
userSocketSessionMap.put(uid, session);
}
session.sendMessage(new TextMessage("handler已连接"));
}
/**
* 消息处理
*/
@Override
public void handleMessage(WebSocketSession session,
WebSocketMessage<?> message) throws Exception {
if (message.getPayloadLength() == 0) return;
// 把消息发送给消息来源方
session.sendMessage(message);
// 自己定义的函数,发送给在线的所有人
sendMessageToAllUsers(message);
}
/**
* 消息传输错误处理
*/
@Override
public void handleTransportError(WebSocketSession session,
Throwable exception) throws Exception {
String uid = session.getAttributes().get("uid").toString();
if (session.isOpen()) {
session.close();
}
System.out.println(uid + " 连接失败");
userSocketSessionMap.remove(session);
}
/**
* 关闭连接后
*/
@Override
public void afterConnectionClosed(WebSocketSession session,
CloseStatus closeStatus) throws Exception {
String uid = session.getAttributes().get("uid").toString();
System.out.println("UserWebSocket:" + uid + " close connection");
// 移除
for (Map.Entry<String, WebSocketSession> userEntry : userSocketSessionMap.entrySet()) {
if (userEntry.getValue().getAttributes().get("uid") == uid) {
System.out.println("WebSocket in UserMap:" + uid + " removed");
userSocketSessionMap.remove(uid);
}
}
}
@Override
public boolean supportsPartialMessages() {
return false;
}
// 发送给所有用户
private boolean sendMessageToAllUsers(WebSocketMessage message) {
boolean allSendSuccess = true;
Set<String> clientIds = userSocketSessionMap.keySet();
WebSocketSession session = null;
for (String clientId : clientIds) {
try {
session = userSocketSessionMap.get(clientId);
if (session.isOpen()) {
session.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
allSendSuccess = false;
}
}
return allSendSuccess;
}
}
HTML连接
打开网页后,js 就会给 WebSocket 服务器发送连接请求,要注意链接的形式及正确性。
连接建立成功后便可以与服务器互相通信
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<html>
<head>
<meta charset="utf-8">
<base href="<%=basePath%>">
</head>
<body>
<script type="text/javascript">
var host = window.location.host;
var webSocket =
new WebSocket("ws://" + host+ "/ws?id=1");
var hum = null;
var s_json = null;
webSocket.onerror = function(event) {
onError(event);
};
webSocket.onopen = function(event) {
onOpen(event);
};
webSocket.onmessage = function(event) {
onMessage(event);
};
function onMessage(event) {
console.log(event.data);
}
function onOpen(event) {
console.log("握手成功");
webSocket.send("连接上了");
}
function onError(event) {
alert(event.data);
}
</script>
</body>
</html>
配置路由
MyController.java
通过建立专门的路由,当用户访问相应链接时,便把上面的 HTML 对应展示,此处可用于用户验证并传递对应 ID,便于服务器区分
package com.stu.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequestMapping("/condition")
public class SerialController {
@RequestMapping("/show")
public String list(HttpServletRequest request) {
/* 此处就是拦截器接收的 session,uid 绑定一个默认值用来测试
生产环境要绑定一个能区分用户的值 */
request.getSession().setAttribute("uid", "con");
return "show";
}
}
转载:https://blog.csdn.net/weixin_44613063/article/details/101483417