WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協定。
WebSocket 使得使用者端和伺服器之間的資料交換變得更加簡單,允許伺服器端主動向使用者端推播資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。
在 WebSocket API 中,瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以資料互相傳送。
簡單的說,就是一次握手,持續通訊。
採用java實現的websocket使用者端與伺服器端除聊天室實現外,因其互動只需建立一次連結關係,極大的節省了記憶體與頻寬,所以也常用於實時資料傳輸與獲取。
某些業務需要在較短的時間間隔下,不斷的去獲取或傳輸資料,便可以考慮採用webSocket。
如:實時公交位置的獲取,實時人員位置的獲取,暴雨天氣中水庫的水位,某裝置的實時溫度等等。
本專案共分兩個模組
不必過分糾結專案中依賴所使用的版本,根據各自專案所需,切換合適的版本即可
首先我們來貼上關鍵程式碼,然後再進行解讀
以下程式碼共三部分:
接下來我們依次來看:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
springboot整合了對webSocket的操作,此處我們使用的版本為2.3.3,同時涉及到資料通訊,難免用到json解析,所以此處我們新增alibaba的fastjson依賴,用作解析json資料
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author zhaiLiMing
* @version 2020-9-16
* webSocket設定開啟websocket支援
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
伺服器端基於5個註解實現,分別是:
package com.modules.web;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.modules.service.StudentServiceImpl;
import com.modules.utils.DataTranslate;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @author zhaiLiMing
* @version 2020-9-16
* webSocket伺服器端
* @ServerEndpoint 將本類註解為webSocket伺服器端,其value為使用者端存取URI
* @Compoent 使得使用者端在spring容器啟動時候就被載入
*/
@ServerEndpoint("/endpoint")
@Component
public class WebSocketServer {
public WebSocketServer()
{
System.out.println("EchoSocket:start");
}
private Session session;
/**
* 範例化service層,此處不能使用autowired等註解自動注入,
* 因spring的bean是預設單例模式
*/
private static StudentServiceImpl studentService=new StudentServiceImpl();
/**
* 開啟連線時執行
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
System.out.println("連線已經開啟");
}
/**
* 收到訊息時執行
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("從使用者端收到的訊息:" + message);
sendMessage(JSON.toJSONString(JSONArray.toJSONString(「返回給使用者端的訊息」)));
}
/**
* 關閉連線時執行
*/
@OnClose
public void onClose(Session session) {
System.out.println("連線已經關閉");
}
/**
* 連線發生錯誤時執行
*/
@OnError
public void onError(Throwable error, Session session) {
System.out.println("連線發生錯誤");
}
/**
websocket session傳送文字訊息有兩個方法:getAsyncRemote()和getBasicRemote()
getAsyncRemote()和getBasicRemote()是非同步與同步的區別,
大部分情況下,推薦使用getAsyncRemote()。
*/
public void sendMessage(String message) throws IOException {
this.session.getAsyncRemote().sendText(message);
}
}
如果需要在webSocket服務類中呼叫service層,使用註解(如@Autowired等)自動注入,會丟擲空指標異常。
此處原因是,websocket每接收到一個使用者端的握手請求,就會開啟一個新的執行緒來處理該使用者端,然而,spring的bean預設是singleton單例模式,所以就會導致此類問題。
針對其的解決方法,可以採用傳統的new方式去建立javaBean,或者修改spring的bean為prototype。
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-Websocket</artifactId>
<version>1.3.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
針對所使用的依賴不再過多贅述
webSocket使用者端的實現基於webSocketClient類實現,範例化webSocketClient並重寫以下四個方法:
import com.alibaba.fastjson.JSON;
import modules.entry.student.Student;
import modules.service.StudentService;
import modules.service.StudentServiceImpl;
import modules.utils.JsonFormat;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author zhaiLiMing
* @version 2020-9-21
* webSocketClient使用者端
*/
public class WebsocketClient {
//建立webSocketClient使用者端
private static WebSocketClient client;
//範例化service層
private static StudentService studentService=new StudentServiceImpl();
//建立一個5個執行緒的執行緒池,用來接收onMessage
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
public static void main(String[] args) throws URISyntaxException, InterruptedException {
//範例化webSocketClient,以ws或wss形式傳送請求,重寫4個方法
client=new WebSocketClient(new URI("ws://localhost:8080/endpoint")) {
//建立連線時執行
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("建立連線");
}
//接收到訊息時執行
@Override
public void onMessage(String s) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(JSON.parseObject(s).getString("code"));
//業務處理,接收返回的訊息,解析JSON字串,存入資料庫
if (JSON.parseObject(s).getString("code").equals("1100")){
String data=JSON.parseObject(s).getString("data");
//JsonFormat是自己寫的工具類
List<Student> studentList= JsonFormat.jsonFormatStudent(data);
//呼叫service接收結果
int result=studentService.insert(studentList);
System.out.println("插入成功:"+result+"條資料!");
}else{
System.err.println("伺服器出錯!");
}
}
};
fixedThreadPool.submit(runnable);
}
//連線關閉時執行
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("連結關閉");
}
//連線出錯時執行
@Override
public void onError(Exception e) {
System.out.println("連結出錯");
}
};
client.connect();
//檢測連線狀態,重複嘗試連線
while (!client.getReadyState().equals(WebSocket.READYSTATE.OPEN)) {
System.out.println("before reconnect statte:"+client.getReadyState());
Thread.sleep(2000);
if(client.getReadyState().equals(WebSocket.READYSTATE.CLOSING) || client.getReadyState().equals(WebSocket.READYSTATE.CLOSED)){
client.reconnect();
}
System.out.println("After reconnect statte:"+client.getReadyState());
}
//傳送資料
client.send("getStudent");
}
}
範例化webSocketClient時,有一個引數URI,URI中傳入地址,有兩種請求方式
請求方式不同時,可能會丟擲異常,兩種請求方式具體區別在此不做解釋,可查閱別的文章
參閱過webSocket API檔案的部分朋友或許會疑惑,為什麼是5種呢?API檔案寫的4種呀!
實際上我們仔細看就會發現,java中(基於其他語言的websocket沒有研究,所以只說java),webSocket的原始碼裡定義了內部列舉類READYSTATE,其中包含以下5種狀態
public static enum READYSTATE {
NOT_YET_CONNECTED,
CONNECTING,
OPEN,
CLOSING,
CLOSED;
private READYSTATE() {
}
}
不難看出,5種狀態表明著webSocket的整個生命週期,這對於我們在使用webSocket時解決一些問題是非常關鍵的
當webSocketClient初始化完畢之後,webSocketClient提供了兩種連結方式,封裝為兩個方法,分別是
那麼這兩者有什麼區別呢?這便設計到了上一個問題,webSocket的5種狀態。
起始時,webSocket狀態為 NOT_YET_CONNECTED ,尚未連結,而當一次連結關閉之後,其狀態為 CLOSED 。這兩者雖然都是沒有連結的狀態,但本質上是有區別的。
NOT_YET_CONNECTED 表示該webSocket範例還未開始連結,並處於等待連結的狀態,形象的講,就是初生的嬰兒;
而 CLOSED 則表示連結關閉,雖然也不是連結狀態,但其表示已經完成了一次生命週期,該webSocket範例到了消亡的時候,形象的講,就是垂暮的老人。
而webSocket想要連結,則只能在 NOT_YET_CONNECTED 狀態下進行,一旦狀態改變,則無法再次連結。這便是connect()連結。針對其解決方法,就是reconnect()連結。
reconnect()連結的實現,便是在connect之前呼叫了reset()方法,重置了當前webSocket,使得狀態又改變成了 NOT_YET_CONNECTED ,從而可以再次執行connect()方法,我們看一下原始碼:
public void reconnect() {
this.reset();
this.connect();
}
瞭解了webSocket的5種狀態以及connect()與reconnect()的區別後,就不難理解斷線重連和心跳機制。
所謂心跳機制,即為每隔一定時間,由使用者端傳送特定的心跳包給伺服器,伺服器也迴應訊息,雙方互相確認對方還"活著"。
例如我們每隔10秒則呼叫 webSocket.send("心跳包")
同時在onMessage中接收到返回的內容,如果能接收到預期返回的內容,則證明雙方都存在,反之則證明有一方掛掉。
至於重連機制,則可以利用reconnect()方法,在檢測到斷線後,重新嘗試連結伺服器端
//開啟一個新執行緒
new Thread(){
@Override
public void run(){
try{
//間隔10秒傳送心跳
Thread.sleep(10000);
webSocketClient.send("心跳包");
}catch (Exception e){
//捕獲異常進行重連
webSocketClient.reconnect();
}
}
}.start();