WebSocket是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
以前的推送技術使用 Ajax 輪詢,瀏覽器需要不斷地向服務器發送http請求來獲取最新的數據,浪費很多的帶寬等資源。
使用webSocket通訊,客戶端和服務端只需要一次握手建立連接,就可以互相發送消息,進行數據傳輸,更實時地進行通訊。
一次握手建立WebSocket連接
瀏覽器先向服務器發送個url以ws://開頭的http的GET請求,響應狀態碼101表示Switching Protocols切換協議,
服務器根據請求頭中Upgrade:websocket把
客戶端的請求切換到對應的協議,即websocket協議。

響應頭消息中包含Upgrade:websocket,表示它切換到的協議,即websocket協議。
響應101,握手成功,http協議切換成websocket協議了,連接建立成功,瀏覽器和服務器可以隨時主動發送消息給對方了,並且這個連接一直持續到客戶端或服務器一方主動關閉連接。
為了加深理解,用websocket實現簡單的在線聊天,先畫個時序圖,直觀感受下流程。
網上有websocket在線測試工具,可以直接使用還是挺方便的,http://www.blue-zero.com/WebSocket/
自定義客戶端代碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket</title>
</head>
<body>
<input id="url" type="text" size="30" value="ws://localhost:9999/a/b" />
<button onclick="openWebSocket()">打開WebSocket連接</button>
<br/><br/>
<textarea id="text" type="text"></textarea>
<button onclick="send()">發送消息</button>
<hr/>
<button onclick="closeWebSocket()">關閉WebSocket連接</button>
<hr/>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null; function openWebSocket() { var url = document.getElementById('url').value.trim(); //判斷當前瀏覽器是否支持WebSocket
if ('WebSocket' in window) { websocket = new WebSocket(url); } else { alert('當前瀏覽器 Not support websocket') } //連接發生錯誤的回調方法
websocket.onerror = function() { setMessageInnerHTML("WebSocket連接發生錯誤"); }; //連接成功建立的回調方法
websocket.onopen = function() { setMessageInnerHTML("WebSocket連接成功"); } //接收到消息的回調方法
websocket.onmessage = function(event) { setMessageInnerHTML(event.data); } //連接關閉的回調方法
websocket.onclose = function() { setMessageInnerHTML("WebSocket連接關閉"); } } //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
window.onbeforeunload = function() { closeWebSocket(); } //將消息顯示在網頁上
function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //關閉WebSocket連接
function closeWebSocket() { websocket.close(); } //發送消息
function send() { var message = document.getElementById('text').value; websocket.send(message); } </script>
</html>
服務端代碼:
引入依賴,我使用的是springboot版本2.0.5.RELEASE,自動管理依賴版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<!-- <version>1.3.5.RELEASE</version> -->
</dependency>
配置類WebSocketConfig,掃描並注冊帶有@ServerEndpoint注解的所有websocket服務端
package com.webSocket; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 開啟WebSocket支持 * @author Administrator */ @Configuration public class WebSocketConfig { /** * 掃描並注冊帶有@ServerEndpoint注解的所有服務端 * @return
*/ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
WebSocketServer類,是websocket服務端,多例的,一次websocket連接對應一個實例
package com.webSocket; import java.io.IOException; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import org.springframework.stereotype.Component; /** * websocket服務端,多例的,一次websocket連接對應一個實例 * @ServerEndpoint 注解的值為URI,映射客戶端輸入的URL來連接到WebSocket服務器端 */ @Component @ServerEndpoint("/{name}/{toName}") public class WebSocketServer { /** 用來記錄當前在線連接數。設計成線程安全的。*/
private static AtomicInteger onlineCount = new AtomicInteger(0); /** 用於保存uri對應的連接服務,{uri:WebSocketServer},設計成線程安全的 */
private static ConcurrentHashMap<String, WebSocketServer> webSocketServerMAP = new ConcurrentHashMap<>();
private Session session;// 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private String name; //客戶端消息發送者
private String toName; //客戶端消息接受者
private String uri; //連接的uri
/** * 連接建立成功時觸發,綁定參數 * @param session * 可選的參數。session為與某個客戶端的連接會話,需要通過它來給客戶端發送數據 * @param name * @param toName * @throws IOException */ @OnOpen public void onOpen(Session session, @PathParam("name") String name, @PathParam("toName") String toName) throws IOException { this.session = session; this.name = name; this.toName = toName; this.uri = session.getRequestURI().toString(); WebSocketServer webSocketServer = webSocketServerMAP.get(uri); if(webSocketServer != null){ //同樣業務的連接已經在線,則把原來的擠下線。
webSocketServer.session.getBasicRemote().sendText(uri + "重復連接被擠下線了"); webSocketServer.session.close();//關閉連接,觸發關閉連接方法onClose()
} webSocketServerMAP.put(uri, this);//保存uri對應的連接服務
addOnlineCount(); // 在線數加1
sendMessage(webSocketServerMAP +"當前在線連接數:" + getOnlineCount()); System.out.println(this + "有新連接加入!當前在線連接數:" + getOnlineCount()); } /** * 連接關閉時觸發,注意不能向客戶端發送消息了 * @throws IOException */ @OnClose public void onClose() throws IOException { webSocketServerMAP.remove(uri);//刪除uri對應的連接服務
reduceOnlineCount(); // 在線數減1
System.out.println("有一連接關閉!當前在線連接數" + getOnlineCount()); } /** * 收到客戶端消息后觸發,分別向2個客戶端(消息發送者和消息接收者)推送消息 * * @param message * 客戶端發送過來的消息 * @param session * 可選的參數 * @throws IOException */ @OnMessage public void onMessage(String message) throws IOException { //服務器向消息發送者客戶端發送消息
this.session.getBasicRemote().sendText("發送給" + toName + "的消息:" + message); //獲取消息接收者的客戶端連接
StringBuilder receiverUri = new StringBuilder("/"); receiverUri.append(toName) .append("/") .append(name); WebSocketServer webSocketServer = webSocketServerMAP.get(receiverUri.toString()); if(webSocketServer != null){ webSocketServer.session.getBasicRemote().sendText("來自" +name + "的消息:" + message); } /*// 群發消息 Collection<WebSocketServer> collection = webSocketServerMAP.values(); for (WebSocketServer item : collection) { try { item.sendMessage("收到群發消息:" + message); } catch (IOException e) { e.printStackTrace(); continue; } }*/ } /** * 通信發生錯誤時觸發 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { System.out.println("發生錯誤"); error.printStackTrace(); } /** * 向客戶端發送消息 * @param message * @throws IOException */
public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 獲取在線連接數 * @return
*/
public static int getOnlineCount() { return onlineCount.get(); } /** * 原子性操作,在線連接數加一 */
public static void addOnlineCount() { onlineCount.getAndIncrement(); } /** * 原子性操作,在線連接數減一 */
public static void reduceOnlineCount() { onlineCount.getAndDecrement(); } }
啟動項目,我這里的端口號為9999,websocket服務端就可以使用了。然后打開兩個客戶端,打開連接ws://localhost:9999/a/b和ws://localhost:9999/b/a,就可以進行通訊了
1.使用客戶端a向b發送消息:
客戶端b收到消息:
2.使用客戶端b向a發送消息:
客戶端a收到消息:
這樣使用websocket簡單在線聊天實踐了下,還有很多細節沒有實現,主要是加深下websocket的理解,以后再慢慢完善。
相比http,減少了請求次數,不需要客戶端多次請求,服務器處理業務完畢后主動向客戶端推送消息。