SpringBoot中的WebSocket搭建详解 通俗易懂
SpringBoot中已经集成了websocket,搭建起来很简单,容易上手,废话少说,开始。
一、基础搭建
导入依赖 (首先需要一个SpringBoot的环境,此文章不再赘述)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
//以下两个依赖用在前端搭建的时候用到
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
导入依赖后 第一步完成。
二、基本的WebSocket配置
接下来进行一个WebSocket配置
需要创建一个配置类WebSocketConfig,定义全局的配置信息,使用JavaConfig的形式
package com.example.websocket.config;
import com.example.websocket.intecepter.HttpHandShakeIntecepter;
import com.example.websocket.intecepter.SocketChannelIntecepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
/**
* 注册端点,发布或者订阅消息的时候需要连接此端点
* setAllowedOrigins 非必须,*表示允许其他域进行连接
* withSockJS 表示开始sockejs支持
*
* 这个方法的作用是添加一个服务端点,来接收客户端的连接。
*/
public void registerStompEndpoints(StompEndpointRegistry registry) {
//表示添加了一个 /endpoint-websocket 端点,客户端就可以通过这个端点来进行连接。
registry.addEndpoint("/endpoint-websocket").addInterceptors(new HttpHandShakeIntecepter())
.setAllowedOrigins("*").withSockJS();
//withSockJS()的作用是开启SockJS支持,
}
/**
* 配置消息代理(中介)
* enableSimpleBroker 服务端推送给客户端的路径前缀
* setApplicationDestinationPrefixes 客户端发送数据给服务器端的一个前缀
*
* 这个方法的作用是定义消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//表示客户端订阅地址的前缀信息,也就是客户端接收服务端消息的地址的前缀信息
registry.enableSimpleBroker("/topic"); //接收
//指服务端接收地址的前缀,意思就是说客户端给服务端发消息的地址的前缀
registry.setApplicationDestinationPrefixes("/app"); //发送
//上面两个方法定义的信息其实是相反的,一个定义了客户端接收的地址前缀,一个定义了客户端发送地址的前缀
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors( new SocketChannelIntecepter());
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors( new SocketChannelIntecepter());
}
}
在上述的配置文件中
registerStompEndpoints 方法详解
registerStompEndpoints
//这个方法的作用是添加一个服务端点,来接收客户端的连接。
可以通俗的理解为 这就像一个屋子(服务端点),而里面配置就是标明屋子的门牌号registry.addEndpoint("/endpoint-websocket")
// 这里表示添加了一个/endpoint-websocket 的端点,客户端可以通过这个端点来进行连接
可以通俗的理解为 这是上述所说的门牌号.addInterceptors(new HttpHandShakeIntecepter())
//这个是添加的一个自定义拦截器(基本配置里可以不加)
.setAllowedOrigins("*")
//经查阅官方文档springwebsocket 4.1.5版本前默认支持跨域访问,之后的版本默认不支持跨域,需要设置.withSockJS();
//withSockJS()的作用是开启SockJS支持
configureMessageBroker 方法详解
configureMessageBroker
//这个方法的作用是定义消息代理,通俗一点讲就是设置消息连接请求的各种规范信息registry.enableSimpleBroker("/topic"); //接收 topic可自定义
//表示客户端订阅地址的前缀信息,也就是客户端接收服务端消息的地址的前缀信息registry.setApplicationDestinationPrefixes("/app"); //发送 app可自定义
//指服务端接收地址的前缀,意思就是说客户端给服务端发消息的地址的前缀
上面两个方法定义的信息其实是相反的,一个定义了客户端接收的地址前缀,一个定义了客户端发送地址的前缀。
比较绕,可以看下我下面举的例子。
一个客户端当作一个人,进入了屋子(服务端点)之后,有接收信息和发送信息的能力,但是房子里的人多了起,你一句我一句会让信息错乱,为了要统一规范,发送和接收前都要加上一个标识,也正是上面所定义的“topic”,“app”。
三、前后端编写
上述的基本配置完成后,就可以编写基本的功能了
package com.example.websocket.controller.v3;
import com.example.websocket.model.InMessage;
import com.example.websocket.model.OutMessage;
import com.example.websocket.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
public class V3ChatRoomContoller {
/**
* 这是 调用convertAndSendToUser来推送消息
*/
@Autowired
private SimpMessagingTemplate template;
/**
* @MessageMapping
* @param message
*/
@MessageMapping("/v3/single/chat")
public void singleChat(InMessage message) {
//意思就是“将给定的对象进行序列化,使用‘MessageConverter’进行包装转化成一条消息,发送到指定的目标”,通俗点讲就是我们使用这个方法进行消息的转发发送!
template.convertAndSend("/topic/single/"+message.getTo(),
new OutMessage(message.getFrom()+" 发送:"+ message.getContent()));
}
}
内容详解
SimpMessagingTemplate
这是调用convertAndSendToUser来推送消息,Spring-WebSocket内置的一个消息发送工具,可以将消息发送到指定的客户端@MessageMapping
//Spring提供的一个注解,功能类似@RequestMapping,存在于Controller中的,定义一个消息的基本请求,支持通配符``的url等
详情用法见Web on Servlet Stack
此后台的基础搭建完成,开始前端部分。
WebSocket前端准备工作
前端需要准备两个js文件:
sockjs.js和stomp.js
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
SockJS:
SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持,实现浏览器和 Web 服务器之间低延迟、全双工、跨域的通讯通道。Stomp
Stomp 提供了客户端和代理之间进行广泛消息传输的框架。Stomp 是一个非常简单而且易用的通讯协议实现,尽管代理端的编写可能非常复杂,但是编写一个 Stomp 客户端却是很简单的事情,另外你可以使用 Telnet 来与你的 Stomp 代理进行交互。
HTML代码:
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<title>Hello WebSocket</title>
<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/v3/app.js"></script>
</head>
<style>
body {
background-color: #f5f5f5;
}
#main-content {
max-width: 940px;
padding: 2em 3em;
margin: 0 auto 20px;
background-color: #fff;
border: 1px solid #e5e5e5;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
</style>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="connect">建立连接通道:</label>
<button id="connect" class="btn btn-default" type="submit">连接</button>
<button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">断开连接</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<input type="text" id="from" class="form-control" placeholder="我是">
<input type="text" id="to" class="form-control" placeholder="发送给谁">
<input type="text" id="content" class="form-control" placeholder="请输入...">
</div>
<button id="send" class="btn btn-default" type="submit">发送</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>记录</th>
</tr>
</thead>
<tbody id="notice">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
JS代码:
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#notice").html("");
}
function connect() {
var from = $("#from").val();
//找到配置好的服务端点(屋子),建立连接
var socket = new SockJS('/endpoint-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/single/'+from, function (result) {
//这个subscribe()方法功能就是定义一个订阅地,址用来接收服务端的信息(在服务端代码中,我们在V3ChatRoomContoller中指定了这个订阅地址“/topic/single”),当收到消息后,在回调函数中处理业务逻辑。
showContent(JSON.parse(result.body));
});
});
}
//断开连接
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
stompClient.send("/app/v3/single/chat", {}, JSON.stringify({'content': $("#content").val(), 'to':$("#to").val(), 'from':$("#from").val()}));
}
function showContent(body) {
//给记录 添加一条聊天记录
$("#notice").append("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleString()+"</td></tr>");
}
$(function () {
$("form").on('submit', function (e) {
e.preventDefault();
});
$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
});
开启Socket(建立通道)
1.var socket = new SockJS('/endpoint-websocket'); //建立一个SocketJS对象
2 stompClient = Stomp.over(socket); //用Stomp将SocketJS进行协议封装
3.stompClient.connect(); //与服务端进行连接,同时有一个回调函数,处理连接成功后的操作信息。
发送消息
stompClient.send("/app/v3/single/chat", {}, value);
stompClient.send("/app/v3/single/chat", {}, JSON.stringify({'content': $("#content").val(), 'to':$("#to").val(), 'from':$("#from").val()}));
这里的value 封装了一个json将数据拼接后发送,后台进行解析并进行业务处理
演示效果
客户端 小小 和 大大准备进行连接
连接成功后 打印对应的sessionId,业务处理时,sessionId用于唯一标识
连接后进行消息发送,此时可以看到 大大这边可以接收到消息。
此时后台打印的日志
可以看到消息体的相关内容都捕获到,可以进行后续的业务处理。
至此,WebSocket基础开发完毕
下面是对应后台的拦截器,监听器等。
拦截器:
package com.example.websocket.intecepter;
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;
/**
* 功能描述:http握手拦截器,可以通过这个类的方法获取resuest,和response
*/
public class HttpHandShakeIntecepter implements HandshakeInterceptor{
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("【握手拦截器】beforeHandshake");
if(request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
HttpSession session = servletRequest.getServletRequest().getSession();
String sessionId = session.getId();
System.out.println("【握手拦截器】beforeHandshake sessionId="+sessionId);
attributes.put("sessionId", sessionId);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
System.out.println("【握手拦截器】afterHandshake");
if(request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
HttpSession session = servletRequest.getServletRequest().getSession();
String sessionId = session.getId();
System.out.println("【握手拦截器】afterHandshake sessionId="+sessionId);
}
}
}
package com.example.websocket.intecepter;
import com.alibaba.fastjson.JSONObject;
import com.example.websocket.Utils.Millisecond;
import com.example.websocket.controller.v6.UserChatController;
import com.example.websocket.model.InMessage;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import java.util.Map;
/**
* 功能描述:频道拦截器 ,类似管道,可以获取消息的一些meta数据
*/
@Slf4j
public class SocketChannelIntecepter extends ChannelInterceptorAdapter{
/**
* 在完成发送之后进行调用,不管是否有异常发生,一般用于资源清理
*/
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel,
boolean sent, Exception ex) {
System.out.println("SocketChannelIntecepter->afterSendCompletion");
super.afterSendCompletion(message, channel, sent, ex);
}
/**
* 在消息被实际发送到频道之前调用
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
System.out.println("SocketChannelIntecepter->preSend");
return super.preSend(message, channel);
}
/**
* 发送消息调用后立即调用
*/
@Override
public void postSend(Message<?> message, MessageChannel channel,
boolean sent) {
System.out.println("SocketChannelIntecepter->postSend");
String s = new String((byte[]) message.getPayload());
//InMessage inMessage = JSONObject.parseObject(s, new TypeReference<InMessage>(){});
//System.out.println(inMessage);
System.out.println(s);
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);//消息头访问器
if (headerAccessor.getCommand() == null ) {
return ;// 避免非stomp消息类型,例如心跳检测
}
Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();
System.out.println(sessionAttributes);
String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString();
System.out.println("SocketChannelIntecepter -> sessionId = "+sessionId);
StompCommand command = headerAccessor.getCommand();
System.out.println(headerAccessor.getCommand());
switch (headerAccessor.getCommand()) {
case CONNECT:
connect(sessionId);
break;
case DISCONNECT:
disconnect(sessionId);
break;
case SUBSCRIBE:
break;
case UNSUBSCRIBE:
break;
default:
break;
}
}
long btime = 0;
long etime = 0;
//连接成功
private void connect(String sessionId){
System.out.println("连接成功 sessionId="+sessionId);
System.out.println("connect sessionId="+sessionId);
btime = System.currentTimeMillis();
System.out.println(System.currentTimeMillis());
}
//断开连接
private void disconnect(String sessionId){
System.out.println("断开连接 sessionId="+sessionId);
System.out.println("disconnect sessionId="+sessionId);
//用户下线操作
UserChatController.onlineUser.remove(sessionId);
etime = System.currentTimeMillis();
long difference = Millisecond.getDifference(btime, etime, 0);
System.out.println("通讯时间--------->"+difference+"秒");
}
}
监听器:
package com.example.websocket.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;
@Component
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> {
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("【ConnectEventListener监听器事件 类型】"+headerAccessor.getCommand().getMessageType());
}
}
package com.example.websocket.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
/**
* 功能描述:springboot使用,订阅事件
*/
@Component
public class SubscribeEventListener implements ApplicationListener<SessionSubscribeEvent> {
/**
* 在事件触发的时候调用这个方法
*
* StompHeaderAccessor 简单消息传递协议中处理消息头的基类,
* 通过这个类,可以获取消息类型(例如:发布订阅,建立连接断开连接),会话id等
*
*/
public void onApplicationEvent(SessionSubscribeEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("【SubscribeEventListener监听器事件 类型】"+headerAccessor.getCommand().getMessageType());
System.out.println("【SubscribeEventListener监听器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
}
}