├── .gitignore ├── README.md ├── example-websocket ├── pom.xml └── src │ └── main │ ├── java │ └── me │ │ └── w1992wishes │ │ └── example_websocket │ │ ├── message │ │ ├── Message.java │ │ ├── MessagePusher.java │ │ └── MessagePusherImpl.java │ │ ├── socketserver │ │ ├── domain │ │ │ ├── SocketRequest.java │ │ │ └── SocketResponse.java │ │ └── websocket │ │ │ ├── MyWebsocket.java │ │ │ ├── MyWebsocketDispatcher.java │ │ │ ├── MyWebsocketHandler.java │ │ │ └── MyWebsocketServlet.java │ │ ├── timer │ │ └── MessageTimer.java │ │ └── util │ │ └── ApplicationContextUtil.java │ ├── resources │ ├── applicationContext.xml │ └── log4j.properties │ └── webapp │ ├── WEB-INF │ └── web.xml │ ├── index.jsp │ └── js │ └── MyWebsocket.js ├── jetty-websocket ├── pom.xml └── src │ └── main │ ├── java │ └── me │ │ └── w1992wishes │ │ └── jetty_websocket │ │ ├── JettyWebSocket.java │ │ └── JettyWebSocketServlet.java │ └── webapp │ ├── WEB-INF │ └── web.xml │ └── index.jsp ├── pom.xml └── tomcat-websocket ├── pom.xml └── src └── main ├── java └── me │ └── w1992wishes │ └── tomcat_websocket │ └── TomcatWebSocket.java └── webapp ├── WEB-INF └── web.xml └── index.jsp /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven # 2 | target/ 3 | 4 | # IDEA # 5 | .idea/ 6 | *.iml 7 | 8 | # Eclipse # 9 | .settings/ 10 | .metadata/ 11 | .classpath 12 | .project 13 | Servers/ 14 | 15 | # log # 16 | logs/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Websocket消息推送 2 | 3 | 本篇结构: 4 | 5 | - 背景 6 | - HTTP协议特点 7 | - 消息推送方案 8 | - Websocket简介 9 | - Websocket实例 10 | 11 | ## 一、背景 12 | 13 | HTTP协议的无状态和被动性,使得B/S架构的服务器主动推送消息给浏览器比较困难,而通用的一些解决方案又有各种各样的问题,比如:ajax轮询会有很多无用的请求,浪费宽带;基于Flash的消息推送又有Flash支持不好,无法自动穿越防火墙等问题...... 14 | 15 | Websocket就是在这种情况下出现的一个协议。 16 | 17 | ## 二、HTTP协议特点 18 | 19 | B/S架构的系统多使用HTTP协议,HTTP协议的特点: 20 | 21 | **1、简单快速** 22 | 23 | 客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。 24 | 25 | **2、灵活** 26 | 27 | HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。 28 | 29 | **3、无状态** 30 | 31 | 即无状态协议。这指的是,HTTP协议不对请求和响应之间的通信状态进行保存。所以使用HTTP协议,每当有新的请求发送,就会有对应的新响应产生。这样做的好处是更快地处理大量事务,确保协议的可伸缩性。 32 | 33 | 然而,随着时间的推移,人们发现静态的HTML着实无聊而乏味,增加动态生成的内容才会令Web应用程序变得更加有用。于是乎,HTML的语法在不断膨胀,其中最重要的是增加了表单(Form);客户端也增加了诸如脚本处理、DOM处理等功能;对于服务器,则相应的出现了CGI(Common Gateway Interface)以处理包含表单提交在内的动态请求。 34 | 35 | 在这种客户端与服务器进行动态交互的Web应用程序出现之后,HTTP无状态的特性严重阻碍了这些交互式应用程序的实现,毕竟交互是需要承前启后的,简单的购物车程序也要知道用户到底在之前选择了什么商品。于是,两种用于保持HTTP状态的技术就应运而生了,一个是Cookie,而另一个则是Session。 36 | 37 | **4、持久连接** 38 | 39 | HTTP协议初试版本中,每进行一次HTTP通信就要断开一次TCP连接。 40 | 41 | 早期这么做的原因是HTTP协议产生于互联网,因此服务器需要处理同时面向全世界数十万、上百万客户端的网页访问,但每个客户端(即浏览器)与服务器之间交换数据的间歇性较大(即传输具有突发性、瞬时性),并且网页浏览的联想性、发散性导致两次传送的数据关联性很低,如果按照上面的方式则需要在服务器端开的进程和句柄数目都是不可接受的,大部分通道实际上会很空闲、无端占用资源。因此HTTP的设计者有意利用这种特点将协议设计为请求时建连接、请求完释放连接,以尽快将资源释放出来服务其他客户端。 42 | 43 | 但是当浏览器请求一个包含多张图片的HTML页面时,会增加通信量的开销。为了解决这个问题,HTTP/1.1相处了持久连接(HTTP keep-alive)方法。其特点是,只要任意一端没有明确提出断开连接,则保持TCP连接状态,在请求首部字段中的Connection: keep-alive即为表明使用了持久连接。 44 | 45 | 这样一来,客户端和服务器之间的HTTP连接就会被保持,不会断开(超过Keep-Alive规定的时间,意外断电等情况除外),当客户端发送另外一个请求时,就使用这条已经建立的连接。 46 | 47 | **5、支持B/S及C/S模式** 48 | 49 | > PS:这节内容多来自博文:[http协议特点](http://blog.csdn.net/u014005836/article/details/51129655) 50 | 51 | ### 三、消息推送方案 52 | 53 | ### 3.1、HTTP使得服务器无法主动推送消息 54 | 55 | HTTP的生命周期通过Request来界定,也就是一个Request,一个Response,在HTTP1.0中,这次HTTP请求就结束了。 56 | 57 | 在HTTP1.1中进行了改进,添加了一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是在HTTP中永远是Request=Response,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。 58 | 59 | 反映在日常生活中就是: 60 | 61 | 客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现。 62 | 63 | 这种机制对于信息变化不是特别频繁的应用可以良好支撑,但对于实时要求高、海量并发的应用来说显得捉襟见肘,在当前业界移动互联网蓬勃发展的趋势下,高并发与用户实时响应是Web应用经常面临的问题,比如金融证券的实时信息、Web导航应用中的地理位置获取、社交网络的实时消息推送等。 64 | 65 | ### 3.2、实现服务器主动推送消息 66 | 67 | 有问题出现,就会有解决方案: 68 | 69 | **1.Ajax轮询** 70 | 71 | 其原理简单易懂,就是客户端定时向服务器发送Ajax请求,询问服务器是否有新信息。 72 | 73 | 但它的问题也很明显:当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,带来很多无谓请求,浪费带宽,效率低下。 74 | 75 | 适于小型应用。 76 | 77 | **2.Flash Socket** 78 | 79 | AdobeFlash通过自己的Socket实现完成数据交换,再利用Flash暴露出相应的接口给JavaScript调用,从而达到实时传输目的。此方式比轮询要高效,且因为Flash安装率高,应用场景广泛。 80 | 81 | 但是移动互联网终端上Flash的支持并不好:IOS系统中无法支持Flash,Android虽然支持Flash但实际的使用效果差强人意,且对移动设备的硬件配置要求较高。2012年Adobe官方宣布不再支持Android4.1+系统,宣告了Flash在移动终端上的死亡。 82 | 83 | **3.长轮询(long poll),长连接** 84 | 85 | keep-alive connection是指在一次TCP连接中完成多个HTTP请求,但是对每个请求仍然要单独发HTTP header;长轮询是指从客户端(一般就是浏览器)不断主动的向服务器发 HTTP 请求查询是否有新数据。这两种模式有一个共同的缺点,就是除了真正的数据部分外,服务器和客户端还要大量交换 HTTP header,信息交换效率很低。它们建立的“长连接”都是伪.长连接,只不过好处是不需要对现有的HTTP server和浏览器架构做修改就能实现。 86 | 87 | 还有就是下面要说的Websocket。 88 | 89 | > PS:关于Ajax轮询,长轮询等解释来自知乎: 90 | https://www.zhihu.com/question/20215561 91 | 92 | ## 四、Websocket简介 93 | 94 | ### 4.1、Websocket是什么 95 | 96 | WebSocket是HTML5下一种新的协议。 97 | 98 | 它是一个新的基于TCP的应用层协议,只需要一次连接,以后的数据不需要重新建立连接,可以直接发送,它是基于TCP的,属于和HTTP相同的地位。 99 | 100 | ![](http://p5maw5o6h.bkt.clouddn.com/20180315_websocket_01.png) 101 | 102 | 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。 103 | 104 | ### 4.2、Websocket的特点 105 | 106 | - 建立在 TCP 协议之上,服务器端的实现比较容易。 107 | - 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 108 | - 数据格式比较轻量,性能开销小,通信高效。 109 | - 可以发送文本,也可以发送二进制数据。 110 | - 没有同源限制,客户端可以与任意服务器通信。 111 | - 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。 112 | 113 | ### 4.3、Websocket的优势 114 | 115 | 1. 是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。 116 | 2. HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。 117 | 118 | ## 五、Websocket实例 119 | 120 | 后台消息推送是很多系统中重要的功能,我现在工作的项目是一个网管项目,当网管服务器收到设备发过来的告警时,需要将告警信息推送到客户端,这里面就用到了消息推送。 121 | 122 | 之前网管的推送是用flash实现的,但flash经常出现被禁用等问题,导致客户端收不到服务器推送来的消息,加上很多浏览器对flash已经不再更新,了解了Websocket的优势后,于是将后台的消息推送用Websocket实现。 123 | 124 | 网管这边是用Jetty做服务器,比较新的Jetty也已经兼容了Websocket,所以就采用了Jetty的api实现。 125 | 126 | 关于如何实现这些api就不介绍了,网上有不少例子,直接上代码(源码可以在https://github.com/w1992wishes/java-websocket的example-websocket模块找到): 127 | 128 | ### 5.1、前台代码 129 | 130 | 将通用的Websocket连接放到MyWebsocket.js中: 131 | 132 | ``` 133 | function MyWebSocket(serviceName, config) { 134 | this.serviceName = serviceName; 135 | this.config = config; 136 | 137 | this.connect(); 138 | } 139 | 140 | MyWebSocket.prototype.connect = function() { 141 | var _me = this; 142 | var serverIp = location.hostname; 143 | var _config = _me.config; 144 | 145 | // 需要判断是否支持websocket,如果不支持,使用flash版本的 146 | if (typeof WebSocket != 'undefined') { 147 | initWebSocket(); 148 | _me.supportWebSocket = true; 149 | } else { 150 | console.log("not support Websocket"); 151 | _me.supportWebSocket = false; 152 | return; 153 | } 154 | 155 | function initWebSocket() { 156 | var url = 'ws://localhost:8080/example/websocket/' + _me.serviceName; 157 | 158 | var firstParam = true; 159 | if(_config.params) { 160 | for(var key in _config.params) { 161 | if(firstParam) { 162 | url += '?' + key + '=' + _config.params[key]; 163 | firstParam = false; 164 | } else { 165 | url += '&' + key + '=' + _config.params[key]; 166 | } 167 | } 168 | } 169 | var socket = new WebSocket(url); 170 | _me.socket = socket; 171 | 172 | socket.onopen = function() { 173 | _config.onopen(); 174 | 175 | // 开启心跳检测,以免一段时间后收不到消息自动失联 176 | heartbeat_timer = setInterval(function () { 177 | keepalive(socket) 178 | }, 10000); 179 | 180 | function keepalive(socket) { 181 | socket.send('~H#B~'); 182 | } 183 | }; 184 | 185 | socket.onmessage = function (message) { 186 | _config.onmessage(message.data); 187 | }; 188 | 189 | socket.onclose = function() { 190 | _config.onclose(); 191 | clearInterval(heartbeat_timer); 192 | }; 193 | 194 | socket.onerror = function(err) { 195 | _config.onerror(err); 196 | }; 197 | } 198 | } 199 | 200 | MyWebSocket.prototype.send = function(message) { 201 | if(this.supportWebSocket) { 202 | this.socket.send(JSON.stringify(message)); 203 | } else { 204 | this.socket.sendRequest(this.serviceName, message, true); 205 | } 206 | } 207 | 208 | MyWebSocket.prototype.close = function() { 209 | if(this.supportWebSocket) { 210 | this.socket.close(); 211 | } else { 212 | this.socket.disconnect(); 213 | } 214 | } 215 | 216 | MyWebSocket.prototype.reconnect = function() { 217 | this.close(); 218 | this.connect(); 219 | } 220 | ``` 221 | 222 | 然后在需要消息推送的页面引入该js,并做相应配置: 223 | 224 | ``` 225 | <%@ page language="java" pageEncoding="UTF-8" %> 226 | 227 | 228 | 229 | 消息推送 230 | 234 | 235 | 236 | 273 | 274 | 275 |
276 | 277 | 278 | ``` 279 | 280 | 这样前台代码就完成了。 281 | 282 | ### 5.2、后台代码 283 | 284 | 继承jetty的WebSocketServlet,实现其configure()方法,给WebSocketServletFactory提供一个WebSocketListener的实现类。这样在收到websocket请求后,会生成这个实现类的一个实例。通过这个实例,可以与前端进行交互。 285 | 286 | ```java 287 | public class MyWebsocketServlet extends WebSocketServlet { 288 | @Override 289 | public void configure(WebSocketServletFactory webSocketServletFactory) { 290 | webSocketServletFactory.register(MyWebsocket.class); 291 | } 292 | } 293 | ``` 294 | 295 | 既然是一个Servlet,就需要在web.xml中配置(Servlet 3.0中可以用注解的方式): 296 | 297 | ``` 298 | 299 | WebSocketServlet 300 | me.w1992wishes.example_websocket.socketserver.websocket.MyWebsocketServlet 301 | 1 302 | 303 | 304 | WebSocketServlet 305 | /websocket/* 306 | 307 | ``` 308 | 309 | MyWebsocket(该类会在每个Websocket请求建立时都重新实例化一个)实现了WebSocketListener,需要实现几个事件监听方法,在对应事件发生时将被触发。: 310 | 311 | ```java 312 | public class MyWebsocket implements WebSocketListener { 313 | //当一个新的websocket连接时,不会把这个覆盖,进去看源代码会找到原因 314 | //因为每次请求过来,会调用MyWebsocketServlet的service方法 315 | //而service方法会调用到factory(也就是MyWebsocket注册进去的那个类)的createWebSocket方法 316 | //该方法每次通过反射初始化一个MyWebsocket 317 | private Session session; 318 | 319 | @Override 320 | public void onWebSocketBinary(byte payload[], int offset, int len) { 321 | 322 | } 323 | 324 | //连接关闭触发 325 | @Override 326 | public void onWebSocketClose(int statusCode, String reason) { 327 | MyWebsocketDispatcher websocketDispatcher = getWebsocketDispatcher(); 328 | websocketDispatcher.dispatcherOnWebSocketClose(session); 329 | } 330 | 331 | //连接开启触发 332 | @Override 333 | public void onWebSocketConnect(Session session) { 334 | this.session = session; 335 | MyWebsocketDispatcher websocketDispatcher = getWebsocketDispatcher(); 336 | websocketDispatcher.dispatcherOnWebSocketConnect(session); 337 | } 338 | 339 | @Override 340 | public void onWebSocketError(Throwable throwable) { 341 | } 342 | 343 | //接收到消息触发 344 | @Override 345 | public void onWebSocketText(String message) { 346 | MyWebsocketDispatcher websocketDispatcher = getWebsocketDispatcher(); 347 | websocketDispatcher.dispatcherOnWebSocketText(session, message); 348 | } 349 | 350 | private MyWebsocketDispatcher getWebsocketDispatcher() { 351 | ApplicationContext context = ApplicationContextUtil.getContext(); 352 | MyWebsocketDispatcher webSocketDispatcher = (MyWebsocketDispatcher) context.getBean("myWebsocketDispatcher"); 353 | return webSocketDispatcher; 354 | } 355 | } 356 | ``` 357 | 358 | ApplicationContextUtil帮助不被spring管理的实例获取applicationContext,进而获取bean容器中的MyWebsocketDispatcher。 359 | 360 | ``` 361 | @Component 362 | public class ApplicationContextUtil implements ApplicationContextAware { 363 | 364 | private static ApplicationContext applicationContext; 365 | 366 | public static ApplicationContext getContext() { 367 | return applicationContext; 368 | } 369 | 370 | @Override 371 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 372 | ApplicationContextUtil.applicationContext = applicationContext; 373 | } 374 | } 375 | ``` 376 | 377 | WebSocketDispatcher是被spring容器管理的分发器,到了这里,就可以与业务层打交道了。当一个Websocket请求到达,会解析该请求,获取前台传来的beanName,然后分发器将该请求放入一个Map,并分发到相应的业务类并触发相应的Websocket事件。 378 | 379 | ``` 380 | @Service("myWebsocketDispatcher") 381 | public class MyWebsocketDispatcher { 382 | private static final Logger LOGGER = LoggerFactory.getLogger(MyWebsocketDispatcher.class); 383 | 384 | //这个map是维持所有的websocket连接,因为消息要推送到所有的客户端 385 | private Map socketRequestMap = new ConcurrentHashMap(); 386 | 387 | public void dispatcherOnWebSocketConnect(Session session){ 388 | SocketRequest socketRequest = initWebSocketRequest(session); 389 | socketRequestMap.put(session, socketRequest); 390 | 391 | // 找到对应的业务service 392 | MyWebsocketHandler handler = getWebsocketHandler(socketRequest.getRequestExecBeanName()); 393 | handler.handleOnWebSocketConnect(socketRequest); 394 | } 395 | 396 | public void dispatcherOnWebSocketText(Session session, String message){ 397 | SocketRequest socketRequest = socketRequestMap.get(session); 398 | 399 | if ("~H#B~".equals(message)) { 400 | // 心跳报文,忽略 401 | return; 402 | } 403 | 404 | JSONObject json = JSONObject.parseObject(message); 405 | for (String key : json.keySet()) { 406 | socketRequest.addRequestParameter(key, json.getString(key)); 407 | } 408 | 409 | try { 410 | // 找到对应的业务service 411 | MyWebsocketHandler handler = getWebsocketHandler(socketRequest.getRequestExecBeanName()); 412 | handler.handleOnWebSocketText(socketRequest); 413 | } catch (Exception e) { 414 | LOGGER.error("", e); 415 | } 416 | 417 | } 418 | 419 | public void dispatcherOnWebSocketClose(Session session) { 420 | SocketRequest socketRequest = socketRequestMap.get(session); 421 | try { 422 | // 找到对应的业务service 423 | MyWebsocketHandler handler = getWebsocketHandler(socketRequest.getRequestExecBeanName()); 424 | handler.handleOnWebSocketClose(socketRequest); 425 | } catch (Exception e) { 426 | LOGGER.error("", e); 427 | } 428 | socketRequestMap.remove(session); 429 | } 430 | 431 | private SocketRequest initWebSocketRequest(Session session) { 432 | SocketRequest socketRequest = new SocketRequest(); 433 | SocketResponse response = new SocketResponse(); 434 | response.setSession(session); 435 | socketRequest.setResponse(response); 436 | 437 | URI uri = session.getUpgradeRequest().getRequestURI(); 438 | socketRequest.setRequestHost(uri.getHost()); 439 | 440 | String path = session.getUpgradeRequest().getRequestURI().getPath(); 441 | if (path.contains("/websocket/")) { 442 | path = path.substring(path.indexOf("/websocket/") + "/websocket/".length()); 443 | String beanName = path.split("/")[0]; 444 | socketRequest.setRequestExecBeanName(beanName); 445 | 446 | String queryStr = session.getUpgradeRequest().getRequestURI().getQuery(); 447 | if (queryStr != null && queryStr != "") { 448 | String[] params = queryStr.split("&"); 449 | for (String param : params) { 450 | if (!param.contains("=")) { 451 | continue; 452 | } 453 | int c = param.indexOf("="); 454 | socketRequest.addRequestParameter(param.substring(0, c), param.substring(c + 1)); 455 | } 456 | } 457 | } else { 458 | return null; 459 | } 460 | return socketRequest; 461 | 462 | } 463 | 464 | private MyWebsocketHandler getWebsocketHandler(String beanName){ 465 | return ApplicationContextUtil.getContext().getBean(beanName, MyWebsocketHandler.class); 466 | } 467 | } 468 | ``` 469 | 470 | WebSocketDispatcher的重点在于SocketRequest和SocketResponse的创建,由它去与具体的业务类打交道,可以降低Jetty api对业务类的侵入,降低耦合。 471 | 472 | ``` 473 | public class SocketRequest { 474 | private String requestExecBeanName; 475 | private final Map params; 476 | private SocketResponse response; 477 | private String requestHost; 478 | 479 | 480 | public SocketRequest(){ 481 | params = new HashMap<>(); 482 | } 483 | 484 | /** 485 | * 添加一个请求参数 486 | * 487 | * @param key 488 | * @param value 489 | */ 490 | public void addRequestParameter(String key, String value) { 491 | params.put(key, value); 492 | response.addRequestParameter(key, value); 493 | } 494 | 495 | public int getInt(String paramName) { 496 | return Integer.parseInt(params.get(paramName)); 497 | } 498 | 499 | public String getString(String paramName) { 500 | return params.get(paramName); 501 | } 502 | 503 | public String getRequestExecBeanName() { 504 | return requestExecBeanName; 505 | } 506 | 507 | public void setRequestExecBeanName(String requestExecBeanName) { 508 | this.requestExecBeanName = requestExecBeanName; 509 | } 510 | 511 | public SocketResponse getResponse() { 512 | return response; 513 | } 514 | 515 | public void setResponse(SocketResponse response) { 516 | this.response = response; 517 | } 518 | 519 | public String getRequestHost() { 520 | return requestHost; 521 | } 522 | 523 | public void setRequestHost(String requestHost) { 524 | this.requestHost = requestHost; 525 | } 526 | 527 | } 528 | 529 | public class SocketResponse { 530 | private Session session; 531 | private boolean closed; 532 | private Map params = new HashMap(); 533 | 534 | public void write(String message) throws IOException { 535 | session.getRemote().sendString(message); 536 | } 537 | 538 | public void flush() throws IOException { 539 | session.getRemote().flush(); 540 | } 541 | 542 | /** 543 | * 添加一个请求参数 544 | * 545 | * @param key 546 | * @param value 547 | */ 548 | public void addRequestParameter(String key, String value) { 549 | params.put(key, value); 550 | } 551 | 552 | public int getInt(String paramName) { 553 | return Integer.parseInt(params.get(paramName)); 554 | } 555 | 556 | public long getLong(String paramName) { 557 | return Long.parseLong(params.get(paramName)); 558 | } 559 | 560 | public String getString(String paramName) { 561 | return params.get(paramName); 562 | } 563 | 564 | public Session getSession() { 565 | return session; 566 | } 567 | 568 | public void setSession(Session session) { 569 | this.session = session; 570 | } 571 | 572 | public boolean isClosed() { 573 | if (!closed) { 574 | return !session.isOpen(); 575 | } 576 | return closed; 577 | } 578 | 579 | public void setClosed(boolean closed) { 580 | this.closed = closed; 581 | } 582 | } 583 | ``` 584 | 585 | 最后就来到具体业务代码: 586 | 587 | ``` 588 | @Service("messagePusher") 589 | public class MessagePusherImpl implements MyWebsocketHandler, MessagePusher{ 590 | 591 | private static final Logger LOGGER = LoggerFactory.getLogger(MessagePusherImpl.class); 592 | 593 | private final Map connections = new ConcurrentHashMap<>(); 594 | private final Map hosts = new ConcurrentHashMap<>(); 595 | private BlockingQueue dataQueue = null; 596 | private ExecutorService executorService; 597 | 598 | @PostConstruct 599 | public void initialize() { 600 | dataQueue = new ArrayBlockingQueue(1000); 601 | executorService = Executors.newSingleThreadExecutor(); 602 | // ------开启一个专用线程用于消息广播推送------// 603 | executorService.execute(new Runnable() { 604 | @Override 605 | public void run() { 606 | Thread.currentThread().setName("MessagePusherThread"); 607 | // ---------如果告警队列对象存在,线程就一直执行----// 608 | while (true) { 609 | // 获取并移除此告警队列的头部,在元素变得可用之前一直等待。 610 | try { 611 | Message message = dataQueue.take(); 612 | if (!connections.isEmpty()) { 613 | Collection responses = connections.values(); 614 | String messageStr = JSONObject.toJSONString(message); 615 | LOGGER.info(messageStr); 616 | String jconnectID = message.getJconnectID(); 617 | if (jconnectID != null) {//单播 618 | SocketResponse response = connections.get(jconnectID); 619 | if (response == null || response.isClosed()) { 620 | connections.remove(jconnectID); 621 | continue; 622 | } 623 | response.write(messageStr); 624 | } else {//广播 625 | for (Iterator it = responses.iterator(); it.hasNext();) { 626 | SocketResponse $response = it.next(); 627 | if ($response.isClosed()) { 628 | it.remove(); 629 | continue; 630 | } 631 | $response.write(messageStr); 632 | } 633 | } 634 | } 635 | } catch (InterruptedException e) { 636 | } catch (Exception ex){ 637 | LOGGER.error("Send message by pusher.", ex); 638 | } 639 | } 640 | } 641 | }); 642 | } 643 | 644 | /** 645 | * 推送消息到前端页面 646 | * 647 | * @param msg 648 | */ 649 | @Override 650 | public void sendMessage(Message msg) { 651 | try { 652 | dataQueue.add(msg); 653 | if (LOGGER.isDebugEnabled()) { 654 | LOGGER.debug("sendMessage dataQueue [" + dataQueue.size() + "]."); 655 | } 656 | } catch (Exception e) { 657 | throw e; 658 | } 659 | } 660 | 661 | @Override 662 | public void handleOnWebSocketText(SocketRequest socketRequest) { 663 | 664 | } 665 | 666 | @Override 667 | public void handleOnWebSocketConnect(SocketRequest socketRequest) { 668 | String connectID = socketRequest.getString("JCONNECTID"); 669 | connections.put(connectID, socketRequest.getResponse()); 670 | hosts.put(connectID, socketRequest.getRequestHost()); 671 | } 672 | 673 | @Override 674 | public void handleOnWebSocketClose(SocketRequest socketRequest) { 675 | 676 | } 677 | } 678 | ``` 679 | 680 | 到这里,基本代码已经结束,但前后台建立Websocket连接后,MessagePusherImpl中的dataQueue并没有消息,是空队列,因此代码会一直阻塞。 681 | 682 | 为此做了一个简单的定时器,模拟向dataQueue中添加消息。 683 | 684 | ``` 685 | @Service 686 | public class MessageTimer { 687 | 688 | private int i; 689 | 690 | @Autowired 691 | private MessagePusher messagePusher; 692 | 693 | @PostConstruct 694 | public void initialize(){ 695 | Timer timer = new Timer(); 696 | timer.schedule(new TimerTask() { 697 | @Override 698 | public void run() { 699 | i++; 700 | Message message = new Message(); 701 | message.setData("a test message, id is " + i); 702 | messagePusher.sendMessage(message); 703 | } 704 | }, new Date(), 10000); 705 | } 706 | 707 | } 708 | ``` 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | -------------------------------------------------------------------------------- /example-websocket/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | java-websocket 5 | me.1992wishes.websocket 6 | 1.0-SNAPSHOT 7 | 8 | 4.0.0 9 | me.1992wishes.websocket 10 | example-websocket 11 | war 12 | 1.0-SNAPSHOT 13 | example-websocket Maven Webapp 14 | http://maven.apache.org 15 | 16 | 17 | org.eclipse.jetty.websocket 18 | websocket-server 19 | 9.4.6.v20170531 20 | provided 21 | 22 | 23 | 24 | example-websocket 25 | 26 | 27 | 28 | org.eclipse.jetty 29 | jetty-maven-plugin 30 | 9.4.6.v20170531 31 | 32 | 33 | 8080 34 | 35 | 3 36 | 37 | /example 38 | 39 | automatic 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/message/Message.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.message; 2 | 3 | /** 4 | * Created by w1992wishes 5 | * on 2018/3/16. 6 | */ 7 | public class Message { 8 | 9 | // 消息实体 10 | private Object data; 11 | // 单播时使用.如果希望消息只发送到某一个客户端,则在消息中配置此值,这个值可以需要从前端请求中携带过来 12 | private String jconnectID; 13 | 14 | public Object getData() { 15 | return data; 16 | } 17 | 18 | public void setData(Object data) { 19 | this.data = data; 20 | } 21 | 22 | public String getJconnectID() { 23 | return jconnectID; 24 | } 25 | 26 | public void setJconnectID(String jconnectID) { 27 | this.jconnectID = jconnectID; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/message/MessagePusher.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.message; 2 | 3 | /** 4 | * Created by w1992wishes 5 | * on 2018/3/16. 6 | */ 7 | public interface MessagePusher { 8 | 9 | void sendMessage(Message msg); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/message/MessagePusherImpl.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.message; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import me.w1992wishes.example_websocket.socketserver.domain.SocketRequest; 5 | import me.w1992wishes.example_websocket.socketserver.domain.SocketResponse; 6 | import me.w1992wishes.example_websocket.socketserver.websocket.MyWebsocketHandler; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.util.Collection; 13 | import java.util.Iterator; 14 | import java.util.Map; 15 | import java.util.concurrent.*; 16 | 17 | /** 18 | * Created by w1992wishes 19 | * on 2018/3/16. 20 | */ 21 | @Service("messagePusher") 22 | public class MessagePusherImpl implements MyWebsocketHandler, MessagePusher{ 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(MessagePusherImpl.class); 25 | 26 | private final Map connections = new ConcurrentHashMap<>(); 27 | private final Map hosts = new ConcurrentHashMap<>(); 28 | private BlockingQueue dataQueue = null; 29 | private ExecutorService executorService; 30 | 31 | @PostConstruct 32 | public void initialize() { 33 | dataQueue = new ArrayBlockingQueue(1000); 34 | executorService = Executors.newSingleThreadExecutor(); 35 | // ------开启一个专用线程用于消息广播推送------// 36 | executorService.execute(new Runnable() { 37 | @Override 38 | public void run() { 39 | Thread.currentThread().setName("MessagePusherThread"); 40 | // ---------如果告警队列对象存在,线程就一直执行----// 41 | while (true) { 42 | // 获取并移除此告警队列的头部,在元素变得可用之前一直等待。 43 | try { 44 | Message message = dataQueue.take(); 45 | if (!connections.isEmpty()) { 46 | Collection responses = connections.values(); 47 | String messageStr = JSONObject.toJSONString(message); 48 | LOGGER.info(messageStr); 49 | String jconnectID = message.getJconnectID(); 50 | if (jconnectID != null) {//单播 51 | SocketResponse response = connections.get(jconnectID); 52 | if (response == null || response.isClosed()) { 53 | connections.remove(jconnectID); 54 | continue; 55 | } 56 | response.write(messageStr); 57 | } else {//广播 58 | for (Iterator it = responses.iterator(); it.hasNext();) { 59 | SocketResponse $response = it.next(); 60 | if ($response.isClosed()) { 61 | it.remove(); 62 | continue; 63 | } 64 | $response.write(messageStr); 65 | } 66 | } 67 | } 68 | } catch (InterruptedException e) { 69 | } catch (Exception ex){ 70 | LOGGER.error("Send message by pusher.", ex); 71 | } 72 | } 73 | } 74 | }); 75 | } 76 | 77 | /** 78 | * 推送消息到前端页面 79 | * 80 | * @param msg 81 | */ 82 | @Override 83 | public void sendMessage(Message msg) { 84 | try { 85 | dataQueue.add(msg); 86 | if (LOGGER.isDebugEnabled()) { 87 | LOGGER.debug("sendMessage dataQueue [" + dataQueue.size() + "]."); 88 | } 89 | } catch (Exception e) { 90 | throw e; 91 | } 92 | } 93 | 94 | @Override 95 | public void handleOnWebSocketText(SocketRequest socketRequest) { 96 | 97 | } 98 | 99 | @Override 100 | public void handleOnWebSocketConnect(SocketRequest socketRequest) { 101 | String connectID = socketRequest.getString("JCONNECTID"); 102 | connections.put(connectID, socketRequest.getResponse()); 103 | hosts.put(connectID, socketRequest.getRequestHost()); 104 | } 105 | 106 | @Override 107 | public void handleOnWebSocketClose(SocketRequest socketRequest) { 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/socketserver/domain/SocketRequest.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.socketserver.domain; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Created by w1992wishes 8 | * on 2018/3/16. 9 | */ 10 | public class SocketRequest { 11 | private String requestExecBeanName; 12 | private final Map params; 13 | private SocketResponse response; 14 | private String requestHost; 15 | 16 | 17 | public SocketRequest(){ 18 | params = new HashMap<>(); 19 | } 20 | 21 | /** 22 | * 添加一个请求参数 23 | * 24 | * @param key 25 | * @param value 26 | */ 27 | public void addRequestParameter(String key, String value) { 28 | params.put(key, value); 29 | response.addRequestParameter(key, value); 30 | } 31 | 32 | public int getInt(String paramName) { 33 | return Integer.parseInt(params.get(paramName)); 34 | } 35 | 36 | public String getString(String paramName) { 37 | return params.get(paramName); 38 | } 39 | 40 | public String getRequestExecBeanName() { 41 | return requestExecBeanName; 42 | } 43 | 44 | public void setRequestExecBeanName(String requestExecBeanName) { 45 | this.requestExecBeanName = requestExecBeanName; 46 | } 47 | 48 | public SocketResponse getResponse() { 49 | return response; 50 | } 51 | 52 | public void setResponse(SocketResponse response) { 53 | this.response = response; 54 | } 55 | 56 | public String getRequestHost() { 57 | return requestHost; 58 | } 59 | 60 | public void setRequestHost(String requestHost) { 61 | this.requestHost = requestHost; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/socketserver/domain/SocketResponse.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.socketserver.domain; 2 | 3 | import org.eclipse.jetty.websocket.api.Session; 4 | 5 | import java.io.IOException; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by w1992wishes 11 | * on 2018/3/16. 12 | */ 13 | public class SocketResponse { 14 | private Session session; 15 | private boolean closed; 16 | private Map params = new HashMap(); 17 | 18 | public void write(String message) throws IOException { 19 | session.getRemote().sendString(message); 20 | } 21 | 22 | public void flush() throws IOException { 23 | session.getRemote().flush(); 24 | } 25 | 26 | /** 27 | * 添加一个请求参数 28 | * 29 | * @param key 30 | * @param value 31 | */ 32 | public void addRequestParameter(String key, String value) { 33 | params.put(key, value); 34 | } 35 | 36 | public int getInt(String paramName) { 37 | return Integer.parseInt(params.get(paramName)); 38 | } 39 | 40 | public long getLong(String paramName) { 41 | return Long.parseLong(params.get(paramName)); 42 | } 43 | 44 | public String getString(String paramName) { 45 | return params.get(paramName); 46 | } 47 | 48 | public Session getSession() { 49 | return session; 50 | } 51 | 52 | public void setSession(Session session) { 53 | this.session = session; 54 | } 55 | 56 | public boolean isClosed() { 57 | if (!closed) { 58 | return !session.isOpen(); 59 | } 60 | return closed; 61 | } 62 | 63 | public void setClosed(boolean closed) { 64 | this.closed = closed; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/socketserver/websocket/MyWebsocket.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.socketserver.websocket; 2 | 3 | import me.w1992wishes.example_websocket.util.ApplicationContextUtil; 4 | import org.eclipse.jetty.websocket.api.Session; 5 | import org.eclipse.jetty.websocket.api.WebSocketListener; 6 | import org.springframework.context.ApplicationContext; 7 | 8 | /** 9 | * Created by w1992wishes 10 | * on 2018/3/15. 11 | */ 12 | public class MyWebsocket implements WebSocketListener { 13 | //当一个新的websocket连接时,不会把这个覆盖,进去看源代码会找到原因 14 | //因为每次请求过来,会调用MyWebsocketServlet的service方法 15 | //而service方法会调用到factory(也就是MyWebsocket注册进去的那个类)的createWebSocket方法 16 | //该方法每次通过反射初始化一个MyWebsocket 17 | private Session session; 18 | 19 | @Override 20 | public void onWebSocketBinary(byte payload[], int offset, int len) { 21 | 22 | } 23 | 24 | //连接关闭触发 25 | @Override 26 | public void onWebSocketClose(int statusCode, String reason) { 27 | MyWebsocketDispatcher websocketDispatcher = getWebsocketDispatcher(); 28 | websocketDispatcher.dispatcherOnWebSocketClose(session); 29 | } 30 | 31 | //连接开启触发 32 | @Override 33 | public void onWebSocketConnect(Session session) { 34 | this.session = session; 35 | MyWebsocketDispatcher websocketDispatcher = getWebsocketDispatcher(); 36 | websocketDispatcher.dispatcherOnWebSocketConnect(session); 37 | } 38 | 39 | @Override 40 | public void onWebSocketError(Throwable throwable) { 41 | } 42 | 43 | //接收到消息触发 44 | @Override 45 | public void onWebSocketText(String message) { 46 | MyWebsocketDispatcher websocketDispatcher = getWebsocketDispatcher(); 47 | websocketDispatcher.dispatcherOnWebSocketText(session, message); 48 | } 49 | 50 | private MyWebsocketDispatcher getWebsocketDispatcher() { 51 | ApplicationContext context = ApplicationContextUtil.getContext(); 52 | MyWebsocketDispatcher webSocketDispatcher = (MyWebsocketDispatcher) context.getBean("myWebsocketDispatcher"); 53 | return webSocketDispatcher; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/socketserver/websocket/MyWebsocketDispatcher.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.socketserver.websocket; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import me.w1992wishes.example_websocket.socketserver.domain.SocketRequest; 5 | import me.w1992wishes.example_websocket.socketserver.domain.SocketResponse; 6 | import me.w1992wishes.example_websocket.util.ApplicationContextUtil; 7 | import org.eclipse.jetty.websocket.api.Session; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.net.URI; 13 | import java.util.Map; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | 16 | /** 17 | * Created by w1992wishes 18 | * on 2018/3/15. 19 | */ 20 | @Service("myWebsocketDispatcher") 21 | public class MyWebsocketDispatcher { 22 | private static final Logger LOGGER = LoggerFactory.getLogger(MyWebsocketDispatcher.class); 23 | 24 | //这个map是维持所有的websocket连接,因为消息要推送到所有的客户端 25 | private Map socketRequestMap = new ConcurrentHashMap(); 26 | 27 | public void dispatcherOnWebSocketConnect(Session session){ 28 | SocketRequest socketRequest = initWebSocketRequest(session); 29 | socketRequestMap.put(session, socketRequest); 30 | 31 | // 找到对应的业务service 32 | MyWebsocketHandler handler = getWebsocketHandler(socketRequest.getRequestExecBeanName()); 33 | handler.handleOnWebSocketConnect(socketRequest); 34 | } 35 | 36 | public void dispatcherOnWebSocketText(Session session, String message){ 37 | SocketRequest socketRequest = socketRequestMap.get(session); 38 | 39 | if ("~H#B~".equals(message)) { 40 | // 心跳报文,忽略 41 | return; 42 | } 43 | 44 | JSONObject json = JSONObject.parseObject(message); 45 | for (String key : json.keySet()) { 46 | socketRequest.addRequestParameter(key, json.getString(key)); 47 | } 48 | 49 | try { 50 | // 找到对应的业务service 51 | MyWebsocketHandler handler = getWebsocketHandler(socketRequest.getRequestExecBeanName()); 52 | handler.handleOnWebSocketText(socketRequest); 53 | } catch (Exception e) { 54 | LOGGER.error("", e); 55 | } 56 | 57 | } 58 | 59 | public void dispatcherOnWebSocketClose(Session session) { 60 | SocketRequest socketRequest = socketRequestMap.get(session); 61 | try { 62 | // 找到对应的业务service 63 | MyWebsocketHandler handler = getWebsocketHandler(socketRequest.getRequestExecBeanName()); 64 | handler.handleOnWebSocketClose(socketRequest); 65 | } catch (Exception e) { 66 | LOGGER.error("", e); 67 | } 68 | socketRequestMap.remove(session); 69 | } 70 | 71 | private SocketRequest initWebSocketRequest(Session session) { 72 | SocketRequest socketRequest = new SocketRequest(); 73 | SocketResponse response = new SocketResponse(); 74 | response.setSession(session); 75 | socketRequest.setResponse(response); 76 | 77 | URI uri = session.getUpgradeRequest().getRequestURI(); 78 | socketRequest.setRequestHost(uri.getHost()); 79 | 80 | String path = session.getUpgradeRequest().getRequestURI().getPath(); 81 | if (path.contains("/websocket/")) { 82 | path = path.substring(path.indexOf("/websocket/") + "/websocket/".length()); 83 | String beanName = path.split("/")[0]; 84 | socketRequest.setRequestExecBeanName(beanName); 85 | 86 | String queryStr = session.getUpgradeRequest().getRequestURI().getQuery(); 87 | if (queryStr != null && queryStr != "") { 88 | String[] params = queryStr.split("&"); 89 | for (String param : params) { 90 | if (!param.contains("=")) { 91 | continue; 92 | } 93 | int c = param.indexOf("="); 94 | socketRequest.addRequestParameter(param.substring(0, c), param.substring(c + 1)); 95 | } 96 | } 97 | } else { 98 | return null; 99 | } 100 | return socketRequest; 101 | 102 | } 103 | 104 | private MyWebsocketHandler getWebsocketHandler(String beanName){ 105 | return ApplicationContextUtil.getContext().getBean(beanName, MyWebsocketHandler.class); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/socketserver/websocket/MyWebsocketHandler.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.socketserver.websocket; 2 | 3 | import me.w1992wishes.example_websocket.socketserver.domain.SocketRequest; 4 | 5 | /** 6 | * Created by w1992wishes 7 | * on 2018/3/16. 8 | */ 9 | public interface MyWebsocketHandler { 10 | 11 | void handleOnWebSocketText(SocketRequest socketRequest); 12 | 13 | void handleOnWebSocketConnect(SocketRequest socketRequest); 14 | 15 | void handleOnWebSocketClose(SocketRequest socketRequest); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/socketserver/websocket/MyWebsocketServlet.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.socketserver.websocket; 2 | 3 | import org.eclipse.jetty.websocket.servlet.WebSocketServlet; 4 | import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; 5 | 6 | /** 7 | * Created by w1992wishes 8 | * on 2018/3/15. 9 | */ 10 | public class MyWebsocketServlet extends WebSocketServlet { 11 | @Override 12 | public void configure(WebSocketServletFactory webSocketServletFactory) { 13 | webSocketServletFactory.register(MyWebsocket.class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/timer/MessageTimer.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.timer; 2 | 3 | import me.w1992wishes.example_websocket.message.Message; 4 | import me.w1992wishes.example_websocket.message.MessagePusher; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.annotation.PostConstruct; 9 | import java.util.Date; 10 | import java.util.Timer; 11 | import java.util.TimerTask; 12 | 13 | /** 14 | * Created by w1992wishes 15 | * on 2018/3/16. 16 | */ 17 | @Service 18 | public class MessageTimer { 19 | 20 | private int i; 21 | 22 | @Autowired 23 | private MessagePusher messagePusher; 24 | 25 | @PostConstruct 26 | public void initialize(){ 27 | Timer timer = new Timer(); 28 | timer.schedule(new TimerTask() { 29 | @Override 30 | public void run() { 31 | i++; 32 | Message message = new Message(); 33 | message.setData("a test message, id is " + i); 34 | messagePusher.sendMessage(message); 35 | } 36 | }, new Date(), 10000); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /example-websocket/src/main/java/me/w1992wishes/example_websocket/util/ApplicationContextUtil.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.example_websocket.util; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Created by w1992wishes 10 | * on 2018/3/15. 11 | */ 12 | @Component 13 | public class ApplicationContextUtil implements ApplicationContextAware { 14 | 15 | private static ApplicationContext applicationContext; 16 | 17 | public static ApplicationContext getContext() { 18 | return applicationContext; 19 | } 20 | 21 | @Override 22 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 23 | ApplicationContextUtil.applicationContext = applicationContext; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example-websocket/src/main/resources/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example-websocket/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger =DEBUG, stdout 2 | 3 | ### 控制台打印 ### 4 | log4j.appender.stdout = org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.Target = System.out 6 | log4j.appender.stdout.layout = org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.ConversionPattern = %-5p [%t] %37c %3x - %m%n 8 | -------------------------------------------------------------------------------- /example-websocket/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | contextConfigLocation 11 | classpath:applicationContext.xml 12 | 13 | 14 | 15 | ContextLoaderListener 16 | org.springframework.web.context.ContextLoaderListener 17 | 18 | 19 | 20 | WebSocketServlet 21 | me.w1992wishes.example_websocket.socketserver.websocket.MyWebsocketServlet 22 | 1 23 | 24 | 25 | WebSocketServlet 26 | /websocket/* 27 | 28 | 29 | 30 | /index.jsp 31 | 32 | 33 | -------------------------------------------------------------------------------- /example-websocket/src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" pageEncoding="UTF-8" %> 2 | 3 | 4 | 5 | 消息推送 6 | 10 | 11 | 12 | 49 | 50 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /example-websocket/src/main/webapp/js/MyWebsocket.js: -------------------------------------------------------------------------------- 1 | function MyWebSocket(serviceName, config) { 2 | this.serviceName = serviceName; 3 | this.config = config; 4 | 5 | this.connect(); 6 | } 7 | 8 | MyWebSocket.prototype.connect = function() { 9 | var _me = this; 10 | var serverIp = location.hostname; 11 | var _config = _me.config; 12 | 13 | // 需要判断是否支持websocket,如果不支持,使用flash版本的 14 | if (typeof WebSocket != 'undefined') { 15 | initWebSocket(); 16 | _me.supportWebSocket = true; 17 | } else { 18 | console.log("not support Websocket"); 19 | _me.supportWebSocket = false; 20 | return; 21 | } 22 | 23 | function initWebSocket() { 24 | var url = 'ws://localhost:8080/example/websocket/' + _me.serviceName; 25 | 26 | var firstParam = true; 27 | if(_config.params) { 28 | for(var key in _config.params) { 29 | if(firstParam) { 30 | url += '?' + key + '=' + _config.params[key]; 31 | firstParam = false; 32 | } else { 33 | url += '&' + key + '=' + _config.params[key]; 34 | } 35 | } 36 | } 37 | var socket = new WebSocket(url); 38 | _me.socket = socket; 39 | 40 | socket.onopen = function() { 41 | _config.onopen(); 42 | 43 | // 开启心跳检测,以免一段时间后收不到消息自动失联 44 | heartbeat_timer = setInterval(function () { 45 | keepalive(socket) 46 | }, 10000); 47 | 48 | function keepalive(socket) { 49 | socket.send('~H#B~'); 50 | } 51 | }; 52 | 53 | socket.onmessage = function (message) { 54 | _config.onmessage(message.data); 55 | }; 56 | 57 | socket.onclose = function() { 58 | _config.onclose(); 59 | clearInterval(heartbeat_timer); 60 | }; 61 | 62 | socket.onerror = function(err) { 63 | _config.onerror(err); 64 | }; 65 | } 66 | } 67 | 68 | MyWebSocket.prototype.send = function(message) { 69 | if(this.supportWebSocket) { 70 | this.socket.send(JSON.stringify(message)); 71 | } else { 72 | this.socket.sendRequest(this.serviceName, message, true); 73 | } 74 | } 75 | 76 | MyWebSocket.prototype.close = function() { 77 | if(this.supportWebSocket) { 78 | this.socket.close(); 79 | } else { 80 | this.socket.disconnect(); 81 | } 82 | } 83 | 84 | MyWebSocket.prototype.reconnect = function() { 85 | this.close(); 86 | this.connect(); 87 | } -------------------------------------------------------------------------------- /jetty-websocket/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | java-websocket 5 | me.1992wishes.websocket 6 | 1.0-SNAPSHOT 7 | 8 | 4.0.0 9 | me.1992wishes.websocket 10 | jetty-websocket 11 | war 12 | 1.0-SNAPSHOT 13 | jetty-websocket Maven Webapp 14 | http://maven.apache.org 15 | 16 | 17 | 18 | 9.4.6.v20170531 19 | 20 | 21 | 22 | 23 | org.eclipse.jetty 24 | jetty-server 25 | ${websocket.version} 26 | 27 | 28 | org.eclipse.jetty 29 | jetty-servlets 30 | ${websocket.version} 31 | 32 | 33 | 34 | org.eclipse.jetty.websocket 35 | websocket-server 36 | ${websocket.version} 37 | 38 | 39 | org.eclipse.jetty 40 | jetty-webapp 41 | ${websocket.version} 42 | 43 | 44 | org.eclipse.jetty 45 | jetty-annotations 46 | ${websocket.version} 47 | 48 | 49 | 50 | jetty-websocket 51 | 52 | 53 | 54 | org.eclipse.jetty 55 | jetty-maven-plugin 56 | ${websocket.version} 57 | 58 | 59 | 8080 60 | 61 | 3 62 | 63 | / 64 | 65 | automatic 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /jetty-websocket/src/main/java/me/w1992wishes/jetty_websocket/JettyWebSocket.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.jetty_websocket; 2 | 3 | import org.eclipse.jetty.websocket.api.Session; 4 | import org.eclipse.jetty.websocket.api.WebSocketListener; 5 | 6 | import java.io.IOException; 7 | import java.util.concurrent.CopyOnWriteArraySet; 8 | 9 | /** 10 | * Created by w1992wishes 11 | * on 2018/3/15. 12 | */ 13 | public class JettyWebSocket implements WebSocketListener { 14 | 15 | //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 16 | private static int onlineCount = 0; 17 | 18 | //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 19 | private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet(); 20 | 21 | //与某个客户端的连接会话,需要通过它来给客户端发送数据 22 | private Session session; 23 | 24 | @Override 25 | public void onWebSocketBinary(byte[] bytes, int i, int i1) { 26 | 27 | } 28 | 29 | @Override 30 | public void onWebSocketClose(int i, String s) { 31 | webSocketSet.remove(this); //从set中删除 32 | subOnlineCount(); //在线数减1 33 | System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); 34 | } 35 | 36 | @Override 37 | public void onWebSocketConnect(Session session) { 38 | System.out.println("jetty"); 39 | this.session = session; 40 | webSocketSet.add(this); //加入set中 41 | addOnlineCount(); //在线数加1 42 | System.out.println("有新连接加入!当前在线人数为" + getOnlineCount()); 43 | } 44 | 45 | @Override 46 | public void onWebSocketError(Throwable throwable) { 47 | 48 | } 49 | 50 | @Override 51 | public void onWebSocketText(String message) { 52 | System.out.println("来自客户端的消息:" + message); 53 | //群发消息 54 | for(JettyWebSocket item: webSocketSet){ 55 | try { 56 | item.sendMessage(message); 57 | } catch (IOException e) { 58 | e.printStackTrace(); 59 | continue; 60 | } 61 | } 62 | } 63 | 64 | private void sendMessage(String message) throws IOException { 65 | this.session.getRemote().sendString(message); 66 | //this.session.getAsyncRemote().sendText(message); 67 | } 68 | 69 | private static synchronized int getOnlineCount() { 70 | return onlineCount; 71 | } 72 | 73 | private static synchronized void addOnlineCount() { 74 | JettyWebSocket.onlineCount++; 75 | } 76 | 77 | private static synchronized void subOnlineCount() { 78 | JettyWebSocket.onlineCount--; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /jetty-websocket/src/main/java/me/w1992wishes/jetty_websocket/JettyWebSocketServlet.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.jetty_websocket; 2 | 3 | import org.eclipse.jetty.websocket.servlet.WebSocketServlet; 4 | import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; 5 | 6 | /** 7 | * Created by w1992wishes 8 | * on 2018/3/15. 9 | */ 10 | public class JettyWebSocketServlet extends WebSocketServlet { 11 | @Override 12 | public void configure(WebSocketServletFactory factory) { 13 | // set a 10 second timeout 14 | //factory.getPolicy().setIdleTimeout(10000); 15 | factory.register(JettyWebSocket.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /jetty-websocket/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Archetype Created Web Application 7 | 8 | 9 | WebSocketServlet 10 | me.w1992wishes.jetty_websocket.JettyWebSocketServlet 11 | 12 | 13 | WebSocketServlet 14 | /websocket 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jetty-websocket/src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" pageEncoding="UTF-8" %> 2 | 3 | 4 | 5 | Java后端WebSocket的Tomcat实现 6 | 7 | 8 | Welcome
9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 | 70 | 71 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | me.1992wishes.websocket 5 | java-websocket 6 | pom 7 | 1.0-SNAPSHOT 8 | 9 | tomcat-websocket 10 | jetty-websocket 11 | example-websocket 12 | 13 | websocket Maven Webapp 14 | http://maven.apache.org 15 | 16 | 17 | 18 | 4.0.2.RELEASE 19 | 20 | 21 | 22 | 23 | org.springframework 24 | spring-core 25 | ${spring.version} 26 | 27 | 28 | org.springframework 29 | spring-web 30 | ${spring.version} 31 | 32 | 33 | org.springframework 34 | spring-webmvc 35 | ${spring.version} 36 | 37 | 38 | org.springframework 39 | spring-context-support 40 | ${spring.version} 41 | 42 | 43 | 44 | org.slf4j 45 | slf4j-api 46 | 1.7.7 47 | 48 | 49 | org.slf4j 50 | slf4j-log4j12 51 | 1.7.7 52 | 53 | 54 | 55 | 56 | com.alibaba 57 | fastjson 58 | 1.2.44 59 | 60 | 61 | 62 | 63 | 64 | java-websocket 65 | 66 | 67 | maven-compiler-plugin 68 | 2.3.2 69 | 70 | 71 | 1.8 72 | 1.8 73 | UTF-8 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /tomcat-websocket/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | java-websocket 5 | me.1992wishes.websocket 6 | 1.0-SNAPSHOT 7 | 8 | 4.0.0 9 | me.1992wishes.websocket 10 | tomcat-websocket 11 | war 12 | 1.0-SNAPSHOT 13 | simple-websocket-example Maven Webapp 14 | http://maven.apache.org 15 | 16 | 17 | javax 18 | javaee-api 19 | 7.0 20 | provided 21 | 22 | 23 | 24 | tomcat-websocket 25 | 26 | 27 | 28 | org.apache.tomcat.maven 29 | tomcat7-maven-plugin 30 | 2.2 31 | 32 | 8080 33 | / 34 | UTF-8 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tomcat-websocket/src/main/java/me/w1992wishes/tomcat_websocket/TomcatWebSocket.java: -------------------------------------------------------------------------------- 1 | package me.w1992wishes.tomcat_websocket; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.CopyOnWriteArraySet; 5 | 6 | import javax.websocket.*; 7 | import javax.websocket.server.ServerEndpoint; 8 | 9 | /** 10 | * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, 11 | * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端 12 | */ 13 | @ServerEndpoint("/websocket") 14 | public class TomcatWebSocket { 15 | //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 16 | private static int onlineCount = 0; 17 | 18 | //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 19 | private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet(); 20 | 21 | //与某个客户端的连接会话,需要通过它来给客户端发送数据 22 | private Session session; 23 | 24 | /** 25 | * 连接建立成功调用的方法 26 | * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 27 | */ 28 | @OnOpen 29 | public void onOpen(Session session){ 30 | this.session = session; 31 | webSocketSet.add(this); //加入set中 32 | addOnlineCount(); //在线数加1 33 | System.out.println("有新连接加入!当前在线人数为" + getOnlineCount()); 34 | } 35 | 36 | /** 37 | * 连接关闭调用的方法 38 | */ 39 | @OnClose 40 | public void onClose(){ 41 | webSocketSet.remove(this); //从set中删除 42 | subOnlineCount(); //在线数减1 43 | System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); 44 | } 45 | 46 | /** 47 | * 收到客户端消息后调用的方法 48 | * @param message 客户端发送过来的消息 49 | * @param session 可选的参数 50 | */ 51 | @OnMessage 52 | public void onMessage(String message, Session session) { 53 | System.out.println("来自客户端的消息:" + message); 54 | //群发消息 55 | for(TomcatWebSocket item: webSocketSet){ 56 | try { 57 | item.sendMessage(message); 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | continue; 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * 发生错误时调用 67 | * @param session 68 | * @param error 69 | */ 70 | @OnError 71 | public void onError(Session session, Throwable error){ 72 | System.out.println("发生错误"); 73 | error.printStackTrace(); 74 | } 75 | 76 | /** 77 | * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 78 | * @param message 79 | * @throws IOException 80 | */ 81 | private void sendMessage(String message) throws IOException{ 82 | this.session.getBasicRemote().sendText(message); 83 | //this.session.getAsyncRemote().sendText(message); 84 | } 85 | 86 | private static synchronized int getOnlineCount() { 87 | return onlineCount; 88 | } 89 | 90 | private static synchronized void addOnlineCount() { 91 | TomcatWebSocket.onlineCount++; 92 | } 93 | 94 | private static synchronized void subOnlineCount() { 95 | TomcatWebSocket.onlineCount--; 96 | } 97 | } -------------------------------------------------------------------------------- /tomcat-websocket/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | Archetype Created Web Application 9 | 10 | -------------------------------------------------------------------------------- /tomcat-websocket/src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" pageEncoding="UTF-8" %> 2 | 3 | 4 | 5 | Java后端WebSocket的Tomcat实现 6 | 7 | 8 | Welcome
9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 | 70 | 71 | --------------------------------------------------------------------------------