TA的每日心情 | 衰 2021-2-2 11:21 |
|---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍( Y' \$ g/ ~6 d1 m: D
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。% ] f1 u- d T7 y- g( z
我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。# h) Y" i6 R- Z& ^: W
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
" j* p% j/ h4 N, C Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。" l+ z! i7 p; c* \ |
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。1 i( s& {7 Y5 w4 S; U8 ?( l1 e
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。* k6 U# U. E' t: {( 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以上的版本才能运行。
: r" `& ?7 E/ x) K9 w
3 c: w+ t6 C ^7 X/ X8 L二、WebSocket协议介绍
& A( Z8 S4 S& z, h" e1 ` WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
; c* h! A+ u& t
* Z% ~% j" } V* ], y2 y2 ]: a握手过程:
: p8 z- s2 ?) bBrowser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。2 E' i2 P& t. I
在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。# |* |" F) F7 f/ i
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。
6 n7 s+ W8 \, T( I# P- o/ KBrowser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
$ K2 ^- y9 O8 c1 v, z3 }" D% P7 o% F" E" b
三、Tomcat 7中的Websocket架构2 S r3 H$ y- x3 @/ x _' ~
' V1 n4 W! E+ z' G, T8 a
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
7 J1 t" _- e4 X- t* P9 }" a) G" W 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。5 d* k9 g! i, w' G/ ]
Tomcat中Websocket的处理流程如下:
) n! t: ^( H# T
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
6 m# s) E- g) y; G2 YContainer中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
/ U' ?- }; g1 z, v; W* @8 f; N' F服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
) l! R# ?4 R0 E& ^如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
% ` c8 D5 k% b2 x$ D: a6 K数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。% i) a [" X) S3 ?0 J
! Z6 j( ]& O7 h$ |1 C! c四、代码实现以及需求) C# ^8 F4 x+ \( i# u1 ~- h
6 V! V( U7 Z$ F# H. {& H1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
. J9 T% s& v$ r) E5 c7 X i& W' `8 y) W, b7 J4 G1 j: M8 Q! @: [
2、相关环境 , Nginx、tomcat7、centos 6.5
4 h8 G4 E" Z8 Q4 s* j/ ]0 n
$ Y1 E7 e* J$ {7 _& k3、项目框架,springMvc 4.0.6、layer. V4 v2 b5 F* [: S" M4 |& C5 e
3 h# W$ |0 | U4 G
4、代码实现:
2 d5 ~2 X6 k: Y0 }% Y
+ ?$ h, [* Y$ X. ]& U9 HWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;7 y7 t& t7 U. s4 n' z$ z& s
- @Configuration+ M2 x$ n/ k0 u4 L
- @EnableWebMvc$ s! y* {: U+ [' H) M& d
- @EnableWebSocket
1 N9 i+ E$ k/ _ - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
9 p9 ~$ g) X6 u8 l - - t, Z5 V ]9 a. ^9 `
- @Override
3 L0 {1 U9 y8 _( ^* y. f - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {/ S) h/ D4 {' b) Z2 s
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
8 h9 Q* R! ~% U8 K' m9 e) _ - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");5 F, f- x! E; q: Q" J
- }
- a/ b! q/ i1 d, g - @Bean
+ ~. D0 j# z6 F( T - public WebSocketHandler systemWebSocketHandler(){
7 b ^- @( q | - return new SystemWebSocketHandler();
! y1 J* @* M) I, d0 F7 ^' t3 m# b6 N - }9 K9 S4 b2 J6 u0 N% {' e
- }
复制代码 SystemWebSocketHandler:
1 b# F& Z" m9 j7 \* y; D1 S) J- public class SystemWebSocketHandler extends TextWebSocketHandler {7 g- M+ O9 c9 ?; g
- ~2 I- Z9 H5 G
- ( ~' ^9 [& Y' ^& _2 h
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;
! l5 @3 ^7 O# ]. p6 y
( `, I; g; Z& x1 X Y7 z- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
: i7 O! K! d) F7 t% P, a - System.out.println("ConnectionEstablished");
& `: N% b! @$ K K - users.add(session);4 |; Q9 K; [9 N5 @ D! K) t
- System.out.println("当前用户"+users.size());
/ C; J X/ h8 X7 H - } o$ f" ~5 I% G: j. J+ D
- /**
4 i8 r$ V: V z/ N$ O2 l - * 在UI在用js调用websocket.send()时候,会调用该方法
6 |5 k; G" @: r) J, p0 D - * @Author 张志朋
1 X" G2 _! q$ M8 D% P2 N% U: A - * @param session
$ s+ b) b3 Y% H& u5 M - * @param message$ A0 A# l; |$ n) ]. t2 }) x& d. Y) }
- * @throws Exception + Q; q/ a i0 O% _: [5 s6 q+ b
- * @Date 2016年3月4日0 @& `/ D1 i& m1 l- X
- * 更新日志+ c5 ^" k& g* I! r! }
- * 2016年3月4日 张志朋 首次创建
% R- e2 r7 G" f: _3 t - *4 k+ x: }: N! ?+ ?
- */
% ^6 `# _% d8 k4 }6 ]3 y- h - @Override5 @, ?7 [) m, h2 n; a9 O2 t R
- protected void handleTextMessage(WebSocketSession session,+ W+ Q$ E4 X7 t ^6 ^4 D
- TextMessage message) throws Exception {
2 H2 M8 b+ y# N' p/ L3 N7 K - super.handleTextMessage(session, message);5 f5 h8 y( p: Q! C
- sendMessageToUsers(session,message);- o% n, i! i9 D
- }1 I. e/ o; D; q+ N2 r
- @Override
' O/ ?0 q& r) u- |; N+ k - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
8 V' e j; z0 D8 w' v5 Q0 N! r - if(session.isOpen()){+ o% @7 v C3 T" Q
- session.close();- u" S, G) k5 G2 q8 A! _
- }
% G7 l# t8 l( t; I# A7 K - users.remove(session);/ O' t4 N$ r2 d1 \, p
- }7 q. S& k4 Q& _( g. D3 G/ r- e7 P
- # Z+ e$ Q" d3 L; C
- @Override* u2 m& b/ J. `. S7 d
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
: p0 N" T: q: ~# p - users.remove(session);! f' ~/ V" {" W* e8 h$ ]
- }
- i7 C2 d& L/ S- `3 |2 ?% R - % R% z. x# Y( o: `# V+ h! {
- @Override; i6 r, y% l. E% B0 S, b# Q8 A
- public boolean supportsPartialMessages() {
' o* a/ Z8 Q- R* ^. V: k - return false;
, v$ ^7 [4 ?5 f# \9 N, u6 w% D - }: Z% p8 _1 E. h; Y0 a
- /**7 J- R- G3 c/ ?: a4 [$ h
- * 给所有在线用户发送消息4 E% I2 l9 W* W, J" H; i
- * @Author 张志朋9 f0 t# N) q0 h! f
- * @param message void. D5 ^' Q5 f G
- * @Date 2016年3月4日( ?, j" M; N. D6 b
- * 更新日志
6 W5 V _/ V( {$ x) m& R+ { - * 2016年3月4日 张志朋 首次创建
8 I$ j$ @" Z" r& N - *
" o9 a! X& ` o8 _* v! S - */* e, {! l: [% a; a) C
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {$ N/ B$ n0 E% t* E6 ` c
- for (WebSocketSession user : users) {
, m. H4 Y6 b, M7 f. t/ W+ ] - try {( O( B. H% x- f. h$ t4 `4 i! m
- if (user.isOpen()) {
+ D* K' b4 U4 ~5 x2 T) m - user.sendMessage(message);% w! H* D" Z# J) Q! `2 F
- }
! |! d: [$ h" X% Z - } catch (IOException e) {
% J, l/ C! q7 w- G - e.printStackTrace();
' o/ {6 p, F; C: N! x6 s+ ^ - }9 a/ r/ }$ V) s6 _
- }
, t5 j/ u% ~! n" N, L4 x7 p4 D - }
4 U" @8 ?' U7 E# P$ T7 ] - }
* o8 y+ T& j! S& H5 z$ W5 o3 ]; p
复制代码 信息输入 index.html:2 S+ ~* B- o9 \; O' f- }- h
- <html xmlns="http://www.w3.org/1999/xhtml">9 H7 i1 Q# |9 R3 U3 {6 p, d+ s' g1 v% p
- <head>
, F$ i- j- `. u' e: n) ? - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1 s% M6 {1 u2 X5 n8 v5 C - <title>请输入任意消息</title>' S" u0 _, ]5 d/ w
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
# B! m* c/ Z7 l, r, y; O - <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>7 R. D$ S- @6 k2 r; t
- <script type="text/javascript">
* Z7 m% S. z& T! X& s - var ws = null;
, ^" K* b1 F3 @: _6 w6 |: D$ t! W - $(function () {' U! G: `. c- S5 W# N
- if ('WebSocket' in window) {6 C0 D2 ]) _/ r: t9 [& Y2 a: q
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); 9 Q0 A: y5 m# z4 J5 |
- } ) ]2 t5 E8 @' ?; G
- else if ('MozWebSocket' in window) {7 H0 l3 T) g/ w7 r; v
- ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");
$ w+ V1 i. y$ {* h0 j - } : O' t( W1 `+ U+ `
- else {
. Y+ m% b& q* r2 P! f* w7 z - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
9 ]9 V3 q5 f% L& y - }
6 h# S( u) b1 }3 M8 b - ws.onopen = function () {# g! @6 c( L6 ^
- ! t; k$ f; G6 {% F( u" V; W
- };
! X7 z- B9 b7 o+ T2 q+ _0 G* {9 z4 p - ws.onmessage = function (event) {* T8 f- [9 [5 O- A
/ r2 U3 q. o& R {- };
x5 l6 [* [1 Z0 ?1 W - ws.onclose = function (event) {4 r( F1 t' | d
. I+ E( Y& p( r& c' N9 G: V- };
# f* E# z2 ^0 s8 [ - });
$ D! \2 O S1 }( ?& R; Z - function stop(){( e/ H% [# n0 J3 y: {8 U2 ^7 n
- var message = $("#message").val();
/ D8 b1 ^( I( h0 T3 @ - ws.send(message);6 O4 V9 h! q( r1 j1 Y
- }) E4 [, ^% L/ P
- </script>
% O* N7 u0 l6 f) F; c- T - </head>; g8 n5 G( f9 f4 B% @
- <body class="keBody">/ k& w! } C1 O
- 请输入提示信息: <textarea id="message"></textarea><br />
5 u% Y3 K( s. o7 b$ I - <input type="button" value="开始" />1 d& ^& \, E( Q9 g$ e/ y8 s o
- </body>
& _6 K) E& K. m, q; s, n0 S - </html>
复制代码
9 G: z$ K1 x7 b5 c6 xwebSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");. Z/ B% \* j( [5 s' D
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");2 Z; B9 t. y i. k O$ Q" j6 s. G7 L
- document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");8 d+ e4 @* x0 l5 d; v, z3 `$ Z
- var ws = null;
$ y) V3 P% b H3 u - var basePath = "ws://127.0.0.1:8080/";6 T+ d& i6 i+ x# r! `; [/ `
- if ('WebSocket' in window) {* z& d5 ~* P1 ~
- ws = new WebSocket(basePath+'webSocketServer');
! c4 f3 A5 C# b. U - }
' B, i0 J/ ?; C U) q* G6 y - else if ('MozWebSocket' in window) {( O" |! O, d0 g4 t& i z
- ws = new MozWebSocket(basePath+"webSocketServer");. I9 ]( s2 i2 N) \8 \' e
- } ' C% z( `. y8 I5 }& N3 x
- else {4 l1 K" N: u( |; i h( T, c+ m
- ws = new SockJS(basePath+"sockjs/webSocketServer");: Q$ `1 d$ b' Q
- }. h( O2 a: [3 V; m) Z
- ws.onopen = function () {3 B0 U* O) { \# | m2 R
/ b! Y0 @( e+ i8 D) Y# J- };
/ Q7 y! n4 E' `* Y8 R, ]+ d- O - ws.onmessage = function (event) {% g: z" }3 S7 V: c) P& S
- pop(event.data);; y+ Z9 C; x4 y$ N$ |
- };
# F8 O: }: c2 \* _7 X: { - ws.onclose = function (event) {
! B# y( u0 P# Z8 d6 \7 G4 w0 q3 I - ws.close();" L$ G5 Q8 ~; e% X
- };
# p# G* X* Z# U3 M- J w1 [ - //提示信息
1 h Q/ t8 Z) ~3 H - function pop(message){: w3 X9 `* L8 H r. x
- layer.alert(message);1 ]/ a) ~: E+ K6 D
- }
复制代码
- g' d7 q/ }/ _( Q+ G5 B5、在项目头部引入% p" H. ~; }; B( q4 H; D# j
<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
, k2 S' [5 `" s" S% J% U# t
/ L+ S( r% {! r: K8 }1 W1 ~8 Q) ` e) n
这时查看后台 会有以下信息 说明 引入成功。
5 b% |$ e+ j; V- A- Z
& ?% f+ f* M* B: U, ]
2 M% u1 \ u+ y然后在打开页面 index.html 输入以下内容 点击开始即可。! ~2 y4 K/ t6 K7 w. y
) O5 b$ V0 L3 J! @. f& {8 H
( c1 k. i) {. D9 X
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。
/ y1 i* h! D( y& B5 ?# L1 b
+ M$ Q+ V7 G& Y+ P& L
4 |. H1 _' q( R' M# C; Z) V
# L! B. |/ O, ~% P2 b7 l) E, g+ `, G项目下载地址:8 z9 ^/ h! ^2 ~+ h5 ~) e- ~
% e4 j: `) \2 e! e- H. g3 U. y. { M @8 j0 R+ Z$ q: `. ^; b
% O& `- w+ H; U8 S( ^
9 b Z3 C* {4 b; u4 w |
|