一、为什么使用WebSocket
WebSocket通过在客户端和服务端之间一次握手后创建持久性的链接进行高效的双向数据传输,一般用于服务端有连续的状态变更需在客户端实时展示。传统的HTTP只能通过轮询机制不断建立HTTP连接获取服务器最新信息,这种方式效率低且浪费资源。
二、WebSocket简介
WebSocket的最大特点是服务端可以主动向客户端推送消息,客户端也可以主动向服务端推送消息,是真正的双向平等对话,属于服务推送技术的一种。
WebSocket和HTTP的连接会话周期区别如图:
其他特点:
- 与http协议有良好的兼容性,默认端口也是80和443,并且握手阶段采用http协议不容易被屏蔽,能通过各种代理服务器
- 数据格式轻量,性能高效,通信效率高
- 可以发送文本和二进制数据
- 没有同源限制,客户端可以和任何服务端通信
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
三、spring boot 集成WebSocket
(1)创建spring boot 工程引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
(2)spring websocket的核心配置
核心配置类代码如下:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
//外部可订阅的socket数据服务(socket/topic为服务端广播消息的客户端订阅地址前缀,socket/user为客户端个性化消息的订阅地址前缀)
private static final String[] brokers = {
"/socket/topic","/socket/user"
};
//配置stomp端点,即客户端的握手连接机制
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket") //注册Stomp端点,websocket客户端订阅或发布消息前,需通过此端点(路径)先连接(握手)
.setAllowedOrigins("*") //允许跨域访问
.withSockJS(); //使用sockJS
}
// 消息代理(消息访问路径)
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker(brokers);// 配置客户端可订阅的消息前缀
registry.setApplicationDestinationPrefixes("/socket");//前端个性化订阅前,用stomp协议发送消息到服务端的地址前缀
registry.setUserDestinationPrefix("/socket/user");//推送消息到订阅用户的前缀地址(如前端订阅为"/user/message",则服务端send_to_user直接为"message")
}
}
(3)创建spring websocket的消息返回通用实体bean
spring-websocket 会将消息代理返回的消息用json lib包装为json格式,本例中创建一个restful的消息包装类如下:
同时创建一个返回消息实体类Order,下文和代码中都以订单为返回消息的实体类
(4)服务端消息服务编写
本例中编写一个消息控制器,代理两类消息:
- 广播订阅消息(以服务端实时推送所有订单列表为例)
- 用户实时订阅个性化消息(以用户实时查询自己的最新订单列表)
代码如下:
package com.darren.demo.controller;
import com.darren.demo.model.Order;
import com.darren.demo.response.MessageCode;
import com.darren.demo.response.WsResponse;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
@Controller
public class SocketApi {
@Autowired
private SimpMessagingTemplate messagingTemplate;//spring websockt通过messagingTemplate可以主动向客户端推送消息
/**
* 向客户端推送所有订单数据
* Scheduled 定义推送频率
*/
@Scheduled(fixedRate = 3000,initialDelay = 3000)
private void pushDeviceInfo(){
WsResponse serverResponse=WsResponse.failure(MessageCode.COMMON_FAILURE);
List<Order> orderList=new ArrayList<Order>();
for(int i=0;i<10;i++){
Order order=new Order();
order.setOrderId(RandomUtils.nextLong());
order.setUserId(RandomStringUtils.randomAlphanumeric(5));
order.setPrice(RandomUtils.nextDouble());
orderList.add(order);
}
if(!CollectionUtils.isEmpty(orderList)){
serverResponse=WsResponse.success(orderList);
}
messagingTemplate.convertAndSend("/socket/topic/order_list",serverResponse);
}
/**
* 客户端个性化订阅自己的实时订单,send路径(/socket/user/order/+{userId}),根据配置类,subscribe路径(/socket/user/+${userId}+/order/)
* @param userId
* @return void
* @author
* @date 2019-07-14 18:17
*/
@MessageMapping("/order/{userId}")
private void getLatestOrder(@DestinationVariable(value="userId") String userId){
WsResponse serverResponse=WsResponse.failure(MessageCode.COMMON_FAILURE);
List<Order> orderList=new ArrayList<Order>();
for(int i=0;i<10;i++){
Order order=new Order();
order.setOrderId(RandomUtils.nextLong());
order.setUserId(userId);
order.setPrice(RandomUtils.nextDouble());
orderList.add(order);
}
if(!CollectionUtils.isEmpty(orderList)){
serverResponse=WsResponse.success(orderList);
}
messagingTemplate.convertAndSendToUser(userId,"order",serverResponse);
}
}
(5)编写客户端测试页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>socket 客户端</title>
</head>
<body onload="disconnect()">
<noscript>
<h2 style="color:#ff0000">貌似你的浏览器不支持websocket</h2>
</noscript>
<div>
<div>
<button id="connect" onclick="connect()">连接</button>
<button id="disconnect" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<label>userId</label> <input type="text" id="name" />
<br>
<button id="send" onclick="send();">发送</button>
<p id="response"></p>
</div>
</div>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
var subcri=null;
var stompClient = null;
var host="http://localhost:8080";
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
$('#response').html();
}
function connect() {
var socket = new SockJS(host+'/socket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected:' + frame);
subcri=stompClient.subscribe('/socket/topic/order_list', function(response) {
showResponse(response.body);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function showResponse(message) {
var response = $('#response');
response.html(message);
}
function userSend(name){
stompClient.send("/socket/order/"+name, {}, null);
}
//由于个性化订阅发送请求后服务端只作一次性推送,需客户端定时send接收服务端的最新消息
function send(){
var name = $('#name').val();
var message = $('#messgae').val();
if(subcri){
subcri.unsubscribe();
}
setInterval(()=>{
userSend(name);
},1000);
subcri=stompClient.subscribe('/socket/user/'+name+'/order', function(response) {
showResponse(response.body);
});
}
</script>
</body>
</html>
注:例子中连接后发送请求为个性化订阅
(6)其他趟坑
1.websocket 跨域问题
websocket请求如通过gateway转发,也一样需要设置允许跨域,单返回给网关时跨域信息有两份,需在网关测去重处理;
如果websocket不设置运行跨域,即注释setAllowedOrigins(“*”) 则会默认启用spring boot的DefaultCorsProcessor 默认跨域处理器进行处理跨域websocket连接
项目源代码:
https://github.com/DarrenJiang1990/awesome-websocket/
参考:
1.http://springcloud.cn/view/36
2.http://doc.okbase.net/PacosonSWJTU/archive/247277.html
3.https://juejin.im/post/5c53e2876fb9a049f23d3159
4.https://blog.csdn.net/qq_31457665/article/details/80223098
5.https://spring.io/guides/gs/messaging-stomp-websocket/
6.https://yq.aliyun.com/articles/652135
7.https://blog.csdn.net/jqsad/article/details/77745379
8.参数传递 http://www.voidcn.com/article/p-ddvhnpyl-dg.html
9.特定用户发送消息 https://segmentfault.com/a/1190000011908831
10. https://cloud.spring.io/spring-cloud-gateway/single/spring-cloud-
gateway.html#_websocket_routing_filter
https://www.xncoding.com/2017/07/15/spring/sb-websocket.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/15018.html