TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍
6 y2 _7 T* n4 g/ F8 C; d5 g5 B, w 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
' p% X: `: J$ K0 N2 w! n 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。, Y4 W; C- g, s' w
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
0 U" F+ J) e: {* p Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
$ J" X. `. o4 M" h7 _/ e n 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
2 }7 W! I; z' n; W5 w& X% ]3 ^ 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。6 C; _# G& i, v/ E: H0 k/ K9 `
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以上的版本才能运行。& J8 x8 _* z; F$ P5 S8 A
( {8 q! y5 R! P% s: v
二、WebSocket协议介绍0 u+ A. G5 q0 @ ?7 T$ Z3 _
WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
! |8 _, O6 d! t: q4 A8 u5 H
0 F- W- y1 [9 u$ o* ^# L7 N
握手过程:
/ X' |: k8 R7 K0 M' N3 rBrowser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。* h3 y7 a; E8 L% F! ~
在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。4 _' A5 T- z A, H
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。$ U7 l: v- _" M4 D1 g1 O0 J, S$ y
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。; ^& [9 W0 M- ~- r+ x
* _. k( k7 c: b& D7 [3 s/ c: E
三、Tomcat 7中的Websocket架构7 N9 _2 Y2 j- @3 |: J' {
2 F& F. D P3 a如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
6 f3 D9 J5 b6 Z6 v0 r, A3 m 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。2 x8 m6 {+ t, _3 t& |
Tomcat中Websocket的处理流程如下:
8 o5 V4 D2 d3 s接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
$ g& e0 h. o2 z0 v6 w) RContainer中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
5 [' {% G! }5 d$ T) t. |服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。; E w7 N2 y4 M2 C0 f: Q5 S
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。+ B6 x7 F! J6 f# ]9 Z
数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
. m: {* t! j' i- b$ J& }: o
9 k: p3 B+ v2 Y( u6 O- |四、代码实现以及需求
3 b1 \5 Z4 o# D4 `: |# t2 M f- I6 l3 T5 `! j* W4 b& G
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。5 J/ G; f- v3 [; G* R
& j$ g7 E6 m5 y& I4 e
2、相关环境 , Nginx、tomcat7、centos 6.5+ O5 J1 U' R& \8 j* b
N/ g- i: F! N9 C9 c# i6 a3 }
3、项目框架,springMvc 4.0.6、layer
3 f. Z' C1 q7 Q- a
- H: V% c- @- {4、代码实现:
( s, \2 b9 Z" J# E9 ?. Y: l8 D: X
) \/ w2 D% W ]+ Y+ |9 o" oWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;
' h E' W- E) H2 c - @Configuration
0 b3 L8 j8 e( |9 D - @EnableWebMvc) r, i4 i2 ^, J' N) L4 X
- @EnableWebSocket
1 T, c9 E2 A9 T9 A - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{; X4 Q. X2 H0 B7 h" I
% E1 w: l' u0 n- @Override: v1 w. c: q- C
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
" ^* w6 X: d4 Z7 f" [; h$ [0 e - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
5 ~) X7 [, }7 ^( j - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");1 A$ }2 A5 P, r( B
- }
; H4 A( a2 t( q$ t( \ - @Bean- E, X% k F1 @4 t% q' a1 t9 c
- public WebSocketHandler systemWebSocketHandler(){0 p) r1 H: Q1 f4 s
- return new SystemWebSocketHandler();
1 B. L$ U: r l - }" d+ @ {7 F. }4 Q
- }
复制代码 SystemWebSocketHandler:. f% q+ X' i4 s& r" f
- public class SystemWebSocketHandler extends TextWebSocketHandler {% d6 G2 Q. T% Q6 N) d
: a2 I: `3 Q; V% n% i! ~- 4 s+ h+ D* g5 X, Y
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;6 y8 @) d, P; T
- 8 z2 l$ c# o2 H; ^! q V; s/ m+ e" N
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {& \0 q5 ^3 o& b' f5 l; ]# r* D+ O. t& ^+ f
- System.out.println("ConnectionEstablished");
& y ?6 x( J/ ?! l* _ - users.add(session);% ^; \( h" c3 j$ y* Q% F* u
- System.out.println("当前用户"+users.size());
% a D. L$ L# v8 d( e( B1 f - }
* W& d5 R7 a) _) P+ w% ^ - /**- U- h# K# H2 k& z! E
- * 在UI在用js调用websocket.send()时候,会调用该方法3 B4 ^( U4 A3 l$ k8 M
- * @Author 张志朋
7 s* \4 B; ]5 s9 {1 v - * @param session
+ k( k. E! I0 a1 z( G& \2 V$ D - * @param message
) n6 Q4 f$ m3 i9 \3 D) a8 [ - * @throws Exception ) ]3 Z9 y% ^. X; i8 M& w. M4 |
- * @Date 2016年3月4日5 P( R6 b, w K( Z( W
- * 更新日志# d9 @6 P/ Q9 J
- * 2016年3月4日 张志朋 首次创建
, b9 H6 T, U E$ L1 E6 X; U - *4 Q$ ?* r! q6 M. _! v
- */! W* `" q( B) c: c
- @Override
' v$ Q1 x3 Z: E. K - protected void handleTextMessage(WebSocketSession session,
' A; a" @- g, {% l* k" d - TextMessage message) throws Exception {
6 f: g @ X; _% x - super.handleTextMessage(session, message);
" p/ H3 }( ^. N* F8 [( Q5 q - sendMessageToUsers(session,message);
2 `5 R: g/ L0 N) W9 i& E( Q - }1 |. d( |& ^, s, m
- @Override
1 X( ]0 K s- B2 l4 \: z* l - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {% f2 L; J6 i; k
- if(session.isOpen()){/ L1 U! \1 c* T
- session.close();
: r, r8 n& V( [ - }- O; ` Z$ T2 x( R& g y
- users.remove(session);
' j. B9 [2 Z; v - }4 F, H5 f; ^6 i0 ~, [( s
9 q: |7 O; w6 K9 O) {! u4 r- @Override
3 E9 a" x5 B) O$ O: ]! t# D: f8 t - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
# [! v2 `; Q0 s* G - users.remove(session);
4 Z: x4 P5 [2 K - }: o+ O" ]6 u- y8 z! I
# X* _$ M* r5 n* ]7 e S) v/ t2 [- @Override
3 \6 B/ _9 T( u' g) H2 c - public boolean supportsPartialMessages() {
" m& z5 Q3 Z8 X% o - return false;; T$ }" C/ I" `: W) t2 e
- }1 o& ?0 I0 C3 b4 G0 m6 }: [0 Z
- /** B/ O2 [+ J$ `( i6 T* M t
- * 给所有在线用户发送消息
% T7 @' K# B6 t3 x: | - * @Author 张志朋
7 s7 ~0 ]" k# o6 U$ R - * @param message void5 j$ k3 h; _) k# p2 q
- * @Date 2016年3月4日 V4 I9 p* X+ f- [9 u( V6 h0 R
- * 更新日志5 P% N8 K& c4 J# N' T; O
- * 2016年3月4日 张志朋 首次创建4 i z8 a0 X! B. S9 X I6 O
- *$ J2 g- U. K& @+ v9 d2 F+ j6 V/ s/ C/ A
- */
( g+ E( F! e! X - public void sendMessageToUsers(WebSocketSession session,TextMessage message) {
# _; U4 M2 t+ P' L$ }4 [ - for (WebSocketSession user : users) {5 F* B$ h" n* Q2 z+ z! r
- try {: F1 J1 U9 U# B
- if (user.isOpen()) {8 Q+ q3 j3 B p; G- m7 M" W
- user.sendMessage(message);
' O8 G) M# W& K' J j! L' E - }
* ~' o4 L: A9 C - } catch (IOException e) {8 |5 I# r3 d: S: D' J8 s
- e.printStackTrace();7 C4 p2 Q0 m6 e5 t: @( d
- }+ A5 J8 ]& x4 ^1 h/ N7 c! G4 ?
- }
4 k5 S: r+ m8 d3 I+ ^0 i: K - }, y# d% Q h2 F$ Q' P
- }; \- f1 O& L/ u6 f
复制代码 信息输入 index.html:) {7 _# T) ?8 h) T
- <html xmlns="http://www.w3.org/1999/xhtml">
' I' k/ m" E# B8 i$ _ - <head>
( B j( Q0 T; P: E8 t( n' F& E - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />$ W6 Q: e. X! m2 s, Q
- <title>请输入任意消息</title>" ^" ?$ [% P" ?! t0 r' W1 `! m4 B
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>/ w9 X6 p" Y2 [ c( l
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
4 F! t) b- O2 I) x$ F- _) h e3 ~ - <script type="text/javascript"> q, i" f" H# |& \" X
- var ws = null;& g. j* K5 p4 W5 c7 y3 b: o9 \
- $(function () {( W3 k. G" z m
- if ('WebSocket' in window) {
+ _2 K8 a/ y$ `. b - ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); # s# t- |! H f5 X1 F; l" j
- }
7 D/ G$ X) g% b- B7 n - else if ('MozWebSocket' in window) {
D- j- C( o" d( V, F' |# d - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");7 v! J" H6 X+ U/ D: E
- }
' [# w( `/ c, b0 X5 B - else {
* u0 c4 k$ B5 B) b - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");$ T: w% m% I* t; `, l
- }
; U+ t. z% Y( g4 F" `. e - ws.onopen = function () {5 T- O3 V/ p9 E
- ' H& G3 x u X4 M, A- s+ I4 V9 y) d9 Y
- };
% @0 f) m3 v4 _1 P- E; T* y( e - ws.onmessage = function (event) {& n; n: w) S+ [7 v
- ' [$ i; `4 x+ F( W y0 m- A
- };0 w( P* V: `5 u+ N8 _& [3 O
- ws.onclose = function (event) {
9 i6 E* [) P8 ?' Y2 f4 R4 g" O# T4 N - - F! n8 Q0 p0 q* u) V i
- };/ A+ I8 L2 H: d) _2 S2 ]4 w
- });6 T: }. z# {( E8 N; T9 r4 m( v$ V
- function stop(){6 q* Z/ b$ A, U' O$ p9 B) L+ o
- var message = $("#message").val();
8 K8 P# F7 O" V2 d: w2 a2 k - ws.send(message);
" |0 _7 D4 G5 C4 D$ M! T. [ - }
m' a) I' j% n - </script>6 V. t/ L @* L) j$ n, K
- </head>
" }! k- @! @) d+ A. Q7 R" k - <body class="keBody">
O/ [% `/ J5 M* I) \, C - 请输入提示信息: <textarea id="message"></textarea><br />
$ V8 _5 a, q* F3 {: L2 F% s* }5 | - <input type="button" value="开始" />7 y) Y$ D, y, r& i
- </body>
' ~) E# r" _& U5 i: u$ ` - </html>
复制代码 7 u8 M$ E) J& H5 T, R8 J- o Y6 {0 }
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");: J$ |- _. D1 d9 g4 m0 ]6 \. a
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
1 G- I/ A v7 V8 G* Z W8 O - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");8 p+ B0 l% ~8 _3 n
- var ws = null;- [* b. V/ Z! @0 \" R" S7 F" p
- var basePath = "ws://127.0.0.1:8080/";7 \" H! M3 |7 {+ B! v, j, a4 o& N
- if ('WebSocket' in window) {# R) h5 x" v3 G) O7 C/ ~/ {5 R
- ws = new WebSocket(basePath+'webSocketServer'); 0 r2 E+ ^; Y! \* F$ u+ z6 a8 C
- } 0 O) i6 h' d4 {, M6 z( A
- else if ('MozWebSocket' in window) {$ c' O" f1 L" n! M8 F' S8 N# S
- ws = new MozWebSocket(basePath+"webSocketServer");
9 `: Q( c* U5 P8 u - }
6 j- T, h9 G6 f+ ?/ d) O - else {
! `. M2 _( b s - ws = new SockJS(basePath+"sockjs/webSocketServer");+ B4 O& X; ^! c/ C! a$ q9 v
- }' C& N: }$ P) Q) R* U7 E
- ws.onopen = function () {
* {# Z2 t4 B9 X9 ~- T: n
! |, e: I- f$ x/ w7 ]# o- };- y% a& \0 }* p! d$ X5 `
- ws.onmessage = function (event) {) X- y4 o0 O; J7 a) x4 V. Y
- pop(event.data);: G' U9 Q; d+ ?
- };8 M) N; c% W; q } {9 O/ T
- ws.onclose = function (event) {7 B2 }6 s& v. m( C, _/ p
- ws.close();
- X" B! o' D" y - };
/ O/ u9 w; x! L+ g9 Q% @8 y6 I - //提示信息
% `6 t8 v/ M4 v' y: f6 a; e. z0 Y4 G - function pop(message){1 U# _4 G) f! q0 m: J6 r
- layer.alert(message);
% ?. N8 _1 ^ i& {& Z% S - }
复制代码
& t. c( U! `( v2 {. n; B5、在项目头部引入
7 ~ Z* E) u6 J) A: P4 Q<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>; \ T W0 S$ i' `$ W b
; X1 Z, L5 @7 `2 @) h9 g
! D, T2 a- v6 S6 w这时查看后台 会有以下信息 说明 引入成功。
# c2 x4 V* ^. j" d. j
1 y6 d4 p8 X, x- R p# }! j
+ A: |, Z M6 P: Z! n4 q3 l4 `
然后在打开页面 index.html 输入以下内容 点击开始即可。
( D0 m- Y* ?$ v: A" j
- `. o; Q! o' ~2 `/ f
1 J. k& v) g: W6 H2 t( R如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。
6 t' z( ~3 S, o' A& l" ?- C
3 S) a5 T8 P+ z% X$ K& t3 w
: S5 X% b- A( ` x5 ]: g2 O' J, b3 ^
项目下载地址:
5 }+ x: b" c: c0 J" S, _2 I8 ?0 A% e
% ~ x9 q$ F+ C
9 \/ a9 H4 E6 y$ d+ x! w; a( R; z+ D2 ?: E7 {/ } ?
$ [4 P) A+ y; P4 H* g9 l; f
|
|