TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍
( C7 g+ V" \! P5 P! _ 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。# y( ^) b0 G$ W4 ~
我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
5 V4 _' Y7 o: M# f0 g$ \ 轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。( M4 v! R, P. K2 o6 _
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。' A+ v) A8 W+ T) ~
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。6 g- W$ J0 X- P( E
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
3 m6 Q4 ]3 L6 N$ ]+ q, _ 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以上的版本才能运行。& P: J) _: l' {5 r P" v- S
- {) O1 p& P! a4 }* W. F二、WebSocket协议介绍
# u# [+ g0 G+ h WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
1 T. z0 `1 N3 B* R6 i( U
- b5 Q; c# e8 u& e. {% r5 v0 c8 V握手过程:( d0 L) w+ t/ L1 m. w) E
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。! A6 s, F+ Z& V$ B; F# g: Y: K/ o$ Y
在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。) Y* j6 O( j, e+ `5 y
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。& U1 Q) ]3 b) W7 x! ?# l
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
3 h* O/ K/ K# d6 J; X- \ V
" m; {9 A5 l, s |/ C) ?三、Tomcat 7中的Websocket架构$ y8 q5 q2 E* R& x
( n7 _& }0 f! z5 s c如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。 3 P! C3 ?* s% q8 g* ?
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。) b9 R; e7 A8 q& r
Tomcat中Websocket的处理流程如下:
6 U6 m) X6 o: Y, o3 h4 j$ {4 Z
接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。! U0 m" l+ } O7 r+ o! ?- X
Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
+ i$ x" a7 S& k* s服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。* W1 j( o3 g$ T0 D4 a/ r
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
6 R6 g1 V# u7 ?: d" N数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
/ M2 r# T; ?; q
1 c' S2 a5 d+ C5 o1 z; M四、代码实现以及需求
, M: p! p2 q" I& V; f6 f6 e# k3 P0 Y' k" q
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
& [ h' k2 O% ]1 i3 b5 _$ `" ^# Y1 V! W2 @8 K
2、相关环境 , Nginx、tomcat7、centos 6.5) ?, j& N% K! N2 c7 z7 t
# O/ N. _3 k7 L( C% O6 h3、项目框架,springMvc 4.0.6、layer
5 U/ a! l: s3 ^& |9 w- i7 [0 U- M$ g H1 W v) a/ {
4、代码实现:
, ?2 X! L5 H+ f
0 ]+ K, ^- Q w( @$ U& g$ IWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;+ j4 F$ {* S2 c2 X& r, T; ~/ o
- @Configuration" E1 |/ A* f8 Z7 E
- @EnableWebMvc- q* X7 }7 J: v1 u) R8 z
- @EnableWebSocket& }! o. g7 U( ?( ^" M* V7 u U2 b1 k
- public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
* f+ e& ?$ }' I! V* Q
5 g+ S8 F1 ~, a2 t# J5 K& v- @Override8 v7 j/ h( S' i
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
. p, _! V5 P; U4 a6 c9 e* l$ ^ - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");- o+ N7 B& e3 b$ G
- registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");
$ V2 l6 _+ x) z - }
7 O- R/ H, B0 ^7 P9 Z$ B - @Bean
' t5 m, ?+ j |% u/ z, i - public WebSocketHandler systemWebSocketHandler(){ ]) t4 c3 n, `' y+ V
- return new SystemWebSocketHandler();
7 a- U% ~, J2 T1 u% |, M$ M" A) J4 p - }2 d$ e0 H! m- T" w0 q ^& [
- }
复制代码 SystemWebSocketHandler:+ ?/ E+ L9 K z1 Q% }: z8 k% v
- public class SystemWebSocketHandler extends TextWebSocketHandler {
0 o' K0 q2 d- h- G3 k
7 i) H) R2 A4 h& K* I% J- ! O7 l/ Q& m% H+ A" }
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;6 j& d8 e& X1 v
8 |5 s. k- N3 W1 G) M" I- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
6 v: j. U$ A$ }+ W/ y+ R2 W - System.out.println("ConnectionEstablished");
0 G. y' c1 O/ h& J$ ] - users.add(session);
; i3 } S3 B2 \9 _' ^ - System.out.println("当前用户"+users.size());
& p: o; }8 E0 S& _; l% _. k2 p! n2 S - }
& n* f0 Q8 D- c* E" D - /**; m* c' x( {9 @$ H) C; B* T2 C8 S5 O
- * 在UI在用js调用websocket.send()时候,会调用该方法+ r( s7 H9 B. X7 Z& K7 B
- * @Author 张志朋
" D3 Y: V- z: q# ` - * @param session) b8 J% p. ^ J
- * @param message
- x! r1 [3 g" x( E! Y! f+ [; U" ]2 D5 K6 Q - * @throws Exception 1 U' W! E; k* L4 f( K+ y; l+ i' N# H
- * @Date 2016年3月4日
$ A' @( ?2 x) g- \3 f/ u* L - * 更新日志
$ S; v3 _) k6 U- l8 c* r - * 2016年3月4日 张志朋 首次创建9 I7 T: t d# X b! R# p9 a' M& e
- *+ Z- u+ P' q- C- [3 D
- */
3 S& G- i3 B8 s9 c! R - @Override
# k% p* A( M5 o# F T( a% v4 L# d - protected void handleTextMessage(WebSocketSession session,, E$ j; D- |# F6 t6 h
- TextMessage message) throws Exception {
/ f7 J$ O2 L$ {% s5 s* ^" Y, ` - super.handleTextMessage(session, message);
4 \5 j# N7 x D) D - sendMessageToUsers(session,message);
I7 y1 w! J4 D( h0 K. U - }
0 R5 [4 o. B4 E: S3 m3 F d0 S) d - @Override
$ M; A1 m: f m7 [ - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
2 P! X% B7 O2 |4 N7 a. P+ T - if(session.isOpen()){
) z5 F, o; p8 N5 J6 B: e3 a - session.close();
7 T6 }( l5 ~9 ~5 Q# R2 M( z - }9 Z, t$ Y. m0 z- C" R; C7 G
- users.remove(session);% k7 P; K4 V+ E/ l4 J: L
- }- D3 F* {2 t, K# Y$ x3 ]
3 s9 w' K i; T/ a& [7 p* V- @Override5 n5 h9 ]. C! d" i3 P
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {* J) W0 K0 y7 x+ b8 F
- users.remove(session);
- n9 ^. L% c* `: d9 \4 I - }
5 @1 d$ Q: U4 _' M: R - ' v/ ~: @* X. ~% i- k
- @Override: S: o& s( q7 T* k- @
- public boolean supportsPartialMessages() {
& O8 h$ G1 D9 p% C- ?& a - return false;) _: ~" v' n# i2 Q. U2 ~
- }6 e4 D+ X4 ? g% \; Y/ `8 V i
- /**
; O8 q0 q/ V# \) t8 s- t' s4 ~* @ - * 给所有在线用户发送消息' I) a; M# N2 r1 C" @. B- }2 [* w
- * @Author 张志朋3 n: e* b1 b# y" `" z7 X
- * @param message void
1 H: |& r" f9 F# C- B - * @Date 2016年3月4日
% N4 K$ r$ c& ]: a" V/ k* q: \( w - * 更新日志
9 ?# Q7 m6 T1 Y# @ - * 2016年3月4日 张志朋 首次创建$ U3 A/ Z/ N; ?% o6 j1 E
- *( O" I* m0 a& \4 ~6 s" v4 G
- */! k& Q" o( s. U9 Z4 r) i% q
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {
& q% V0 b3 c& W4 ~3 _ - for (WebSocketSession user : users) {+ P: e# A' E9 l7 @6 l1 U" h
- try {
2 G, @$ u: J! @) i# ?" p6 M - if (user.isOpen()) {* _4 G2 }6 N7 M7 `7 t
- user.sendMessage(message);
5 M3 w/ ^: M4 K* s - }7 y. j: e/ Q, N2 A/ R
- } catch (IOException e) {* K' N( f; N) V" T4 U0 y7 ?+ G
- e.printStackTrace();. s/ D6 Q" s/ a+ T
- }* I& k! a* B y" _+ M- j
- }5 z3 o$ G* Q g" X" v1 l1 X, i
- }
, {: W4 G, |; U1 v! b6 q4 w) x - }% u& n4 X& E& i
复制代码 信息输入 index.html:. M8 |# O2 O9 s3 o$ I o
- <html xmlns="http://www.w3.org/1999/xhtml">, A2 ^6 t1 R1 ]
- <head>" C1 _0 Y- T* `& A
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
. \. r: Z E- c: G; [7 H& E - <title>请输入任意消息</title>
+ J$ I+ e1 |; J6 g - <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>2 h4 Z3 Q* o2 _ ~, R( P0 c/ `5 }
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
! v. R5 j7 e; k- C/ t M) g - <script type="text/javascript">6 D( ^! p# J5 Y1 n0 `# G3 q
- var ws = null;
3 x8 B! |3 C: y5 y8 J I0 o - $(function () {1 q4 S4 b4 _2 T' w" e0 V; `
- if ('WebSocket' in window) {; c& m+ ^& {- I. e2 J, F: p
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); ) h7 g) L3 \6 Y1 D1 |9 a; t" f
- } / l6 K) y8 U+ L) n1 o% G
- else if ('MozWebSocket' in window) {
/ ]# `: V, v% ^$ w - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");( W6 _9 F, \$ E3 U# Y; O
- }
3 V6 [8 h5 J; E$ Y# P - else {
; U. e" j7 b8 F) u! s# T - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
/ O8 b, @: Q. _( K2 I% T- c$ B - }
9 h1 X. o d7 T( t5 w5 A- f8 F - ws.onopen = function () {
3 L. n2 c o- X$ d2 u0 J' C) p0 R
0 |" A# i. T( }* [! T- };6 m l G( Z% J. @0 j2 L4 V! Q
- ws.onmessage = function (event) {; N4 ]. T, @" _, c. Q) \; H
# b; U" C2 e6 u- };
4 F* _3 |2 {! a$ r8 B/ |3 s - ws.onclose = function (event) {5 q# f$ O! p# ` d
- 2 p" |$ T# [# A9 c! o1 t- p) K1 Y# c
- };6 L. D- ?2 O4 u) \1 O* u4 y/ P8 L! K
- });) i6 b" W- [ b9 p' e3 R. K4 Z
- function stop(){& u$ Q! P, W2 I4 b1 z9 b8 V: ~
- var message = $("#message").val();
7 @& R: K. ]( h# i' p/ P9 _6 b - ws.send(message);- m7 m# y: B- V6 W5 n. q
- }: x8 T5 n5 @- d x( r1 |
- </script>4 E. Z+ U( ~" _6 |
- </head>! Y+ l" W7 N/ f6 O/ Y- K+ E
- <body class="keBody">, _/ t3 ^% r9 W- X3 g
- 请输入提示信息: <textarea id="message"></textarea><br />9 L/ C# d) a7 h
- <input type="button" value="开始" />
7 u' G- a3 U! L4 ]! f - </body>5 w: G2 M. r( ~; `3 o, v% Z' j
- </html>
复制代码 : y, M |- p/ C+ d# |
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
6 d3 z" Z2 ?8 | g - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
+ `: U' v) G4 a K+ A2 P$ g - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");0 I; t# x$ m+ U. d* m; ]' w
- var ws = null;$ Y- M0 m9 s( J: n+ q8 ~2 D& s3 w
- var basePath = "ws://127.0.0.1:8080/";
$ `0 `5 k& {9 Y' d9 D% b9 f - if ('WebSocket' in window) {. w8 J* D( G+ z, G
- ws = new WebSocket(basePath+'webSocketServer'); # v! B' }) ^2 O0 C1 S6 n
- }
) b" ^5 l" p! Q# F4 E7 |: D$ U: y - else if ('MozWebSocket' in window) {( M7 f `7 N( r" e
- ws = new MozWebSocket(basePath+"webSocketServer");
% _7 |! v1 |. Z) k - }
8 O. |! d" p" r1 J5 ]% E: w - else {0 K) g/ f! g, \6 N% N8 C8 O u
- ws = new SockJS(basePath+"sockjs/webSocketServer");
; ~+ T r' q$ h- W. L. B - }0 a& k& Q: @0 ^- L/ p' Y* f
- ws.onopen = function () {. C" A) I$ E4 k+ \+ Q( u% }
- + V- ]( m) ?8 _+ B
- };
1 G5 H q( p* W. j, F - ws.onmessage = function (event) {6 J" g; H- r2 t* h8 E
- pop(event.data);
2 r: Z& W) L- Y$ x' k6 f# h* ~ - };5 C& q3 q4 M( U& a- a
- ws.onclose = function (event) {
0 ^$ x& \) h4 u- }/ H - ws.close(); i# L+ S2 {: E2 [! F" e: W1 ~
- };6 t% L' m+ t/ B: m
- //提示信息
# f' ] f7 G9 z& W/ S M# J6 ` - function pop(message){
$ i& K) M# C2 g/ p3 D( x1 S- l - layer.alert(message);) D+ E; g" E+ g, Z
- }
复制代码
" P# t* `7 i) o3 r. u5、在项目头部引入
. ~ R; X! C+ M1 F# f<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script># j1 h) N% {- x
/ V2 B# h3 F3 G0 _2 `* [. {0 b; J+ `
这时查看后台 会有以下信息 说明 引入成功。: ~9 }, k8 K1 B! T' H, i
% ?& m! ~ C8 j' V3 A
6 K3 b, H$ b/ Q$ r4 H9 w
然后在打开页面 index.html 输入以下内容 点击开始即可。
9 s) k) i& L1 d' ]9 t2 J0 ^2 U4 ?, x
" u3 w+ s9 u) {
1 v# l T' q# A2 b2 i# L
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。
$ P0 F/ u, i; l1 H) B
+ N6 @7 i6 j5 @5 s0 @; c _" _9 x t X/ s b1 l5 y% Z! J9 u
* d+ v# A [: ?0 \. q
项目下载地址:
( Z) t8 @: ~1 N# o$ _. s! ]* X2 u( M) z* e1 @5 M$ [' V
( x9 h0 |* J* D3 E9 f: q
% X0 Z: d% a0 g# p
9 G* y6 \- w" b( S* k3 c. W |
|