目錄
1.需求
2.原理
2.1握手協定:
2.2優點
3.步驟
3.1後端springboot內建websocket
3.2建立配置類, 開啟WebSocket支援
3.3建立WebSocketServer服務端
3.4前端
3.5編寫通路接口模仿服務端消息推送
3.6服務端推送對象資料(WebSocket-發送對象-自定義Encoder)
3.7結果
4異常
1.需求
前後端實作資料實時傳輸,采用長連接配接的模式 websocket
前端vue項目,後端Springboot
2.原理
WebSocket是一種在單個TCP連接配接上進行全雙工通信的協定。WebSocket通信協定于2011年被IETF定為标準RFC 6455,并由RFC7936補充規範。WebSocketAPI也被W3C定為标準。
WebSocket使得用戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向用戶端推送資料。在WebSocket API中,浏覽器和伺服器隻需要完成一次握手,兩者之間就直接可以建立持久性的連接配接,并進行雙向資料傳輸。
2.1握手協定:
WebSocket 是獨立的、建立在 TCP 上的協定。
Websocket 通過HTTP/1.1 協定的101狀态碼進行握手。
為了建立Websocket連接配接,需要通過浏覽器送出請求,之後伺服器進行回應,這個過程通常稱為“握手”(handshaking)。
2.2優點
優點:
- 較少的控制開銷。在連接配接建立後,伺服器和用戶端之間交換資料時,用于協定控制的資料標頭部相對較小。在不包含擴充的情況下,對于伺服器到用戶端的内容,此頭部大小隻有2至10位元組(和資料包長度有關);對于用戶端到伺服器的内容,此頭部還需要加上額外的4位元組的掩碼。相對于HTTP請求每次都要攜帶完整的頭部,此項開銷顯著減少了。
- 更強的實時性。由于協定是全雙工的,是以伺服器可以随時主動給用戶端下發資料。相對于HTTP請求需要等待用戶端發起請求服務端才能響應,延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間内更多次地傳遞資料。
- 保持連接配接狀态。與HTTP不同的是,Websocket需要先建立連接配接,這就使得其成為一種有狀态的協定,之後通信時可以省略部分狀态資訊。而HTTP請求可能需要在每個請求都攜帶狀态資訊(如身份認證等)。
- 更好的二進制支援。Websocket定義了二進制幀,相對HTTP,可以更輕松地處理二進制内容。
- 可以支援擴充。Websocket定義了擴充,使用者可以擴充協定、實作部分自定義的子協定。如部分浏覽器支援壓縮等。
- 更好的壓縮效果。相對于HTTP壓縮,Websocket在适當的擴充支援下,可以沿用之前内容的上下文,在傳遞類似的資料時,可以顯著地提高壓縮率。
3.步驟
3.1後端springboot內建websocket
gradle中內建websocket
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
3.2建立配置類, 開啟WebSocket支援
WebSocketConfig.java
package com.trgis.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 開啟WebSocket支援
**/
@Configuration
@ConditionalOnWebApplication
public class WebSocketConfig {
//使用boot内置tomcat時需要注入此bean
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.3建立WebSocketServer服務端
WebSocket.java
@ServerEndpoint("/websocket")
@Component
@Slf4j
public class WebSocket {
//與某個用戶端的連接配接會話,需要通過它來給用戶端發送資料
private Session session;
//concurrent包的線程安全Set,用來存放每個用戶端對應的WebSocket對象。
private static CopyOnWriteArraySet<WebSocket> webSocketSet=new CopyOnWriteArraySet<>();
/**
* 建立連接配接成功
* @param session
*/
@OnOpen
public void onOpen(Session session){
this.session=session;
webSocketSet.add(this);
log.info("【websocket消息】 有新的連接配接,總數{}",webSocketSet.size());
}
/**
* 連接配接關閉
*/
@OnClose
public void onClose(){
this.session=session;
webSocketSet.remove(this);
log.info("【websocket消息】 連接配接斷開,總數{}",webSocketSet.size());
}
/**
* 接收用戶端消息
* @param message
*/
@OnMessage
public void onMessage(String message){
log.info("【websocket消息】 收到用戶端發來的消息:{}",message);
}
/**
* 發送消息
* @param message
*/
public void sendMessage(String message){
log.info("【websocket消息】 發送消息:{}",message);
for (WebSocket webSocket:webSocketSet){
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@ServerEndpoint 注解是一個類層次的注解,它的功能主要是将目前的類定義成一個websocket伺服器端。注解的值将被用于監聽使用者連接配接的終端通路URL位址。
onOpen 和 onClose 方法分别被@OnOpen和@OnClose 所注解。他們定義了當一個新使用者連接配接和斷開的時候所調用的方法。
onMessage 方法被@OnMessage所注解。這個注解定義了當伺服器接收到用戶端發送的消息時所調用的方法。
用onMessage()接收前端使用者發來的消息。
用sendMessage()給前端使用者發送消息。
注意@ServerEndpoint("/websocket")是你連接配接時的url,如果後端為192.168.1.88:9997,那麼前端websocket連接配接url寫為: ws:http://192.168.1.88:9997/websocket
3.4前端
<template>
<div class="Task">
<div class="" style="height: 100px;width: 100px;color: #fff;" >
<button @click="open()">打開連接配接</button>
<button @click="close()">關閉連接配接</button>
<input type="text" id="name" v-model="message"/><button @click="send()">發送消息</button>
</div>
</div>
</template>
<script>
import {baseURL} from 'src/const/config'
export default {
name: "Task",
data(){
return{
baseURL,
websock: null,
message: "",
}
},
mounted() {
this.initWebSocket()
},
methods:{
initWebSocket(){ //初始化weosocket
const wsuri = 'ws://192.168.1.88:9997/websocket';//ws位址
this.websock = new WebSocket(wsuri);
this.websock.onopen = this.websocketonopen;
this.websock.onerror = this.websocketonerror;
this.websock.onmessage = this.websocketonmessage;
this.websock.onclose = this.websocketclose;
},
websocketonopen() {
console.log("WebSocket連接配接成功");
websocket.send(""WebSocket連接配接成功");//發送消息
},
websocketonerror(e) { //錯誤
console.log("WebSocket連接配接發生錯誤");
},
websocketonmessage(e){ //資料接收
const redata = JSON.parse(e.data);//接收對象的
//注意:長連接配接我們是背景直接1秒推送一條資料,
//但是點選某個清單時,會發送給背景一個辨別,背景根據此辨別傳回相對應的資料,
//這個時候資料就隻能從一個出口出,是以讓背景加了一個鍵,例如鍵為1時,是每隔1秒推送的資料,為2時是發送辨別後再推送的資料,以作區分
console.log(redata.total,1111);
},
websocketclose(e){ //關閉
console.log("connection closed");
},
close(){
this.websock.close();
},
send(){
if(this.websock && this.websock.readyState==1){
this.websock.send("S1");
}else{
console.log("連接配接已關閉")
}
},
open(){
this.initWebSocket();
}
},
destroyed: function() {
//頁面銷毀時關閉長連接配接
this.websocketclose();
},
}
</script>
<style scoped >
</style>
3.5編寫通路接口模仿服務端消息推送
/**
* 發送場景模拟
* @param msg
* @return
*/
@GetMapping("/send")
@ResponseBody
public String sendMessage(String msg) {
//如果通路的位址中msg參數不為空值,發送msg的值給前端
if (!StringUtils.isEmpty(msg)) {
webSocket.sendMessage(msg);
return "服務端發送消息:" + msg;
}
return "服務端未發送消息:" + msg;
}
3.6服務端推送對象資料(WebSocket-發送對象-自定義Encoder)
Websocket發送對象,通過Encoder 自定義規則(轉換為JSON字元串),前端收到後再轉換為JSON對象
3.6.1自定義Encoder
package com.trgis.config;
import com.alibaba.fastjson.JSON;
import com.trgis.vo.SocketVO;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import java.util.Map;
public class WebSocketCustomEncoding implements Encoder.Text<SocketVO> {
@Override
public String encode(SocketVO vo) {
assert vo!=null;
return JSON.toJSONString(vo);
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
3.6.2 SocketVo
package com.trgis.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
/**
* @PackageName: com.trgis.vo
* @ClassName: socketVO
* @Author: zoe
* @Date: 2021/4/9 0013 11:02
* @Description: socket對象 傳給前端
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SocketVO {
@ApiModelProperty("名稱")
private String name;
@ApiModelProperty("總計條數")
private Integer total;
@ApiModelProperty("x")
private ArrayList<String> x;
@ApiModelProperty("y")
private ArrayList<String> y;
}
3.6.3 Websocket發送自定義對象
在@ServerEndpoint 指定endocers
修改剛剛的webSocket.java中的WebSocket中的@ServerEndpoint 并增加sendMessage(SocketVO vo)方法 代表傳回對象
@ServerEndpoint(value = "/websocket",encoders = WebSocketCustomEncoding.class)
@Component
@Slf4j
public class WebSocket {
/**
* 新增 發送消息 對象模式
* @param vo
*/
public void sendMessage(SocketVO vo){
log.info("【websocket消息】 發送消息:{}",vo);
for (WebSocket webSocket:webSocketSet){
try {
webSocket.session.getBasicRemote().sendObject(vo);
} catch (IOException | EncodeException e) {
e.printStackTrace();
}
}
}
}
如果不在 @ServerEndpoint 指定endocers,直接通過sendObject(Object o)發送對象,
會報javax.websocket.EncodeException: No encoder specified for object of class xxxx異常
3.6.4Controll中調用修改
/**
* 發送場景模拟
* @param msg
* @return
*/
@GetMapping("/send")
@ResponseBody
public String sendMessage(String msg) {
//如果通路的位址中msg參數不為空值,發送msg的值給前端
if (!StringUtils.isEmpty(msg)) {
//webSocket.sendMessage(msg);
SocketVO vo = new SocketVO();
vo.setX("x");
vo.setY("y");
vo.setName(msg);
vo.setTotal(10);
socket.sendMessage(vo);
return "服務端發送消息:" + msg;
}
return "服務端未發送消息:" + msg;
}
3.7結果
4異常
可能遇到的錯誤及注意事項:
檢查new WebSocket("ws://localhost:9997/websocket");的路徑是否正确、是否以ws://開頭,端口是否對應正确
url是否和後端配置的一緻,單詞是否拼寫正确,導包是否正确
前後端端口号是否重複占用
gradle引包是否正确
tomcat是否使用7以上版本,建議使用tomcat8以及較新的springboot版本
後端配置檔案是否注入spring
是否設定了攔截器