TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍9 s7 s, P( x% X+ f5 X! {# Q3 s2 q
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
- M2 v& L+ ]8 s# n# J- N 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。' u) F1 I( e" f' H# z
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
1 i2 h. |% Y% q. H4 P- o' q Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
2 F* g; [, |9 Z- x1 D8 h; o 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
' p2 [% S* x% `. Z7 e5 Z 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
% @& F" D3 G$ t+ D N% 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以上的版本才能运行。
0 e# t+ S+ L$ X; F+ h9 k( y8 E9 b. L9 {% I' P
二、WebSocket协议介绍7 N9 N; \0 @6 r$ y1 @. o
WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:+ t7 t9 A9 x* z' A" Z
5 k. x; t. O8 s2 O9 l握手过程:
3 ~3 n- O, C/ b4 _/ q( U7 Z5 J$ \. {Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。2 [1 [1 t% d" s7 ^! v/ X
在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。+ G. G. J% U& j% W5 ~: ^) R4 G# c
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。" G3 `& u* P1 t
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。! B( w3 c& |3 z' X- [
# ]8 v6 B; _# `) O/ F* ]
三、Tomcat 7中的Websocket架构
% u( m/ y0 n4 y/ g$ T' @/ O: e* V. O3 }% U) b2 q7 c4 C7 Q
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
; y( |* d5 L) f 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
4 d& K; @7 g, {% m' h- N# u5 s, O Tomcat中Websocket的处理流程如下:
+ ?: W6 }% m) d
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。: B0 F5 N, ^+ u, N2 _
Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。$ G4 |$ r( e3 a9 ]! h
服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。. @0 Z+ e" Z, N; l; a
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
2 Y/ Y; K! [! Y7 Z5 `数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
9 ^8 s' ?; |' M
/ @- ?! M6 f# q7 Y/ P; J! _& B四、代码实现以及需求 k7 y! s: Y7 P) M! Q* M
$ P# |% M$ K" x f1 z' Y8 ?% P
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
D2 d* @4 S/ I& @3 _* _: \; c9 _; ^0 | l
2、相关环境 , Nginx、tomcat7、centos 6.5
$ ^! N5 [0 C4 I, m3 b' U m' G# G' v# u
3、项目框架,springMvc 4.0.6、layer
5 w) t- E, @6 U# n+ `) W( k* @& @! ^# `5 G S7 N, {
4、代码实现:
' ~% A' Q# R2 |6 K! p8 x
( a2 Q! N6 E3 l1 y" a' G5 OWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;
7 u, k1 m" j6 a3 ~2 T - @Configuration
4 S4 X( u! b' O* _/ [/ ~$ o - @EnableWebMvc
! y B" R$ l- Y - @EnableWebSocket
' _- M; O8 G' d - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{6 U. J$ K3 i' z3 H
; f, ? m* {! a5 `- @Override
/ {/ F1 h& _) J7 {, P - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
9 e d, X `3 M$ [) Q - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
- K* @2 `+ a0 A9 T/ \ - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");
! Q g/ v5 X6 ]5 A - }; `/ X) N6 n2 u( q
- @Bean$ }, \7 W$ Q& U2 Q
- public WebSocketHandler systemWebSocketHandler(){
1 A9 B3 N5 l; ]' c D: D+ S - return new SystemWebSocketHandler();
( C3 F& G6 v7 ^) P( m7 ^3 w: l' u. C - }
2 V+ K. e* j5 [% q4 s - }
复制代码 SystemWebSocketHandler:( k# I! q1 }! k5 K
- public class SystemWebSocketHandler extends TextWebSocketHandler {
( L" c! M# o; q& R$ G - 7 B6 g3 `+ v5 u7 _) \4 n' H! q
- ( R; Y/ J$ u6 N3 N0 b
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();; }6 N! P g2 K0 A4 T) u5 }
+ a/ a P, g a- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
' \4 ?2 M; S1 Y9 O8 V - System.out.println("ConnectionEstablished");) F" P/ D6 _5 V! y
- users.add(session);7 n3 h: v9 b0 W3 E
- System.out.println("当前用户"+users.size());
, P3 {; t$ z& @% U: [9 q - }
$ t! P7 @9 B0 C8 D - /**! v& G: a! [" J3 I$ a
- * 在UI在用js调用websocket.send()时候,会调用该方法
2 f* z6 C8 g8 U' h9 u K( X - * @Author 张志朋
+ v! u% _) ^4 H. V0 I; S# K" t8 C - * @param session3 [6 z* k+ d' f! f- W5 O% w3 Z/ `# j
- * @param message
8 ?0 B9 Q& s" t' _8 U2 s$ c7 O' g - * @throws Exception ! X% z! _4 H2 u# @
- * @Date 2016年3月4日, u8 C( `6 U; y. Q" b
- * 更新日志
, R8 H/ y% W ?2 o& N; n - * 2016年3月4日 张志朋 首次创建; Q: `8 O) x% J" t- r
- *
2 c0 K& F& d& A6 A! H- {0 v9 | - */
6 N( ? P6 a! z2 ~8 [5 g - @Override& U6 h4 s) E2 M6 ?
- protected void handleTextMessage(WebSocketSession session," W* W- T: c: a( i/ N; Q( E6 B
- TextMessage message) throws Exception {! P% Q: H; y. `! w; _
- super.handleTextMessage(session, message); G* d4 z( i5 G
- sendMessageToUsers(session,message);
9 [4 X* R% f/ H5 S. X - }& h' U/ H7 F! d- `! Z4 Q
- @Override! D+ r! @3 z0 d9 H4 p! ~+ H9 Z
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
% D2 ^( d( g9 ~4 d% F - if(session.isOpen()){6 [: q! `( l# q8 [2 ]. a
- session.close();! L0 X% D3 K2 S
- }4 a8 Z' z' r) {1 q/ \3 j
- users.remove(session);
/ p5 f6 W& \7 s% o - }
: h) B! ]1 D" o j. ?; P" W4 S - ) d; Q2 d7 f5 d% Z) S% v* V& d
- @Override
& I) {3 v- y6 d% Y - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {) {! r$ W- [) v- O
- users.remove(session);4 _ I* T$ Y0 n; z, H7 y
- }9 b& o, P; H' \. W/ h' M) l
# U4 @2 P: k! h% b- @Override
% T+ v4 L- k& f8 v) k4 a - public boolean supportsPartialMessages() {
/ Q; J) y, {2 o/ O7 G - return false;
, |7 \9 I' b# g9 z - }5 H" r3 l* I! d8 o/ T; B0 \
- /**( ]) t* x- H( H. Y! J
- * 给所有在线用户发送消息5 u+ l6 w) C1 n% J% n
- * @Author 张志朋
7 U8 s# A1 ?0 p& a - * @param message void
) K# u3 F8 v F! |$ C - * @Date 2016年3月4日
8 q- L$ @. Q7 E$ [8 E; F - * 更新日志
9 u2 j: r$ N* O' A/ X' ^6 j! K - * 2016年3月4日 张志朋 首次创建( }% |' {0 l5 K
- *4 q3 _- |( j8 X& S9 v$ k5 m
- */
$ M! X9 _# Z3 X# _ D2 u - public void sendMessageToUsers(WebSocketSession session,TextMessage message) {% M+ ]- W- P a: |% B8 H
- for (WebSocketSession user : users) {
0 @) T$ n- f8 {- E. Q - try {
, Z% z! E# Y( l6 p. S+ \' i* C - if (user.isOpen()) {
! i% m* \8 ~4 c0 Y( C) Y' k( t5 D - user.sendMessage(message);
+ \/ h! W: E6 ?8 V% A) v1 r - }7 y! z7 O' a/ Q, {+ E; `. S( z
- } catch (IOException e) {" ]: N ?4 r# p$ p; Q( X8 v8 i
- e.printStackTrace();
3 [( ~5 `. d* e2 w. ~5 W0 |: M) T - }
3 z9 x# y, Q- ]# u) [; v5 q - }
2 r2 F5 }( _4 D1 M% g. T - }5 B* Z9 d1 K7 a' `# \5 U( b
- }" c' v2 ~% G" H: W
复制代码 信息输入 index.html:
( D5 ?! W" F2 ?9 y- r; H" ]3 }- <html xmlns="http://www.w3.org/1999/xhtml">! F9 Z5 m! }; |% K; I9 \" K
- <head>, s9 t' s: g; o2 X
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />; `, u- w1 F: S+ x- V2 |
- <title>请输入任意消息</title>
, }8 q8 p F" g* X% t5 q9 M - <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
+ H6 o- H2 s" L - <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>. W! n8 o0 S1 [+ U' |+ f0 W
- <script type="text/javascript"># G5 o9 S/ |. Y0 r# V3 N+ T
- var ws = null;: B. u8 f! @% K! \
- $(function () {% j6 Y& H: |8 `* w" }/ B/ v% i
- if ('WebSocket' in window) {
8 I2 R( M" L/ M* d N - ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer');
- n" P: Y# J) Q4 |, k! z" Y - }
! F; g8 W5 F$ c! X - else if ('MozWebSocket' in window) {
* H) m6 [8 g1 o - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");, I5 Q+ |, W7 V4 R/ p9 o
- } 5 U# w0 R6 `! W+ W P0 {
- else {
" q" V: T5 W, g1 n( m - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
1 m. Q$ P( {4 o- b( ] - }" n& t% @2 b& R$ t. Q" l' @
- ws.onopen = function () {
- c# I, o4 D0 [. V$ j$ j
9 C" K. } {, j- };2 g; z, r+ ?, I9 V8 P
- ws.onmessage = function (event) {2 A+ t% Y* p$ ] q
- / }, }3 ~, @: M* N/ I7 A
- };
9 u7 `% \6 J2 z- ]( X - ws.onclose = function (event) {7 A+ S8 f8 G) |) A
- # P0 D1 P0 Z. j/ W Q. d
- };
" a9 n$ w; r, X, d" A. C$ A - });
$ g+ Q, e6 p) n( }, j' O - function stop(){
2 \6 K; [' @4 J4 J$ v5 [% x/ \ - var message = $("#message").val();" | j! e( C2 ^
- ws.send(message);
) y+ Y e0 F% Y* w- u - }
& X( b: r% u! H5 h - </script>/ O, p" }% s. {2 u& a
- </head>) P5 @& L; z& @' i
- <body class="keBody">
- {2 d; b- O; d3 ^* C' U - 请输入提示信息: <textarea id="message"></textarea><br />
6 n0 z( b( }2 ]/ p - <input type="button" value="开始" />
9 I) p: ?' n/ A3 E+ K# B1 a# u4 n - </body>
/ ?9 \' m* l, y2 V$ ]! f - </html>
复制代码
& Z$ z) }1 c' t9 j3 mwebSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");& ?) @& g" I" B9 Y
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
! y5 b" N0 {, R: f' F) X( I - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");4 H" l' |7 a# m* A
- var ws = null;
# V+ X, s& l; `& G7 Q1 ~- ] - var basePath = "ws://127.0.0.1:8080/";8 p9 g S% w. `8 l# |& h% \4 o' b+ Z
- if ('WebSocket' in window) {
: T' N/ ^! l+ X* V$ T - ws = new WebSocket(basePath+'webSocketServer'); 2 ^) l3 A. a7 d1 u
- }
8 e$ a. m+ @+ d6 B9 t( q$ } - else if ('MozWebSocket' in window) {+ f! b) j) K- s
- ws = new MozWebSocket(basePath+"webSocketServer");
! H8 g$ x1 J- s2 W, Z2 z - } 0 [+ p" ~7 J- J
- else {/ k% I2 Y/ h" `+ [
- ws = new SockJS(basePath+"sockjs/webSocketServer");% N9 J8 j# A) X0 p( Q q2 a
- }
3 y' I& \+ W, B+ j - ws.onopen = function () {
$ |; t# e0 i& ^
- d; A) ~% ?. C1 `" R$ Q2 ]/ K7 J- };
. r5 Y& r7 m8 } - ws.onmessage = function (event) {4 j5 _8 k& U$ g R% A7 f2 F* c
- pop(event.data);
( z0 g/ A, X& g0 C$ r4 ] - };0 N3 _8 H' {5 }: b% u, }1 R, Q
- ws.onclose = function (event) {
3 e- i. Z, @! w7 k - ws.close();
% b$ h+ L8 _% z- A# T# F, e - };
8 c; H& v0 K1 `0 l \! j5 N - //提示信息
1 [; i/ s, d+ S @$ V - function pop(message){
3 u9 [+ S3 G5 o: B3 j3 H+ K% ^3 i - layer.alert(message);: j& v, n2 V$ a# }, G9 J g
- }
复制代码
6 i8 m: U G! z5 H) j w5、在项目头部引入6 O" x$ p# I h0 @8 r5 ^) t9 r! O! e
<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>- N' h, r# t) ]3 J) O
9 |$ D" I( \8 v4 c+ T2 Z- X" s' V$ n- `) C4 {0 j% Q
这时查看后台 会有以下信息 说明 引入成功。
0 U {0 t( \2 ~
. b" X* w6 D# B. N2 n& j- f1 C/ \; \$ z6 Q* Y) Z1 i L( G
然后在打开页面 index.html 输入以下内容 点击开始即可。
+ i; c) @1 ?6 q) ?9 h3 Q
& N P$ P7 h! j2 F' R& j8 O- f1 e- G8 e8 U
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。# S& S. l5 M& V' Z6 t5 h5 D
& c7 ~9 `/ p7 N
! i& V& b; G+ h ^. d4 \2 g% u& V
项目下载地址:
6 ?6 o$ \7 M1 t& E+ F0 ?0 W1 R
6 q# _2 \! \8 T# X# }4 j$ w: L1 S
; F z, @! t6 v7 g" z, ?
6 Q; d# ?8 N" j" D% n1 z! Y& A" s6 t. t1 x' [: Q9 ]# h
|
|