TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍0 c/ L6 ?! y: g& U& m
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。" O" @2 d; P5 @: d
我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。! i- O5 t' \% |) c: L0 E& w# u1 H
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
0 N# u* r4 A& o Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。, s5 ~0 z- E+ }7 `9 z$ C6 A& z
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
1 H9 S3 }) C" ]; ^1 M 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
& F8 T7 i P9 |# K8 U7 y 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以上的版本才能运行。; U( }4 h) Z% }% a$ f
* d0 c& d; O) _5 s4 ~' U r1 \二、WebSocket协议介绍
6 e8 I+ ?4 b# p/ }# q WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
) b$ q5 o. V! S4 u9 @2 F' _; n
9 j6 I% V2 d7 g9 \
握手过程:/ `8 @: {. \0 K3 p- G! I, ]
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
, f& B4 {* J2 M* n9 K" n在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。% Z* _/ {' Q- l$ }# M" ?
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。: f1 b- _; s* y' | V
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
" R3 p7 l' v. g. M( D( c; w/ T \! R
* u+ Q) t5 G x: C$ W三、Tomcat 7中的Websocket架构4 l- O5 F! H# Z5 R* _9 K! k
6 a; W9 @( i- W Y$ D: D如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。 % ^, k5 W) f3 H4 Y6 ]
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
8 R8 {; [9 A, l$ [6 T Tomcat中Websocket的处理流程如下:
4 B, g! F5 ~. c& L接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。# \6 }. N4 J$ j
Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。# l% i- |# k/ S' [6 j
服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。8 B8 L0 Y! ~: X0 e) \
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。4 z& `5 @7 {$ }* N& Z
数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
3 A; X# I, p) T+ i; x, S6 y
0 O/ `( O3 K, |1 R四、代码实现以及需求$ C+ `' E3 l* y$ f0 ^1 d+ I! s: F7 A
$ @+ O+ o' a* z# {" W4 Y6 Y1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。& o% n6 F# f& W7 n) Z e: U
, d! P9 n n( O* q, g6 B6 ^
2、相关环境 , Nginx、tomcat7、centos 6.5( {2 j% v9 ~& x- v# f n/ d
" t) I# @$ q8 i$ a p3 f& m8 w3、项目框架,springMvc 4.0.6、layer- l$ j0 p" s. }$ w# Y; c, @
: a* E2 w; w3 H! B3 p1 b
4、代码实现:+ ~9 [9 r' D3 U' [$ z+ ~
, v$ A4 U9 k$ t
WebSocketConfig:- import websocket.handler.SystemWebSocketHandler;
/ L O# D) {( R7 ?! O) }: |0 J( z6 ? - @Configuration
* ?/ f Q8 ?; h1 L9 @ - @EnableWebMvc
5 U- E7 N i' Q1 b" F$ O - @EnableWebSocket
2 I$ C& S4 j$ n$ g - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
! J7 a5 F& I+ M0 N0 ^9 l: J - . W0 Q z' M5 m! A9 X
- @Override
' x9 r5 k- }9 Q5 e - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {% Z) w- D7 U/ S2 w
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
6 h- S2 q+ a+ i/ B! T& ^ - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");
/ ?* [5 W, a' e* c, P - }
: z* l3 T2 [8 D+ y& L - @Bean, _7 j. ` D7 f7 W1 i3 _
- public WebSocketHandler systemWebSocketHandler(){9 |; B2 w2 E) d7 i4 _/ D1 g
- return new SystemWebSocketHandler();
( p. c! _" I- q$ D* |$ V& D5 M( W - }" F1 O7 p7 O8 C" c2 K, ^ u
- }
复制代码 SystemWebSocketHandler:
$ u! E7 K( m" I \% E0 A1 H- public class SystemWebSocketHandler extends TextWebSocketHandler {
/ {, p7 O% [. f
4 m1 S: z a& z
0 w2 s# O* ]8 x- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;4 e; H# O1 p$ u" W% C
) c- | ?. a5 I% X- J& {7 `& O" h- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
6 o" S( @8 b2 E( X - System.out.println("ConnectionEstablished");/ Y$ p. Z# C- A
- users.add(session);, ^: s, Y4 d9 g7 v
- System.out.println("当前用户"+users.size());
8 O$ l: E) D8 {% E3 X. L - }- O$ z6 j C3 \& P
- /*** g* w, a) w" z- l
- * 在UI在用js调用websocket.send()时候,会调用该方法
# d7 o4 ^6 G5 C8 Q6 h6 w: b - * @Author 张志朋; B" k" s0 b+ g3 u5 r: Q- h, j8 D% n
- * @param session
; A" _( \' A% E - * @param message3 D# k: {( [2 D' e5 T$ f5 f- m) P
- * @throws Exception
8 }( ]5 {0 g1 l% {( W( k - * @Date 2016年3月4日
9 L5 N' C2 u* K! v( H - * 更新日志+ s3 n- b# |. |8 K& @8 a& k
- * 2016年3月4日 张志朋 首次创建
6 s$ B+ E- k9 I4 }7 g. i5 Z+ z5 _ - *: l) B9 D) ]* d( X- A
- */
. A% A/ d2 Y6 `, A8 Y - @Override/ A8 }% H1 _4 a6 O$ F
- protected void handleTextMessage(WebSocketSession session,
3 m) h. i8 E7 z - TextMessage message) throws Exception {
& y7 y7 c" }1 u2 ^( V - super.handleTextMessage(session, message);
/ P8 C9 r$ n8 J6 w, }1 l - sendMessageToUsers(session,message);$ b, B# F5 R0 J- m, W
- }
# `( N( d! n9 k7 D$ X - @Override
! t7 v, I5 w5 b5 }" B6 w2 \4 x - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
) m7 B O# m2 ~: i - if(session.isOpen()){8 l7 _: Y' d9 y* c* F
- session.close();
) u; O7 |/ o( S3 B: J" [ - }6 ?" S* y [# G' t
- users.remove(session);) s2 p! n7 r# [ y
- }
4 Z* I: q' D$ E
# K7 U% p/ A0 ^( k) Z& Z+ j- @Override9 C) C1 ~0 q* S. h8 o5 T
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
) T" K: Y. ^; O% O7 p: ]" j, { - users.remove(session);- d, [/ k, r3 w) l% t
- }' r/ z* R$ C1 B5 G
- + {' @. [- X7 [; t. M1 d/ m
- @Override/ y9 h$ X' g$ W' i4 V$ f3 t8 L
- public boolean supportsPartialMessages() {9 D5 B' O I9 p+ t) D" ?3 J
- return false;: ]2 p. s" |- u1 M4 c0 I
- }. ~' r% a+ N0 f% G' h9 R4 s3 s! j) Y
- /**
7 F! C4 x6 b" o# T4 h; [ - * 给所有在线用户发送消息& n. a' F% S3 |( T7 o4 d
- * @Author 张志朋4 I+ @' X) I# z# J! H
- * @param message void; d8 a' \% h. a6 @9 R. O( G
- * @Date 2016年3月4日' V" k0 W0 U; r1 E) `+ I0 `
- * 更新日志" a/ q+ \5 L# d: L1 n. Z0 q
- * 2016年3月4日 张志朋 首次创建3 Z* G# O! e* R l1 Q1 k* [
- *+ k, p) p9 L- Y) ^4 o
- */7 w2 b; v( I6 p9 x7 ^/ B; A( f
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {* o; z+ O3 D! b" s! c
- for (WebSocketSession user : users) {
4 `- K, Y# P8 u4 ]1 P- ` - try {$ n. Z4 u+ _2 H$ ^/ d; k$ y
- if (user.isOpen()) {% o, j( |1 L7 A% J
- user.sendMessage(message);
- n+ F8 C! j% p( n5 ? - }" j3 }/ u5 I; |& a6 W: g
- } catch (IOException e) {% r k) ^( u* I3 y6 c1 }( z
- e.printStackTrace();! R0 V! @. B4 J h: o) z
- }
8 ]& J8 `& o+ @( p3 R; X - }/ v2 L& i$ |! r' }4 a
- }! h3 U$ W: T- D
- }
/ K1 J3 d$ Y3 R* t" U, G
复制代码 信息输入 index.html:4 F% Q+ N) Y- ~2 I' }# a
- <html xmlns="http://www.w3.org/1999/xhtml">6 x# M# w+ c, j* j# k
- <head>
) d7 Z4 n, F1 x6 a2 } - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />/ e& @- d7 \/ A' x; k
- <title>请输入任意消息</title>
( k d! e, r r# \/ F - <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
: B- C+ u5 W. C2 k5 j) X - <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script># l( ~/ ^# [! a+ [! B& ]2 W
- <script type="text/javascript">
1 E( `$ }* Q: E% m8 u& D - var ws = null;8 f) D! e4 k. \2 D# l# ^- f
- $(function () {
5 l( e9 I& p" R* s% J1 P. `4 Y - if ('WebSocket' in window) {1 F9 x* `5 U; X/ r+ W
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer');
0 C( o- z: d# I* c( w; j4 s - } 3 q) u. K d) C' ?, r
- else if ('MozWebSocket' in window) {+ @. l. P! k( g+ G
- ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");2 K9 Y. l) P* \2 k7 r
- } ; L% o8 ?- U) h6 V0 L: D) y9 i
- else {. P3 d" J" S# \8 R& j
- ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");. Q; G" b& b: |
- }
8 h6 V! x$ D7 A. Y$ ^* p - ws.onopen = function () {2 C# C) l! m) M- d) E
6 }7 s. F6 T. N, ]- };1 n; t. H0 i D
- ws.onmessage = function (event) {
- ~1 u0 a3 I+ {# G4 p6 g - $ f# @+ K+ {8 W) I3 w" }2 Z
- };5 z1 v+ G" ?- U! d. e. d
- ws.onclose = function (event) {( z; z, i6 r; O: I" O; ~
" `* s, n) K, T' {- };
. w1 z% X) Q$ W. B) F$ S6 m, l- O - });
; L- n0 R$ M4 a9 w7 _4 J5 c; T8 ` - function stop(){" N2 e" J8 U/ N' x$ \
- var message = $("#message").val();
4 o$ p+ D4 f. u* d1 `) i/ y3 X - ws.send(message);+ X& V# w+ E, Q& l" i5 h. R' D
- }, M8 U8 j6 G( x9 s; T: m
- </script>
8 |, G3 C, k+ F* t - </head>. Q' X7 w) E- ~9 [" ^. h2 G
- <body class="keBody">, g2 y0 y# I* O9 G
- 请输入提示信息: <textarea id="message"></textarea><br />
( t2 {/ U' K4 L& L) }% w - <input type="button" value="开始" />
5 p0 w3 C+ z4 y/ M9 u6 a; o# d - </body>' |+ V; R* x- ~9 \# `# u1 E
- </html>
复制代码 * Y6 D6 P8 p) N, d
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");. b1 l* Q7 B* X1 u3 e2 A6 F/ j
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
, e9 a0 B2 S; O$ h, b6 N/ j9 G0 m - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");6 ]' z7 c" `( |
- var ws = null;7 k* K7 D9 `6 z: N0 h8 \
- var basePath = "ws://127.0.0.1:8080/";
* O' |2 R' y y' ]& d; Q - if ('WebSocket' in window) {
" Z5 `1 z! m+ @# u D. {. k( g - ws = new WebSocket(basePath+'webSocketServer'); 7 R; J; A ~) H: t2 h+ n0 ?+ i
- }
J/ J! b6 N+ _+ R - else if ('MozWebSocket' in window) {- s2 S/ Q% V, m) B% D
- ws = new MozWebSocket(basePath+"webSocketServer");6 z7 H# `- r! p& S- ]$ n
- }
% `. e; @; A2 ]/ x# C) r% t - else {
8 N" o% r7 c, n - ws = new SockJS(basePath+"sockjs/webSocketServer");+ z) X8 i1 i: q% L0 j/ m
- }, _9 h$ a& Q$ ]5 b* A, Y8 F
- ws.onopen = function () { l+ P: l3 R" y* c
s# z7 h. B" @8 l+ W- };
5 F- U1 M- g. M0 L+ d" H - ws.onmessage = function (event) {
2 N1 Y; u$ B/ I6 g% c% u( b2 a - pop(event.data);. q' W) g' I! r" X g+ @1 q
- };
4 N- q, b3 s9 k2 ?& b1 C - ws.onclose = function (event) {, x2 ]. J, n$ z9 r+ t
- ws.close();3 g @1 }3 [/ R3 k' m( [3 o
- };
$ w: B7 {2 R" b- m" `- j - //提示信息" i* v4 N% J/ ~8 k
- function pop(message){
+ H7 W2 c. Y/ H& B7 v2 ]) X( d: S5 V - layer.alert(message);
7 I# @ w! r0 K6 \ ?9 H9 h - }
复制代码
& a' P4 i6 [- w+ \% t7 s0 @2 F5、在项目头部引入
1 Z( m* C: X6 J* Z. `+ N" p/ w<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
% ?2 a' M+ ?1 s3 |8 @* N; p5 ?* f- G( P% } P& Q) h' ?3 |7 a
: h. ^# Y4 i H& v0 F
这时查看后台 会有以下信息 说明 引入成功。' v# y- w& W$ e) h+ z9 L
3 _- s3 K1 U2 q- _# d- r- W4 {
# V+ I7 m) z, ?5 l6 H& f: N2 X然后在打开页面 index.html 输入以下内容 点击开始即可。
2 ~1 k3 ~2 q4 I+ O7 L) W5 t& F
8 l) \7 t1 u7 ?1 ]* N! j# }# W
D( ]- A* x2 I$ Z
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。, ?# D; q3 T+ J7 y
% B4 a/ j% B0 y& }$ e
; O) r' _: E5 Q5 h# k" b
3 o# [/ _% h' N" j1 O5 O. M6 ~项目下载地址:* @: W, g4 v+ p
/ L4 y2 B9 w; D/ [1 k2 e' O! g* C4 _2 ^* Z
" S& @/ W/ | S+ }+ Z+ D4 c1 D! s" A$ r( R1 U5 W. n$ Z6 v3 K
|
|