一.WebSocket簡單介紹 隨着互聯網的發展,傳統的HTTP協議已經很難知足Web應用日益複雜的需求了。近年來,隨着HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與服務器的全雙工通訊,擴展了瀏覽器與服務端的通訊功能,使服務端也能主動向客戶端發送數據。$ j6 s6 P/ d( K- G' m 咱們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來講形成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通訊、實時數據、訂閱推送等功能的應 用。在WebSocket規範提出以前,開發人員若要實現這些實時性較強的功能,常常會使用折衷的解決方法:輪詢(polling)和Comet技術。其實後者本質上也是一種輪詢,只不過有所改進。 輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端發送請求,頻繁地查詢是否有新的數據改動。明顯地,這種方法會致使過多沒必要要的請求,浪費流量和服務器資源。 Comet技術又能夠分爲長輪詢和流技術。長輪詢改進了上述的輪詢技術,減少了無用的請求。它會爲某些數據設定過時時間,當數據過時後纔會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的狀況。流技術一般是指客戶端使用一個隱藏的窗口與服務端創建一個HTTP長鏈接,服務端會不斷更新鏈接狀態以保持HTTP長鏈接存活;這樣的話,服務端就能夠經過這條長鏈接主動將數據發送給客戶端;流技術在大併發環境下,可能會考驗到服務端的性能。 這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了必定流量在相同的頭部信息上,而且開發複雜度也較大。. C w( i) E4 k. P, m; g 伴隨着HTML5推出的WebSocket,真正實現了Web的實時通訊,使B/S模式具有了C/S模式的實時通訊能力。WebSocket的工做流程是這 樣的:瀏覽器經過javaScript向服務端發出創建WebSocket鏈接的請求,在WebSocket鏈接創建成功後,客戶端和服務端就能夠經過 TCP鏈接傳輸數據。由於WebSocket鏈接本質上是TCP鏈接,不須要每次傳輸都帶上重複的頭部數據,因此它的數據傳輸量比輪詢和Comet技術小 了不少。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。# k- L0 {# i. k 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以上的版本才能運行。# W" f) D, @: u) Y0 L' ~
2、WebSocket協議介紹 WebSocket協議是一種雙向通訊協議,它創建在TCP之上,同http同樣經過TCP來傳輸數據,可是它和http最大的不一樣有兩點:1.WebSocket是一種雙向通訊協議,在創建鏈接後,WebSocket服務器和Browser/UA都能主動的向對方發送或接收數據,就像Socket同樣,不一樣的是WebSocket是一種創建在Web基礎上的一種簡單模擬Socket的協議;2.WebSocket須要經過握手鍊接,相似於TCP它也須要客戶端和服務器端進行握手鍊接,鏈接成功後才能相互通訊。簡單的創建握手的時序圖以下:+ h; ~; q4 `/ { <ignore_js_op> 握手過程: Browser與WebSocket服務器經過TCP三次握手創建鏈接,若是這個創建鏈接失敗,那麼後面的過程就不會執行,Web應用程序將收到錯誤消息通知。 在TCP創建鏈接成功後,Browser/UA經過http協議傳送WebSocket支持的版本號,協議的字版本號,原始地址,主機地址等等一些列字段給服務器端。 WebSocket服務器收到Browser/UA發送來的握手請求後,若是數據包數據和格式正確,客戶端和服務器端的協議版本號匹配等等,就接受本次握手鍊接,並給出相應的數據回覆,一樣回覆的數據包也是採用http協議傳輸。- s" j: [- c" o- k Browser收到服務器回覆的數據包後,若是數據包內容、格式都沒有問題的話,就表示本次鏈接成功,觸發onopen消息,此時Web開發者就能夠在此時經過send接口想服務器發送數據。不然,握手鍊接失敗,Web應用程序會收到onerror消息,而且能知道鏈接失敗的緣由。5 V, k5 ?2 l7 M( \2 b H ' z7 g9 w, D$ O% k) U 3、Tomcat 7中的Websocket架構3 E7 s, g+ f% r% u4 Y% ` ) F' S5 |4 q- W 如圖所示,由於Websocket通訊分爲握手和數據傳輸兩個過程,兩個過程當中須要用到的處理方式是不同的,握手過程是基於HTTP 1.1基礎上的,而數據傳輸是直接基於TCP的流傳輸。 ' ]4 R- C. L! S5 x; y* j 握手過程當中,在HttpServletRequest的基礎上,封裝了WsHttpServletRequest類,添加了對Request的失效操做函數invalidate()。而在數據通訊時,接受和處理數據過程當中,基於org.apache.coyote.http11.upgrade.UpgradeInbound從新封裝了用於處理數據輸入流的類StreamInbound,並在StreamInbound的基礎上擴展生成了用於消息處理的類MessageInbound。在這兩個數據處理類中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操做函數接口,這些接口將在載入的代碼類中實現業務邏輯。在用於數據輸出流的類WsOutbound則是封裝了UpgradeOutbound對象實例,基於UpgradeOutbound對象的基礎上,添加了websocket響應有關的處理邏輯。這裏處理函數均爲同步調用的函數,保證websocket響應的時序性。 Tomcat中Websocket的處理流程以下:<ignore_js_op> 接收客戶端發來的握手請求,Coyote.http11鏈接器對socket進行解析,造成HttpServletRequest發送給Container。 Container中的相應WebsocketServlet處理請求,如不接受鏈接請求,則返回,如接受鏈接請求,則對請求做出響應,創建起客戶端和服務器的socket鏈接。 服務器此時能夠經過WsOutbound發送數據給客戶端,同時經過StreamInbound監聽socket。/ d3 C$ S' N4 s" m 若是接收到客戶端發來的數據,則將socket數據解析成frame,判斷frame類型,經過事件分發數據到不一樣的邏輯處理流程。 數據返回時調用WsOutbound對返回的數據進行封裝處理,發送給客戶端。
4、代碼實現以及需求
一、項目須要,定時向全部在線用戶推送一個廣告或是推送一個通知之類的(好比服務器升級,請保存好手頭工做之類的)。: f |, D6 R. _0 ~3 d ' p- `, F0 Y* g4 B. g9 ]# ` 二、相關環境 , Nginx、tomcat七、centos 6.5 7 } m0 R% \$ F' u1 A 三、項目框架,springMvc 4.0.六、layer ! u- Q0 {: B2 u- e. `6 G 四、代碼實現:) W R/ N6 U/ C& L/ t3 R) a& h
WebSocketConfig:
- import websocket.handler.SystemWebSocketHandler;$ `, D+ H. H9 E J7 R
- @Configuration
- @EnableWebMvc
- @EnableWebSocket9 Z9 l6 i" h) g4 G$ S" ^& Q
- public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{6 K% ]3 U1 O# C- {% n
- @Override+ J+ `% d% m |3 j6 I7 A+ u' \
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");4 H0 k% Y9 o8 `/ L6 y7 W/ B' n
- registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");
- }
- @Bean' \6 ], m4 }% f' E' U" \
- public WebSocketHandler systemWebSocketHandler(){
- return new SystemWebSocketHandler();: S- |& U# }/ C" G$ v5 a
- }
- }
複製代碼
SystemWebSocketHandler:
- public class SystemWebSocketHandler extends TextWebSocketHandler {
- % }- u" Q6 D# F# P# w; P2 d
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;4 ?+ A. V$ Y/ }& h
- x- y) r8 U/ n; M+ Y$ l
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
- System.out.println("ConnectionEstablished");
- users.add(session);% X7 y- v( \* u! ^
- System.out.println("當前用戶"+users.size());
- }! k5 d' v$ [3 b, u5 l, z
- /**( b# S3 }0 }* ?; x* ^
- * 在UI在用js調用websocket.send()時候,會調用該方法
- * @Author 張志朋7 f& A# K4 g8 X. z- |) S3 K
- * @param session
- * @param message- n) Q; i2 ?5 Y, d4 m
- * @throws Exception
- * @Date 2016年3月4日
- * 更新日誌& n. c% Q; Y# ]
- * 2016年3月4日 張志朋 首次建立
- *
- */
- @Override
- protected void handleTextMessage(WebSocketSession session,5 l. }, a5 j4 W8 j, U% x
- TextMessage message) throws Exception {( ~( P! W; T. H$ G* U8 W
- super.handleTextMessage(session, message);
- sendMessageToUsers(session,message);
- }
- @Override9 m3 d( B# t% r" z0 e
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
- if(session.isOpen()){% x j0 I! c0 S5 Y
- session.close();
- }
- users.remove(session);5 @6 y! F$ k, D- s/ N9 v5 V
- }) m1 |, z8 r$ ]0 B
- @Override
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {& u* X6 C/ N* }. ?. e2 w
- users.remove(session);, o, ?0 A. J7 o2 z6 `' b
- }8 p# X, ?0 m7 `+ J
- % V3 r% E, m; }1 ]5 D
- @Override) A1 [; K/ }6 D% [! ^
- public boolean supportsPartialMessages() {& Y' N& U! ~* z( m! h* {. f
- return false;
- } t9 ]* K% z7 C( C# E- |- F+ i
- /**! Z3 L$ S' Q. \1 G- p: I3 {. D1 E9 W
- * 給全部在線用戶發送消息
- * @Author 張志朋
- * @param message void* Y) `$ f0 T4 M+ b6 q8 X2 p5 F
- * @Date 2016年3月4日
- * 更新日誌1 q% I4 Y E. @. Z# H- E
- * 2016年3月4日 張志朋 首次建立
- *5 V5 u3 a+ G5 r# P+ f, Y/ {
- */2 H6 z3 A/ m4 ?0 h( ~5 o% X
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {/ l f: d! @" [% g% ]% j! ?
- for (WebSocketSession user : users) {
- try {; K! [3 k) B! }. v
- if (user.isOpen()) {
- user.sendMessage(message);, ?$ r7 A% ? n1 ]. [7 @$ j
- }
- } catch (IOException e) {
- e.printStackTrace();: c1 t* |1 j$ I% o4 t8 H
- }% ]6 X6 _9 e) z, `) j% `! H2 N
- }
- }2 ^# o( i `( h' Z7 L
- }
複製代碼
信息輸入 index.html:- \0 z& k; w5 d$ U1 ~( ~* T
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>7 a8 f3 O5 v; b# g3 B: ^
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <title>請輸入任意消息</title>
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>7 @( l1 e# @) X
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
- <script type="text/javascript">
- var ws = null;
- $(function () {
- if ('WebSocket' in window) {
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); 5 S+ j {# E8 F2 o( ^2 L8 A: f: F" d
- }
- else if ('MozWebSocket' in window) {$ j1 }4 R% _- _& z3 F
- ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");
- }
- else {
- ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");+ P& w: ?* O' _/ [6 e' t, {0 F
- }. v- m! b& `; f0 P3 C5 t
- ws.onopen = function () {/ B$ e Q$ b) D
- / K$ l6 b6 S7 m2 j' A; m/ j
- };
- ws.onmessage = function (event) {3 O! h. f! h$ m7 Q* M: s2 Y
- " V1 p. q# `/ K
- };
- ws.onclose = function (event) {
- 6 D7 _" h$ ?, Y
- };! e8 }; o, ^! }( o, Q2 r6 \+ z. o( M
- });
- function stop(){+ x8 U6 s7 p. c( U; G0 i
- var message = $("#message").val();$ w. u: E" p5 `! I
- ws.send(message);6 G u8 P3 a( O |5 W8 g8 [
- }
- </script>; u% \/ M8 b7 O0 D5 t5 h" _
- </head>9 U H/ y, ^' M- f# N
- <body class="keBody"> X0 D& H9 u5 d( e0 C: F/ `: R) Q
- 請輸入提示信息: <textarea id="message"></textarea><br />
- <input type="button" value="開始" />
- </body>5 ?% i; h6 ?2 b4 M. [/ f/ b+ j
- </html>
複製代碼
6 q0 F$ y& l t8 y" a( U( I& n webSocket.js 用於導入項目。
- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");; Z" F* y3 X" ^* A6 {! Q9 @( M
- document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");
- var ws = null;0 K' _) I, _( z: |( t$ b. g
- var basePath = "ws://127.0.0.1:8080/";9 i2 {9 p1 H' T9 b' U
- if ('WebSocket' in window) {# w8 N+ p" v, F/ v% K
- ws = new WebSocket(basePath+'webSocketServer'); 4 q. Z! u% i( I' }. t) o1 |
- }
- else if ('MozWebSocket' in window) {
- ws = new MozWebSocket(basePath+"webSocketServer");0 }9 y3 Y9 T% m3 ?/ [; b; Y7 p
- }
- else {, p. l% Z1 e6 t/ D. m8 q
- ws = new SockJS(basePath+"sockjs/webSocketServer");
- }" C8 y7 f/ C. }# [& W# G
- ws.onopen = function () {
- + J/ T- r4 L" d) [' b9 L2 T
- };3 R6 @8 v! b1 K# O
- ws.onmessage = function (event) {
- pop(event.data);- C6 d! {5 ^1 q5 a
- };
- ws.onclose = function (event) {
- ws.close();1 \8 x$ [- o2 \/ Z
- };
- //提示信息
- function pop(message){. ] Q% }1 `; B
- layer.alert(message);* d4 H2 b0 }) I( j, ~, k
- }
複製代碼
9 \; ^. D: M; Y% o5 B2 [ 五、在項目頭部引入% D0 _; w8 y) {( C: n6 e$ K <script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
這時查看後臺 會有如下信息 說明 引入成功。# T; \, y% L c+ d1 ~% U <ignore_js_op>
而後在打開頁面 index.html 輸入如下內容 點擊開始便可。/ d% h3 B% d: \9 E <ignore_js_op>
若是在網站出現一下提示說明配置成功,這時候全部網站登陸用戶均可以收到此信息。, N5 ^, y) H4 C2 ]3 c* } <ignore_js_op> |