TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍( Q: m9 L! |* A2 j, i
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
; W+ T2 M4 z G7 M 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
1 y. Z4 }; K. C. J 轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。 p8 U, r( ^" |; ^3 ?) w7 P V! P) s
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
- f5 A' J9 r6 Y. E! _/ ? 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。& B% c; f6 A, y3 k' X/ |" M
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
: P! c5 ~3 i& ~ U" H G 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以上的版本才能运行。
$ p. i4 M# v! D, D5 S% p- Y5 B _' t2 n& f! u0 F8 ?& o3 t9 J3 I
二、WebSocket协议介绍
7 m( Y0 O- h, i$ N6 y" W$ j WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:8 n" f4 K& [7 t' S4 P' C
b/ h0 o; d5 O, A6 A% e# n握手过程:) k* ^/ ]- S( \4 Y3 T
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
3 E; u' |, F/ A7 T1 z; R在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。) `( A/ n' w( D' N) ]/ A9 q) Q
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。3 U& Q4 X6 m5 t
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。" Y; e9 w* g [3 [
2 T$ S( j$ m8 Z& V' S3 S
三、Tomcat 7中的Websocket架构! ~0 W! p" k2 C7 v6 j# W
& h+ m/ J. @% c: k
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。 . P* O u9 Z5 U' t% g
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
. F& r) W6 [+ n8 w, P1 F Tomcat中Websocket的处理流程如下:
: L6 J ?0 V$ f3 w) r接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
7 \; f+ l. y `5 s" H+ c. ^, |* wContainer中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
* D( S2 z+ w4 `' w- j H1 G服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
9 \6 s$ R$ Q, m0 B1 b3 R: J' i如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
# i8 k3 U; T( ]+ r数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。: S# k3 y) k% P
6 W( p+ W. V# p5 t& Z四、代码实现以及需求
6 ]' a3 N3 H/ g& e9 x6 i6 R4 @+ O- K" r+ p# W9 w! e; R
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
: B6 g9 ?0 k, ?/ E& P3 v7 u6 b% L6 } G4 Z# \2 ]. @
2、相关环境 , Nginx、tomcat7、centos 6.5
. \$ u1 h; Q5 ]2 l0 k2 i& T% c! R$ o% {6 E, _
3、项目框架,springMvc 4.0.6、layer
. ]0 _- Y; ]! T7 |0 _* ^) N) m7 b/ v
4、代码实现:# v. T: n1 K5 N4 H4 L
, f- F% |* x$ \ `WebSocketConfig:- import websocket.handler.SystemWebSocketHandler;
# m1 G* E0 x2 h: G( S( q* K; U- l - @Configuration
9 [) p6 T* m; J - @EnableWebMvc
; W. t L2 _0 ~& i/ W% A' N - @EnableWebSocket4 o1 B, }: [) z ^) |- @2 i+ L: ?
- public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{/ P F5 u7 _: ]
3 W. P& d0 y; V- @Override
2 h3 T! H- j3 s7 R; ~) O - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {9 l0 L- {% s; U
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
+ Z2 T2 B& Q8 Y/ z& P* W; z/ M1 z/ { - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");3 [( d5 O7 N+ y$ R
- }; m5 o) P7 E) n/ J
- @Bean
; P8 P4 h8 r# [7 J. @' e; P - public WebSocketHandler systemWebSocketHandler(){
( Q5 K* @9 {3 c/ O - return new SystemWebSocketHandler();4 c- i0 ?" T" \1 m3 I
- }' M( v8 b j3 A; ?- E* |* ]
- }
复制代码 SystemWebSocketHandler:
4 m# g8 G( H- S# p$ p- d- public class SystemWebSocketHandler extends TextWebSocketHandler {2 i$ o* O- ^! }3 z% T6 L
- 4 _5 a/ M/ M: H
- # e+ o: W8 B( h: u. ]- j
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;
' w3 Q p$ c5 E/ X0 R
% m$ _( m8 c" C- q# n- public void afterConnectionEstablished(WebSocketSession session) throws Exception {! f* I% i: L( c v, m6 ]* [
- System.out.println("ConnectionEstablished");
8 J7 G, _' ]# u" U$ M - users.add(session);
( l- I+ j; ^$ j7 `! w* P4 _, @ - System.out.println("当前用户"+users.size());
) u: g& J" V4 q0 a! ] - }
' i: i5 j, g. r( H0 ]5 T x* i* D - /**0 S! O* ]7 O6 y- X
- * 在UI在用js调用websocket.send()时候,会调用该方法8 t9 w+ K! v$ X
- * @Author 张志朋
% a( h/ ]8 b+ p2 w8 L. k; } - * @param session
; a+ A* H6 u9 r: `7 ?! P - * @param message2 r8 G7 B' U% N; P1 }$ U
- * @throws Exception
( P1 r6 u. G7 E7 B3 N) e3 I7 b - * @Date 2016年3月4日
. ?* U2 u; O) s+ ] O - * 更新日志6 w: G- @( |( h! o- n6 G
- * 2016年3月4日 张志朋 首次创建( N8 R: m' ?7 f# p5 B
- *
+ j9 z- Y4 e. V6 m% E3 s - */
6 R _& y! n6 Y) ^, U; n - @Override
" F" W- M6 q, s9 O - protected void handleTextMessage(WebSocketSession session,
- B6 P! m) Y( c8 z6 `$ H - TextMessage message) throws Exception {, O I2 _ {6 i" a; m- b1 x
- super.handleTextMessage(session, message);, q! d- a% T, T* T+ h( \
- sendMessageToUsers(session,message);
- p k& c! A; M; K - }! f8 B3 C% A/ A+ H5 r# \0 {: N
- @Override6 q) Q, E8 K- J/ ^, C" T0 H. x
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {* I+ u& A7 K; w2 P' }+ L' U6 { |
- if(session.isOpen()){
. ^- n( `+ ^- ~1 Y" C - session.close();
4 U' q* o' ]) `8 N; Z - }
p' \% Z$ }, N9 E" |0 n7 t - users.remove(session);
! ?, v9 r/ r5 Y; I9 l+ T1 [ - }
& y9 T: X6 i9 e0 K4 R! m - 2 H% v. }% P; S# \" `3 _; J
- @Override" f4 u {9 `" A e, y* D, {
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {8 t8 A1 Q( L: ?2 @4 m# z" B+ u
- users.remove(session);7 J& q1 \6 c2 N/ k- c" r; J# B- [& E
- }. |( ]; J C$ E6 m( q6 x
- ' N0 j8 I3 I5 i# I: @
- @Override& @1 H8 ~! b7 N6 E
- public boolean supportsPartialMessages() {, c4 P1 J7 u7 j& c5 A
- return false;
9 Q6 I o% G/ }' d: L2 u' d - }
/ p a! r# w6 i$ R2 T; V- @4 @% l4 @ - /**2 a: K6 _; W; {+ d; @) v
- * 给所有在线用户发送消息, a: W4 H, ]7 Y5 ?2 p9 Y; R
- * @Author 张志朋8 W |6 p: _: H+ O7 Q$ o
- * @param message void$ ]( ^1 D5 w7 a4 ~
- * @Date 2016年3月4日
/ _5 ]! t( U$ n$ K4 `& Q - * 更新日志
2 w/ s. R' Z1 V - * 2016年3月4日 张志朋 首次创建8 n4 B& j3 H2 I. n
- *
3 x6 l- m$ F: f/ X) B' f* R - */. y* |; i7 N' V2 v( Q Z
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {( t- s5 N) ] M' Z
- for (WebSocketSession user : users) {
. c1 ?$ s3 l4 X - try {$ C. R( B3 d/ ]+ T ~
- if (user.isOpen()) {
' m9 k2 _1 m. w$ n! } - user.sendMessage(message);2 Y* }6 s& N$ Q7 T- S
- }
* Y& ^# B- h3 i; W - } catch (IOException e) {) S3 ~$ L8 B! Y; e! ^1 v9 l
- e.printStackTrace();
2 ]# t8 U' }2 J - }
7 }: x, M# o; M% W0 a - }
$ n4 G& l, O* D# f - }- R% P. t2 Y9 X
- }
7 }* l' j# q3 ?$ Z$ y; n- r
复制代码 信息输入 index.html:* T6 q! \, W( U4 \) E3 A
- <html xmlns="http://www.w3.org/1999/xhtml">
0 M' J7 A g: ?9 {! J* M - <head> m& w |0 t) z9 F* j" i8 e
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
. i1 C4 G" M b - <title>请输入任意消息</title>/ h9 E: `( e/ x: P/ E4 G( R
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
; B* l" ]+ d; r - <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>' ~; W1 s$ \5 z1 G* a$ M
- <script type="text/javascript">" q" v/ D& l1 G/ r
- var ws = null;3 L9 U6 x9 I$ @4 u6 L" g! _: _/ P
- $(function () {5 p9 ?* ^9 T7 Y+ ~# t5 C( q1 e1 W
- if ('WebSocket' in window) {7 U* V7 c( Z w) ?
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer');
# k6 ~4 q, x+ w4 ?0 x - } 9 r; Y& |- k3 z9 T# w4 N; w
- else if ('MozWebSocket' in window) {
! V! f) @& _& F5 ?. p1 M7 j( i# H - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");
( [3 A" |$ O b, s3 s! |/ c - }
- w: ^3 E1 ?6 E. ~/ J' V+ y+ j - else {; l* {" a5 b$ p7 E% r
- ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");7 E0 I, Q. `3 o
- }
: P: X& h. X% _( \' f - ws.onopen = function () {. ^) r" a( @4 A' y5 L) Q! B
- & G1 I( N- x; Y9 {1 x$ T9 s
- };
" C7 M8 E! o6 h/ d! H# y - ws.onmessage = function (event) {$ s! I/ G0 P5 B
( A" n% Q! x( b+ b; i- M- };
' |% q6 Z5 q; b% {4 R - ws.onclose = function (event) {" M W% K( t, `& O) b7 ]
- ) B' ?( x4 ]& R2 O
- };
" i2 R# ], X+ h0 Y9 u x - });
2 B2 ?" o& m0 Z3 ^ - function stop(){
B6 k! [- z1 U6 f - var message = $("#message").val();
5 S; n+ k# E% I. W! p6 Q - ws.send(message);- Q, l5 E2 _9 ~$ Y
- }
, a# c# q4 P$ ^6 q2 Q* L8 `' v+ x7 P - </script>9 o* ]5 t6 K4 l0 |+ E5 G
- </head>2 \0 d4 r$ @7 q/ e5 z
- <body class="keBody">. }* s0 |( m" d: T3 O
- 请输入提示信息: <textarea id="message"></textarea><br />
1 z7 T% z/ C) X `2 l - <input type="button" value="开始" />( l% ]! z$ g6 t& ^( Y
- </body>% _# s* k7 D$ e. W! G* Q
- </html>
复制代码
% W- c- B% b! x- M/ SwebSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
& f7 I8 ?/ w7 \ - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");7 L) M J$ m( V/ A( w+ s& f, \6 Y$ Z
- document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");9 E; D3 b r9 D+ k! B' Z
- var ws = null;
" j' t$ e; C; h) G3 s' t' n$ H, { - var basePath = "ws://127.0.0.1:8080/";
$ M+ u' [+ Z1 M1 S& x - if ('WebSocket' in window) {
; H* ^: g% f9 C2 r - ws = new WebSocket(basePath+'webSocketServer'); 6 b# j3 A; W" R# a8 c
- } % w3 c: j& o7 n; R
- else if ('MozWebSocket' in window) {
. J) Z; G) ]/ j8 I - ws = new MozWebSocket(basePath+"webSocketServer");
5 u% K1 U5 y+ _/ t0 M+ v" t4 n6 E - } - k0 U8 L2 j* U0 ~" M) X; ?: O) R3 f
- else {
; W/ ^7 z& V7 u! b& c& W# X - ws = new SockJS(basePath+"sockjs/webSocketServer");8 M& v+ \8 h+ y. q) l: T5 C
- }
: P0 Y7 C# Q% J/ ]3 u# y - ws.onopen = function () {* H9 |9 d0 R9 Z# _
- 4 I0 _: p: }* @( i2 \
- };
! ]( e# M1 _. o/ o1 H: G - ws.onmessage = function (event) {2 L! h, j7 x% D" i
- pop(event.data);5 b" N% W' }2 r$ \
- };8 S3 F3 A# E7 h$ r \; k# k
- ws.onclose = function (event) {
- L, {/ j9 w% _+ r B - ws.close();
1 x# F& \" A- J8 B: \" ] - };+ ~3 J( g2 y2 I' d7 X Z7 ~& B
- //提示信息
, p" a, E5 g9 O8 J. t - function pop(message){
# k# g3 q- Z. w7 Z" L' u - layer.alert(message);
) b' j& U# x( ?2 Z, R1 C - }
复制代码 $ G; q( l+ E1 c/ C3 k0 q" l; g
5、在项目头部引入+ n$ T! j1 O) [- T2 C1 X. `8 Y9 D& T1 s
<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>% i- u: Q: Y- O' a
4 G$ B) O! z0 s* k& ~, x% l5 Q2 I! C/ h# ~: Z- ?
这时查看后台 会有以下信息 说明 引入成功。
0 q# b# ^, a5 ]( B6 w
& B# h4 F9 r# A# }. E: R# x
" Y$ ]5 t) U$ v; a+ R然后在打开页面 index.html 输入以下内容 点击开始即可。& |# j9 F/ Q' A9 p4 h! {
) [/ @/ u. y8 [6 f H9 J
1 M2 O! z1 c( d, W$ i如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。7 o$ q. o! _3 z2 o. z
) Q+ [3 _: i, S( {
/ U9 Q; Z+ r L8 |, w& x9 {
6 q4 E( t; `6 I$ r7 Z) [8 y项目下载地址:0 R( o7 N; i$ A2 g& G' o
4 M* r5 s6 F. P4 o& y+ b
8 o/ q, N( k/ \) v3 f
9 M1 M- n: ~# E2 z7 W, P0 i
7 [# W5 B5 T, S. G9 j; R5 [ a N0 I |
|