零、前言
http有http的有點,但也有其不足,即只能從客戶端發起請求給服務端,服務端不能推數據給客戶端。而websocket就不一樣了,是雙工的,不僅可以由客戶端向服務器發送數據,服務端也能主動像客戶端推數據。這其中也就是單工跟雙工的概念。
一、websocket簡介
什麼是WebSocket?
WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通信——允許服務器主動發送信息給客戶端。
爲什麼需要 WebSocket?
初次接觸 WebSocket 的人,都會問同樣的問題:我們已經有了 HTTP 協議,爲什麼還需要另一個協議?它能帶來什麼好處?答案很簡單,因爲 HTTP 協議有一個缺陷:通信只能由客戶端發起,HTTP 協議做不到服務器主動向客戶端推送信息。舉例來說,我們想要查詢當前的排隊情況,只能是頁面輪詢向服務器發出請求,服務器返回查詢結果。輪詢的效率低,非常浪費資源(因爲必須不停連接,或者 HTTP 連接始終打開)。因此WebSocket 就是這樣發明的。
與http進行對比:
二、spring boot中引入websocket的步驟
創建spring boot項目,結構如下:
(1)pom.xml引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket
</artifactId>
</dependency>
(2)websocket配置類
package com.example.utils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Description
* @ClassName WebSocketConfig
* @Author User
* @date 2020.05.31 18:45
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(3)允許跨域配置
由於我在springboot中放了一個websocket客戶端html,訪問該html時用的html與spring boot的端口號不一致,造成了跨域,因此進行了配置。
package com.example.utils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
}
注意,網絡上有些在config的最後還有一句:
config.addExposedHeader("*");
這句在我這會導致項目啓動失敗,不知道爲什麼我也沒去研究,我刪掉了他就可以了。
(4)websocket類
package com.example.websocket;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description ws的server,相當於controller
* @ClassName MyWsServer
* @Author User
* @date 2020.05.31 18:24
*/
@ServerEndpoint("/api/v1/websocket/{userId}")
@Component
public class MyWsServer {
// 引入log4j日誌
static Logger logger = Logger.getLogger(MyWsServer.class);
// 靜態變量,用來記錄當前在線連接數
private static int onlineCount = 0;
// concurrent包的線程安全Set,用來存放客戶端對象
private static ConcurrentHashMap<String, MyWsServer> clients = new ConcurrentHashMap<>();
// 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
// 用戶唯一標識符
private String userId = "";
/**
* @return
* @Description websocket的連接函數
* @Param {Session} session
* @Author User
* @Date 2020.05.31 19:13
**/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
logger.info("有新的客戶端連接進來了,客戶端id是: " + session.getId());
if (clients.containsKey(userId)) {
clients.remove(userId);
clients.put(userId, this);
} else {
clients.put(userId, this);
MyWsServer.addOnlineCount();
}
logger.info("用戶:" + userId + ", 當前在線人數爲:" + getOnlineCount());
try {
sendMessage("連接成功");
} catch (IOException e) {
logger.error("用戶:" + userId + ",網絡異常!");
}
}
/**
* @return
* @Description 關閉事件處理函數
* @Param
* @Author User
* @Date 2020.06.12 22:15
**/
@OnClose
public void onClose() {
if (clients.containsKey(userId)) {
clients.remove(userId);
subOnlineCount();
}
logger.info("用戶退出:" + userId + ",當前在線人數爲:" + getOnlineCount());
}
/**
* @return
* @Description 接收消息
* @Param
* @Author User
* @Date 2020.06.12 22:29
**/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("用戶" + userId + "發來消息, 報文:" + message);
}
/**
* @return
* @Description
* @Param
* @Author User
* @Date 2020.06.12 22:31
**/
@OnError
public void onError(Session session, Throwable error) {
logger.error("用戶錯誤:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* @return {null}
* @Description 向客戶端發送消息
* @Param {String} message 要發送的消息
* @Author User
* @Date 2020.05.31 19:14
**/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* @return
* @Description 發送自定義消息到指定用戶或者羣發消息
* @Param
* @Author User
* @Date 2020.06.12 22:55
**/
public static void sendInfo(String message, String fromUserId, String toUserId) {
logger.info("推送消息給用戶" + toUserId + ",推送內容:" + message);
for (MyWsServer client : clients.values()) {
try {
//這裏可以設定只推送給這個userId的,爲null則全部推送
if ("every".equals(toUserId) && !client.userId.equals(fromUserId)) {
client.sendMessage("來自" + fromUserId + "的羣發消息:" + message);
} else if (client.userId.equals(toUserId)) {
client.sendMessage("來自" + fromUserId + "發給" + toUserId + "的消息:" + message);
}
} catch (IOException e) {
System.out.println(e.toString());
continue;
}
}
}
/**
* @return {null}
* @Description 在線客戶端數加一
* @Param {null}
* @Author User
* @Date 2020.05.31 19:12
**/
public static synchronized void addOnlineCount() {
MyWsServer.onlineCount++;
}
/**
* @return {null}
* @Description 在線客戶端數減一
* @Param {null}
* @Author User
* @Date 2020.05.31 19:12
**/
public static synchronized void subOnlineCount() {
MyWsServer.onlineCount--;
}
/**
* @return
* @Description 獲取在線連接數
* @Param
* @Author User
* @Date 2020.05.31 19:12
**/
public static synchronized int getOnlineCount() {
return onlineCount;
}
}
最重要的一個文件,裏面註釋很詳細。
(5)controller配置demo
package com.example.controller;
import com.example.websocket.MyWsServer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
* @Description
* @ClassName MyController
* @Author User
* @date 2020.06.12 23:05
*/
@RestController
@RequestMapping(("/api/v1"))
public class MyController {
@GetMapping("/index")
public ResponseEntity<String> index() {
return ResponseEntity.ok("<h1>請求成功</h1>");
}
@GetMapping("/pushMessage/{fromUserId}/{toUserId}")
public ResponseEntity<String> pushToClients(@RequestParam("message") String message, @PathVariable("fromUserId") String fromUserId, @PathVariable("toUserId") String toUserId) throws IOException {
MyWsServer.sendInfo(message, fromUserId, toUserId);
return ResponseEntity.ok("MSG SEND SUCCESS");
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>websocket測試工具</title>
</head>
<script src="./jquery.js"></script>
<script>
var socket;
function openSocket() {
if (typeof (WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
} else {
if ($('#userId').val() === "") {
alert("請設置您的個性id");
return;
}
console.log("您的瀏覽器支持WebSocket");
//實現化WebSocket對象,指定要連接的服務器地址與端口 建立連接
var socketUrl = "ws://127.0.0.1:8000/api/v1/websocket/" + $("#userId").val();
console.log(socketUrl);
if (socket != null) {
socket.close();
socket = null;
}
socket = new WebSocket(socketUrl);
//打開事件
socket.onopen = function () {
// alert("websocket已打開");
//socket.send("這是來自客戶端的消息" + location.href + new Date());
};
//獲得消息事件
socket.onmessage = function (msg) {
alert(msg.data);
//發現消息進入 開始處理前端觸發邏輯
};
//關閉事件
socket.onclose = function () {
console.log("websocket已關閉");
};
//發生了錯誤事件
socket.onerror = function () {
console.log("websocket發生了錯誤");
}
}
}
function disconnectSocket() {
if (typeof (WebSocket) == "undefined") {
alert("您的瀏覽器不支持WebSocket");
}
if (socket === undefined) {
alert("您尚未連接服務器");
} else {
socket.close();
}
}
function sendMessage() {
if (typeof (WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
} else {
if ($('#userId').val() === "") {
alert("請設置您的個性id");
return;
}
if (socket === undefined) {
alert("您尚未連接服務器");
} else {
if (socket.readyState === socket.CLOSED) {
alert("您已斷開與服務器的連接");
} else {
var obj = {
toUserId: $("#toUserId").val(),
message: $('#message').val()
}
// 推送數據到指定客戶端,不填toUserId則認爲是羣發消息
var toWho = "";
$('#toUserId').val() ? toWho = $('#toUserId').val() : toWho = "every"
var url = "http://127.0.0.1:8000/api/v1/pushMessage/"+$('#userId').val() + "/" + toWho + '?message=' + $('#message').val();
// console.log(url);
$.ajax(url);
// console.log(JSON.stringify(obj));
// socket.send(JSON.stringify(obj));
}
}
}
}
</script>
<body>
<h1 align="center" style="color: red">websocket測試工具</h1>
<div align="left">
<div>【我的id】: <input id="userId" name="userId" type="text" placeholder="設置一個個性的id"> 
<button id="open" onclick="openSocket()">連接socket</button>
 
<button onclick="disconnectSocket()">斷開socket</button>
</div>
<br>
<div>【發送至】: <input id="toUserId" name="toUserId" type="text" placeholder="發送給某個用戶"></div>
<br>
<div>【mesgs】: <input id="message" name="message" type="text" placeholder="要發送的消息"> 
<button onclick="sendMessage()">發送消息</button>
</div>
</div>
</body>
</html>
只是一個簡單的頁面,但是功能齊全,包括連接斷開,單發消息以及羣發消息
三、演示截圖
(1)單發消息
啓動三個客戶端,
客戶端1向客戶端2發送消息:
只有客戶端2收到了消息:
(2)user1羣發消息
用戶3收到消息:
用戶2收到消息:
四、總結
文章簡單介紹了,springboot中如何使用websocket,demo完善,方便後續參考,擴展。
五、demo下載地址
由於經常在csdn下載東西,積分不夠用,這個demo就上傳到csdn下載了,多多見諒。
csdn下載地址:https://download.csdn.net/download/qianlixiaomage/12520760
六、結束
再見!