TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍1 a, ], ~6 ^0 X. \% J+ L, S
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
9 b+ q, y1 U$ O$ j 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。1 Z9 l2 T$ C$ |* a' E" ^$ z
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
- c0 b! o& S2 P Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
" Y" l& p+ S2 p/ n) s) p 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。7 ~" S7 c. s6 j# q; k7 u
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。2 B- K. d: h7 X$ e
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以上的版本才能运行。
9 `% Z( r8 ?3 U6 W# }
+ l, k# R) G2 \二、WebSocket协议介绍
( R& M- v" m2 I# _ WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:7 I5 Z( i1 O! p; |. L
- j3 g: t9 u/ H# B7 N握手过程:
" E# q7 r2 O1 r5 [Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
) j! s, i2 p' Z# t x9 a在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。
) Q0 w# Q4 G" ~/ Q5 ZWebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。+ C4 L6 G( P- \; D
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。9 I1 K6 ~7 ?+ C2 i$ O
( @: u) [! n1 {" @ m) r三、Tomcat 7中的Websocket架构
. b" X" I) J# c7 _2 m1 C
3 E, P2 ?9 a& u9 P8 G' g# k如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。 3 j& R4 x. v- f# @
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
' }& g$ {5 @8 W' I( P0 m/ D* B Tomcat中Websocket的处理流程如下:
+ I6 y* m# S1 r# D
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。* ?) T8 P: U" f
Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
8 a4 I9 r! y' r3 h; b: C0 S, U服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。9 f7 r8 Q" F! r" I8 ?: }+ {+ Q& c
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。) m4 p- D; H) J
数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。; }$ O* I7 v4 F
' O0 h P b f# g1 W
四、代码实现以及需求
# v j( ?$ k& Q U- Y
' c. `, G B% O& G1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
! h, x, T) J* N0 ?7 c
; L8 N, c3 S/ Q; }. ]* _, z2、相关环境 , Nginx、tomcat7、centos 6.5/ V+ P' {" X+ s4 ?, b; U6 K$ p& d
: }) I$ v9 }9 |! u3、项目框架,springMvc 4.0.6、layer( c2 A8 }, n% Z" ?. z0 P
6 d* q3 N: E/ d! q
4、代码实现:' d# t/ q+ e+ s1 L
( C/ k3 v! w `5 S3 K& C0 o4 ]: A% FWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;' H9 L4 X# t' G
- @Configuration& W; X5 g3 O: L) D" y
- @EnableWebMvc
& I- W( g1 w- @. m - @EnableWebSocket J \) x2 X7 K R) _
- public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
& v* M0 \. j. W% ^- Q {7 N - , |( c6 C8 N* h H6 p) r
- @Override
3 P- |$ D' C7 p - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
) b6 a. o( U* M: Q8 A4 s5 v - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");6 w6 g$ F6 _2 M' ^) s: a1 G
- registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");1 c2 a) w1 j/ y5 _7 M
- }
7 o( S% D- i B& { - @Bean R" O4 G3 S8 h+ O
- public WebSocketHandler systemWebSocketHandler(){
$ t5 X! R: D' B- R6 [# f- G9 m - return new SystemWebSocketHandler();
5 J" W" l5 {' M2 s - }
/ N/ o0 d2 l. S( t - }
复制代码 SystemWebSocketHandler:$ \9 k4 _% v1 S4 w4 g4 p9 d& i" N
- public class SystemWebSocketHandler extends TextWebSocketHandler {0 T' v9 B& S" G
- - V$ F# S: ^! X9 Y6 b
- + P8 J7 N3 t9 z7 ~ N V
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;
1 N2 ~+ O! a8 H7 R& Z& S* F
" Q6 G3 _& S* d7 j9 O3 W9 @- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
: |. N- X9 n! [4 ~1 Q |4 G' { - System.out.println("ConnectionEstablished");
# j/ [ y! q- b - users.add(session);/ X, I9 K' h( z H9 b
- System.out.println("当前用户"+users.size());6 f6 h5 q# B3 \) X8 i
- }
' p7 [! ~/ k1 Y- y, V4 q: | - /**
$ n. w# S* G, Y* y0 L) l. W - * 在UI在用js调用websocket.send()时候,会调用该方法4 p4 L% K$ z3 E4 d. a1 `
- * @Author 张志朋/ j, t2 B) L, S# ^8 U
- * @param session
" U, p: @. D3 v& a9 I* ] - * @param message
) a1 Z9 g [& V& _9 @ - * @throws Exception 2 T* u5 w8 X" p8 u' D
- * @Date 2016年3月4日2 D- Z5 d; k' S- J8 C, c& z" F
- * 更新日志1 K( S* B; Y' V
- * 2016年3月4日 张志朋 首次创建
6 o# k$ X3 t" m& A - *
: y( E) k( J' k, m h - */& G, O/ R& B! C
- @Override( H) k$ m. e; {" A% ] ^4 P
- protected void handleTextMessage(WebSocketSession session,
' j! a3 e9 Z( `! s6 i7 m - TextMessage message) throws Exception {2 U4 X% i- \0 H9 l+ }; x# C
- super.handleTextMessage(session, message);
9 V/ q/ Z. `1 n7 Z3 ]1 S# {) ^ - sendMessageToUsers(session,message);- f7 Q0 t1 o. ]* V
- }
5 O& G/ I0 F8 m# E9 s) P - @Override
1 X! ]2 e8 T0 S5 M6 Q2 u: q - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
8 v3 \& r# k! q0 x% p - if(session.isOpen()){
! K, m; s. ^# X4 z5 i - session.close();# x6 G! \, G; |7 ~
- }
9 D) \6 |1 |! F - users.remove(session);# q& \# W$ c' y( g0 q+ Q; u) m
- }
0 [4 C* W& o0 a& l; z/ S% F - ; X8 f* n- N! h8 \! R
- @Override
# J r& g) n+ [2 P) v: l - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {* n2 L' X7 w5 Y- W2 ]: j5 F. i
- users.remove(session);: y% R2 {- j; O( L2 P
- }
& z2 C' `5 L T" u0 e9 W: G - " c8 N9 e' c, y8 B
- @Override! O! \1 N; y% C
- public boolean supportsPartialMessages() {
- {1 K' {# k" f; U. | - return false;
' V2 _( U5 Q/ x2 W# a% T4 J - }
2 b2 \% W4 x& j% n7 ^0 r - /**6 ^5 Q- M( V5 u9 b; Y/ ?) v
- * 给所有在线用户发送消息
3 x( U8 C9 }8 i - * @Author 张志朋4 n% O2 v4 W0 @/ j) K
- * @param message void% T. H8 d% m, f* d- J/ j
- * @Date 2016年3月4日8 x4 I: q! r1 F; v7 Y9 ^
- * 更新日志+ \/ E' x# c1 P* k8 O4 I5 y0 Z
- * 2016年3月4日 张志朋 首次创建
5 n. w) e; P. F) n* x) A' \1 ^ - *
% l. J5 \) [1 C! b# M; P - */% M* t5 G% s7 {: O- O* ]
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {
; u1 y: _* V* Z$ @4 C - for (WebSocketSession user : users) {
# M* R6 s, z$ a. n: N' P - try {. P2 \/ }/ j4 M0 n7 E% k
- if (user.isOpen()) {% V# M- c, b( D9 f" G
- user.sendMessage(message);1 o; ]$ n- f% o g6 a/ E/ K
- }
. D- k+ ^5 a+ k0 \6 O4 A" K - } catch (IOException e) {
* k" x5 _$ T1 i# D, { - e.printStackTrace();8 G+ i m# x: A# \
- }# N! W2 Z$ U: E7 q/ r
- }
. Z/ g9 C! G4 S5 |* O5 F - }
; c+ S! w6 ~, \6 j" V5 ? - }
L5 q" L& {( x
复制代码 信息输入 index.html:# C3 @: Z8 s' r- B& W
- <html xmlns="http://www.w3.org/1999/xhtml">2 a# g; [4 L( m: P# l" O* a/ E
- <head>" J3 z& \7 s' U# U
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />2 m4 t- ? |+ H
- <title>请输入任意消息</title>6 q" @5 j5 E! A! M8 z/ W1 h
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
' q* R, v. m/ ^6 U5 w - <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>& E2 K1 p8 j# M! Q% M: x* f" Q
- <script type="text/javascript">
% Z- V$ U" S% S; E, a. B0 N - var ws = null;5 z9 P( z6 l0 w5 e" i& r
- $(function () {
1 m2 P8 I! |: L' q9 H* R- g& P - if ('WebSocket' in window) {7 S8 W% Y2 [' L( G( U
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer');
) A3 _. O! l& F$ A3 S - } " }4 e0 x5 r: r
- else if ('MozWebSocket' in window) {
# F- f: c% t" _. H4 k4 ` - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");+ \% v4 Z; q$ }$ g
- }
+ V" J, _0 y/ n1 x" `% ]5 s$ G# P - else {
# S- J7 O& f1 U" y2 o& Y - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
& S4 ~( u3 H: f3 z4 {* ? - }
* C1 X& R! U1 W6 f i) y& q - ws.onopen = function () {
, m/ r3 C# d0 I0 q5 t( ]% E - 0 H3 e; x8 n1 B
- };
5 _% N6 }0 p! y$ ] - ws.onmessage = function (event) {7 }* F5 h) A3 ~
0 L% g+ K1 x, e& \7 l8 j- };$ r* i1 Z( x6 V4 F/ x! l6 `
- ws.onclose = function (event) {+ U. D; r! T$ I- S3 j
. e/ N* D* O; b3 P* J+ q- };
& I6 ~$ Z. W: f) a) ~" E - });# s* Q4 |* D* }. p/ K: @# L- w
- function stop(){5 C1 h) |4 C3 |4 W% ~3 t0 J. T3 d
- var message = $("#message").val();
; P0 c1 w, O5 Z! S% |7 ]4 s; E - ws.send(message);4 Y0 _" Q( _3 g
- } {" ~( r* Z1 V
- </script>" o8 G' B2 h& B* O9 f* i
- </head>
8 S5 A) X4 B+ g C- u; c$ k - <body class="keBody">/ g# ?& X8 o3 s D1 ^
- 请输入提示信息: <textarea id="message"></textarea><br />
, J! T/ S1 l1 z* c* w - <input type="button" value="开始" />
0 S. t, i* i7 C. ^) e$ c: {" Q - </body>
+ H2 q2 F9 t: ?7 \. p4 `! U. C - </html>
复制代码
8 u6 A6 p7 c" M1 k# n% NwebSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");) Y, ]7 i; G3 x6 H4 p
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
% G5 l- S, P6 F- y9 ^ - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");
% s( o7 I* ?( W' I9 Z& R - var ws = null;
+ L* h9 }9 }( q3 N- u0 \" R - var basePath = "ws://127.0.0.1:8080/";3 y( q$ K7 G& p; A( c
- if ('WebSocket' in window) {
/ i5 ~7 a2 |. t* Y/ |4 c - ws = new WebSocket(basePath+'webSocketServer'); + C+ a) f. q: p" |; t& B4 h+ I
- }
& [! ~: v' B% V' |/ Y9 d- m - else if ('MozWebSocket' in window) {# `( }2 D0 I0 L+ P
- ws = new MozWebSocket(basePath+"webSocketServer");
6 q8 Y( u& k2 B9 q - }
4 P- K+ M( S- j% H - else { A' [# N' V, H2 [: G
- ws = new SockJS(basePath+"sockjs/webSocketServer");
5 e- Q- U. O; e& A; v4 v - }+ q) d0 u8 Z5 u: A
- ws.onopen = function () {; f- x! y& d k( S9 B
: R4 U, T, i& C/ }; ` f- };
. T5 t/ \. b" _# @" c9 b( u - ws.onmessage = function (event) {/ G' \" c3 Y! c, n
- pop(event.data);
7 O4 Q7 H1 x6 C: x2 Z8 U9 ^4 O1 X$ O - };- T. \6 _3 V5 o b3 b" S1 O9 ^ o
- ws.onclose = function (event) {& ^$ Y3 J _. V! }
- ws.close();/ \+ O& S% X" l
- };6 H, w- h* H, y7 c! T. d
- //提示信息% m. F. f4 l3 M; n
- function pop(message){
% c; u2 }; a- W' ^" a- K( |! l4 \, h - layer.alert(message);0 q0 K/ b. S3 E( k6 L5 O0 h; [) G3 U1 j
- }
复制代码
; T! T# A2 k' G7 M1 ]9 s5、在项目头部引入
P$ e, i) F- Y. @; \<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
5 ?% v1 W/ P( m$ D
- Z6 e( x/ Z2 S: d
5 O# z/ }4 y) W: q* Q0 q+ `7 B/ {这时查看后台 会有以下信息 说明 引入成功。 Y3 k5 x8 e8 L" A; \( ^
/ H; v! S, @% _7 T; M9 Z
: d2 H: H9 Z, r然后在打开页面 index.html 输入以下内容 点击开始即可。# {1 T4 n+ l' L! Z7 g1 S
x# q a( o8 t8 \+ t$ X
/ A Q0 [. A- ]& X( K8 U
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。
3 C& r# R5 @3 p1 U4 X2 G
9 o" v" l8 p/ b9 b2 j
8 C' a4 h" ?: Q4 H' n' v6 o# q& M9 o! o2 W
项目下载地址: \7 U! y z: b* d
$ W: v$ j0 H+ _, B. D* w2 t+ U0 @: |# ^0 I+ j
# B! f! ]6 x6 w: A9 f9 a
5 j- J2 W% D/ d L; ~& w |
|