TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍+ ^: [# w* J$ O( k
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
% ~" ?+ K# P! W( Z 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。: K/ C1 h/ Z( q `( P
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
# ]2 n# L1 C& n5 H {4 w Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。8 j+ i) k E8 b% [) s# }) Q+ y
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。) D9 \ V7 I6 m7 x
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。/ w/ ]" o6 V/ @+ \, X3 @: Y
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以上的版本才能运行。. w+ p$ _, a9 \+ T0 W: l
' d0 y6 R3 S1 s; g/ p: ~5 C! W0 N
二、WebSocket协议介绍
6 S# q" F1 z! J! k& @3 C WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:9 p! G7 M& v- s2 g! e m7 e
6 p O& a' o8 ?4 S7 i6 m, z握手过程:6 L2 c, s6 _8 ]% t5 P
Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
2 p( c ^( r" a9 `* q在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。& Y3 F6 I* ]& H7 ^' K3 i( ?
WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。* z, H' b, {0 U# ^) R7 X' e
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。! D7 m: i5 K, |- ^$ M8 C D& u+ j" V
U3 c$ }6 t, I1 \+ i1 I, e
三、Tomcat 7中的Websocket架构
^4 R. p' w* s# n$ X
2 e' d- I0 j0 N4 q( H2 ?5 C4 ~7 G Q如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
5 G* \5 h, Q: k: k5 J2 k 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。7 Y5 d1 B% A! q0 L% ]9 x
Tomcat中Websocket的处理流程如下:
5 Q( e' ~) b/ {! P. R0 Z接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。, M# B9 I: e) N( Z3 i1 O7 R+ ?
Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。" k; `1 R# o" q8 h Y. ?
服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
* r0 i( h4 _; E) o" B4 G7 P如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
% f( e M* a" Z# R9 z数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。% C& F$ i( {* {$ P3 C
9 g6 V# ^) s9 O3 `( I
四、代码实现以及需求& Y+ \) R* \! J
2 n3 r% s/ X7 u& q& h0 `6 P
1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。
" \" R3 E$ U4 b, I! e# u/ }3 L5 u: n9 y0 ]/ J
2、相关环境 , Nginx、tomcat7、centos 6.5
( n2 ~: i/ b8 I5 e4 y* S ~0 \/ r/ y; ^: [* j
3、项目框架,springMvc 4.0.6、layer! X# z2 C: f; r/ j6 e6 K2 h
2 i, q% ^: t+ b4、代码实现:/ A6 _, ~7 Z7 X
6 E* e5 D+ T/ k. PWebSocketConfig:- import websocket.handler.SystemWebSocketHandler;8 d2 b$ C" O) B- x7 `3 \& z8 k
- @Configuration) l: w4 J; `: i; b
- @EnableWebMvc
* j; m' A- a$ b: C- Z* \ - @EnableWebSocket
% K8 C8 H, M' \" R) g, ]( o - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{5 C" `! w G0 K* b3 U0 i2 U7 [$ S
# B$ b# d& |0 m/ `- @Override& _0 s) O/ S# o! S E# ?
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
! K! n' K5 V7 v - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
' C1 {5 ?2 }. [# k- M0 Z' R - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");- Z9 m2 C/ ^% U$ t {( t" _
- }
8 \5 K/ P# `$ r/ v1 q - @Bean
: @. g% e) g; y) o$ q - public WebSocketHandler systemWebSocketHandler(){
. ~ I5 A% C3 R$ ^& u4 a - return new SystemWebSocketHandler();
7 [2 v4 J. ?$ z1 s# h3 [- K - }) z" v+ C3 H! `
- }
复制代码 SystemWebSocketHandler:/ W) B3 R4 @* H; x: B: w& l
- public class SystemWebSocketHandler extends TextWebSocketHandler {# @1 Q, D4 U% }* a9 N1 I s
& z& Z m8 s& e
, ~/ p L$ i% M- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;
/ X7 q' b* z. b0 ~, p4 G7 d
* m1 s# @0 B l9 x7 A- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
7 n% |6 j3 o& O, {6 c - System.out.println("ConnectionEstablished");: M, l3 W# C( K3 x% {3 R
- users.add(session);
+ ^8 E! c0 x r/ B4 \ - System.out.println("当前用户"+users.size());
6 ~8 |" \4 v# m- N4 a: w0 s. ~6 | - }
! n$ f6 a: M, K1 A* y - /**
3 u" ^; ~$ i5 Q% K: w& Z - * 在UI在用js调用websocket.send()时候,会调用该方法
" W8 t ?' d) a# R - * @Author 张志朋
5 n' o. s v6 B) w9 b - * @param session9 ] K) B* |' J. d/ W
- * @param message
$ ~/ ~, w% f( m - * @throws Exception
/ t. l4 ~- o4 Q) D - * @Date 2016年3月4日
- {& U R2 g4 q4 u" H2 W+ b - * 更新日志4 Y# C( g8 O8 I4 z& l7 t, }
- * 2016年3月4日 张志朋 首次创建
0 A! [. h7 j5 W& ?* Y. C" @ - *
$ x" g+ ?) h9 G. T: y r - */0 r- f% B, u# E; Q' y
- @Override' L+ o/ i d. r: M ?
- protected void handleTextMessage(WebSocketSession session,
- ]' [6 O6 G( {, ?3 D" O9 R+ s1 u - TextMessage message) throws Exception {% {% o$ [; |; [! A9 |6 J5 A9 c8 r
- super.handleTextMessage(session, message);4 H) F! J3 G. B
- sendMessageToUsers(session,message);
! _' f7 c$ K0 P# ^6 o! `* {3 m- I - }- K, j* D. ^3 C; b* y
- @Override* U. M& h) a% j% ^3 @! W! o
- public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {) s% o2 E& ^5 H; `1 m
- if(session.isOpen()){
}% `7 z9 ^5 {! z8 [, q) p2 o - session.close();
9 m/ k8 B j1 Q6 |0 a# Z - }2 o/ \$ q U. }
- users.remove(session);
* ], { u% L) y) D' [ - }
& y# h/ o1 T, t - / N0 J+ p9 \* _) | o" m i4 \
- @Override
* G+ e/ f, k% z. J+ o - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
# Y/ H+ g) G5 ]5 [/ g) n - users.remove(session);; P) T5 C- b4 n( R; d4 I# Y Z
- }( _, b) l$ ~1 V, |$ q; s$ Q, I/ ?
- * V0 X( j7 s* h. P9 r4 O0 g
- @Override
4 F4 x# y4 u. \, t! N- w. h& o0 ~ - public boolean supportsPartialMessages() {! Q! O- ?" U; p9 v
- return false;2 H% T- a: D$ m8 I9 w
- }
9 q! a# u0 j* m4 L - /**) A' Y. G2 ~ p3 B
- * 给所有在线用户发送消息
. z: |% J# K; Y' g - * @Author 张志朋
* _) J6 h- c: W S - * @param message void
+ m' n% \) k& q: a7 j - * @Date 2016年3月4日
5 q5 D* j% C2 g: Q; k+ T - * 更新日志
/ F, a' T* ?$ U+ A- X; S! H. V - * 2016年3月4日 张志朋 首次创建$ t+ V9 O, [2 d/ ]% h: C* A
- *6 R0 v$ m) [0 ?9 m8 A9 n, ?: D& g' j7 ~
- */
) G- X* X e1 y; F, S - public void sendMessageToUsers(WebSocketSession session,TextMessage message) {' k7 |" c" `$ j
- for (WebSocketSession user : users) {$ `5 M n5 ~+ R
- try {# W- v& c( w y
- if (user.isOpen()) {8 T; u* e6 z! U" e" c: c
- user.sendMessage(message);7 \+ _, y& Z1 a" F3 _" Z. [
- }8 h, T e- u, A' K
- } catch (IOException e) {, U, d$ o* c- ?0 \* h
- e.printStackTrace();9 e8 h P3 ?* `$ `$ }
- }" J0 m0 t5 N% ?# N8 h5 n
- }$ S \0 }3 e6 u# t0 L5 {& ]
- }: Q1 }$ Q: |& H4 j
- }2 l. T4 ^- f: M& N1 _; e8 b# k
复制代码 信息输入 index.html:
( r% B, m6 u* [% N o# I; X6 r- <html xmlns="http://www.w3.org/1999/xhtml">
6 B5 G1 @% H0 M+ g( X1 _) k - <head>: T6 t1 k5 r( q9 I
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />$ B5 q+ a9 S7 O( P
- <title>请输入任意消息</title>
' m$ t. ~+ v7 S! d' G7 @; o) d - <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script> h( N+ K9 N8 S' K$ j
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>7 d p6 ^! Z' h$ R9 s
- <script type="text/javascript">9 Q6 t% g' X- N. T! k
- var ws = null;
) F! ^ ?/ K8 c$ D6 {. \% G9 p - $(function () {
3 J" S; H T0 n - if ('WebSocket' in window) {
$ X( ^# x' z3 D- L) [+ M - ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); ' {' n# [' o1 D- s$ K! J
- }
! N0 [- s: K+ c: O ^ - else if ('MozWebSocket' in window) { S6 i% b/ t# Y" Y! O
- ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");
, ~( T4 |2 e3 `- M* I3 y# o I' `2 ^# [ - }
$ K" H. W' I: v0 n7 E - else {
9 T: A3 G& U$ F( L - ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");( X; C$ t7 k/ h* @9 n* i( @
- }
; X4 V8 ^0 R( g: L( A - ws.onopen = function () {5 h% O( J& j7 }1 p/ z, b
3 E9 G& y8 y, X% t7 i# Z$ _. R- };/ J9 k, j' u4 H3 u% f) r3 a8 u
- ws.onmessage = function (event) {
" t0 n2 R* G3 G, F6 _8 T1 f - $ Q- j) [$ T i, a/ K) C
- };" q/ [& X/ X" f) T
- ws.onclose = function (event) {9 E! b7 m3 N! a/ N2 J
- 1 G+ F& r7 w0 k. v* O* ?' S Z, I
- };4 X6 q4 e/ I7 V4 c7 j! q& j
- });8 H5 H: {3 ^7 H- ^2 T1 M
- function stop(){/ |) i7 d; T9 f
- var message = $("#message").val();
; ~1 J7 {6 d/ I0 @ H2 Q: A - ws.send(message);
/ \. y; F' g9 i# i - }
( `. @; I* |. I - </script>; n1 J9 d$ i* I% Q
- </head>5 l" x: n& c6 x4 H1 d# Q! i
- <body class="keBody">, u3 b4 t/ }& [5 P3 P& M! A/ S5 v0 s
- 请输入提示信息: <textarea id="message"></textarea><br />
6 I% j0 S2 S# t% ~' y - <input type="button" value="开始" />( h* Z8 Q" U, Z% w2 V
- </body>
1 R6 M! q3 Q" Z1 ~$ g4 S - </html>
复制代码 % g1 s) J$ @ u1 k7 H2 q- Z' I9 s3 {3 h
webSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
* |5 v3 ~. W# s# _* Q/ J1 v- Y; o - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");+ b2 g5 g2 h6 G* T2 R \
- document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");
/ l% R' u7 }0 H2 d - var ws = null;# o7 _6 _4 ]' S/ |+ G. W. W( ^
- var basePath = "ws://127.0.0.1:8080/";
- V: U1 z% v' K; h9 G - if ('WebSocket' in window) {6 B' \: o& N/ u6 v1 C
- ws = new WebSocket(basePath+'webSocketServer'); |* ?8 V: c5 Q1 _ [5 W
- } 6 f: v: M- z7 ]2 r5 s W: f0 ^; M
- else if ('MozWebSocket' in window) {# t& Z$ D. r1 e4 P* s, Q0 z0 |
- ws = new MozWebSocket(basePath+"webSocketServer");
: a3 _9 X& H# F/ Z4 L0 s$ H - } " x' X" V; N; s' L% c: p
- else {
: V' D6 G. u1 x0 b6 w - ws = new SockJS(basePath+"sockjs/webSocketServer");
* Z$ Z4 _+ S) W; I - }
( W5 [; N# L: l; p' M - ws.onopen = function () {/ H' q% F' v5 E; N
0 w/ s6 _3 r ~- };% r8 {4 q) K: _) m. i0 H% T
- ws.onmessage = function (event) {
3 X/ k2 m% K6 I7 m! j9 y2 q - pop(event.data);) o' s7 v% `* [" X2 q
- };3 G/ ~4 Q+ m1 w+ l9 `
- ws.onclose = function (event) {. X7 d% C Y! |+ F4 Q6 _. G
- ws.close();
" V1 ^ U9 n7 i$ B" ^ - };3 J# h A( x) P* T0 g
- //提示信息2 {: V$ b' m! i6 P; a
- function pop(message){9 c& Z+ C" |0 M8 g$ A
- layer.alert(message);
! X6 L* ^# m/ P0 R - }
复制代码 4 Z; N7 h9 [8 L/ k n! C$ V
5、在项目头部引入
7 W1 h: c- [6 w/ }! O<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
3 D+ q. u6 p) W, e6 l5 {
8 r5 T8 k( b# p& j1 N8 X5 L
) n3 k. q/ {, {这时查看后台 会有以下信息 说明 引入成功。
8 O0 Z/ B2 D( U" @
+ p! y: p6 l$ e+ q
, {2 b1 B# ~5 v/ G8 f" d然后在打开页面 index.html 输入以下内容 点击开始即可。9 @5 V# r: b5 d' q+ u9 F
; P5 a; p5 Q( C2 Y) _3 J. @5 K* e+ |% O- M. Y
如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。
, c$ V' j8 W8 O* m1 M
, z% ^; ^6 w7 e: i/ G9 H6 K$ e- n' D: s5 H3 D- f2 \$ d
* J" o7 L @* o& \, v项目下载地址:; K) @/ R" k, _
3 w& q! q x; B k% h8 j8 y$ s# G' @) O0 |5 X
/ E2 R! Y% Y' J- f0 ~
5 g. D/ A! D1 j# L. \2 c
|
|