TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍
( ^5 p. y$ Y, }1 i; A 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
" _" @! j% h- R7 ?' P% z 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。0 ?& K5 w+ P. e) C7 F
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
6 X- m0 a9 e: R! c7 m* w' w Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
+ P2 G3 \: w6 \) i- e% F 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
! |9 B9 N0 S" M( X* Z 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
, P4 i. ~# W; e 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以上的版本才能运行。
6 q7 I" l- ?- R- s) t7 u# x* Z! ~* N& K9 q' S9 i
二、WebSocket协议介绍7 I" X3 e6 y* Y5 L$ h2 M
WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:& }& M0 z" }: N/ d# F9 J# j
3 K! @! w& B; }8 [7 I. A- F- t
握手过程:' Y9 A) B3 E* p
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
/ R- ?9 J+ |# R) [在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。+ N$ Q! h- d% P4 U5 i
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。
- I- [* [9 C4 W, ^/ h$ SBrowser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
+ o$ [' P: v0 t9 x# @
& ]5 i! C0 @) M# S! `7 B. C三、Tomcat 7中的Websocket架构: d( ?! U+ l* K6 j* C- U* m
$ p3 G% [& W" a* A2 W2 x; @如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。 & O0 i! `# z0 R# q" T# ^6 N
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。! e, T0 m* K# B; n
Tomcat中Websocket的处理流程如下:
: {1 X- [5 W; {
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
6 b" n7 n( Z( Z. I4 jContainer中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
$ a8 q6 \: `& `7 |% y( _服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。8 F4 e. i4 @. P. X7 H" h
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
3 M! o! `6 o5 a3 F; x3 f( t4 T+ N2 l' Y* H数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。) u0 i6 D& B" R2 L7 p. J1 A
6 n H6 p" {: G
四、代码实现以及需求) l& B/ G- B+ B9 S! S0 e
c# Z3 i, K, W- ^+ b
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
+ r7 u( i5 ^( \4 J9 [* `% X) _7 l3 |- d7 y+ d
2、相关环境 , Nginx、tomcat7、centos 6.5
9 |$ |! D7 V$ _3 l
7 h. }# [- e7 p( Q" Y: F8 h0 c3、项目框架,springMvc 4.0.6、layer
* v0 O6 S( r9 A6 t, B
8 S @$ [! \3 C1 Q9 n2 o4、代码实现:
2 } E+ w1 N; l4 r' P; y+ J1 b) A6 K# Y4 M. l6 v
WebSocketConfig:- import websocket.handler.SystemWebSocketHandler;
" P p" |# u: k$ N2 A4 L: F - @Configuration7 f& e3 i5 P. e- r r8 W! F/ f
- @EnableWebMvc7 D: c1 M. L( {* B1 B
- @EnableWebSocket
1 u; l9 T4 r+ _+ j9 r: @ - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{: ]2 M9 z9 H5 i! f
- / x8 E& n& ]! x5 y8 G* l
- @Override& z! L; }( o3 q* U3 y" J
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {. O% X9 w7 v6 n6 m# w
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");. X! Z, k5 C# y) X9 k) R% N
- registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");+ ]+ z( m8 M, U$ a/ j. P" F
- }/ B# y/ A7 g" |6 h% \5 v' H! S
- @Bean$ u4 S6 z. H; F N" V) a
- public WebSocketHandler systemWebSocketHandler(){
: t; O4 s$ Y T. n" b% c - return new SystemWebSocketHandler();
& U2 `, W4 G3 L7 S9 i& f# b - }3 I3 Y3 N( S( p
- }
复制代码 SystemWebSocketHandler:9 N. [6 p+ p0 v, {: ?+ C
- public class SystemWebSocketHandler extends TextWebSocketHandler {
% q* O4 a" W+ q- q" o8 D7 N6 p9 ^ - 7 w1 [' b) ]( a" U Y1 Y
- 3 ~4 [% V: M7 G7 U( ~+ x( `
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;- `- t, U' G& g0 f# o4 p
! w/ ~- a, Z) M V' z! i4 N- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
/ k! I7 o. Q: h# u6 Q - System.out.println("ConnectionEstablished");
8 |* o0 O& w. i& V( B' j - users.add(session);
$ P! D0 b; K" d( t# B - System.out.println("当前用户"+users.size());
% ~5 C1 l5 p+ D$ N6 O2 A7 x0 ? - }' r5 \, ?+ G- g, @) n
- /**
4 `$ T- V8 e4 O# B0 H5 Z% b' O - * 在UI在用js调用websocket.send()时候,会调用该方法
P# Q% w, W3 y* d5 P - * @Author 张志朋
. H0 c* q7 \3 t, J" S2 p - * @param session6 _9 O" c" _% i8 i
- * @param message" }7 O3 O+ B3 ^- e0 @
- * @throws Exception - p" A: n2 Y7 k4 G% H+ z$ c; o0 _
- * @Date 2016年3月4日
n7 o3 {, P$ Q s9 t - * 更新日志" T. l8 ~' }: J5 m$ I
- * 2016年3月4日 张志朋 首次创建5 u9 F i) c0 K( N! b. O+ [2 |
- *
/ ^8 W" T% z; Q0 z- E# g - */
! P% b8 U0 j% i$ o' y - @Override- }" Q8 b8 ?/ B8 D' z# f
- protected void handleTextMessage(WebSocketSession session,
2 l. t# `( z; S, J - TextMessage message) throws Exception {+ L; |" r- w! G+ O$ I3 m
- super.handleTextMessage(session, message);
$ O# e; L2 }# V: Q - sendMessageToUsers(session,message);2 f5 I) `. K. p5 [) o) K
- }: w T. V4 H$ N( ~: i- i* q7 I
- @Override3 L- E. p" H6 v* q% Y: ]
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {( H" ~5 b+ Y5 t! j% A: ?" y
- if(session.isOpen()){
t0 i" O3 `( I$ s s7 u - session.close();# _) ~5 H+ ?0 Z. d; v7 E
- }
1 L5 @; E2 o, E- B/ C - users.remove(session);8 M k( L" {' z( X; N2 h T
- }/ @) ]& r3 E3 T- Q1 _" |# [9 o
- 2 Y' x& u6 d4 M) ~; l
- @Override% G+ c9 C$ t& M9 A, }6 u
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
; F' C5 O# P) M; { - users.remove(session);
* K4 [5 d( A7 @ - }
; t0 r9 c1 {- V: e, l- ~ - & [( n! M9 D& K4 Y
- @Override" V6 V7 r! x, u4 |: K- A
- public boolean supportsPartialMessages() {# z1 |2 g$ i! @, o7 \- W, l
- return false;2 G- D2 B7 O1 L# q% V* ~( }6 H
- }- b0 I Q7 o& [: r1 S) i3 d
- /**7 m# g6 k! T' {+ o+ V
- * 给所有在线用户发送消息
* Q: r; Y7 h; |) o0 [5 x - * @Author 张志朋
+ R, m) L: q" y. \ - * @param message void
& G# f3 w- p" ?' e; z" h0 Q" l7 W$ F4 P - * @Date 2016年3月4日
4 S; d) O+ W) Q2 w4 E. D' ]1 W - * 更新日志
& P0 t6 j, z6 u" W W' ^: a6 }# |6 W - * 2016年3月4日 张志朋 首次创建3 M) E. Z1 V* L* \1 V" G
- *) ?4 N* U: v0 x# a6 ?( `/ x0 s4 k
- */; r" ?1 m/ F. T
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {
% x; J }6 L( _$ g - for (WebSocketSession user : users) {
( B( }+ s; R6 P$ r- g5 o& w - try {: q2 ?: {5 ]9 j1 K" p8 v. W
- if (user.isOpen()) {
& U! D# q- M# v - user.sendMessage(message);
" g5 l; m" u' d9 C) r( s - }
6 }# L) x$ m1 F1 h' J - } catch (IOException e) {# m" l x' I; y) A' u5 s' X% [
- e.printStackTrace();1 O/ ]5 t4 t$ r1 f
- }
/ w1 x: ]2 \) H; q! _8 { - }; q& J5 D1 y4 l* r
- }
( D: G. A7 Y% ^8 s7 V2 A5 i9 d - } @4 t# ~1 K _. f9 F
复制代码 信息输入 index.html:0 H) {1 E$ s8 s
- <html xmlns="http://www.w3.org/1999/xhtml">
5 G4 b, n. O4 M - <head>* v5 Z8 l" h% s7 v
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> ^" W7 P9 S( D: z! d" r
- <title>请输入任意消息</title>' P4 y O7 G3 W
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>4 Z2 \) g3 |3 _
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>' y8 O) z" X/ ?2 o1 ?
- <script type="text/javascript">
- r; ?" D( }" p; \; Q! V - var ws = null;2 @3 m1 e1 D6 W4 i, G5 O1 I3 {
- $(function () {
# J* p+ w, Y# R - if ('WebSocket' in window) {
* ^$ p$ X7 t7 n: s s( a - ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); $ L( G3 Q D: x8 {* i/ Z
- }
* Y, I# e) p i% N - else if ('MozWebSocket' in window) {
/ p9 \/ G/ X/ g8 X) u5 b - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");3 D* M& [" b6 G3 E
- }
! |7 }, i9 Y9 d4 _/ d - else {
9 Y6 Q( X- t- l# p- O+ w% H! S - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");% w1 s1 }3 ~+ h- y- `+ g
- }
- m- `- ^) x. I" K0 r9 {! \' l - ws.onopen = function () {8 h; X. P4 L( O2 F
- , S6 f* m1 x8 P. x
- };
& Y. d+ s8 g' L4 n; K - ws.onmessage = function (event) {
. E0 m6 R1 l0 w% v
6 V: l' ]7 f+ k& f* \, g7 H- };9 z9 V* a: w9 h" N) J7 }( z5 Y2 Q
- ws.onclose = function (event) {1 x& W3 r! W1 w- I& I
- ; Z* |9 A6 v) ]' |
- };5 _8 c. F/ M/ h% e, ?5 L5 ?
- });
3 k: d" X0 I2 | a4 s) \3 u8 w$ ]0 [ - function stop(){
/ q! P( a2 y5 Z# |$ f! [& k* J - var message = $("#message").val();
; i% `; u& L* X8 B2 p7 A, f) I4 t - ws.send(message);
8 R+ }% {% Q+ r) J3 g# G& j - }2 E+ p- ^3 y0 v* U- W
- </script>
; Q0 u9 y5 `0 {' K1 Q - </head>
* ?" y2 n) ?8 r; U - <body class="keBody">1 `! E( R; a% T+ [
- 请输入提示信息: <textarea id="message"></textarea><br />
" g: @. p, ~% r - <input type="button" value="开始" />
- T4 i/ _1 t O `; p) R - </body>
! |) L% v w- V& D9 U) N$ k - </html>
复制代码 3 `- b- f( |9 {% s) z% F* G
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
0 u4 K1 D# ^- M" o" ^4 x$ I6 W; s - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
! S" U1 r8 V, V. s2 O - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");% Q- u! h6 d( \2 d9 H2 `& f( m
- var ws = null;% A$ X. @8 R M* a1 u" N# N$ h. H }9 ^ c
- var basePath = "ws://127.0.0.1:8080/";0 ]. T: K0 |5 s' ]. L
- if ('WebSocket' in window) {
$ C& j5 l# F u" L' Q' g" S - ws = new WebSocket(basePath+'webSocketServer'); ! q' ?+ \$ Z+ S8 X
- }
6 A7 o8 m; A ^1 x - else if ('MozWebSocket' in window) {- M1 o; ]; `: w+ w- N
- ws = new MozWebSocket(basePath+"webSocketServer");" G1 X: a: D Z
- }
: q- w7 I9 A! v7 U6 b1 X - else {9 ^' y* ~" h$ Z; T% E. ]! t; g: {
- ws = new SockJS(basePath+"sockjs/webSocketServer");
/ ~# l5 g: |! t3 t7 d - }) S9 a. u/ q- m0 W
- ws.onopen = function () {* c, ?- f0 M; B: {
6 }( G2 n* E! [3 L' }3 X4 I+ u- };1 S; Q# U; S" L" M2 T/ Z
- ws.onmessage = function (event) {. T% L w/ J0 p0 E/ O3 j2 o
- pop(event.data);
# L1 y2 k: d! F7 P }4 U" A+ T# R - };8 k' ]/ Y5 Y0 U# d, |
- ws.onclose = function (event) {, C! r5 q; r* o+ S
- ws.close();
g4 ]% Z1 u3 U6 a: Y5 r - };; U3 T* ]5 p7 b- {
- //提示信息, w! y4 t! I4 s' G
- function pop(message){+ h: t5 t; F! X+ X- W
- layer.alert(message);
" M$ C1 O. Y. O; X$ | - }
复制代码 , F0 s2 a$ d+ Y
5、在项目头部引入
( ^% l6 g" I- q<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>! ]; E5 I. [) y2 M; n- D" A
6 y, L, ~8 s$ l5 b: r
( ~4 K! F; p. `& Y% H: A7 m
这时查看后台 会有以下信息 说明 引入成功。
! X l) L) d* o
& u$ S6 n' b( z# P d
5 f3 P! ^7 u- \7 x4 _然后在打开页面 index.html 输入以下内容 点击开始即可。: Y$ T. j* G( G) |8 }
' a- x* v$ s X0 \* Z8 H. K$ h& H0 d" t8 R2 m$ `( D/ m! h
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。8 d: q& K4 \- P; B8 G+ P0 J0 F( j* c
; s" j: W$ {( w+ P* F
9 y5 e" C! d; \6 N# J# t! `
" w# P+ G. f3 W8 G! n项目下载地址:
8 h" ~/ [( L: V1 v2 X: v
2 T4 X5 b, B8 @; L
( Y$ c/ C8 Q' r D8 F
0 g9 ?$ N0 E+ Q% b" G, ~, U! N7 u! H$ J4 W
|
|