詳解springboot集成websocket的兩種實(shí)現(xiàn)方式
WebSocket跟常規(guī)的http協(xié)議的區(qū)別和優(yōu)缺點(diǎn)這里大概描述一下
一、websocket與http
http協(xié)議是用在應(yīng)用層的協(xié)議,他是基于tcp協(xié)議的,http協(xié)議建立鏈接也必須要有三次握手才能發(fā)送信息。http鏈接分為短鏈接,長鏈接,短鏈接是每次請求都要三次握手才能發(fā)送自己的信息。即每一個request對應(yīng)一個response。長鏈接是在一定的期限內(nèi)保持鏈接。保持TCP連接不斷開??蛻舳伺c服務(wù)器通信,必須要有客戶端發(fā)起然后服務(wù)器返回結(jié)果??蛻舳耸侵鲃拥?,服務(wù)器是被動的。
WebSocket是HTML5中的協(xié)議, 他是為了解決客戶端發(fā)起多個http請求到服務(wù)器資源瀏覽器必須要經(jīng)過長時間的輪訓(xùn)問題而生的,他實(shí)現(xiàn)了多路復(fù)用,他是全雙工通信。在webSocket協(xié)議下客服端和瀏覽器可以同時發(fā)送信息。
二、HTTP的長連接與websocket的持久連接
HTTP1.1的連接默認(rèn)使用長連接(persistent connection),
即在一定的期限內(nèi)保持鏈接,客戶端會需要在短時間內(nèi)向服務(wù)端請求大量的資源,保持TCP連接不斷開??蛻舳伺c服務(wù)器通信,必須要有客戶端發(fā)起然后服務(wù)器返回結(jié)果??蛻舳耸侵鲃拥?,服務(wù)器是被動的。
在一個TCP連接上可以傳輸多個Request/Response消息對,所以本質(zhì)上還是Request/Response消息對,仍然會造成資源的浪費(fèi)、實(shí)時性不強(qiáng)等問題。
如果不是持續(xù)連接,即短連接,那么每個資源都要建立一個新的連接,HTTP底層使用的是TCP,那么每次都要使用三次握手建立TCP連接,即每一個request對應(yīng)一個response,將造成極大的資源浪費(fèi)。
長輪詢,即客戶端發(fā)送一個超時時間很長的Request,服務(wù)器hold住這個連接,在有新數(shù)據(jù)到達(dá)時返回Response
websocket的持久連接 只需建立一次Request/Response消息對,之后都是TCP連接,避免了需要多次建立Request/Response消息對而產(chǎn)生的冗余頭部信息。
Websocket只需要一次HTTP握手,所以說整個通訊過程是建立在一次連接/狀態(tài)中,而且websocket可以實(shí)現(xiàn)服務(wù)端主動聯(lián)系客戶端,這是http做不到的。
springboot集成websocket的不同實(shí)現(xiàn)方式:
pom添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
因涉及到j(luò)s連接服務(wù)端,所以也寫了對應(yīng)的html,這里集成下thymeleaf模板,前后分離的項(xiàng)目這一塊全都是前端做的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置文件:
server: port: 8885 #添加Thymeleaf配置 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mode: HTML5 encoding: UTF-8 content-type: text/html
1:自定義WebSocketServer,使用底層的websocket方法,提供對應(yīng)的onOpen、onClose、onMessage、onError方法
1.1:添加webSocketConfig配置類
/**
* 開啟WebSocket支持
* Created by huiyunfei on 2019/5/31.
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
1.2:添加webSocketServer服務(wù)端類
package com.example.admin.web;
/**
* Created by huiyunfei on 2019/5/31.
*/
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {
//靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計(jì)成線程安全的。
private static int onlineCount = 0;
//concurrent包的線程安全Set,用來存放每個客戶端對應(yīng)的MyWebSocket對象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
//與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
private Session session;
//接收sid
private String sid="";
*/
/**
* 連接建立成功調(diào)用的方法*//*
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在線數(shù)加1
log.info("有新窗口開始監(jiān)聽:"+sid+",當(dāng)前在線人數(shù)為" + getOnlineCount());
this.sid=sid;
try {
sendMessage("連接成功");
} catch (IOException e) {
log.error("websocket IO異常");
}
}
*/
/**
* 連接關(guān)閉調(diào)用的方法
*//*
@OnClose
public void onClose() {
webSocketSet.remove(this); //從set中刪除
subOnlineCount(); //在線數(shù)減1
log.info("有一連接關(guān)閉!當(dāng)前在線人數(shù)為" + getOnlineCount());
}
*/
/**
* 收到客戶端消息后調(diào)用的方法
*
* @param message 客戶端發(fā)送過來的消息*//*
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到來自窗口"+sid+"的信息:"+message);
//群發(fā)消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
*/
/**
*
* @param session
* @param error
*//*
@OnError
public void onError(Session session, Throwable error) {
log.error("發(fā)生錯誤");
error.printStackTrace();
}
*/
/**
* 實(shí)現(xiàn)服務(wù)器主動推送
*//*
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
*/
/**
* 群發(fā)自定義消息
* *//*
public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException {
log.info("推送消息到窗口"+sid+",推送內(nèi)容:"+message);
for (WebSocketServer item : webSocketSet) {
try {
//這里可以設(shè)定只推送給這個sid的,為null則全部推送
if(sid==null) {
item.sendMessage(message);
}else if(item.sid.equals(sid)){
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
return webSocketSet;
}
}
1.3:添加對應(yīng)的controller
@Controller
@RequestMapping("/system")
public class SystemController {
//頁面請求
@GetMapping("/index/{userId}")
public ModelAndView socket(@PathVariable String userId) {
ModelAndView mav=new ModelAndView("/socket1");
mav.addObject("userId", userId);
return mav;
}
//推送數(shù)據(jù)接口
@ResponseBody
@RequestMapping("/socket/push/{cid}")
public Map pushToWeb(@PathVariable String cid, String message) {
Map result = new HashMap();
try {
WebSocketServer.sendInfo(message,cid);
result.put("code", 200);
result.put("msg", "success");
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
1.4:提供socket1.html頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"></meta>
<title>Title</title>
</head>
<body>
hello world!
</body>
<script>
var socket;
if(typeof(WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
}else{
console.log("您的瀏覽器支持WebSocket");
//實(shí)現(xiàn)化WebSocket對象,指定要連接的服務(wù)器地址與端口 建立連接
//等同于
index = new WebSocket("ws://localhost:8885/websocket/2");
//socket = new WebSocket("${basePath}websocket/${cid}".replace("http","ws"));
//打開事件
index.onopen = function() {
console.log("Socket 已打開");
//socket.send("這是來自客戶端的消息" + location.href + new Date());
};
//獲得消息事件
index.onmessage = function(msg) {
console.log(msg.data);
//發(fā)現(xiàn)消息進(jìn)入 開始處理前端觸發(fā)邏輯
};
//關(guān)閉事件
index.onclose = function() {
console.log("Socket已關(guān)閉");
};
//發(fā)生了錯誤事件
index.onerror = function() {
alert("Socket發(fā)生了錯誤");
//此時可以嘗試刷新頁面
}
//離開頁面時,關(guān)閉socket
//jquery1.8中已經(jīng)被廢棄,3.0中已經(jīng)移除
// $(window).unload(function(){
// socket.close();
//});
}
</script>
</html>
總結(jié):
瀏覽器debug訪問 localhost:8885/system/index/1跳轉(zhuǎn)到socket1.html,js自動連接server并傳遞cid到服務(wù)端,服務(wù)端對應(yīng)的推送消息到客戶端頁面(cid區(qū)分不同的請求,server里提供的有群發(fā)消息方法)
2.1:基于STOMP協(xié)議的WebSocket
使用STOMP的好處在于,它完全就是一種消息隊(duì)列模式,你可以使用生產(chǎn)者與消費(fèi)者的思想來認(rèn)識它,發(fā)送消息的是生產(chǎn)者,接收消息的是消費(fèi)者。而消費(fèi)者可以通過訂閱不同的destination,來獲得不同的推送消息,不需要開發(fā)人員去管理這些訂閱與推送目的地之前的關(guān)系,spring官網(wǎng)就有一個簡單的spring-boot的stomp-demo,如果是基于springboot,大家可以根據(jù)spring上面的教程試著去寫一個簡單的demo。
提供websocketConfig配置類
/**
* @Description:
registerStompEndpoints(StompEndpointRegistry registry)
configureMessageBroker(MessageBrokerRegistry config)
這個方法的作用是定義消息代理,通俗一點(diǎn)講就是設(shè)置消息連接請求的各種規(guī)范信息。
registry.enableSimpleBroker("/topic")表示客戶端訂閱地址的前綴信息,也就是客戶端接收服務(wù)端消息的地址的前綴信息(比較繞,看完整個例子,大概就能明白了)
registry.setApplicationDestinationPrefixes("/app")指服務(wù)端接收地址的前綴,意思就是說客戶端給服務(wù)端發(fā)消息的地址的前綴
* @Author:hui.yunfei@qq.com
* @Date: 2019/5/31
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
// 這個方法的作用是添加一個服務(wù)端點(diǎn),來接收客戶端的連接。
// registry.addEndpoint("/socket")表示添加了一個/socket端點(diǎn),客戶端就可以通過這個端點(diǎn)來進(jìn)行連接。
// withSockJS()的作用是開啟SockJS支持,
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//表示客戶端訂閱地址的前綴信息,也就是客戶端接收服務(wù)端消息的地址的前綴信息
registry.enableSimpleBroker("/topic");
//指服務(wù)端接收地址的前綴,意思就是說客戶端給服務(wù)端發(fā)消息的地址的前綴
registry.setApplicationDestinationPrefixes("/app");
}
}
2.2:controller提供對應(yīng)請求的接口
//頁面請求
@GetMapping("/socket2")
public ModelAndView socket2() {//@PathVariable String userId
ModelAndView mav=new ModelAndView("html/socket2");
//mav.addObject("userId", userId);
return mav;
}
/**
* @Description:這個方法是接收客戶端發(fā)送功公告的WebSocket請求,使用的是@MessageMapping
* @Author:hui.yunfei@qq.com
* @Date: 2019/5/31
*/
@MessageMapping("/change-notice")//客戶端訪問服務(wù)端的時候config中配置的服務(wù)端接收前綴也要加上 例:/app/change-notice
@SendTo("/topic/notice")//config中配置的訂閱前綴記得要加上
public CustomMessage greeting(CustomMessage message){
System.out.println("服務(wù)端接收到消息:"+message.toString());
//我們使用這個方法進(jìn)行消息的轉(zhuǎn)發(fā)發(fā)送!
//this.simpMessagingTemplate.convertAndSend("/topic/notice", value);(可以使用定時器定時發(fā)送消息到客戶端)
// @Scheduled(fixedDelay = 1000L)
// public void time() {
// messagingTemplate.convertAndSend("/system/time", new Date().toString());
// }
//也可以使用sendTo發(fā)送
return message;
}
2.3:提供socket2.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring Boot+WebSocket+廣播式</title>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的瀏覽器不支持websocket</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">連接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">斷開連接</button>
</div>
<div id="conversationDiv">
<label>輸入你的名字</label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">發(fā)送</button>
<p id="response"></p>
</div>
</div>
<script th:src="@{/js/sockjs.min.js}"></script>
<script th:src="@{/js/stomp.min.js}"></script>
<script th:src="@{/js/jquery-3.2.1.min.js}"></script>
<script type="text/javascript">
var stompClient = null;
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('/socket'); //1
stompClient = Stomp.over(socket);//2
stompClient.connect({}, function(frame) {//3
setConnected(true);
console.log('開始進(jìn)行連接Connected: ' + frame);
stompClient.subscribe('/topic/notice', function(respnose){ //4
showResponse(JSON.parse(respnose.body).responseMessage);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = $('#name').val();
stompClient.send("/app/change-notice", {}, JSON.stringify({ 'name': name }));//5
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
</script>
</body>
</html>
2.4:對應(yīng)的js引用可以去網(wǎng)上下載
2.5:瀏覽器debug訪問localhost:8885/system/socket2,點(diǎn)擊連接連接到服務(wù)器,數(shù)據(jù)內(nèi)容可以推送到服務(wù)器以及服務(wù)器消息回推。
2.6:實(shí)現(xiàn)前端和服務(wù)端的輪訓(xùn)可以頁面Ajax輪訓(xùn)也可以后端添加定時器
@Component
@EnableScheduling
public class TimeTask {
private static Logger logger = LoggerFactory.getLogger(TimeTask.class);
@Scheduled(cron = "0/20 * * * * ?")
public void test(){
System.err.println("********* 定時任務(wù)執(zhí)行 **************");
CopyOnWriteArraySet<WebSocketServer> webSocketSet =
WebSocketServer.getWebSocketSet();
int i = 0 ;
webSocketSet.forEach(c->{
try {
c.sendMessage(" 定時發(fā)送 " + new Date().toLocaleString());
} catch (IOException e) {
e.printStackTrace();
}
});
System.err.println("/n 定時任務(wù)完成.......");
}
}
代碼在https://github.com/huiyunfei/spring-cloud.git 的admin項(xiàng)目里
基于STOMP協(xié)議的廣播模式和點(diǎn)對點(diǎn)模式消息推送可參考:
https://www.cnblogs.com/hhhshct/p/8849449.html
https://www.cnblogs.com/jmcui/p/8999998.html
到此這篇關(guān)于springboot集成websocket的兩種實(shí)現(xiàn)方式的文章就介紹到這了,更多相關(guān)springboot集成websocket內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)的漢語拼音工具類完整實(shí)例
這篇文章主要介紹了Java實(shí)現(xiàn)的漢語拼音工具類,結(jié)合完整實(shí)例形式分析了java基于pinyin4j包實(shí)現(xiàn)編碼轉(zhuǎn)換的相關(guān)操作技巧,需要的朋友可以參考下2017-11-11
SpringBoot+Redis防止惡意刷新與暴力請求接口的實(shí)現(xiàn)
這篇文章主要為大家介紹了如何利用springboot和Redis來實(shí)現(xiàn)防止惡意刷新與暴力請求接口,文中的示例代碼講解詳細(xì),需要的可以參考一下2022-06-06
Mybatis-Plus最優(yōu)化持久層開發(fā)過程
Mybatis-plus(簡稱MP)是一個Mybatis的增強(qiáng)工具,在mybatis的基礎(chǔ)上只做增強(qiáng)不做改變,提高效率,自動生成單表的CRUD功能,這篇文章主要介紹了Mybatis-Plus最優(yōu)化持久層開發(fā),需要的朋友可以參考下2024-07-07
JAVA求兩直線交點(diǎn)和三角形內(nèi)外心的方法
本文提供了JAVA求兩直線交點(diǎn)、三角形外心、三角形內(nèi)心的代碼和算法講解,大家可以參考使用2013-11-11
Mybatis-Plus根據(jù)時間段去查詢數(shù)據(jù)的實(shí)現(xiàn)示例
這篇文章主要介紹了Mybatis-Plus根據(jù)時間段去查詢數(shù)據(jù)的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
java8 stream 由一個list轉(zhuǎn)化成另一個list案例
這篇文章主要介紹了java8 stream 由一個list轉(zhuǎn)化成另一個list案例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Spring項(xiàng)目中使用Cache?Redis實(shí)現(xiàn)數(shù)據(jù)緩存
這篇文章主要為大家介紹了項(xiàng)目中使用Spring?Cache?Redis實(shí)現(xiàn)數(shù)據(jù)緩存,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
sprintboot使用spring-security包,緩存內(nèi)存與redis共存方式
這篇文章主要介紹了sprintboot使用spring-security包,緩存內(nèi)存與redis共存方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10

