TA的每日心情![](source/plugin/dsu_paulsign/img/emot/shuai.gif) | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
一.WebSocket简单介绍
- T3 F3 A+ C- l, |8 m( n 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
3 q7 ?- S7 ~$ A7 Q 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
1 |/ M5 I7 n3 y7 N8 E 轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。$ h0 q0 W4 o* a1 o2 l
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。- f! U2 N. y' w F2 L+ v/ o
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。, s2 k4 l# q. }
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过javaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。7 k+ d; W' p$ b D B
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以上的版本才能运行。
! W8 B. ]$ Z2 |5 P, l
+ J. K+ B0 l) l" y1 O- u二、WebSocket协议介绍
3 m' p% `! D+ \4 X WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
" p% d! t' d, c5 D. G/ g
x8 e, n" f, v) L( L) ?+ P& a
握手过程:
; ?$ L4 J! c. m; _. V, jBrowser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
7 t+ V4 W. H1 _- F- p在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。
9 k t- n. ?( C6 v* \WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。- ?) n3 i0 ^7 h0 S( o! ?
Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
# s4 K& w% p1 f7 D7 A- `- L
. Y1 ~+ [' |- j三、Tomcat 7中的Websocket架构. H* L+ l1 U# V8 @* y, _$ D
) A8 R3 y- x4 H q如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
5 d5 h3 d: d+ J9 X2 R 握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。+ P9 {; ~+ o) U' j5 r& f
Tomcat中Websocket的处理流程如下:
+ s; l) z3 g3 J! Z/ u& Y* I接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
8 v& `; J0 j, V: F! n! cContainer中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
* d3 L) E' l# c& `* h服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。 G" s; t6 c9 O* s) B! g9 m9 _
如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
/ a2 s3 i/ ]- b7 W# p数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端。
: `$ C3 [+ m7 s, p5 o+ p4 [; t* a4 [
四、代码实现以及需求
; @9 N& [. m: N- @; O" M6 @
, K; b/ J6 Z3 a, C- L8 g1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。% d' B5 c! ~ ]& b! }
. G/ G" A3 E: h; M) u1 X2、相关环境 , Nginx、tomcat7、centos 6.59 c# F1 P) k, m6 L4 c3 g
* a: Q6 K" K) G& W! v2 a2 h3、项目框架,springMvc 4.0.6、layer/ `/ s. }& H4 A2 J1 G
4 c. c* p: j2 w3 G9 W2 }
4、代码实现:6 Z+ r' E! [3 t6 `; Q
$ {& I& q, ?" X I! M# K+ E% {* c6 S2 w
WebSocketConfig:- import websocket.handler.SystemWebSocketHandler;
* u6 N8 y0 x' `! j - @Configuration
+ K: b7 w* p; l. H, m4 L - @EnableWebMvc- B! c7 p5 Y! H8 |3 [7 ~
- @EnableWebSocket
: T8 u8 b+ T* i - public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{, w/ F; Z! z; |' V2 [
Q2 h- K- _( Q% ], {- @Override9 s4 m2 I" h2 J) U
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
( ?( W+ c; A; A& @, ^, x7 b' B$ q - registry.addHandler(systemWebSocketHandler(),"/webSocketServer");
5 u6 v& {& H4 I; c% s- {6 D/ P - registry.addHandler(systemWebSocketHandler(),"/sockjs/webSocketServer");
# {8 |$ l6 O; q7 h" M' W - }
( s3 w! ?- Q! H - @Bean
6 h8 ~' L* G; ~3 z ` - public WebSocketHandler systemWebSocketHandler(){. p) Q( v) @# i) @( ^/ f W
- return new SystemWebSocketHandler();6 |2 Y5 A5 e) O$ j) K7 w
- }7 _6 j8 ?0 e) R, s5 X F8 z: S
- }
复制代码 SystemWebSocketHandler: L; _! h3 W0 C2 L* y
- public class SystemWebSocketHandler extends TextWebSocketHandler {$ e4 n% E/ I0 s+ x& J! O6 X
- * z/ |1 ~8 S+ u
- 0 d3 d9 p' d$ `5 B% n! @
- private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;
, `7 B* }! S o6 {- f% Z - 4 t; W J( f/ X& [; h
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {* c( i% k! n; }1 W" }
- System.out.println("ConnectionEstablished");
: T3 Y" A% t) |7 D5 c - users.add(session);# D: h4 h; w: X) |: [ @
- System.out.println("当前用户"+users.size());4 H4 g' q2 T+ f, \' H" M- D _7 a
- }
$ k: b- g2 a, l- r8 c - /**
! i4 A. c4 u$ R b, }- @% A" Q5 b - * 在UI在用js调用websocket.send()时候,会调用该方法" J. ] ]4 x( }# ]6 a/ ?5 w
- * @Author 张志朋
d& I u! A0 ]; b4 ]2 r+ i - * @param session( [$ W) ^' ~4 g! {9 R
- * @param message
* b6 m* T) {3 O9 U! D! K# R - * @throws Exception
; ]: Z6 T# D. T! G4 L( p& j - * @Date 2016年3月4日5 }, H) X4 g3 [
- * 更新日志- [' _. A$ n0 J2 a$ O$ i- R* ?
- * 2016年3月4日 张志朋 首次创建& N2 Y3 b# \+ Q& F% ]; u
- *1 j8 U7 M9 K; y$ j K
- */; L- V+ t* d8 R p9 ~
- @Override
& e g& q( I7 b$ O+ T5 @ - protected void handleTextMessage(WebSocketSession session,
1 c5 u' x7 [- f% W" O6 A0 K - TextMessage message) throws Exception {; B% J. d- r1 p2 H4 u
- super.handleTextMessage(session, message);7 ~: q/ l" g9 M9 p4 w
- sendMessageToUsers(session,message);
3 Y7 G1 F; K' }0 E- b& R - }8 E+ u' @- ?6 e3 t
- @Override
7 K7 R3 l/ O6 B4 [+ S - public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {2 C1 P! {8 Z I$ V# }/ V
- if(session.isOpen()){ H# C9 x! K0 K. D0 Q$ `0 D
- session.close();
( S6 h3 _# M0 U- x; F( s4 B - }! s2 }; t5 l* @: _! M
- users.remove(session);
! y0 J; a4 c: G5 R8 {8 Z - }* ]# U( l' Q4 I) Z- u* K
* N2 j" l2 @, b' T/ |. r- @Override. P7 J5 N, F1 i: h5 e7 o7 b6 U
- public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
3 ? o1 g8 b7 L" k8 d - users.remove(session);
: e7 n8 i$ P; ^, J$ m5 D - }
" u2 ] r+ G d& t. @* r) l8 E - 4 k7 B. E0 n# S
- @Override
D( f) F& Z: [) e - public boolean supportsPartialMessages() {
# F! z$ r2 {5 f- h - return false;
+ ^2 D" D/ _% i4 Q5 g0 u6 }1 L - }$ U4 d5 ?# ~- k
- /**0 z+ j2 F9 H" v4 g' _% R, B/ y! D# K
- * 给所有在线用户发送消息0 a# S. p e5 i1 g( [; y, \6 M
- * @Author 张志朋0 B+ o6 V& I- r
- * @param message void5 F' n4 `, `" q' J. s9 r( j7 {
- * @Date 2016年3月4日' f; ~# ]; z; D/ l$ G
- * 更新日志3 I* k$ G1 |% R7 D5 a Q1 V
- * 2016年3月4日 张志朋 首次创建( ?2 G4 }' {) `8 A
- *
, ^2 Y$ t" d1 ?2 [5 X- v - */' M( |* r' s5 Y5 W: ~
- public void sendMessageToUsers(WebSocketSession session,TextMessage message) {
4 c1 O* p( t" t: K - for (WebSocketSession user : users) {
8 X& j: J5 S7 V) \% l- P - try {% y( C- @& N: X( ^2 [# X& C w
- if (user.isOpen()) {6 G; `( S# `" e: |
- user.sendMessage(message);2 ?% ~& T: P; Q( C6 J( u2 {7 P
- }
- S1 } ~9 q( i3 O. c: Y - } catch (IOException e) {9 m, T; j5 q. N/ p
- e.printStackTrace();; k2 J T' W; C: M5 E3 ~" D
- }
0 R3 l4 F" I" z% A9 m3 Y( n - }
$ Q F4 x2 J5 d, ?( f - }8 M! H7 }7 E" v
- }* l' d& C2 M9 ^( ?- A+ C" U
复制代码 信息输入 index.html:
" t4 I( ?- l( @ c. ?0 @1 r! [( | o, K- <html xmlns="http://www.w3.org/1999/xhtml">
8 K5 h; i/ E1 K5 F - <head>( H* t, e7 O+ y' q* ~1 y
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
" k) u: Y, B4 T8 ~" s7 U2 U - <title>请输入任意消息</title>6 L7 K: [* N8 O; w
- <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>% D$ b" s# Y' k" F7 G- A
- <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
( S7 h0 p: K7 L - <script type="text/javascript">
5 m I* ^0 `* {8 a$ S" j - var ws = null;
5 c4 Y4 l' a8 Y - $(function () {
1 C0 [+ \; P3 E - if ('WebSocket' in window) {7 W6 Z- o$ s" g3 K+ z& R
- ws = new WebSocket('ws://127.0.0.1:8080/webSocketServer'); * o- R- r: z, i. E0 d" U
- }
0 U( d+ a4 [& c - else if ('MozWebSocket' in window) {
$ A7 A, q+ G/ H' L1 ^ - ws = new MozWebSocket("ws://127.0.0.1:8080/webSocketServer");
6 k I, V" }$ ?3 C- W. Z7 U7 R; _ - } " m* d1 q" Q: m# Q4 q
- else {1 k5 S; u. j3 ~+ l" Q& A9 G& _
- ws = new SockJS("ws://127.0.0.1:8080/webSocketServer");
% Z6 Q( x) q/ d - }) Y, ]; W6 m k8 O
- ws.onopen = function () {
& |! X( @1 J) H: u z5 j# h- w
. ?$ Y5 E4 N5 Y6 W" v6 t- };) R( O5 q7 F3 g K x
- ws.onmessage = function (event) {& M( `" p+ X0 {0 n/ s7 x* F3 v8 D
( X; f3 ~" d, Z6 @, z' m- };8 W" [7 ?. ^" q6 L% W4 O
- ws.onclose = function (event) {) ~0 G5 l) \3 R- o* d' U& K
8 W* g% N2 G) Z% |' y- };/ d: f; _4 J) p. W' u2 V/ M
- });
* g2 l! E* a2 t U+ L - function stop(){
+ A5 }0 J# ^1 j% q - var message = $("#message").val();
# q- G1 B/ E9 T5 @ - ws.send(message);* V/ \0 [" Z2 } X7 B
- }
# L& W1 _5 L6 D4 T1 `, E2 X1 z$ Y - </script>
" }# s/ x: |# w! u1 F- X- E) [ - </head>
# \, I9 g# z6 f" @7 j2 x - <body class="keBody">; p7 U0 i- W- A
- 请输入提示信息: <textarea id="message"></textarea><br />
( l m$ ?' G/ d2 R) Q8 W - <input type="button" value="开始" />
; O/ Z* |% C7 t: t8 | - </body>& k2 n' T! r- d
- </html>
复制代码
+ t7 F/ x# p' G: mwebSocket.js 用于导入项目。- document.write("<script language=javascript src='http://127.0.0.1:8080/js/jquery-1.10.2.min.js'></script>");
9 v2 O9 z9 J' P6 F! x9 q - document.write("<script language=javascript src='http://127.0.0.1:8080/layer/layer.js'></script>");
& J1 U! y: v7 \9 B, n% B - document.write("<script language=javascript src='http://cdn.sockjs.org/sockjs-0.3.min.js'></script>");
! W l! C0 I% y+ q' E9 Y0 n - var ws = null;6 M B- M. N$ }7 B) ^& M
- var basePath = "ws://127.0.0.1:8080/";
' e$ d( D, n1 I2 ]8 V* q4 T# b - if ('WebSocket' in window) {0 a+ z5 T1 g) v! w$ L8 c
- ws = new WebSocket(basePath+'webSocketServer');
$ s0 ] z0 r; J. u! _* q - }
3 S1 r2 P2 u6 X - else if ('MozWebSocket' in window) {
Q. Y, ?4 J) l' L; {6 ? - ws = new MozWebSocket(basePath+"webSocketServer");6 O8 I. o- C* x% ^9 _
- }
8 e0 A: c! n3 u, Q$ T% |! F: ] - else {
3 h/ k- H' K! v5 Z. P5 ~ - ws = new SockJS(basePath+"sockjs/webSocketServer");
! d4 o- ~4 W* D0 Y7 [8 x8 y# M. P - }9 B/ `5 R& P4 j$ a- s
- ws.onopen = function () {) t8 U0 K& Y. j% Q) C7 n
- " R4 Z7 a" J5 V# k$ ^3 F* x/ u$ e
- };; F) i' G8 k4 o
- ws.onmessage = function (event) {
L, I# V& r3 n2 f' S$ ]3 E - pop(event.data);' T* R2 D/ h5 a) n7 c' n9 K
- };
# h8 g$ j: ?$ |0 ^: ` - ws.onclose = function (event) {$ x9 _ {( h3 [' w* z" N
- ws.close();
* Z$ c, ~3 `/ ? ?6 e1 n9 K( d8 f - };: p, I( c/ |: b9 }
- //提示信息" T$ m# E: w: w% e! H8 k" V
- function pop(message){& m9 q) i! f1 J1 ~% Z" G8 X+ z- Y6 F0 `2 e
- layer.alert(message);; v1 _' k7 Q5 u) y: n
- }
复制代码
7 A# q$ U. `# G5、在项目头部引入
; x0 C& X* C+ N0 s- R/ Z# o' u<script language=javascript src='http://127.0.0.1:8080/webSocket.js '></script>
( h' }* A) ^6 W9 @. f- ]9 c, ?7 V( J( V( c% z S7 l
# t$ U- M6 a: W$ a+ ?
这时查看后台 会有以下信息 说明 引入成功。6 t# C! N& z9 X/ b2 r
- a: A" r& ^+ V, X0 U) f1 T( b& p1 r) }
然后在打开页面 index.html 输入以下内容 点击开始即可。5 w! z' ?) Z9 k! e
. Z3 ?7 K! ^$ `& O: E x' O- h
$ J L6 `8 a3 M+ v如果在网站出现一下提示说明配置成功,这时候所有网站登录用户都可以收到此信息。/ {( V( u; }* C6 f6 ?1 |- |& |
' s$ v9 [, E6 i
# B. J6 A& o _/ z0 e: V) t$ k
- k$ q: [* H- Q" ]项目下载地址:2 A. _& O( w5 e! W+ ~" G6 z: _
( N+ K; k! A8 C8 H5 {5 G
! t# \/ _3 v7 F; z$ H( P/ ~, P, ^) ?+ v
9 Q; R, Y/ n* D) Y7 v' E# J |
|