1. 程式人生 > >springboot 中websocket的使用

springboot 中websocket的使用

一.WebSocket簡單介紹

隨著網際網路的發展,傳統的HTTP協議已經很難滿足Web應用日益複雜的需求了。近年來,隨著HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與伺服器的全雙工通訊,擴充套件了瀏覽器與服務端的通訊功能,使服務端也能主動向客戶端傳送資料。

我們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端傳送資料;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於資訊變化不頻繁的Web應用來說造成的麻煩較小,而對於涉及實時資訊的Web應用卻帶來了很大的不便,如帶有即時通訊、實時資料、訂閱推送等功能的應 用。在WebSocket規範提出之前,開發人員若要實現這些實時性較強的功能,經常會使用折衷的解決方法:輪詢(polling)

Comet技術。其實後者本質上也是一種輪詢,只不過有所改進。

輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端傳送請求,頻繁地查詢是否有新的資料改動。明顯地,這種方法會導致過多不必要的請求,浪費流量和伺服器資源。

Comet技術又可以分為長輪詢流技術長輪詢改進了上述的輪詢技術,減小了無用的請求。它會為某些資料設定過期時間,當資料過期後才會向服務端傳送請求;這種機制適合資料的改動不是特別頻繁的情況。流技術通常是指客戶端使用一個隱藏的視窗與服務端建立一個HTTP長連線,服務端會不斷更新連線狀態以保持HTTP長連線存活;這樣的話,服務端就可以通過這條長連線主動將資料傳送給客戶端;流技術在大併發環境下,可能會考驗到服務端的效能。

這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了一定流量在相同的頭部資訊上,並且開發複雜度也較大。

伴隨著HTML5推出的WebSocket,真正實現了Web的實時通訊,使B/S模式具備了C/S模式的實時通訊能力。WebSocket的工作流程是這 樣的:瀏覽器通過JavaScript向服務端發出建立WebSocket連線的請求,在WebSocket連線建立成功後,客戶端和服務端就可以通過 TCP連線傳輸資料。因為WebSocket連線本質上是TCP連線,不需要每次傳輸都帶上重複的頭部資料,所以它的資料傳輸量比輪詢和Comet技術小 了很多。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。

JavaEE 7中出了JSR-356:Java API for WebSocket規範。不少Web容器,如Tomcat,Nginx,Jetty等都支援WebSocket。Tomcat從7.0.27開始支援 WebSocket,從7.0.47開始支援JSR-356,下面的Demo程式碼也是需要部署在Tomcat7.0.47以上的版本才能執行。

二,具體專案實現(springBoot)

本專案使用Gradle構建

compile('org.springframework.boot:spring-boot-starter-websocket')

配置檔案

@Configuration

@EnableWebSocket

public class  WebSocketConfig {

@Bean

public ServerEndpointExporter serverEndpointExporter() {

return new ServerEndpointExporter();

  }

}

webSocket實現類

@ServerEndpoint("/websocket")

@Component

public classMyWebSocket {

private static finalLoggerLOGGER= LoggerFactory.getLogger(MyWebSocket.class);

private static intonlineCount=0;

private staticCopyOnWriteArraySetwebSocketSet=newCopyOnWriteArraySet<>();

privateSessionsession;

/**

* 獲取線上人數

*

*@return線上人數

*/

private static synchronized intgetOnlineCount() {

returnMyWebSocket.onlineCount;

  }

/**

* 新增線上人數

*/

private static synchronized voidaddOnlineCount() {

MyWebSocket.onlineCount++;

  }

/**

* 減少線上人數

*/

private static synchronized voidsubOnlineCount() {

MyWebSocket.onlineCount--;

   }

/**

* 有人進入房間

*

*@paramsessionsession

*/

@OnOpen

public voidonOpen(Session session) {

this.session= session;

webSocketSet.add(this);

addOnlineCount();

LOGGER.info("有新使用者加入!當前線上人數為:{}",getOnlineCount());

  }

/**

* 有人離開房間

*/

@OnClose

public voidonClose() {

webSocketSet.remove(this);

subOnlineCount();

System.out.println("有一使用者關閉!當前線上人數為"+getOnlineCount());

   }

/**

* 發訊息

*

*@parammessagemessage

*@throwsIOException IOException

*/

@OnMessage

public voidonMessage(String message)throwsIOException {

String date ="<font color='green'>"+newDate() +"</font></br>";

// 群發訊息

for(MyWebSocket item :webSocketSet) {

item.sendMessage(date + message);

   }

LOGGER.info("客戶端訊息:{}",message);

   }

/**

* 傳送訊息

*

*@parammessagemessage

*@throwsIOException IOException

*/

private voidsendMessage(String message)throwsIOException {

this.session.getBasicRemote().sendText(message);

    }

}

頁面js實現

varwebsocket =null;

//判斷當前瀏覽器是否支援WebSocket

if('WebSocket'inwindow) {

websocket =newWebSocket("ws://localhost:8081/websocket");

  }

else{

alert('Not support websocket')

  }

//連線發生錯誤的回撥方法

websocket.onerror=function() {

setMessageInnerHTML("error");

  };

//連線成功建立的回撥方法

websocket.onopen=function(event) {

console.log(JSON.stringify(event));

   };

//接收到訊息的回撥方法

websocket.onmessage=function(event) {

setMessageInnerHTML(event.data);

  };

//連線關閉的回撥方法

websocket.onclose=function(event) {

console.log(JSON.stringify(event));

   };

//監聽視窗關閉事件,當視窗關閉時,主動去關閉websocket連線,防止連線還沒斷開就關閉視窗,server端會拋異常。

window.onbeforeunload=function() {

websocket.close();

  };

//將訊息顯示在網頁上

functionsetMessageInnerHTML(innerHTML) {

document.getElementById('message').innerHTML+= innerHTML +'</br>';

  }

//關閉連線

functioncloseWebSocket() {

websocket.close();

  }

//傳送訊息

functionsend() {

if(!document.getElementById('text').value||document.getElementById('text').value===" ") {

return;

  }

varusername =getQueryString("userName");

varmessage ="<span style='background: lightpink'>"+ username +"</span> :<span class='text-background'>"+document.getElementById('text').value+"</span><br/>";

websocket.send(message);

document.getElementById('text').value="";

   }

functionkeyEnter(event) {

if(event.keyCode=="13") {

send();

  }

  }

functionclearMsg(){

document.getElementById('message').innerHTML='';

  }

/**

* 獲取url引數

*@paramname

*@returns{string}

*/

functiongetQueryString(name) {

varreg =newRegExp("(^|&)"+ name +"=([^&]*)(&|$)","i");

varr =window.location.search.substr(1).match(reg);

if(r !=null)returndecodeURI(r[2]);

return"";

 }

三,原理解析

介面的內容可以分為三類:狀態變數、網路功能和訊息處理等。

建構函式WebSocket(url, protocols):構造WebSocket物件,以及建立和伺服器連線; protocols可選欄位,代表選擇的子協議

狀態變數readyState: 代表當前連線的狀態,短整型資料,取值為CONNECTING(值為0), OPEN(值為1), CLOSING(值為2), CLOSED(值為3)

方法變數close(code, reason): 關閉此WebSocket連線。

狀態變數bufferedAmount: send函式呼叫後,被快取並且未傳送到網路上的資料長度

方法變數send(data): 將資料data通過此WebSocket傳送到對端

回撥函式onopen/onmessage/onerror/onclose: 當相應的事件發生時會觸發此回撥函式

WebSocket協議

WebSocket看成是一種類似TCP/IP的socket技術;此socket在Web應用中實現,並獲得了和TCP/IP通訊一樣靈活方便的全雙向通訊功能。

WebSocket協議由RFC 6455定義。協議分為兩個部分: 握手階段和資料通訊階段。

WebSocket為應用層協議,其定義在TCP/IP協議棧之上。WebSocket連線伺服器的URI以"ws"或者"wss"開頭。ws開頭的預設TCP埠為80,wss開頭的預設埠為443。

1 握手階段

客戶端和伺服器建立TCP連線之後,客戶端傳送握手請求,隨後伺服器傳送握手響應即完成握手階段。如下圖所示:

3145530-2497e8d760f6535c.png

客戶端握手請求類似如下:

3145530-8fb682fa9cec0f6c.png

伺服器的握手響應類似如下:

3145530-808df1a5b2c3b237.png

需要關閉連線時,任意一方直接傳送型別為關閉幀(Close frame)的控制幀資料給對方即可。

2 資料通訊

WebSocket的資料在傳送時,被組織為依次序的一串資料幀(data frame),然後進行傳送。

傳送的幀型別分為兩類:資料幀(data frame)和控制幀(Control frame)。資料幀可以攜帶文字資料或者二進位制資料;控制幀包含關閉幀(Close frame)和Ping/Pong幀。

幀的格式如下所示:

3145530-7a9cbafc8dc3949b.png

其中最重要的欄位為opcode(4bit)和MASK(1bit)

MASK值,從客戶端進行傳送的幀必須置此位為1,從伺服器傳送的幀必須置為0。如果任何一方收到的幀不符合此要求,則傳送關閉幀(Close frame)關閉連線。

opcode的值: 0x1代表此幀為文字資料幀, 0x2代表此幀為二進位制資料幀, 0x8為控制幀中的連線關閉幀(close frame), 0x9為控制幀中的Ping幀, 0xA(十進位制的10)為控制幀中的Pong幀。

Ping/Pong幀: Ping幀和Pong幀用於連線的保活(keepalive)或者診斷對端是否線上。這兩種幀的傳送和接收不對WEB應用公開介面,由實現WebSocket協議的底層應用(例如瀏覽器)來實現它

3 連線關閉

任何一端傳送關閉幀給對方,即可關閉連線。關閉連線時通常都帶有關閉連線的狀態碼(status code)。常見狀態碼的含義如下:

1000 連線正常關閉

1001 端點離線,例如伺服器down,或者瀏覽器已經離開此頁面

1002 端點因為協議錯誤而中斷連線

1003 端點因為受到不能接受的資料型別而中斷連線

1004 保留

1005 保留, 用於提示應用未收到連線關閉的狀態碼

1006 端點異常關閉

1007 端點收到的資料幀型別不一致而導致連線關閉

1008 資料違例而關閉連線

1009 收到的訊息資料太大而關閉連線

1010 客戶端因為伺服器未協商擴充套件而關閉

1011 伺服器因為遭遇異常而關閉連線

1015 TLS握手失敗關閉連線

四. 對比

WebSocket和傳統的HTTP互動方式的區別如下圖:

3145530-21de914ef20719c7.png

WebSocket的結論如下:

基於TCP/IP協議實現

是一種全雙向的通訊, 具有底層socket的特點

節約頻寬,節省伺服器資源

是HTML5的技術之一,具有巨大的應用前景