TA的每日心情 | 衰 2021-2-2 11:21 |
|---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍
. K! C6 n7 Z9 C H 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。& N3 v7 \) X( P- |
我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。3 A3 g' W6 w0 ]7 Q2 g
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。) ]0 `2 Z- {; R- d
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。, B/ F& i6 J5 Y' b
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
/ y' {0 d5 Y2 y! V/ W 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
) ?7 \, @# W7 A3 `7 k M 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以上的版本才能运行。
7 `$ _( x. {: \' @& a: R+ F3 t4 N. {
二、WebSocket协议介绍
! y, l0 o9 L, f WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
2 m- }) d6 ^6 j1 Z Z8 x
0 c& q& e# k* o; Y握手过程:" r& m6 K6 r: V9 O/ W& @$ D
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
$ r$ R! S. G& N2 ^, a1 k: n在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。( @1 Z( e$ ^6 }( s/ g
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。
* H% J7 m9 ?3 }! b; V o( l& \# CBrowser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
$ B. n1 \' t1 { l
- m. ?$ x; [$ m7 w9 c三、Tomcat 7中的Websocket架构; f) m! z( k$ Z4 s/ Q
7 B% a& |4 \1 p
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
, r3 e1 b/ D" [6 [ 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
0 K' @# Z: X- W( H, c \9 {* v Tomcat中Websocket的处理流程如下:
8 V" g O$ V- T' ]
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
2 d; r/ ?$ |7 b2 {6 u; WContainer中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。8 ]. F4 ?, g- `, { z7 m1 m
服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
4 i6 ?, H8 e/ x+ O如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。/ Y( z5 C, f& C, f. B9 _ G
数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
$ X, [" i/ w6 p" ~; Q4 h
# S' [: a& s7 X- M) B% I4 `9 Y四、代码实现以及需求
6 u% C* ^; a1 q
+ [) [0 l4 O7 c b) }1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。) l, F9 u- _' [0 r5 c6 i* @% z0 s
$ y& U/ A" H; u1 ^/ i+ n
2、相关环境 , Nginx、tomcat7、centos 6.5
7 V1 ?# N/ o) Z! w8 N0 t, e* H+ g
3、项目框架,springMvc 4.0.6、layer. O2 @0 `# d8 x5 k! p$ Z) O) v Q# u1 Y
7 f7 x' ?, S! D5 T4、代码实现:
) T$ ~5 X2 P3 ]/ i& u5 e ~
. n; S7 o" a& a! {) D. G& ^; }WebSocketConfig:- import websocket.handler.SystemWebSocketHandler;, y3 L3 q8 Y$ z7 e4 @
- @Configuration$ |9 F% ~( A6 Q* T, Q
- @EnableWebMvc4 n5 t8 H- z' Y, f7 g6 B' A, X% `2 x
- @EnableWebSocket
" v# v8 y% e9 u* n* V- }/ ?# Y - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{ Y1 N' R4 }; @0 e8 X
- % c1 l5 _" q7 C& N, y/ [* x L. C6 R
- @Override O! }' X( E+ q, Z
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {* g# ?, y: N: B
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
* }0 q y3 j: a' V/ G0 w1 o M - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");* p5 M. m+ P* k" K. {/ B
- }* J. F2 f6 e5 B: J8 _2 A
- @Bean- U+ v1 x$ ]. P& P6 j. |
- public WebSocketHandler systemWebSocketHandler(){* h# y7 C3 B7 z' {: z
- return new SystemWebSocketHandler();5 D" o0 ^ [* L/ _
- }1 H& ~; p+ U8 T2 t9 N; K) C
- }
复制代码 SystemWebSocketHandler:+ Y0 Z. o. l# G# l' J% s+ a6 j
- public class SystemWebSocketHandler extends TextWebSocketHandler { p0 w5 ^' S0 B+ v$ S0 h7 J
8 G4 n# I' o9 ^& _
) D' T5 u# M+ l1 c: J8 I6 H- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;
0 [; S$ u, r; ^3 v! _ - * g0 h8 N: i# f% ]
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {$ T: O5 q I4 p* W7 u
- System.out.println("ConnectionEstablished");
& A/ J/ y; k% ]( D: d - users.add(session);
3 N) `+ | x/ |/ a3 S( k - System.out.println("当前用户"+users.size());; j Y. j! F# Q* r0 A+ F3 z1 K1 g2 y
- }5 U1 |: t6 v# o0 D m
- /**1 S3 a2 d9 o. ~
- * 在UI在用js调用websocket.send()时候,会调用该方法
( a" _, k( R$ r# B2 }6 Q - * @Author 张志朋' D2 q/ d7 C: c7 `/ F, j% J9 B
- * @param session, v$ X& |' m# v6 c
- * @param message+ k- A2 e) R" O6 O
- * @throws Exception
; k( D |& Q" j - * @Date 2016年3月4日0 m! L3 j4 Q& G2 U% o4 ~9 M
- * 更新日志
" m5 }! N; a: a - * 2016年3月4日 张志朋 首次创建$ N) g0 @ g5 m+ Z
- *8 t; x2 n" @( ]4 [1 l- i
- */% I7 d4 R1 f$ L9 b
- @Override @2 M% J- D4 d4 n2 X6 v
- protected void handleTextMessage(WebSocketSession session,
, _( S/ B7 K; D: h9 h( l - TextMessage message) throws Exception {
. V% b2 V% E5 r8 M! o0 \ - super.handleTextMessage(session, message);
) T8 i" o4 r: X+ l, V9 I - sendMessageToUsers(session,message);
$ G8 F/ f. O9 m/ k7 t: A+ B - }
1 b9 P" e; Q" C( r" r K - @Override
7 S# I. H# y+ p4 n3 h0 _ - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
# O- b. t; t* r+ }+ Q3 }1 O0 {2 o - if(session.isOpen()){2 e: P+ v% F) S# j
- session.close();2 @& v4 ~6 {/ H$ \8 H0 [( j
- }1 U) ]/ \$ V7 j$ s( ~, c. L
- users.remove(session);
7 d9 z$ Q1 w! C( p2 _" I - }
& [; \ \: W6 F$ `1 g8 L5 b9 \, h" w8 S - ; S2 J. O. D$ W( e# g, c9 m
- @Override, L, z0 E' W7 g# ]: R
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
0 S+ C" b$ v6 S" K) r/ @/ x - users.remove(session);2 m; M, ]0 X6 k
- }. \% w2 r; w& j) T) ~4 M+ o" C
- 8 H' m, |4 t* N X
- @Override
4 h$ a; B \1 A+ m - public boolean supportsPartialMessages() {& D: _) f- X/ Y
- return false;9 { I7 V/ s y2 |' r& D
- }
5 j5 M3 w; ~: B/ A - /**
5 o. U! W; Q- X2 l - * 给所有在线用户发送消息
7 V/ c3 r8 d; l3 o7 D5 M' R - * @Author 张志朋
% C2 q" _$ |& f& b5 c - * @param message void% K. ?& P9 G7 g, j
- * @Date 2016年3月4日: i5 E1 p& w9 U* S
- * 更新日志
" E3 N' }, j8 a) y% x) C8 M! l - * 2016年3月4日 张志朋 首次创建' t' G- N: J4 h
- *
2 Y$ D! R7 o1 G- h - */
; i+ }6 h w9 X4 P* c: w: s - public void sendMessageToUsers(WebSocketSession session,TextMessage message) {
# {4 G& g* F2 C, s; p - for (WebSocketSession user : users) {
2 |: b. @5 C- d$ }0 T& c) F - try {
4 n) D0 ~+ x8 @* d$ u' I, P% v - if (user.isOpen()) {& `0 F3 N T" s2 h
- user.sendMessage(message);( j, o$ | n: [% ~! m* {* G
- }
$ B# W3 m* ]+ y6 E: K" w. d% O - } catch (IOException e) {8 E! m) I0 }+ j" G3 G
- e.printStackTrace();
3 F3 ~: Z L% V1 ]/ j& ?; l: i - }7 `8 V- v0 O+ o9 S
- }: {) E* F, w: `# X
- }# S. ~5 w$ c- }8 _4 D5 C
- }
% T2 |! \: x6 w1 S& [1 K
复制代码 信息输入 index.html:
: Y" t* R- i/ ~& S1 ]$ Y. ?6 i4 |- <html xmlns="http://www.w3.org/1999/xhtml">) m. q+ y' D/ }" L& C' x8 H' A
- <head>: J. U, r9 p2 b6 j) Z
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />8 V4 R; B' g6 k4 K3 I) R/ d
- <title>请输入任意消息</title>
4 [( }& g D! W - <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>9 y& J4 I' {$ S3 f
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
/ |6 r( A' L' n! r3 Y$ K - <script type="text/javascript">
1 ?% \7 t# A5 j, d0 L - var ws = null;4 M& d( m' @! V- u# v( S
- $(function () {2 W2 F6 G% E6 f. n' G2 q+ ]
- if ('WebSocket' in window) {$ H, q# U+ l: i+ L$ a9 @
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); ) q3 E4 s4 C9 X& {+ r
- }
3 h* W9 ^9 Z5 ~$ u3 z# M - else if ('MozWebSocket' in window) {
7 J* G* u3 ?+ d4 v3 s, a! v - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");! w! }+ j0 v; h( }' b0 Q7 e2 d
- } 1 S0 y' k9 z) P" H6 ~+ S
- else {/ Y. o# c7 f/ g
- ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
7 ?6 o* g$ t8 n. h+ w - }
$ ]' d! F& J, I' r6 l* d, U) u - ws.onopen = function () {
, n3 G5 W8 Z3 C4 x7 |% x/ ~ - ! P, X) G' J: H
- };
( r: k- a- e3 F K - ws.onmessage = function (event) {9 \8 G: [& p& h
) R6 }$ ~# h7 F& _2 n- };+ C/ Y$ H" K& S% A; j
- ws.onclose = function (event) {& r) u; m+ N c3 }6 |) l9 h" H
9 C( g: B0 [- ?5 R. P1 V" }- };3 a- K3 V! B- p8 I7 Q5 L+ c
- });9 D4 K2 @0 w' Z# g: w2 w% i
- function stop(){
/ Y' G$ Q( C: m# ^: I& c - var message = $("#message").val();
5 ?* _, L; W3 \' k. F9 o1 k - ws.send(message);
* [6 x M- j/ ` - }
6 N" _: [: q! C9 S) Z; Z: F - </script>- J" V6 `# w9 [( ~
- </head>6 i0 V- n( e$ A" h$ `
- <body class="keBody">
3 v' s4 r8 x5 E- C3 l& f* R! M- R - 请输入提示信息: <textarea id="message"></textarea><br />( c$ J5 v" p+ h& l" M" U
- <input type="button" value="开始" />' `, X. e: r! w) V' n( X
- </body>4 X) W6 m7 Q1 ~! T( m- ^& s
- </html>
复制代码 ( [! D- a5 X0 [4 _# j
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
4 i! f9 ]5 {; u0 l: G6 j - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
9 e4 M+ x p: K1 H3 z* E - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");" R4 P# n' R$ L; }2 ~+ Z3 p
- var ws = null;
3 ~! i( ?" j- d& I: x P' W - var basePath = "ws://127.0.0.1:8080/";( L- t5 ~1 H" ~ U5 o8 t! `# _
- if ('WebSocket' in window) {/ B; J, r! t+ F: D1 L
- ws = new WebSocket(basePath+'webSocketServer'); , v2 h* A" u1 G2 b) z* K0 g" O
- }
4 l% ~. ~0 u- e) t; B; G - else if ('MozWebSocket' in window) {
, h* B4 f# X: A0 a( l - ws = new MozWebSocket(basePath+"webSocketServer");
+ {4 J3 b0 o3 Q1 Q1 D - }
" K1 |: D5 r: X2 L- c1 }$ L - else {2 S" ^! M) F _' W& f6 w0 T: q
- ws = new SockJS(basePath+"sockjs/webSocketServer");
/ u6 V, b5 _3 ]: s - }$ I0 C5 K6 h' H8 a, U" O5 p
- ws.onopen = function () {3 y) M. S) S: o( ]
- 2 m: Q/ d( c9 @% k
- };, u$ s$ C( G3 o5 u( Y1 ]
- ws.onmessage = function (event) {
% B4 M% H/ n$ a- y1 ? - pop(event.data); I9 n& R F$ C2 T$ V0 x" }
- };
: ?0 x: t! {$ D) |' a4 a - ws.onclose = function (event) {
* E a1 V! t: a* U2 |" a& Z6 r - ws.close();
+ x* `7 N0 K( V% T3 s- [ - };* B: E$ G" n) a6 m& Y0 {
- //提示信息
" W$ F' |9 _! Y, w - function pop(message){
5 I3 @0 _( L- W/ O; M - layer.alert(message);* ]+ ~, S. H$ p
- }
复制代码
$ H8 M) j& p# B" m5 G( i# F+ w5 l; [5、在项目头部引入
1 p! d- m6 X3 {9 h. I) f<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
' R0 V! @7 _1 k3 _& m x# {. @$ Q. b$ l" C
! h9 \: d+ ~4 m
这时查看后台 会有以下信息 说明 引入成功。3 U6 u+ q! o L, p( E. }
( { C$ X& T! ~3 W
+ ~( n! G- A1 w; G( C- X然后在打开页面 index.html 输入以下内容 点击开始即可。
4 c$ G; u' D* ?! |$ p
4 U# W% I; l$ Z; u7 O$ n8 a8 U0 Q {+ i/ {3 C6 a
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。% g+ l- d) J7 T3 p3 y8 i9 X! u6 }
8 o9 t1 l* q5 [# A
& c6 v- N( D, p( ~9 c
' \, Y& v: @' \1 k2 O; R$ S* D2 n3 X项目下载地址:: `7 O* }+ ^% a8 M1 m
, ]. S- k; X) `# ^1 y0 ]! f
* d$ f+ }; @* B5 z9 T' O( s8 E% I: n: i+ i/ J" ~
4 a1 Z% ]) S4 y |
|