1.1 服务器推送
WebSocket作为一种通信协议,属于服务器推送技术的一种,IE10+支持。
服务器推送技术不止一种,有短轮询、长轮询、WebSocket、Server-sent Events(SSE)等,他们各有优缺点:
# | 短轮询 | 长轮询 | Websocket | sse |
---|---|---|---|---|
通讯方式 | http | http | 基于TCP长连接通讯 | http |
触发方式 | 轮询 | 轮询 | 事件 | 事件 |
优点 | 兼容性好容错性强,实现简单 | 比短轮询节约资源 | 全双工通讯协议,性能开销小、安全性高,有一定可扩展性 | 实现简便,开发成本低 |
缺点 | 安全性差,占较多的内存资源与请求数 | 安全性差,占较多的内存资源与请求数 | 传输数据需要进行二次解析,增加开发成本及难度 | 只适用高级浏览器 |
适用范围 | b/s服务 | b/s服务 | 网络游戏、银行交互和支付 | 服务端到客户端单向推送 |
短轮询最简单,在一些简单的场景也会经常使用,就是隔一段时间就发起一个ajax请求。那么长轮询是什么呢?
长轮询(Long Polling)是在Ajax轮询基础上做的一些改进,在没有更新的时候不再返回空响应,而且把连接保持到有更新的时候,客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。它是一个解决方案,但不是最佳的技术方案。
如果说短轮询是客户端不断打电话问服务端有没有消息,服务端回复后立刻挂断,等待下次再打;长轮询是客户端一直打电话,服务端接到电话不挂断,有消息的时候再回复客户端并挂断。
SSE(Server-Sent Events)与长轮询机制类似,区别是每个连接不只发送一个消息。客户端发送一个请求,服务端保持这个连接直到有新消息发送回客户端,仍然保持着连接,这样连接就可以支持消息的再次发送,由服务器单向发送给客户端。然而IE直到11都不支持,不多说了....
1.2 WebSocket的特点
为什么已经有了轮询还要WebSocket呢,是因为短轮询和长轮询有个缺陷:通信只能由客户端发起。
那么如果后端想往前端推送消息需要前端去轮询,不断查询后端是否有新消息,而轮询的效率低且浪费资源(必须不停 setInterval 或 setTimeout 去连接,或者 HTTP 连接始终打开),WebSocket提供了一个文明优雅的全双工通信方案。一般适合于对数据的实时性要求比较强的场景,如通信、股票、直播、共享桌面,特别适合于客户端与服务频繁交互的情况下,如聊天室、实时共享、多人协作等平台。
特点
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。服务器与客户端之间交换的标头信息大概只有2字节;
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是
ws
(如果加密,则为wss),服务器网址就是 URL。ex:ws://example.com:80/some/path
- 不用频繁创建及销毁TCP请求,减少网络带宽资源的占用,同时也节省服务器资源;
- WebSocket是纯事件驱动的,一旦连接建立,通过监听事件可以处理到来的数据和改变的连接状态,数据都以帧序列的形式传输。服务端发送数据后,消息和事件会异步到达。
- 无超时处理。
HTTP与WS协议结构
WebSocket协议标识符用ws
表示。`wss协议表示加密的WebSocket协议,对应HTTPs协议。结构如下:
- HTTP: TCP > HTTP
- HTTPS: TCP > TLS > HTTP
- WS: TCP > WS
- WSS: TCP > TLS > WS
以下是简单代码记录,亲测可用,基于springMVC的服务器推送技术websocket。
1.gradle构建,websocket引入的包:"org.springframework:spring-websocket:4.2.4.RELEASE"
2.context.xml配置需要加的地方
xmlns:websocket="http://www.springframework.org/schema/websocket"
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd
3.websocket的配置类
4.websocke接受消息以及处理的类
5.页面js代码
6.简单的运行效果
涉及到的代码就这几个,拷贝到springmvc项目里就可以。
1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.stereotype.Service; 4 import org.springframework.web.socket.CloseStatus; 5 import org.springframework.web.socket.TextMessage; 6 import org.springframework.web.socket.WebSocketMessage; 7 import org.springframework.web.socket.WebSocketSession; 8 import org.springframework.web.socket.handler.TextWebSocketHandler; 9 10 import java.io.IOException; 11 import java.util.HashMap; 12 import java.util.Map; 13 import java.util.Set; 14 15 /** 16 * Created by liuxn on 2018/5/22 0022. 17 */ 18 @Service 19 public class MyHandler extends TextWebSocketHandler { 20 //在线用户列表 21 private static final Map<Integer, WebSocketSession> users; 22 //用户标识 23 private static final String CLIENT_ID = "userId"; 24 25 static { 26 users = new HashMap<>(); 27 } 28 29 @Override 30 public void afterConnectionEstablished(WebSocketSession session) throws Exception { 31 System.out.println("成功建立连接"); 32 Integer userId = getClientId(session); 33 if (userId != null) { 34 users.put(userId, session); 35 session.sendMessage(new TextMessage("你已成功建立socket连接")); 36 System.out.println(userId); 37 System.out.println(session); 38 } 39 } 40 41 @Override 42 public void handleTextMessage(WebSocketSession session, TextMessage message) { 43 // ... 44 System.out.println("收到客户端消息:"+message.getPayload()); 45 46 WebSocketMessage message1 = new TextMessage("server:"+message); 47 try { 48 session.sendMessage(message1); 49 } catch (IOException e) { 50 e.printStackTrace(); 51 } 52 } 53 54 @Override 55 public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { 56 if (session.isOpen()) { 57 session.close(); 58 } 59 System.out.println("连接出错"); 60 users.remove(getClientId(session)); 61 } 62 63 @Override 64 public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 65 System.out.println("连接已关闭:" + status); 66 users.remove(getClientId(session)); 67 } 68 69 /** 70 * 发送信息给指定用户 71 * @param clientId 72 * @param message 73 * @return 74 */ 75 public boolean sendMessageToUser(Integer clientId, TextMessage message) { 76 if (users.get(clientId) == null) return false; 77 WebSocketSession session = users.get(clientId); 78 System.out.println("sendMessage:" + session+",msg:"+message.getPayload()); 79 if (!session.isOpen()) return false; 80 try { 81 session.sendMessage(message); 82 } catch (IOException e) { 83 e.printStackTrace(); 84 return false; 85 } 86 return true; 87 } 88 89 /** 90 * 广播信息 91 * @param message 92 * @return 93 */ 94 public boolean sendMessageToAllUsers(TextMessage message) { 95 boolean allSendSuccess = true; 96 Set<Integer> clientIds = users.keySet(); 97 WebSocketSession session = null; 98 for (Integer clientId : clientIds) { 99 try { 100 session = users.get(clientId); 101 if (session.isOpen()) { 102 session.sendMessage(message); 103 } 104 } catch (IOException e) { 105 e.printStackTrace(); 106 allSendSuccess = false; 107 } 108 } 109 110 return allSendSuccess; 111 } 112 113 @Override 114 public boolean supportsPartialMessages() { 115 return false; 116 } 117 118 /** 119 * 获取用户标识 120 * @param session 121 * @return 122 */ 123 private Integer getClientId(WebSocketSession session) { 124 try { 125 Integer clientId = (Integer) session.getAttributes().get(CLIENT_ID); 126 return clientId; 127 } catch (Exception e) { 128 return null; 129 } 130 } 131 }
1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.web.bind.annotation.PathVariable; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.ResponseBody; 8 import org.springframework.web.servlet.ModelAndView; 9 import org.springframework.web.socket.TextMessage; 10 11 import javax.servlet.http.HttpSession; 12 13 14 @Controller 15 public class SocketController { 16 17 @Autowired 18 MyHandler handler; 19 20 //玩家登录 21 @RequestMapping("/external/login/{userId}") 22 public ModelAndView login(HttpSession session, @PathVariable("userId") Integer userId) { 23 System.out.println("登录接口,userId=" + userId); 24 session.setAttribute("userId", userId); 25 System.out.println(session.getAttribute("userId")); 26 27 return new ModelAndView("phone/websocket_test"); 28 } 29 30 //模拟给指定玩家发消息 31 @RequestMapping("/external/message") 32 @ResponseBody 33 public String sendMessage(Integer userId,String message) { 34 boolean hasSend = handler.sendMessageToUser(userId, new TextMessage(message)); 35 System.out.println(hasSend); 36 return "success"; 37 } 38 39 40 //模拟给所有玩家发消息 41 @RequestMapping("/external/message/all") 42 @ResponseBody 43 public String sendAll(String message) { 44 boolean hasSend = handler.sendMessageToAllUsers(new TextMessage(message)); 45 System.out.println(hasSend); 46 return "success"; 47 } 48 49 }
1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.web.socket.WebSocketHandler; 6 import org.springframework.web.socket.config.annotation.EnableWebSocket; 7 import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 8 import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 9 10 11 @Configuration 12 @EnableWebSocket 13 public class WebSocketConfig implements WebSocketConfigurer { 14 15 @Override 16 public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 17 registry.addHandler(myHandler(), "/external/myHandler").addInterceptors(new WebSocketInterceptor()); 18 } 19 20 @Bean 21 public WebSocketHandler myHandler() { 22 return new MyHandler(); 23 } 24 }
1 package com.kitty.activity.common.websocket; 2 3 import org.springframework.http.server.ServerHttpRequest; 4 import org.springframework.http.server.ServerHttpResponse; 5 import org.springframework.http.server.ServletServerHttpRequest; 6 import org.springframework.web.socket.WebSocketHandler; 7 import org.springframework.web.socket.server.HandshakeInterceptor; 8 9 import javax.servlet.http.HttpSession; 10 import java.util.Map; 11 12 /** 13 * 14 */ 15 public class WebSocketInterceptor implements HandshakeInterceptor { 16 17 @Override 18 public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> map) throws Exception { 19 if (request instanceof ServletServerHttpRequest) { 20 System.out.println("*****beforeHandshake******"); 21 ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; 22 HttpSession session = serverHttpRequest.getServletRequest().getSession(); 23 // Map parameterMap = serverHttpRequest.getServletRequest().getParameterMap(); 24 // System.out.println(parameterMap); 25 if (session != null) { 26 map.put("userId", session.getAttribute("userId")); 27 } 28 29 } 30 return true; 31 } 32 33 @Override 34 public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { 35 System.out.println("******afterHandshake******"); 36 } 37 }
1 <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 4 <c:set var="ctx" value="${pageContext.request.contextPath}"/> 5 <html> 6 <head> 7 <meta charset="utf-8"> 8 <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport"> 9 <meta content="yes" name="apple-mobile-web-app-capable"> 10 <meta content="black" name="apple-mobile-web-app-status-bar-style"> 11 <meta content="telephone=no" name="format-detection"> 12 <meta content="email=no" name="format-detection"> 13 <meta name="baseUrl" content="${ctx}"/> 14 <meta name="roleId" content="${roleId}"/> 15 <title>websocket</title> 16 <script src="${ctx}/assets/js/jquery.min.js"></script> 17 <script src="http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js"></script> 18 19 </head> 20 21 <body> 22 <div class="act-outline" width="100%"> 23 it is work. 24 </div> 25 <script> 26 //websocket 27 $(function () { 28 var baseUrl = $('meta[name="baseUrl"]').attr("content"); 29 //判断当前浏览器是否支持WebSocket 30 var websocket; 31 if ('WebSocket' in window) { 32 websocket = new WebSocket('ws://' + window.location.host + '/activity/external/myHandler'); 33 // var ws = new WebSocket('ws://192.168.3.26:8999/activity/external/myHandler') //也可以指定ip 34 } else if ('MozWebSocket' in window) { 35 websocket = new MozWebSocket("ws://" + window.location.host + '/activity/external/myHandler'); //未测试 36 } else { 37 websocket = new SockJS("http://" + window.location.host + '/activity/external/myHandler'); //未测试 38 } 39 40 websocket.onopen = function () { 41 console.log("正在打开连接,准备发消息给服务器..."); 42 websocket.send("{text:hello}"); 43 } 44 websocket.onclose = function () { 45 console.log("服务器关闭连接:onclose"); 46 } 47 48 websocket.onmessage = function (msg) { 49 console.log("收到服务器推送数据:"+msg.data); 50 } 51 52 53 }) 54 </script> 55 </body> 56 </html>
备注:http://ip:port/activity 是项目根目录。
参考链接地址:
https://blog.csdn.net/u014520745/article/details/62046396
https://www.cnblogs.com/interdrp/p/7903736.html