赞
踩
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>Title</title> </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 Box</el-button> </div> <!--1.导入Vue.js--> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script> <!-- 引入样式element-ui --> <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("<tr><td>" + receiveMessage.content + "</td></tr>"); }); } /* 断开连接 */ 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
就可以收到消息了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。