TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍( h3 i$ b( x3 }
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
9 Q, U2 y7 X1 _ 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
/ X' ~: g- P: h6 { 轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
4 D; J2 ?9 @$ W% n/ \ Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。& x4 V9 ^6 B" l% V
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
( T! m, Z" [! `, w3 g9 ^% g 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。9 J! k2 x2 k: F* S
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以上的版本才能运行。
: t: o% B. Z; I1 k3 k( p( K7 g8 D4 l/ u- {! Y. c1 l8 A- V
二、WebSocket协议介绍
- a: w6 x9 U! n- D. W/ `) } WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:; b, S) Y& W6 m3 h0 ?; }% c5 H
! D' `7 H0 f v) i$ |9 Z3 x1 i
握手过程:
) g" ]* r$ ^6 g6 r* Q" \6 [( Z8 ]Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
- o0 \& a0 a j9 q' L+ \8 P在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。2 L7 `7 D% g# Q k1 ]- l
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。
, v. H5 F. }' D7 u4 \Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
# b$ o# i& U: G# \) c9 ~
$ d) T% g* [( m% l三、Tomcat 7中的Websocket架构
5 E" e& m! o5 F( z) H1 U' a% C, l1 P. k7 u
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。 - B/ N6 H* c+ A. ]
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
3 }# y6 J* H, `2 X Tomcat中Websocket的处理流程如下:
5 ]% {3 p+ }8 |8 l' A7 w9 z- U接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
+ g9 z3 m" |3 ~; ?3 ?Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。. e4 F6 {8 M1 _- k9 a6 E/ y8 z
服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
9 h- q/ t% m* B2 g如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。, u4 l6 M; L2 p5 z9 |7 y3 S
数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。1 s) q0 i. a, u1 X0 b
7 n! e, ^+ }$ Z/ K( K% i# Q
四、代码实现以及需求$ x- y4 S7 K3 F4 A+ e. p
5 e7 x3 z. [& g7 | H
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
. A f- Q5 k$ x1 a' S" Y+ M9 O0 G y- D
2、相关环境 , Nginx、tomcat7、centos 6.5
$ j& |4 o4 S% F/ t! b8 c n5 }/ l! P; j' L7 P3 m2 n
3、项目框架,springMvc 4.0.6、layer
( P% a s) Y* n1 v- I# I3 e6 n/ H$ q: v( p j
4、代码实现:$ o, i* h. y& w$ \2 g* u5 [
" B( ]& C9 \5 K0 {$ w1 D
WebSocketConfig:- import websocket.handler.SystemWebSocketHandler;6 f1 g/ s+ s" l( c
- @Configuration
+ Z' q$ a5 q9 u Q% @ - @EnableWebMvc
- b% j( N' c: c+ w7 W) S' X) { - @EnableWebSocket( E3 F. H/ }: S0 `& i% [" ^
- public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
; v/ Z4 Z- a. I0 H- K2 w' [/ C* }
, E8 }: V ]5 v8 [# X: g- @Override
! [$ |$ V8 t: f! m" k - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {: j3 z T# y \2 F
- registry.addHandler(systemWebSocketHandler(),"/webSocketServer");6 R. | A9 O$ {, f- v8 O
- registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");9 ^' H. ]- F/ y e, \( L$ t" T
- }1 Q k) G: \7 ]
- @Bean
9 w- }2 b# B! i% s# h' g. {3 G! D - public WebSocketHandler systemWebSocketHandler(){
9 s* w; f: b( e3 d9 l9 R3 [* I - return new SystemWebSocketHandler();' H) t7 L; E! o _ n$ L. e: k/ G
- }7 A5 M2 O6 q& p6 U+ L
- }
复制代码 SystemWebSocketHandler:
; {1 [% Y% V( v( A+ d- public class SystemWebSocketHandler extends TextWebSocketHandler {
4 B$ ^- F5 |" o; E' s5 J5 _
) ^8 s/ c) J0 _: E2 E3 ]9 N
- \' R; e# {' `! _! J3 r- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;3 V3 _$ G4 D4 u
- 7 e8 x+ X% l( f/ d! F B
- public void afterConnectionEstablished(WebSocketSession session) throws Exception { o" m* [" n) K/ K" V8 A, ]+ E
- System.out.println("ConnectionEstablished");
. ~% L: q1 w, ]- U! X/ u - users.add(session);
~% {3 X: L9 a% I7 l8 |0 F - System.out.println("当前用户"+users.size());
9 H. w# p; X, v( a7 } - }3 _- Z% g! A2 Y; Q* n0 @ H
- /*** L& i9 t# K& U8 X, C
- * 在UI在用js调用websocket.send()时候,会调用该方法
2 V# X e) M4 ]- e' A+ ` - * @Author 张志朋
. c2 ?; Q3 D! q* `% n. h8 O+ F - * @param session4 m) M6 I" u$ K7 J, U& G
- * @param message
! h# ?4 p: s; P$ m - * @throws Exception
) e( J6 U) P( P7 ` - * @Date 2016年3月4日8 s2 Y! f8 ~! A- h4 ^
- * 更新日志2 }7 G7 i+ X) P3 g6 M& R% J. w
- * 2016年3月4日 张志朋 首次创建% n: c0 x" L3 `% e+ W# E3 ~
- *
9 n- D8 ]! L. j' j9 \) R( _ - */: R+ f9 z/ |3 K
- @Override
' } B: E/ Y1 f6 |3 E, N - protected void handleTextMessage(WebSocketSession session,
3 T2 z" x7 _( ] y9 ]' H - TextMessage message) throws Exception {
2 l/ ^* f, J* Y( G4 L - super.handleTextMessage(session, message);$ K) {# N) b! n' \5 R- K O
- sendMessageToUsers(session,message);- C: E( ^/ v0 D, h/ t" E2 o
- }* ]7 M# j; n( I# ]
- @Override! U* m/ N; |% ?' x* T6 ]: {( P
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {, h' @4 U7 z& a7 o3 j$ x. T" ^+ ~
- if(session.isOpen()){
0 D3 k2 I6 Z" b% O - session.close();8 V+ q2 t: V" N3 H6 D, D
- }
, I% a% Z: S, I6 s* _ - users.remove(session);! s$ K; T8 C) x
- }* ~, Q/ o. |# k- I
- ( b v, p: s6 s. C) E
- @Override
8 }* A3 s& [ v- g! e) L - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {9 B8 t" n" {7 s: j5 C# V' p
- users.remove(session);$ p% y& F- H: ]) P/ e: Q
- }, k% z6 F n7 w1 {" @) ]0 l
+ b7 n7 U& e* J$ K) ]1 R- @Override
$ I) U! `6 `! {, }% b6 N5 z - public boolean supportsPartialMessages() {; q" T: R3 V; r3 Y, M9 s
- return false;
6 q( d+ h5 X" m) V, c - }
" V' h, e$ l i0 P - /**" T p( v9 p1 r2 {
- * 给所有在线用户发送消息4 O7 A. M7 m3 [5 }, U3 ^
- * @Author 张志朋9 z; b! m+ S" d. C K! e
- * @param message void. v$ I4 i* W6 b9 l0 T+ a" y" S- A p
- * @Date 2016年3月4日. ~, f1 j. R2 c v9 n
- * 更新日志
- s: K! r8 I& M6 W - * 2016年3月4日 张志朋 首次创建
# q0 y1 q; m/ d4 { - *
9 Q3 \) U7 `3 u" C* |5 Y - */ r! ^* d/ Q5 k
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {: a, A# e2 X: _$ ^7 n, n7 T
- for (WebSocketSession user : users) {0 `& i- d: H" \$ J$ K+ Z
- try {
* o3 g. w" P9 G% r+ f - if (user.isOpen()) {
7 j) `5 J$ G- W& K( R - user.sendMessage(message);
N7 X/ k+ C v, J; p4 y - }* J: L$ z* u# B7 {" @' c9 _
- } catch (IOException e) {
1 Z0 P) d! y/ X1 V& @ - e.printStackTrace();7 b- ]$ n5 c+ y+ w& Y5 O
- }5 ^% z% X1 S8 I6 B% _- o8 c
- }
5 q- ?, H4 Z' X- k - }
% N/ f- w o* i# J8 e) I - }
! h- B) N1 n# t; G
复制代码 信息输入 index.html:" I, I7 G: X" m' M2 }+ J, G
- <html xmlns="http://www.w3.org/1999/xhtml">. Z! @, l: S2 O' C
- <head>
" n' A8 Q( I. i- z: g3 ]( ] - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />( q& D* o' F; E
- <title>请输入任意消息</title>; W9 h" R2 ?# ] l
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>. r b6 E c& M% q- U) G' a/ d3 v1 I1 i
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
# @5 y6 |# k; B6 |8 d/ J% j - <script type="text/javascript">0 I0 {& i1 o1 `$ Z4 G) S: O
- var ws = null;9 P, ^! ?" O( F7 U8 \' S
- $(function () {$ p5 @; r4 P" p B" T$ t
- if ('WebSocket' in window) {
; V/ b- s* v4 D6 z1 b' m9 f( T - ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); 4 i0 r. H/ U& c7 Q9 A$ Y
- } / D& p( v- z6 ~( N, g4 ] W; N
- else if ('MozWebSocket' in window) {7 K+ W4 d$ l* n8 M
- ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");
: s8 c$ u! n+ {5 U - }
6 D- y- M4 f# |5 Y/ u0 X - else {
( o; e* W1 N8 H3 P, G i4 N* K$ C - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");+ M' B9 L) ?+ g
- }
0 n7 x5 u- I8 ?. g - ws.onopen = function () {
2 C, a) j5 I1 G0 L) M
/ \; E) Q5 q+ `- };! F' _/ h' M4 z# s$ u
- ws.onmessage = function (event) {
: q: M: B, \$ O
: k* a8 f! K" O$ ?- };
2 U1 j! j/ w' @; D% n - ws.onclose = function (event) {/ f$ s$ ~3 R2 v; a6 P2 b
9 D6 T$ `+ g$ m; ~& I9 d- };
, E+ O+ T# f# ^1 t0 S8 I - }); g; Q0 b& _) C
- function stop(){8 r6 G/ W. W2 _
- var message = $("#message").val();
# U8 s+ f2 G6 n5 l - ws.send(message);
$ y6 U7 P% Y( G) E' ] - }% _& i9 y* `" r9 f; a
- </script>; h/ W8 C$ e8 S* k
- </head>1 h5 e8 B/ z1 z+ k1 U
- <body class="keBody"> S0 D5 i& Y$ ?+ Q5 `* @
- 请输入提示信息: <textarea id="message"></textarea><br />$ P0 h0 c5 X% b1 C5 h! d
- <input type="button" value="开始" />
" L# ]: D. U$ d8 a) |' i' U! K4 e - </body>
2 M, _' J: M# T) P/ l - </html>
复制代码
' d5 |, f, b6 H7 f7 T4 }webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>"); W, i! H# a+ h4 ?
- document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
7 n: O4 [4 ~" L3 }4 n* J2 \ - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");
$ U! Y3 C# {% e) X/ _: }# N: | - var ws = null;
- w5 W. d) X6 Q8 C# v% a - var basePath = "ws://127.0.0.1:8080/";
) @6 _, g0 F+ w& O3 { - if ('WebSocket' in window) {) S4 o0 q0 j3 j9 D) y/ p. T+ [
- ws = new WebSocket(basePath+'webSocketServer'); 3 i* E: M, N& \
- } G; N8 R2 Q) i& X& O
- else if ('MozWebSocket' in window) {. q j3 u, m9 a( A3 E- @
- ws = new MozWebSocket(basePath+"webSocketServer");; l2 F: r. A; f& C- V% L5 W+ J
- } + r. p* F k/ \" L
- else {
7 Q( u. H: r% O - ws = new SockJS(basePath+"sockjs/webSocketServer");
6 Y1 u* @7 c6 f' f; ^7 g - }1 k- M7 I. y/ i3 a
- ws.onopen = function () {
( ^- h; S( C# P7 v' M$ U7 o - 0 u) e& |+ p7 X6 b `
- };4 E) |, }3 Y$ j* h5 y. F
- ws.onmessage = function (event) {
' i! I1 @8 m9 W' o. w - pop(event.data);
+ ^" f0 q3 k* J: Q5 ]4 c - };
, C& V$ D/ S8 b2 @. l8 X5 t- U2 p - ws.onclose = function (event) {5 E4 P2 w$ w6 T4 }: q) y
- ws.close();
% l1 j. s& ?9 L - };
2 K; F# h* n( A+ ^ - //提示信息
2 _: [' Z3 L8 R L+ N - function pop(message){
; C* f& S3 e7 O' Y* b, V( U - layer.alert(message);2 K. y: V2 F$ I2 H* I! u3 C+ ~
- }
复制代码 3 m9 n) W5 l" Z
5、在项目头部引入( E/ W5 h& C: B0 i2 Y) h8 T
<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
% X( n# {1 i# u0 l0 B4 p7 Z7 G9 l5 S1 c) S2 R! _' T8 W
- ^& o( I* A, q S9 q# q这时查看后台 会有以下信息 说明 引入成功。
, N0 x1 B) t% T; p% f' ?/ W/ r
& E, g# C) ^ F& p
. D* ^- v- T9 F) v然后在打开页面 index.html 输入以下内容 点击开始即可。
/ n+ T/ f. O' U$ ]
* |* j6 X( D7 r& V3 a% \
0 G# p& C, J2 [6 B如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。+ O, T' Z- x! H9 n' k
' i2 D7 V, {, T& k
: L+ E5 u# Q% B) Q. \# h
* w4 h' e$ D. }$ g2 \0 h: s( T. J项目下载地址:% d. [+ p5 I4 p8 n- ]+ M2 T
/ k0 W: d2 G' o- L X1 J
5 e4 l/ p; p2 G; }; H# ^, K) e) m% _! O7 G" T
0 s7 D/ L5 P; g3 q. { |
|