SpringBoot WebSocket 消息交互
1. Websocket原理
- Websocket协议本质上是一个基于TCP的独立协议,能够在浏览器和服务器之间建立双向连接,以基于消息的机制,赋予浏览器和服务器间实时通信能力。
- WebSocket资源URI采用了自定义模式:ws表示纯文本通信,其连接地址写法为“ws://**”,占用与http相同的80端口;wss表示使用加密信道通信(TCP+TLS),基于SSL的安全传输,占用与TLS相同的443端口。
2. Websocket与HTTP比较
WebSocket 和 HTTP 都是基于 TCP 协议;
TCP是传输层协议,WebSocket 和 HTTP 是应用层协议
HTTP是用于文档传输、简单同步请求的响应式协议,本质上是无状态的应用层协议,半双工的连接特性。Websocket与 HTTP 之间的唯一关系就是它的握手请求可以作为一个升级请求(Upgrade request)经由 HTTP 服务器解释(也就是可以使用Nginx反向代理一个WebSocket)。
联系:
客户端建立WebSocket连接时发送一个header,标记了Upgrade的HTTP请求,表示请求协议升级;
服务器直接在现有的HTTP服务器软件和端口上实现WebSocket,重用现有代码(比如解析和认证这个HTTP请求),然后再回一个状态码为101(协议转换)的HTTP响应完成握手,之后发送数据就跟HTTP没关系了。
区别:
-
持久性:
HTTP协议:HTTP是非持久的协议(长连接、循环连接除外)
WebSocket协议:Websocket是持久化的协议
-
生命周期:
HTTP的生命周期通过Request来界定,也就是一个Request 一个Response
在HTTP1.0中,这次HTTP请求就结束了;
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,并接收多个Respouse;
在HTTP中永远都是一个Request只有一个Respouse,而且这个Respouse是被动的,不能主动发起。
3. SpringWeb项目搭建
3.1.1 pom.xml
该项目基于maven搭建,使用SpringBoot2.0版本,引入Spring Websocket所需的jar包,以及对传输的消息体进行JSON序列化所需的jar包。
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
3.1.2 WebSocketHandler接口实现
/** * Echo messages by implementing a Spring {@link WebSocketHandler} abstraction. */ @Slf4j public class EchoWebSocketHandler extends TextWebSocketHandler { /** * Map 来存储 WebSocketSession,key 用 USER_ID 即在线用户列表 */ private static final Map<String, WebSocketSession> users = new HashMap<String, WebSocketSession>(); /** * 用户唯一标识【WebSocketSession 中 getAttributes() 方法获取到的 Map 集合是不同的,因为不同用户 WebSocketSession 不同, * 所以不同用户可以使用相同的 key = WEBSOCKET_IDCARD,因为 Map 不同,互不影响】 */ private static final String IDCARD = "WEBSOCKET_IDCARD"; private final EchoService echoService; public EchoWebSocketHandler(EchoService echoService) { this.echoService = echoService; } /** * 连接成功时候,会触发页面上onopen方法 */ @Override public void afterConnectionEstablished(WebSocketSession session) { Map<String, Object> attributes = session.getAttributes(); log.info("EchoWebSocketHandler = {}, session = {}, attributes = {}", this, session, attributes); String idcard = (String) session.getAttributes().get(IDCARD); log.info("idcard = {}用户成功建立 websocket 连接", idcard); users.put(idcard, session); } /** * js 调用 websocket.send 时候,会调用该方法 */ @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String echoMessage = this.echoService.getMessage(message.getPayload()); log.info("前端发送消息,echoMessage = {}", echoMessage); session.sendMessage(new TextMessage(echoMessage)); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { session.close(CloseStatus.SERVER_ERROR); } /** * 关闭连接时触发 */ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) { log.info("关闭websocket连接"); String userId = (String) session.getAttributes().get(IDCARD); log.info("用户 userId = {} 已退出!", userId); users.remove(userId); } /** * 给某个用户发送消息 */ public void sendMessageToUser(String idcard, TextMessage message) { try { if (users.containsKey(idcard)) { WebSocketSession session = users.get(idcard); if (session.isOpen()) { session.sendMessage(message); } } } catch (IOException e) { log.error("发送消息异常, errerMsg = {}", e.getMessage()); } } /** * 给所有在线用户发送消息 */ public void sendMessageToUsers(TextMessage message) { for (String userId : users.keySet()) { try { if (users.get(userId).isOpen()) { users.get(userId).sendMessage(message); } } catch (IOException e) { log.error("给所有在线用户发送消息异常, errorMsg = {}", e.getMessage()); } } } }
3.1.3 HttpSession存储属性值【key-value】
@RestController @Slf4j public class WebSocketController { @Autowired EchoWebSocketHandler echoWebSocketHandler; @RequestMapping("/websocket/login") public String login(HttpServletRequest request) { String idcard = request.getParameter("idcard"); log.info("idcard = {} 登录 ", idcard); HttpSession session = request.getSession(); session.setAttribute("WEBSOCKET_IDCARD", idcard); return "登录成功"; } @RequestMapping("/websocket/send") @ResponseBody public void send(HttpServletRequest request) { String username = request.getParameter("idcard"); echoWebSocketHandler.sendMessageToUser(username, new TextMessage("你好,给您推送消息啦!")); } }
HttpSession可以记录当前访问的会话用户,但WebSocketSession不能记录当前用户会话,必须要从HttpSession中存储当前用户相关信息idcard,在建立通信握手之前,需要将HttpSession中用户相关属性值存储到WebSocketSession中,服务器才能知道通话的当前用户;【目前怎么将HttpSession中的key-value的值存储到Map集合,并将Map集合同步到WebsocketSession的attributes属性中,这点我没有跟踪到源码,但是我敢确认,肯定设置进去了】
3.1.4 WebSocket激活
@Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @EnableWebSocket @Slf4j public class SampleTomcatWebSocketApplication extends SpringBootServletInitializer implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //设置自定义的 WebSocket 握手拦截器 registry.addHandler(echoWebSocketHandler(), "/echo").addInterceptors(webSocketHandlerIntereptor()).withSockJS(); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(SampleTomcatWebSocketApplication.class); } @Bean public EchoService echoService() { return new DefaultEchoService("Did you say \"%s\"?"); } @Bean public GreetingService greetingService() { return new SimpleGreetingService(); } @Bean public WebSocketHandler echoWebSocketHandler() { return new EchoWebSocketHandler(echoService()); } @Bean public WebSocketController webSocketController() { return new WebSocketController(); } @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } @Bean public WebSocketHandlerIntereptor webSocketHandlerIntereptor() { return new WebSocketHandlerIntereptor(); } public static void main(String[] args) { SpringApplication.run(SampleTomcatWebSocketApplication.class, args); } }
Websocket拦截器类:
HttpSession和WebSocketSession是不同的会话对象,如果想记录当前用户的Session对象的属性值,必须要在建立通信握手之前,将HttpSession的值copy到WebSocketSession中,否则获取不到;
import lombok.extern.slf4j.Slf4j; 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; import javax.servlet.http.HttpSession; import java.util.Map; @Slf4j public class WebSocketHandlerIntereptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { log.info("Before Handshake"); if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(); if (session != null) { //使用 idcard 区分 WebSocketHandler,以便定向发送消息【一般直接保存 user 实体】 String idcard = (String) session.getAttribute("idcard"); if (idcard != null) { attributes.put("WEBSOCKET_IDCARD", idcard); } } } 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); } }
3.2 WebSocket前端
<!DOCTYPE html> <html> <head> <title>Apache Tomcat WebSocket Examples: Echo</title> <style type="text/css"> #connect-container { float: left; width: 400px } #connect-container div { padding: 5px; } #console-container { float: left; margin-left: 15px; width: 400px; } #console { border: 1px solid #CCCCCC; border-right-color: #999999; border-bottom-color: #999999; height: 170px; overflow-y: scroll; padding: 5px; width: 100%; } #console p { padding: 0; margin: 0; } </style> <script src="https://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script> <script type="text/javascript"> var ws = null; function setConnected(connected) { document.getElementById('connect').disabled = connected; document.getElementById('disconnect').disabled = !connected; document.getElementById('echo').disabled = !connected; } function connect() { var target = document.getElementById('target').value; ws = new SockJS(target); ws.onopen = function () { setConnected(true); log('Info: WebSocket connection opened.'); }; ws.onmessage = function (event) { log('Received: ' + event.data); }; ws.onclose = function () { setConnected(false); log('Info: WebSocket connection closed.'); }; } function disconnect() { if (ws != null) { ws.close(); ws = null; } setConnected(false); } function echo() { if (ws != null) { var message = document.getElementById('message').value; log('Sent: ' + message); ws.send(message); } else { alert('WebSocket connection not established, please connect.'); } } function log(message) { var console = document.getElementById('console'); var p = document.createElement('p'); p.style.wordWrap = 'break-word'; p.appendChild(document.createTextNode(message)); console.appendChild(p); while (console.childNodes.length > 25) { console.removeChild(console.firstChild); } console.scrollTop = console.scrollHeight; } </script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div> <div id="connect-container"> <div> <input id="target" type="text" size="40" style="width: 350px" value="/echo"/> </div> <div> <button id="connect" onclick="connect();">Connect</button> <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button> </div> <div> <textarea id="message" style="width: 350px">Here is a message!</textarea> </div> <div> <button id="echo" onclick="echo();" disabled="disabled">Echo message</button> </div> </div> <div id="console-container"> <div id="console"></div> </div> </div> </body> </html>