WebSocket簡介
??隨著互聯(lián)網(wǎng)的發(fā)展,傳統(tǒng)的HTTP協(xié)議已經(jīng)很難滿足Web應(yīng)用日益復(fù)雜的需求了。近年來,隨著HTML5的誕生,WebSocket協(xié)議被提出,它實現(xiàn)了瀏覽器與服務(wù)器的全雙工通信,擴展了瀏覽器與服務(wù)端的通信功能,使服務(wù)端也能主動向客戶端發(fā)送數(shù)據(jù)。
??我們知道,傳統(tǒng)的HTTP協(xié)議是無狀態(tài)的,每次請求(request)都要由客戶端(如 瀏覽器)主動發(fā)起,服務(wù)端進行處理后返回response結(jié)果,而服務(wù)端很難主動向客戶端發(fā)送數(shù)據(jù);這種客戶端是主動方,服務(wù)端是被動方的傳統(tǒng)Web模式。對于信息變化不頻繁的Web應(yīng)用來說造成的麻煩較小,而對于涉及實時信息的Web應(yīng)用卻帶來了很大的不便,如帶有即時通信、實時數(shù)據(jù)、訂閱推送等功能的應(yīng)用。在WebSocket規(guī)范提出之前,開發(fā)人員若要實現(xiàn)這些實時性較強的功能,經(jīng)常會使用折衷的解決方法:輪詢(polling)和Comet技術(shù)。其實后者本質(zhì)上也是一種輪詢,只不過有所改進。
??輪詢是最原始的實現(xiàn)實時Web應(yīng)用的解決方案。輪詢技術(shù)要求客戶端以設(shè)定的時間間隔周期性地向服務(wù)端發(fā)送請求,頻繁地查詢是否有新的數(shù)據(jù)改動。明顯地,這種方法會導(dǎo)致過多不必要的請求,浪費流量和服務(wù)器資源。
??這兩種技術(shù)都是基于請求-應(yīng)答模式,都不算是真正意義上的實時技術(shù);它們的每一次請求、應(yīng)答,都浪費了一定流量在相同的頭部信息上,并且開發(fā)復(fù)雜度也較大。
??伴隨著HTML5推出的WebSocket,真正實現(xiàn)了Web的實時通信,使B/S模式具備了C/S模式的實時通信能力。WebSocket的工作流程是這 樣的:瀏覽器通過JavaScript向服務(wù)端發(fā)出建立WebSocket連接的請求,在WebSocket連接建立成功后,客戶端和服務(wù)端就可以通過 TCP連接傳輸數(shù)據(jù)。因為WebSocket連接本質(zhì)上是TCP連接,不需要每次傳輸都帶上重復(fù)的頭部數(shù)據(jù),所以它的數(shù)據(jù)傳輸量比輪詢和Comet技術(shù)小 了很多。本文不詳細地介紹WebSocket規(guī)范,主要介紹下WebSocket在Java Web中的實現(xiàn)。
??本文主要介紹了websocket在spring-boot上的搭建原理,已經(jīng)部分demo的參考。
引入websocket依賴
<dependency>
<groupId>org.springframework.boot
<artifactId>spring-boot-starter-websocket
</dependency>
配置WebSocketConfig
/*
* File: WebSocketConfig.java
* Created By: fengtao.xue@gausscode.com
* Date: 2019-03-20
*/
package cn.feng.dev.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author fengtao.xue
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer
/*
* File: WebSocketServer.java
* Created By: fengtao.xue@gausscode.com
* Date: 2019-03-21
*/
package cn.gausscode.calo.frontend.rest.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務(wù)器端,
* 注解的值將被用于監(jiān)聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務(wù)器端
* @author fengtao.xue
*/
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
//靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計成線程安全的。
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
//concurrent包的線程安全Set,用來存放每個客戶端對應(yīng)的MyWebSocket對象。若要實現(xiàn)服務(wù)端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標(biāo)識
private static ConcurrentHashMap<String, WebSocketServer> webSocketSet = new ConcurrentHashMap<String,WebSocketServer>();
//與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
private Session WebSocketsession;
//當(dāng)前發(fā)消息的人員userId
private String userId = "";
/**
* 連接建立成功調(diào)用的方法*/
@OnOpen
public void onOpen(@PathParam(value = "userId") String param, Session WebSocketsession, EndpointConfig config) {
userId = param;
//log.info("authKey:{}",authKey);
this.WebSocketsession = WebSocketsession;
webSocketSet.put(param, this);//加入map中
int cnt = OnlineCount.incrementAndGet(); // 在線數(shù)加1
logger.info("有連接加入,當(dāng)前連接數(shù)為:{}", cnt);
sendMessage(this.WebSocketsession, "連接成功");
}
/**
* 連接關(guān)閉調(diào)用的方法
*/
@OnClose
public void onClose() {
if (!userId.equals("")){
webSocketSet.remove(userId);//從set中刪除
int cnt = OnlineCount.decrementAndGet();
logger.info("有連接關(guān)閉,當(dāng)前連接數(shù)為:{}", cnt);
}
}
/**
* 收到客戶端消息后調(diào)用的方法
*
* @param message 客戶端發(fā)送過來的消息*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("來自客戶端的消息:{}",message);
sendMessage(session, "收到消息,消息內(nèi)容:"+message);
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("發(fā)生錯誤:{},Session ID: {}",error.getMessage(),session.getId());
error.printStackTrace();
}
/**
* 發(fā)送消息,實踐表明,每次瀏覽器刷新,session會發(fā)生變化。
* @param message
*/
public void sendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
//session.getBasicRemote().sendText(String.format("%s",message));
} catch (IOException e) {
logger.error("發(fā)送消息出錯:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 群發(fā)消息
* @param message
* @throws IOException
*/
public void broadCastInfo(String message) {
for (String key : webSocketSet.keySet()) {
Session session = webSocketSet.get(key).WebSocketsession;
if(session != null && session.isOpen() && !userId.equals(key)){
sendMessage(session, message);
}
}
}
/**
* 指定Session發(fā)送消息
* @param message
* @throws IOException
*/
public void sendToUser(String userId, String message) {
WebSocketServer webSocketServer = webSocketSet.get(userId);
if ( webSocketServer != null && webSocketServer.WebSocketsession.isOpen()){
sendMessage(webSocketServer.WebSocketsession, message);
}
else{
logger.warn("當(dāng)前用戶不在線:{}",userId);
}
}
}
Controller
/*
* File: Controller.java
* Created By: fengtao.xue@gausscode.com
* Date: 2019-03-20
*/
package cn.feng.dev.websocket.web;
import cn.feng.dev.websocket.service.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* @author fengtao.xue
*/
@RestController
public class Controller {
static Logger logger = LoggerFactory.getLogger(Controller.class);
@Autowired
WebSocketServer webSocketServer;
/**
* 群發(fā)消息內(nèi)容
*
* @param message
* @return
*/
@RequestMapping(value = "/ws/sendAll", method = RequestMethod.GET)
public String sendAllMessage(@RequestParam(required = true) String message) {
try {
webSocketServer.broadCastInfo(message);
} catch (IOException e) {
e.printStackTrace();
}
return "success";
}
/**
* 指定會話ID發(fā)消息
*
* @param message 消息內(nèi)容
* @param userId 連接會話ID
* @return
*/
@RequestMapping(value = "/ws/sendOne", method = RequestMethod.GET)
public String sendOneMessage(@RequestParam(required = true) String message,
@RequestParam(required = true) String userId) {
try {
webSocketServer.sendToUser(userId, message);
} catch (IOException e) {
e.printStackTrace();
}
return "success";
}
}
index.html
<!DOCTYPE html>
<!--
功能:WebSocket使用示例
-->
<html>
<head>
<!--<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/main.css" rel="stylesheet">
<script src="/webjars/jquery/jquery.min.js"></script>-->
<meta charset="UTF-8">
<title>websocket測試</title>
<style type="text/css">
h3,h4{
text-align:center;
}
</style>
</head>
<body>
<h3>WebSocket測試,在<span style="color:red">控制臺</span>查看測試信息輸出!</h3>
<h4>http://okcdev.cn</h4>
<h4>
[url=/ws/sendOne?message=單發(fā)消息內(nèi)容&id=none]單發(fā)消息鏈接[/url]
[url=/ws/sendAll?message=群發(fā)消息內(nèi)容]群發(fā)消息鏈接[/url]
</h4>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>Greetings</th>
</tr>
</thead>
<tbody id="greetings">
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
function setMessageInnerHTML(sendMessage) {
document.getElementById('greetings').innerHTML += sendMessage + '<br/>';
}
var socket;
var userId = 11111111111;
if (typeof (WebSocket) == "undefined") {
console.log("遺憾:您的瀏覽器不支持WebSocket");
} else {
console.log("恭喜:您的瀏覽器支持WebSocket");
//實現(xiàn)化WebSocket對象
//指定要連接的服務(wù)器地址與端口建立連接
//注意ws、wss使用不同的端口。我使用自簽名的證書測試,
//無法使用wss,瀏覽器打開WebSocket時報錯
//ws對應(yīng)http、wss對應(yīng)https。
socket = new WebSocket("ws://localhost:8080/websocket/" + userId);
//連接打開事件
socket.onopen = function() {
console.log("Socket 已打開");
//socket.send("消息發(fā)送測試(From Client)");
};
//收到消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
setMessageInnerHTML(msg.data);
//$("#greetings").append("<tr><td>" + msg.data + "</td></tr>");
};
//連接關(guān)閉事件
socket.onclose = function() {
console.log("Socket已關(guān)閉");
};
//發(fā)生了錯誤事件
socket.onerror = function() {
alert("Socket發(fā)生了錯誤");
}
//監(jiān)聽窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時,主動去關(guān)閉websocket連接,防止連接還沒斷開就關(guān)閉窗口,server端會拋異常。
window.onbeforeunload = function () {
closeWebSocket();
}
//關(guān)閉WebSocket連接
function closeWebSocket() {
socket.close();
}
//窗口關(guān)閉時,關(guān)閉連接
/*window.unload=function() {
socket.close();
};*/
}
</script>
</body>
</html>
工程結(jié)構(gòu)路徑

運行效果
