Spring-WebSocket
- 一、WebSocket介绍
- 二、Spring中使用WebSocket
- 三、使用WebSocket API
- 1、下面是WebSocket API的组件Maven引入方式
- 2、下面是用于测试通信的前端单页面
- 3、基与java全注解方式配置WebSocket
- 四、基于stomp协议的WebSocket配置
- 1、JavaConfig方式的配置
- 2、使用到的实体类、控制器、拦截器
- 3、前端测试代码
- 五、两个配置类的详细介绍
- 1、WebSocketConfigurer
- 2、WebSocketMessageBrokerConfigurer
一、WebSocket介绍
WebSocket是作为HTML5计划的一部分而开发的一种规范,可以实现在客户端和服务器之间发送消息的全双工单套接字连接。过去,需要实现更新功能的Web应用程序会通过打开多个连接或是使用长轮询来定期轮询服务器端组件以获取更新数据。WebSocket的单套接字能够避免每个客户端需要多个连接并减少开销。
二、Spring中使用WebSocket
从版本4.0开始,Spring框架开始支持WebSocket样式的消息传递以及STOMP作为应用程序级别的子协议。在框架内,可以在spring-websocket模块中找到对WebSocket的支持,该模块与JSR-356兼容。需要注意的是不是所有浏览器都支持WebSocket协议,为了处理这种情况,Spring通过SockJS协议提供了透明的后被选项(根据浏览器的真实情况自动决定使用匹配的协议)。
三、使用WebSocket API
1、下面是WebSocket API的组件Maven引入方式
注意:SpringMVC项目依赖的其他组件不再这里列出
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
2、下面是用于测试通信的前端单页面
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Tester</title>
<script type="text/javascript"
src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script type="text/javascript">
var ping;
var websocket;
jQuery(function($) {
function writePing(message) {
$('#pingOutput').append(message + '\n');
}
function writeStatus(message) {
$('#statusOutput').val($("#statusOutput").val() + message + '\n');
}
function writeMessage(message) {
$('#messageOutput').append(message + '\n');
}
$('#connect').click(function doConnect() {
websocket = new WebSocket($("#target").val());
websocket.onopen = function(evt) {
writeStatus("CONNECTED");
var ping = setInterval(function() {
if (websocket != "undefined") {
websocket.send("ping");
}
}, 3000);
};
websocket.onclose = function(evt) {
writeStatus("DISCONNECTED");
};
websocket.onmessage = function(evt) {
if (evt.data === "ping") {
writePing(evt.data);
} else {
writeMessage('ECHO: ' + evt.data);
}
};
websocket.onerror = function(evt) {
onError(writeStatus('ERROR: ' + evt.data))
};
});
$('#disconnect').click(function() {
if (typeof websocket != 'undefined') {
websocket.close();
websocket = undefined;
} else {
alert("Not connected.");
}
});
$('#send').click(function() {
if (typeof websocket != 'undefined') {
websocket.send($('#message').val());
} else {
alert("Not connected.");
}
});
});
</script>
</head>
<body>
<h2>WebSocket Tester</h2>
Target:
<input id="target" size="40"
value="ws://127.0.0.1:8081/wholesmart-springmvc-web/echoHandler">
<br>
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
<br>
<br>Message:
<input id="message" value="">
<button id="send">Send</button>
<br>
<p>Status output:</p>
<pre>
<textarea id="statusOutput" rows="10" cols="50"></textarea>
</pre>
<p>Message output:</p>
<pre>
<textarea id="messageOutput" rows="10" cols="50"></textarea>
</pre>
<p>Ping output:</p>
<pre>
<textarea id="pingOutput" rows="10" cols="50"></textarea>
</pre>
</body>
</html>
3、基与java全注解方式配置WebSocket
package com.wholesmart.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import com.wholesmart.websocket.EchoHandler;
/**
* WebSocket配置类,因为我的项目是在主配置类中使用@Import()注解引入该配置类,所以这个类上没有使用@Configuration注解。
*
* @author dyw
* @date 2020年6月23日
*/
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
/**
* 使用HttpSessionHandshakeInterceptor设置setAllowedOrigins,否则会报403
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler(), "/echoHandler").addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*");
}
@Bean
public EchoHandler echoHandler() {
return new EchoHandler();
}
}
package com.wholesmart.websocket;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
/**
* 消息处理器
*
* @author dyw
* @date 2020年6月23日
*/
public class EchoHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
session.sendMessage(new TextMessage(message.getPayload()));
System.out.println(new TextMessage(message.getPayload()));
}
}
四、基于stomp协议的WebSocket配置
1、JavaConfig方式的配置
package com.wholesmart.config;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.SockJsServiceRegistration;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.StompWebSocketEndpointRegistration;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import com.wholesmart.websocket.UserHandshakeInterceptor;
/**
* 为WebSocket客户端定义简单的传输协议配置方法,通常需要配合使用@EnableWebSocketMessageBroker注解开启相应功能
*
* @author dyw
* @date 2020年6月30日
*/
@EnableWebSocketMessageBroker
public class WebSocketStompConfiguration implements WebSocketMessageBrokerConfigurer {
/**
* 有关处理来自客户端的消息和发送到客户端的消息的配置选项
*/
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
}
/**
* 注册STOMP端点(可以是多个),并将端点映射到特定的URL上,并(可选)启用和配置SockJS回退选项。
* 这里的配置主要用于构建WebSocketHandlerMapping
*
* <pre>
* 这里的配置项有两种类型:
* 1、{@code StompEndpointRegistry} 接口定义的配置
* 2、{@code StompWebSocketEndpointRegistration} 接口定义的配置
* 3、{@code SockJsServiceRegistration} 接口定义的配置
* </pre>
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*
* 从WebMvcStompEndpointRegistry.getHandlerMapping()方法中可以看出,
* WebSocketHandlerMapping优先使用我们这里配置的UrlPathHelper,如果我们这里没有配置这个值,
* WebSocketHandlerMapping会使用在其父类AbstractHandlerMapping中创建的UrlPathHelper。
*/
registry.setUrlPathHelper(null);
/*
* 设置STOMP所使用的HandlerMapping的优先级,默认值为1
*/
registry.setOrder(1);
/*
* 自定义一个处理客户端错误帧的处理器,
* 这里的StompSubProtocolErrorHandler会被设置到WebMvcStompEndpointRegistry类持有的StompSubProtocolHandler对象中
*/
registry.setErrorHandler(stompSubProtocolErrorHandler());
/*
* websocket的端点,客户端需要注册这个端点进行链接
*/
StompWebSocketEndpointRegistration stompWebSocketEndpointRegistration = registry.addEndpoint("/stomp/ws");
/*
* 配置允许的浏览器Origin Header的值,这个配置主要是针对浏览器设计的。HTTP 协议中的 Origin Header
* 存在于请求中,用于指明当前请求来自于哪个站点。默认空表示只支持同源请求,“*”表示支持所有站点请求,其他以“http://”、“https://”
* 表示支持特定站点的请求。
*/
stompWebSocketEndpointRegistration.setAllowedOrigins("*");
/*
* 没有设置时默认使用DefaultHandshakeHandler
*/
stompWebSocketEndpointRegistration.setHandshakeHandler(null);
/*
* OriginHandshakeInterceptor为默认拦截器,用于验证
* OriginHeader,这里的配置只是多添加一个拦截器,不会覆盖掉默认拦截器。
*/
stompWebSocketEndpointRegistration.addInterceptors(userHandshakeInterceptor());
/*
* SockJsServiceRegistration为返回值,可以配置SockJS
*/
SockJsServiceRegistration sockJsServiceRegistration = stompWebSocketEndpointRegistration.withSockJS();
/*
*
*/
sockJsServiceRegistration.setHttpMessageCacheSize(100);
}
@Bean
public HandshakeInterceptor userHandshakeInterceptor() {
return new UserHandshakeInterceptor();
}
@Bean
public StompSubProtocolErrorHandler stompSubProtocolErrorHandler() {
return new StompSubProtocolErrorHandler();
}
/**
* 消息中介的相关配置
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 客户端订阅消息的基础路径
config.setApplicationDestinationPrefixes("/app");
// 服务器广播消息的基础路径
config.enableSimpleBroker("/topic");
}
/**
* 配置消息转换器,以便在从带注解的方法上提取消息和发送消息时使用。返回的boolean值类型用于确定是否还要添加默认转换器。
*/
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
return true;
}
/**
* 客户端消息传入通道的相关配置。默认情况下,通道由大小为1的线程池支持。建议生产环境设置合适的自定义线程池配置。
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
WebSocketMessageBrokerConfigurer.super.configureClientInboundChannel(registration);
}
/**
* 客户端消息传出通道的相关配置。默认情况下,通道由大小为1的线程池支持。建议生产环境设置合适的自定义线程池配置。
*/
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
WebSocketMessageBrokerConfigurer.super.configureClientOutboundChannel(registration);
}
/**
* 添加解析器以支持自定义控制器方法参数类型。不会修改内置方法参数解析器,如果需要修改内置方法参数解析器,直接配置SimpAnnotationMethodMessageHandler
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
WebSocketMessageBrokerConfigurer.super.addArgumentResolvers(argumentResolvers);
}
/**
* 添加处理程序以支持自定义控制器方法返回值类型。内置处理器的修改直接配置SimpAnnotationMethodMessageHandler
*/
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
WebSocketMessageBrokerConfigurer.super.addReturnValueHandlers(returnValueHandlers);
}
}
2、使用到的实体类、控制器、拦截器
package com.wholesmart.websocket.stomp;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Stock implements Serializable {
private static final long serialVersionUID = 7704093854068084650L;
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private String code;
private double price;
private Date date;
private String formatDate;
public Stock() {
}
public Stock(String code, double price) {
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
date = new Date();
formatDate = dateFormat.format(date);
this.code = code;
this.price = price;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
formatDate = dateFormat.format(date);
this.date = date;
}
public String getFormatDate() {
return formatDate;
}
public void setFormatDate(String formatDate) {
this.formatDate = formatDate;
}
public static String getDateFormat() {
return DATE_FORMAT;
}
@Override
public String toString() {
return "Stock [code=" + code + ", price=" + price + "]";
}
}
package com.wholesmart.web;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Controller;
import com.wholesmart.websocket.stomp.Stock;
@Controller
public class StockController {
private TaskScheduler taskScheduler;
private SimpMessagingTemplate simpMessagingTemplate;
private List<Stock> stocks = new ArrayList<Stock>();
private Random random = new Random(System.currentTimeMillis());
public StockController() {
stocks.add(new Stock("VMW", 1.00D));
stocks.add(new Stock("EMC", 1.00D));
stocks.add(new Stock("GOOD", 1.00D));
stocks.add(new Stock("IBM", 1.00D));
}
@MessageMapping("/addStock")
public void addStock(Stock stock) {
stocks.add(stock);
}
@Autowired
public void setTaskScheduler(TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
}
@Autowired
public void setSimpMessagingTemplate(SimpMessagingTemplate simpMessagingTemplate) {
this.simpMessagingTemplate = simpMessagingTemplate;
}
private void broadcastUpdatePrices() {
System.out.println("currentThread==" + Thread.currentThread().getName());
System.out.println("Stocks SIZE=======" + stocks.size());
for (Stock stock : stocks) {
System.out.println("stock===" + stock);
stock.setPrice(stock.getPrice() + (getUpdatedStockPrice() * stock.getPrice()));
stock.setDate(new Date());
}
simpMessagingTemplate.convertAndSend("/topic/price", stocks);
}
private double getUpdatedStockPrice() {
double priceChange = random.nextDouble() * 5.0;
if (random.nextInt(2) == 1) {
priceChange = -priceChange;
}
return priceChange / 100.0;
}
@PostConstruct
private void broadcastTimePeriodically() {
taskScheduler.scheduleAtFixedRate(() -> {
broadcastUpdatePrices();
}, 6000);
}
}
package com.wholesmart.websocket;
import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
/**
* 自定义WebSocket握手拦截器
*
* @author dyw
* @data 2020年7月2日
*/
public class UserHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("UserHandshakeInterceptor.beforeHandshake()");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
System.out.println("UserHandshakeInterceptor.afterHandshake()");
}
}
3、前端测试代码
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Tester</title>
<script type="text/javascript"
src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js">
</script>
<script type="text/javascript"
src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.2/stomp.min.js">
</script>
<script type="text/javascript">
var stomp = Stomp.over(new SockJS("/wholesmart-springmvc-web/stomp/ws"));
function displayStockPrice(frame) {
var prices = JSON.parse(frame.body);
$('#price').empty();
console.log(prices);
for ( var i in prices) {
var price = prices[i];
$('#price').append(
$('<tr>').append($('<td>').html(price.code),
$('<td>').html(price.price.toFixed(2)),
$('<td>').html(price.formatDate)));
}
}
var connectCallback = function() {
stomp.subscribe('/topic/price', displayStockPrice);
}
var errorCallback = function(error) {
alert(error.headers.message);
}
stomp.connect("guest", "guest", connectCallback, errorCallback);
$(document).ready(function() {
$('.addStockButton').click(function(e) {
e.preventDefault();
var jsonstr = JSON.stringify({
'code' : $('.code').val(),
'price' : Number($('.price').val())
});
stomp.send("/app/addStock", {}, jsonstr);
return false;
});
});
</script>
</head>
<body>
<h1>
<b>Stock Ticker</b>
</h1>
<table border="1">
<thead>
<tr>
<th>Code</th>
<th>Price</th>
<th>Time</th>
</tr>
</thead>
<tbody id="price"></tbody>
</table>
<p class="addStock">
Code:<input class="code" /><br> Price:<input class="price" /><br>
<button class="addStockButton">And Stock</button>
</p>
</body>
</html>
五、两个配置类的详细介绍
1、WebSocketConfigurer
2、WebSocketMessageBrokerConfigurer
大家可以试图做各种理性分析,但最终回避不了这样的事实:人是一个个愚昧的、情绪化的个体,我们在有生之涯不知所措,用有限的视野辨识光明,凭着本能在惊恐中寻找猎物和栖息处。