WebSocket 是一种基于 TCP 的网络协议。是一种全双工通信的协议,既允许客户端向服务器主动发送消息,也允许服务器主动向客户端发送消息。在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,进行双向数据传输。
谈起为什么需要 WebSocket 前,那得先了解在没有 WebSocket 那段时间说起,那时候基于 Web 的消息基本上是靠 Http 协议进行通信,而经常有"聊天室"、“消息推送”、"股票信 息实时动态"等这样需求,而实现这样的需求常用的有以下几种解决方案:
(1)、短轮询(Traditional Polling)
短轮询是指客户端每隔一段时间就询问一次服务器是否有新的消息,如果有就接收消息。这样方式会增加很多次无意义的发送请求信息,每次都会耗费流量及处理器资源。
(2)、长轮询(Long Polling)
长轮询是段轮询的改进,客户端执行 HTTP 请求发送消息到服务器后,等待服务器回应,如果没有新的消息就一直等待,知道服务器有新消息传回或者超时。这也是个反复的过程,这种做法只是减小了网络带宽和处理器的消耗,但是带来的问题是导致消息实时性低,延迟严重。而且也是基于循环,最根本的带宽及处理器资源占用并没有得到有效的解决。
(3)、服务器发送事件(Server-Sent Event)
服务器发送事件是一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如"自动重新连接"、“事件ID” 及 "发送任意事件"的能力。
服务器发送事件是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。
显然,上面这几种方式都有各自的优缺点,虽然靠轮询方式能够实现这些一些功能,但是其对性能的开销和低效率是非常致命的,尤其是在移动端流行的现在。现在客户端与服务端双向通信的需求越来越多,且现在的浏览器大部分都支持 WebSocket。所以对实时性和双向通信及其效率有要求的话,比较推荐使用 WebSocket。
(1)、客户端先用带有 Upgrade:Websocket 请求头的 HTTP 请求,向服务器端发起连接请求,实现握手(HandShake)。
Connection: Upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: IRQYhWINfX5Fh1zdocDl6Q==
Sec-WebSocket-Version: 13
Upgrade: websocket
(2)、握手成功后,由 HTTP 协议升级成 Websocket 协议,进行长连接通信,两端相互传递信息。
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置类
@Configuration
public class WebSocketConfig{
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
websoket类
@Slf4j
@Component
@ServerEndpoint("/websocket")
public class WebSocket {
private Session session;
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this);
log.info("【websocket消息】有新的连接, 总数:{}", webSocketSet.size());
}
@OnClose
public void onClose() {
webSocketSet.remove(this);
log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size());
}
@OnMessage
public void onMessage(String message) {
log.info("【websocket消息】收到客户端发来的消息:{}", message);
}
public void sendMessage(String message) {
for (WebSocket webSocket: webSocketSet) {
log.info("【websocket消息】广播消息, message={}", message);
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
订单实体类
@Data
@AllArgsConstructor
public class Order {
private String orderId;
private String buyerName;
private String buyerPhone;
private String buyerAddress;
private Double orderAmount;
private Integer state;
private String createDate;
}
controller
@Slf4j
@RestController
public class OrderController {
@Autowired
private WebSocket webSocket;
static List<Order> orderList=new ArrayList();
{
orderList.add(new Order("1","张三","13512341234","上海市",11.11,1,"2022-10-01"));
orderList.add(new Order("2","李四","18367445678","北京市",22.22,1,"2022-10-02"));
orderList.add(new Order("3","王五","13812345678","天津市",33.33,1,"2022-10-03"));
}
/**
* 模拟下单方法
*/
@GetMapping("/create")
public void createOrder(){
//1.扣库存
//2.插入数据
orderList.add(new Order("4","赵六","13452057018","辽宁市",44.44,1,"2022-10-04"));
//3.websocket通知客户端有新订单
webSocket.sendMessage("有新的订单!");
}
@GetMapping("/getOrder")
public List getOrder(){
return orderList;
}
}
前端页面
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<div id="app">
<el-table :data="items" stripe style="width: 100%">
<el-table-column prop="orderId" label="订单号" width="180">el-table-column>
<el-table-column prop="buyerName" label="姓名" width="180">el-table-column>
<el-table-column prop="buyerPhone" label="电话" width="180">el-table-column>
<el-table-column prop="buyerAddress" label="地址" width="180">el-table-column>
<el-table-column prop="orderAmount" label="订单金额" width="180">el-table-column>
<el-table-column prop="state" label="状态" width="180">el-table-column>
<el-table-column prop="createDate" label="创建时间" width="180">el-table-column>
el-table>
<el-button type="text" @click="open">点击打开 Message Boxel-button>
div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js">script>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://unpkg.com/element-ui/lib/index.js">script>
<script src="https://unpkg.com/axios/dist/axios.min.js">script>
<script>
var vm = new Vue({
el: "#app",
data() {
return {
items: [],
message: ""
}
},
methods: {
open() {
this.$alert(this.message, {
confirmButtonText: '查看',
callback: action => {
this.init();
}
});
},
init(){
axios.get('http://localhost:8080/getOrder').then(response => (this.items = response.data));
}
},
mounted() {
this.init();
}
})
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket('ws://localhost:8080/websocket');
} else {
alert('该浏览器不支持websocket!');
}
websocket.onopen = function (event) {
console.log('建立连接');
}
websocket.onclose = function (event) {
console.log('连接关闭');
}
websocket.onmessage = function (event) {
console.log('收到消息:' + event.data)
vm.message = event.data;
vm.open();
}
websocket.onerror = function () {
alert('websocket通信发生错误!');
}
window.onbeforeunload = function () {
websocket.close();
}
script>
body>
html>
启动项目,测试报错
开启跨域
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 拦截所有的请求
.allowedOriginPatterns("*") // 可跨域的域名,可以为 *
.allowCredentials(true)
.allowedMethods("*") // 允许跨域的方法,可以单独配置
.allowedHeaders("*"); // 允许跨域的请求头,可以单独配置
}
}
效果:
开始测试:
1.请求新增订单
点击查看
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接
*
* @param registry STOMP 端点
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mydlq").withSockJS();
}
/**
* 配置消息代理选项
*
* @param registry 消息代理注册配置
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。
// ⽤户可以订阅来⾃以"/topic"为前缀的消息,客户端只可以订阅这个前缀的主题
registry.enableSimpleBroker("/topic/abc");
// 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里
//客户端发送过来的消息,需要以"/app"为前缀,再经过Broker转发给响应的Controller
registry.setApplicationDestinationPrefixes("/app");
}
}
实体类
@Data
@ToString
public class MessageBody {
/** 消息内容 */
private String content;
/** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */
private String destination;
}
cotroller
@Controller
public class MessageController {
/** 消息发送工具对象 */
@Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;
/** 广播发送消息,将消息发送到指定的目标地址 */
@MessageMapping("/test")
public void sendTopicMessage(MessageBody messageBody) {
// 将消息发送到 WebSocket 配置类中配置的代理中(/topic)进行消息转发
System.out.println(messageBody);
simpMessageSendingOperations.convertAndSend(messageBody.getDestination(), messageBody);
}
}
前端页面
<!DOCTYPE html>
<html>
<head>
<title>Hello WebSocket</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<!-- <script src="app-websocket.js"></script>-->
</head>
<body>
<div id="main-content" class="container" style="margin-top: 10px;">
<div class="row">
<form class="navbar-form" style="margin-left:0px">
<div class="col-md-12">
<div class="form-group">
<label>WebSocket 连接:</label>
<button class="btn btn-primary" type="button" onclick="connect();">进行连接</button>
<button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button>
</div>
<label>订阅地址:</label>
<div class="form-group">
<input type="text" id="subscribe" class="form-control" placeholder="订阅地址">
</div>
<button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button>
</div>
</form>
</div>
</br>
<div class="row">
<div class="form-group">
<label for="content">发送的消息内容:</label>
<input type="text" id="content" class="form-control" placeholder="消息内容">
</div>
<button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button>
</div>
</br>
<div class="row">
<div class="col-md-12">
<h5 class="page-header" style="font-weight:bold">接收到的消息:</h5>
<table class="table table-striped">
<tbody id="information"></tbody>
</table>
</div>
</div>
</div>
</body>
</html>
<script>
// 设置 STOMP 客户端
var stompClient = null;
// 设置 WebSocket 进入端点
var SOCKET_ENDPOINT = "/mydlq";
// 设置订阅消息的请求前缀
var SUBSCRIBE_PREFIX = "/topic"
// 设置订阅消息的请求地址
var SUBSCRIBE = "";
// 设置服务器端点,访问服务器中哪个接口
var SEND_ENDPOINT = "/app/test";
/* 进行连接 */
function connect() {
// 设置 SOCKET
var socket = new SockJS(SOCKET_ENDPOINT);
// 配置 STOMP 客户端
stompClient = Stomp.over(socket);
// STOMP 客户端连接
stompClient.connect({}, function (frame) {
alert("连接成功");
});
}
/* 订阅信息 */
function subscribeSocket(){
// 设置订阅地址
SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val();
// 输出订阅地址
alert("设置订阅地址为:" + SUBSCRIBE);
// 执行订阅消息
stompClient.subscribe(SUBSCRIBE, function (responseBody) {
var receiveMessage = JSON.parse(responseBody.body);
$("#information").append("" + receiveMessage.content + " ");
});
}
/* 断开连接 */
function disconnect() {
stompClient.disconnect(function() {
alert("断开连接");
});
}
/* 发送消息并指定目标地址(这里设置的目标地址为自身订阅消息的地址,当然也可以设置为其它地址) */
function sendMessageNoParameter() {
// 设置发送的内容
var sendContent = $("#content").val();
// 设置待发送的消息内容
var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';
// 发送消息
stompClient.send(SEND_ENDPOINT, {}, message);
}
</script>
输入地址 http://localhost:8080/index.html
访问测试的前端页面,然后执行下面步骤进行测试:
1.点击进行连接按钮,连接 WebSocket 服务端;
2.在订阅地址栏输入订阅地址
3.点击订阅按钮订阅对应地址的消息;
4.在发送消息内容的输入框中输入hello world!,然后点击发送按钮发送消息;
流程:先连接服务端,订阅一个地址(当这个地址有消息,服务端就会发送过来,实时显示在界面上),然后发送消息
例:
连接服务端后,订阅了/topic/qqq
这个地址
调用/app/test
接口,参数为MessageBody(content=999, destination=/topic/qqq)
此接口向/topic/qqq
这个地址发送消息999
WebSocket 配置类中配置的代理中(/topic/abc)进行消息转发,变成了向/topic/abc
发消息999
而前端订阅的是/topic/qqq
,所以收不到消息
当订阅的是/topic/abc
就可以收到消息了