TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍
3 W5 h$ \& ~: O7 j 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。% P/ B/ \# s9 {" d' n# i, {$ [
我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
; j7 S3 p( l y6 b* j% v6 { 轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。% d# @* I& L; w0 F, T+ X
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
$ }3 D- a) r t5 {% [# J 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
9 R9 v: }. p+ J3 A7 M' r 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。/ f" I2 K9 p' a: P1 P
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以上的版本才能运行。& q7 O; G! Z# R0 Z
& [, L- F! ]- H$ G. C" ]
二、WebSocket协议介绍, I2 V; ^* u7 J7 A: B* B5 E5 x0 g
WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:- x' r5 v& Q* t
$ s' b; u7 F7 M L+ @1 Y
握手过程:) r; O: U# ]9 k% n
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
7 |5 T% o( e7 q: p8 Z在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。3 ~( p' E) [. u$ ^: }* D
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。3 H0 c. @" J2 b% [& @/ J3 k
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
* z3 w2 d: N! x8 `$ L: z' I* |5 }# Q6 {5 M" N, i! c7 }9 V+ N
三、Tomcat 7中的Websocket架构 K" ^4 d% `: ]$ t. R" r
* ^* _2 _' Q( p+ E& Y- k+ l
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
D q+ t: s3 H% b 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。! _7 Q& |9 c0 k1 U+ e5 o# w
Tomcat中Websocket的处理流程如下:
" p% g/ Y6 l: g$ ?3 x
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。, R0 M3 D. L+ }* R1 ]- g0 g' Y
Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。1 M# k, e b; A, A: y
服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
' Q, g) |5 u. b3 `如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。; V; _' @% u5 h5 I* p& Q
数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
5 r* x! v2 {$ D" ~6 b: C
' m+ |! c; h1 M) |四、代码实现以及需求" _( o2 |8 [* f4 a- o
9 l' {' A9 O' s) K3 h$ t1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。7 u1 ?3 s" W" v' {6 n
" Q( U: L% d2 E* o7 r/ K8 V2、相关环境 , Nginx、tomcat7、centos 6.5
& C3 e% |' j& F; a) j
( c$ {# D# M) F3 ?9 |( ~& Q' {% B3、项目框架,springMvc 4.0.6、layer
1 F4 v7 g* [( H0 _
3 X0 C6 ]/ b* Z8 P4、代码实现:( ~! W4 T% U7 k, [) C
' A; Z" p: c3 hWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;" x z8 I4 R- m+ I; }
- @Configuration1 S. l/ n/ m- v# X
- @EnableWebMvc
; n9 ~3 W- F4 ^+ A- Q: Q - @EnableWebSocket3 b& D& s/ v k! D# t: \( x4 `+ d
- public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
9 z, z7 r. u- f+ H
6 v7 d4 N K* X2 \* S$ c, ]* ]- @Override
3 J" e4 J% b* z8 m - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
7 g- ^$ ~& R* p0 y" P a( i8 W/ p" X - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
# ?1 M' W: x0 q+ A' P0 Z0 C - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");' z8 W2 D2 Q$ g: S( H
- }
( R4 j+ q. c, } a5 R - @Bean
0 }0 P9 s) C. {# l - public WebSocketHandler systemWebSocketHandler(){7 U2 X5 l. `# s$ Q6 i. x- Z
- return new SystemWebSocketHandler();
: O; Z% s9 M4 o5 C - }
6 i& B2 |5 [9 V; ~5 t1 v - }
复制代码 SystemWebSocketHandler:
w3 o- v% s. | ~2 \- public class SystemWebSocketHandler extends TextWebSocketHandler {
/ O. u- p: O4 w6 U# o' L
) F- k+ i8 X* G. V* [8 }
/ m3 P6 |" a9 C% c: }' e$ G- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;, d" Y: k( y I2 a
0 @) u: c6 u4 _- l- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
r% }( i# @/ v4 |% y, e4 p* E6 l - System.out.println("ConnectionEstablished");! Q5 D" S0 G! \" A: m
- users.add(session);
5 U+ c& ?+ @8 q; D$ V. E - System.out.println("当前用户"+users.size());
. G/ y( I6 ^. R' r - }
' u5 j6 y% o. }) [. e# Z4 v - /**3 d) c) V5 ?; z
- * 在UI在用js调用websocket.send()时候,会调用该方法
8 O8 V' U9 T# V% K- j8 U! u - * @Author 张志朋- P- W9 P" a* B, G& m
- * @param session
6 O m. F- E- ^7 u- w% o# D5 q5 O - * @param message
/ J4 V) a3 ^7 x# | - * @throws Exception
" ?; e( _" o7 x' h4 P6 e4 h - * @Date 2016年3月4日/ ^0 i5 C" R% s% j
- * 更新日志4 J. A, q0 H5 Q0 {
- * 2016年3月4日 张志朋 首次创建& U4 |* N# B6 _0 O; T
- *
6 J) S1 E; E$ a) e4 m - */) q3 M2 [( |5 @5 J8 \
- @Override- |/ s. c' Z; v0 Q
- protected void handleTextMessage(WebSocketSession session," w6 D z" i' K- X% D. _9 t- Y' h V
- TextMessage message) throws Exception {
8 t8 k4 d8 K; b/ _/ Q. z - super.handleTextMessage(session, message);$ E+ n3 z1 W3 n: R0 X
- sendMessageToUsers(session,message);
% T! h3 a, O' {2 {1 o - }
$ V' o% ^/ p* e( a6 W6 N - @Override+ C& z: ?2 i! T
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {3 \, R6 }; R% a- \$ x
- if(session.isOpen()){
% k& g" \8 } m- f% t' G$ O6 {: `8 q - session.close();. K' f" k9 T* Y v
- }
0 T6 i9 h8 @* Y8 x' k; F) v1 K - users.remove(session);
$ }! j* P/ H& z6 @) h' D - } f; f6 r1 n0 j- d0 N, @: u6 q
- i2 E6 Z0 B1 X# m; v4 g w7 |
- @Override1 x* @9 B7 b9 i8 G5 B
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
9 I& C9 P4 M' I# [ - users.remove(session);
/ I2 y0 }9 e( K+ t% E - }
" `( x, f* m. {1 q- x0 O) `
3 C- A0 \/ j6 b- @Override
~& p Z" O% w5 Y/ R/ o/ l$ U0 X - public boolean supportsPartialMessages() {! i7 O% [/ b: w1 B/ e! ?2 L- J
- return false;! H4 `: _' H- s, ^
- }7 f% n: J p( U3 t% w0 k' m
- /**
# _, Y q" S7 M8 J5 } - * 给所有在线用户发送消息
& @6 D G' o# V9 \: b - * @Author 张志朋0 {/ Q6 x$ u% o8 x& W! P' P
- * @param message void
. a/ ?9 l% s+ S - * @Date 2016年3月4日
* ^' N6 r0 F. w+ S$ U, F' { - * 更新日志
9 a1 H& t% d% a& i - * 2016年3月4日 张志朋 首次创建
7 i$ F) S! ]3 D8 t. o+ R3 L1 V2 L - *6 F1 W& k, d; A0 R8 Y
- */$ L. _3 A7 q& U8 l8 p2 B8 N
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {1 N2 V/ ]. t$ y, T/ N
- for (WebSocketSession user : users) {
, F# I, R, k6 e - try {( H- F$ S& {6 `- V' _3 `# U
- if (user.isOpen()) {0 m+ W$ e7 u- Z1 i" {
- user.sendMessage(message);, q+ D* c# X, P, K
- }
! ^$ ~# |7 b' S d+ f% b6 S3 H - } catch (IOException e) {
" a$ j7 M- C. j# y2 ~1 _ - e.printStackTrace();
# {9 q7 E/ R* R0 N' ?# c6 ?5 } - } U/ O4 Q5 J0 ]1 Z! ~9 c
- }1 ]" P; Y6 }$ m8 y
- }
- ^* {, u: g0 p) E/ b - }
6 _+ H9 A9 D! z
复制代码 信息输入 index.html:
3 t* n9 ]$ V% C6 P- <html xmlns="http://www.w3.org/1999/xhtml">/ h3 ^/ Q- b- x" j7 x& ?
- <head>
: R- p; m6 d* u6 S" h) _' p - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />7 @( V, i0 i& a1 ^) A
- <title>请输入任意消息</title>
, k7 }: V8 k; p) r* I1 F% x+ @; \ - <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
* _' q( i7 y, b( S - <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>. B$ N4 `' x) v
- <script type="text/javascript">9 f$ B8 U* k: g3 q! F! \9 s
- var ws = null; c+ A5 q( j* P" k, u/ u
- $(function () {- H7 [# e: G1 e0 R
- if ('WebSocket' in window) {
2 e7 `$ {6 e k - ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer');
/ S& K4 j4 F6 l2 d$ \ - } 2 g# ~" H2 Z$ @" |
- else if ('MozWebSocket' in window) {
" C7 E6 Z/ @$ w. U$ ^ - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");- g% P. l9 f/ c; c' H: p- W) V
- }
3 A( T# }" x" f7 V% G1 {- D - else {- p: [ ^. {! e5 D, E
- ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
1 `) h' F: Y9 ~" [8 B8 j; \ - }
1 i' _( [( E- v1 y+ Z - ws.onopen = function () {3 ]. |, v( h1 w
1 D' X! r3 j, K/ z, B) N* A- };
; Z" |( W0 p: G ]) c6 q \: Q J - ws.onmessage = function (event) {
' a7 \, g+ }7 Q6 R+ M9 [% |" K
" x9 R- U( `/ w3 Y- |" W- };! `( l) m( z' S; d
- ws.onclose = function (event) {
) U, z6 |0 J( _$ l" r/ ?! x
3 ^& E/ Y& X/ Y; q; k0 [- };
, | H9 f- ]! g* P - });
: o9 S+ z8 a' w9 k2 u8 t - function stop(){% i- f$ V0 y3 w
- var message = $("#message").val();
! @! R3 m3 Y6 o2 `" W+ N# l - ws.send(message);
/ _; p7 \. O3 j - }
Y- a, C e! h5 T# w0 l1 X& h - </script>
; v& T3 F: s' M' f - </head># I5 T% D! L0 D6 c$ \
- <body class="keBody">3 o% w" Y m5 v. V y
- 请输入提示信息: <textarea id="message"></textarea><br />& K* K/ a' o* P# c0 @# z+ h
- <input type="button" value="开始" />
( X5 W1 Z% v+ y) L - </body> [" c Y- R/ ]* d( e2 V
- </html>
复制代码 5 ~# D c' r) b1 _1 t5 l6 c
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
0 H/ W2 _% n- z2 n - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");, A% X2 k% G+ b% F' \9 Q! i
- document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");" `+ z& Y+ d$ a% w) ?& R2 L
- var ws = null;# x& k% c# ?9 e7 y0 ~5 u
- var basePath = "ws://127.0.0.1:8080/";) f* E+ A) u' b/ ]9 H) s
- if ('WebSocket' in window) {( O! E( _7 E' C1 K) ?( w+ d5 M
- ws = new WebSocket(basePath+'webSocketServer'); 4 j4 n2 l' N' D- y! p" x
- }
4 I" b* u$ M& n1 f( w3 D' Q - else if ('MozWebSocket' in window) {
* {) B3 D( `' \4 v$ ~ U& O - ws = new MozWebSocket(basePath+"webSocketServer");" {# K3 o) F' L1 [9 j
- } ( ~5 H$ {' S$ M" {7 D% C# ^
- else { t" Q0 U$ L* i5 U. V( j
- ws = new SockJS(basePath+"sockjs/webSocketServer");+ }" r9 e$ o% Z1 G
- }- u: f. V8 r# A
- ws.onopen = function () {
; a3 n/ d5 t+ E1 u h4 K - % ]7 Z( J; l, r; K6 a, h5 h4 Z
- };
. j9 |- @& w4 G# h3 v) o - ws.onmessage = function (event) {
/ c4 k& @# }$ }: F, M* g u3 R - pop(event.data);2 Z) g8 Z" Q& ?
- };
8 c9 h, K1 f, {* Z% E! {, i - ws.onclose = function (event) {
7 V, i0 R1 I7 `6 |/ R3 V) Q. K - ws.close();, t0 F1 W1 a% m# h7 M6 ]
- };& G* J! P( k- G6 Q9 A8 Y
- //提示信息
6 `$ f( d/ X6 W0 ~' B( V* l; y - function pop(message){; L: s" g$ [5 [
- layer.alert(message);
* x# h. f3 @0 l) J, L1 u# C5 s - }
复制代码
! n f6 p/ J3 Q7 ~7 V$ s# C1 V" c5、在项目头部引入
; t. i# N9 W; l8 X8 P$ I<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
% q1 X2 ?& _' m1 V& z1 t. G4 j& g D
. f3 j# G+ J* r$ z' o! a5 J2 C( M' R这时查看后台 会有以下信息 说明 引入成功。
3 |5 T8 D5 j: t$ R: T) M: p
" f* H3 I/ B% F
A% I, S& F! r) B. Q0 q- Z然后在打开页面 index.html 输入以下内容 点击开始即可。
+ c6 A% o; Q- w0 o7 Q
' R/ P) S% c; H" f$ i
: Q& j0 d* ?$ S如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。
6 e: a! k# ^- P; M
) M( t4 j+ Y6 M% T8 q, s- W, V! i0 G" p) ?& W5 j6 _$ A
& N' V3 p7 q, z. f5 E; I+ ^+ j
项目下载地址:5 g3 w& G8 C @- B' C
; ]/ Z4 a% e2 z4 e r U' h5 F8 B! r: |0 x0 F# v( `! }
; O' d7 @' j9 A4 n7 t: {
: d4 \, }9 h; s: t- b6 C- s |
|