WebSocket
特点:事件驱动、异步,使用ws或者wss协议的客户端socket,能够实现真正意义上的推送功能
缺点:少部分浏览器不支持,浏览器支持的程度与方式有区别。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>FORM Text</title> <script src="jquery-3.3.1.min.js"></script> </head> <body> <div id="test"></div> <script> $(function(){ if(window.WebSocket){ alert("Support"); }else{ alert("Unsupport"); } }); </script> </body> </html>
1、WebSocket是HTML5出的东西(协议),与Http协议没关系
2、WebSocket 支持全双工长连接,Http不支持长连接,一个request只能有一个response。而且这个response也是被动的,不能主动发起。
3、WebSocket 建立连接进行了一次Http握手。
Http响应模式: WebSocket响应模式:
4、轮询:
客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有Http的Header,会很耗流量,也会消耗CPU的利用率。
HTTP的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,而且后台无法主动向前端发送数据,只能被动等前端http请求。
eg:放羊人,男孩,狼
放羊人:狼来了吗?
男孩:没有
放羊人:狼来了没?
男孩:没有
.....(此过程,狼已经来了)
放羊人:狼来了没?
男孩:来了
5、长轮询长轮询是轮询的改进,也是采取轮询模式,不同的是采取阻塞模式(类似一直打电话,没收到就不挂电话) 也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
放羊人1:狼来了吗?
男孩:....(等待,一直等狼来)
.....(此过程,狼已经来了)
放羊人2:狼来了吗?
男孩:....(等待,一直等狼来)
.....(此过程,狼已经来了)
男孩放羊1:来了
男孩放羊2:来了
6、WebSocket
放羊人1:狼来了吗?
男孩->放羊人1:没有,狼还在十里外
放羊人2:狼来了吗?
男孩->放羊人2:没有,狼还在八里外
男孩->放羊1:狼在五里外
男孩->放羊2:狼在五里外
.....(此过程,狼已经来了)
男孩->放羊1:来了
男孩->放羊2:来了
7、使用Spring实现webSocket工程如下:
package com.example.demo.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Map; @Controller @RequestMapping(value = "/webSocket") public class WebSocketController { private final Logger log = LoggerFactory.getLogger(getClass()); @RequestMapping("/toLogin") private String toLoginPage(HttpServletRequest request, Map<String, Object> map) { return "login"; } @RequestMapping("/login") private String loginPage(HttpServletRequest request, Map<String, Object> map) { String userName = request.getParameter("userName"); HttpSession session = null; if(!request.isRequestedSessionIdValid()){ //会话失效重新登录 session = request.getSession(true); //用户名放入Session session.setAttribute("SESSION_USERNAME",userName); } return "test_webSocket"; } }
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>WebSocket演示</title> <!-- js 引用 --> <script th:src="@{~/static/js/jquery_3.1.0_jquery.min.js}"></script> <script th:src="@{~/static/js/webSocket-1.0.js}"></script> </head> <body> <form action="login" method="post"> 请输入:<input type="text" value="" name="userName" placeholder="请输入用户名"/> <input type="submit" value="登录"> </form> </body> <script> </script> </html>
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>WebSocket演示</title> <!-- js 引用 --> <script th:src="@{~/static/js/jquery_3.1.0_jquery.min.js}"></script> <script th:src="@{~/static/js/webSocket-1.0.js}"></script> </head> <body> 请输入:<textarea rows="5" cols="10" id="inputMsg" name="inputMsg"></textarea> <input type="button" id="send" value="发送"/> <input type="button" id="close" value="关闭"/> <!--<button id="s-close">关闭</button>--> </body> <script> var websocket = WebSkt.createNew("localhost:8080/websocket/socketServer.do"); $("#send").on("click",function(){ if (websocket.readyState == websocket.OPEN) { var msg = document.getElementById("inputMsg").value; websocket.send(msg); alert("发送成功!"); } else { alert("连接失败!"); } }); $("#close").on('click',function(){ websocket.close(); }); </script> </html>
------
package com.example.demo.interceptor; import java.util.Map; import javax.servlet.http.HttpSession; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; /** * 继承HttpSessionHandshakeInterceptor对象。该对象作为页面连接websocket服务的拦截器 * * @author Administrator **/ public class SpringWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("Before Handshake"); if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(false); if (session != null) { // 使用userName区分WebSocketHandler,以便定向发送消息 String userName = (String) session.getAttribute("SESSION_USERNAME"); if (userName == null) { userName = "default-system"; } attributes.put("WEBSOCKET_USERNAME", userName); } } return super.beforeHandshake(request, response, wsHandler, attributes); } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { super.afterHandshake(request, response, wsHandler, ex); } }
package com.example.demo.handle; import java.io.IOException; import java.util.ArrayList; import com.example.demo.config.SpringWebSocketConfig; import com.example.demo.util.BloomFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import javax.annotation.Resource; /* * 继承WebSocketHandler对象 该对象提供了客户端连接,关闭,错误,发送等方法,重写这几个方法即可实现自定义业务逻辑 * * @author Administrator */ public class SpringWebSocketHandler extends TextWebSocketHandler { //这个会出现性能问题,最好用Map来存储,key用userid private static final ArrayList<WebSocketSession> users; // private Logger logger = LoggerFactory.getLogger(SpringWebSocketConfig.class); /** * 利用布隆算法,判断Session用户是否在线 */ @Resource private BloomFilter bloomFilter; static { users = new ArrayList<WebSocketSession>(); } public SpringWebSocketHandler() { } /** * 连接成功时候,会触发页面上onopen方法 */ public void afterConnectionEstablished(WebSocketSession session) throws Exception { String sessionId = (String) session.getAttributes().get("HTTP.SESSION.ID"); //这边也可以用Map<String,WebSocketSession> usersMap 的方式判断Session是否存在 if(!bloomFilter.contains(sessionId)){ //不在 bloomFilter.add(sessionId); users.add(session); } System.out.println("connect to the websocket success......当前数量:" + users.size()); // 这块会实现自己业务,比如,当用户登录后,会把离线消息推送给用户 // TextMessage returnMessage = new TextMessage("你将收到的离线"); // session.sendMessage(returnMessage); } /** * 关闭连接时触发 */ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { logger.debug("websocket connection closed......"); String username = (String) session.getAttributes().get("WEBSOCKET_USERNAME"); System.out.println("用户" + username + "已退出!"); users.remove(session); System.out.println("剩余在线用户" + users.size()); } /** * js调用websocket.send时候,会调用该方法 */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); super.handleTextMessage(session, message); System.out.println("收到消息:" + message.getPayload()); //交互 TextMessage returnMessage = new TextMessage("Server has recieved Your message and will be processed later... "); session.sendMessage(returnMessage); } public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if (session.isOpen()) { session.close(); } logger.debug("websocket connection closed......"); users.remove(session); } public boolean supportsPartialMessages() { return false; } /** * 给某个用户发送消息 * * @param userName * @param message */ public void sendMessageToUser(String userName, TextMessage message) { for (WebSocketSession user : users) { if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) { try { if (user.isOpen()) { user.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } break; } } } /* * * 给所有在线用户发送消息 * * @param message */ public void sendMessageToUsers(TextMessage message) { for (WebSocketSession user : users) { try { if (user.isOpen()) { user.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } } }
package com.example.demo.config; import com.example.demo.handle.SpringWebSocketHandler; import com.example.demo.interceptor.SpringWebSocketHandlerInterceptor; import com.example.demo.util.BloomFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.handler.TextWebSocketHandler; /** * 建立一个类实现WebSocketConfigurer接口 * * @author Administrator * */ @Configuration //@EnableWebMvc @EnableWebSocket public class SpringWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer { private Logger logger = LoggerFactory.getLogger(SpringWebSocketConfig.class); @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { logger.info("SpringWebSocketConfig registerWebSocketHandlers ................"); registry.addHandler(webSocketHandler(), "/websocket/socketServer.do").addInterceptors( new SpringWebSocketHandlerInterceptor()); //根据用途,添加其他WebSocket路径监听 /*registry.addHandler(webSocketHandler(), "/sockjs/socketServer.do") .addInterceptors(new SpringWebSocketHandlerInterceptor()).withSockJS();*/ } @Bean public TextWebSocketHandler webSocketHandler() { return new SpringWebSocketHandler(); } @Bean public BloomFilter getBloomFilter(){ return new BloomFilter(); } }
运行测试:---------------
相对于Http协议,多了Upgrade: websocket Connection: Upgrade,告诉Apache、Nginx等服务器:注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理。
Sec-WebSocket-Key: FrZGy1EVeL//7yZDEYrM4g==Sec-WebSocket-Version: 13
Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:知道啦,给你看我的ID CARD来证明行了吧。。
参考:
https://www.zhihu.com/question/20215561
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket