spring boot + websocket
websocket 简介
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
java 服务端
-
IDEA 创建 spring boot 工程。pom文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.local</groupId> <artifactId>WebSocketDemo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> <version>2.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>5.1.9.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> </dependencies> </project>
-
配置 freemarker,application.yml如下:
server: port: 8080 spring: freemarker: allow-request-override: false allow-session-override: false cache: true charset: UTF-8 check-template-location: true content-type: text/html enabled: true expose-request-attributes: false expose-session-attributes: false expose-spring-macro-helpers: true prefer-file-system-access: true suffix: .ftl template-loader-path: classpath:/templates/
-
通过继承 TextWebSocketHandler 实现 Websocket 服务端。
package com.local.websocket; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; 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 java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * @ProjectName: WebSocketDemo * @Author: Qiao * @Description: * @Date: 2020/3/9 11:18 */ @Slf4j public class MyWebSocketHandler extends TextWebSocketHandler { // 在线用户列表 private static final Map<String, WebSocketSession> users = new HashMap<String, WebSocketSession>(); // 用户标识 private static final String CLIENT_ID = "userId"; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { /*super.afterConnectionEstablished(session);*/ log.info("连接建立成功"); String userId = getUserId(session); if (!StringUtils.isEmpty(userId)) { users.put(userId,session); log.info("用户ID:{},Session:{}", userId, session.toString()); // sendMessageToUser("admin",new TextMessage("https://www.baidu.com")); } } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { /* super.handleTextMessage(session, message);*/ log.info("收到客户端消息:{}", message.getPayload()); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { /* super.afterConnectionClosed(session, status);*/ log.info("连接已关闭:" + status); users.remove(getUserId(session)); } /** * 推送消息给指定用户 * @param userId * @param message * @return */ public boolean sendMessageToUser(String userId, TextMessage message) { if (users.get(userId) == null) return false; WebSocketSession session = users.get(userId); log.info("sendMessage:{} ,msg:{}", session, message.getPayload()); if (!session.isOpen()) { log.info("客户端:{},已断开连接,发送消息失败", userId); return false; } try { session.sendMessage(message); } catch (IOException e) { log.info("sendMessageToUser method error:{}", e); return false; } return true; } /** * 广播消息 * @param message * @return */ public boolean sendMessageToAllUsers(TextMessage message) { boolean allSendSuccess = true; Set<String> userIds = users.keySet(); WebSocketSession session = null; for (String userId : userIds) { try { session = users.get(userId); if (session.isOpen()) { session.sendMessage(message); }else { log.info("客户端:{},已断开连接,发送消息失败", userId); } } catch (IOException e) { log.info("sendMessageToAllUsers method error:{}", e); allSendSuccess = false; } } return allSendSuccess; } /** * 获取用户标识 * @param session * @return */ private String getUserId(WebSocketSession session) { try { String userId = session.getAttributes().get(CLIENT_ID).toString(); return userId; } catch (Exception e) { return null; } } }
-
通过实现 HandshakeInterceptor 定制初始HTTP WebSocket握手请求 。
package com.local.websocket;
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.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* @ProjectName: WebSocketDemo
* @Author: Qiao
* @Description:
* @Date: 2020/3/9 11:53
*/
@Slf4j
public class MyWebSocketInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (serverHttpRequest instanceof ServletServerHttpRequest) {
log.info("==============beforeHandshake====================");
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
HttpSession httpSession = httpServletRequest.getSession(true);
if (httpSession != null) {
map.put("sessionId",httpSession.getId());
map.put("userId",httpServletRequest.getParameter("userId"));
}
}
return true;
}
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
log.info("================afterHandshake=================");
}
}
- 通过实现 WebSocketConfigurer 接口暴露 Websocket 端点并加入定制的拦截器。
package com.local.conf;
import com.local.websocket.MyWebSocketHandler;
import com.local.websocket.MyWebSocketInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* @ProjectName: WebSocketDemo
* @Author: Qiao
* @Description:
* @Date: 2020/3/9 11:24
*/
@Component
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(myWebSocketHandler(),"/myWebSocketHandler")
.addInterceptors(new MyWebSocketInterceptor()).setAllowedOrigins("*");
}
@Bean
public WebSocketHandler myWebSocketHandler() {
return new MyWebSocketHandler();
}
}
-
Controller
package com.local.controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; /** * @ProjectName: WebSocketDemo * @Author: Qiao * @Description: * @Date: 2020/3/9 10:10 */ @RestController @RequestMapping("/user") public class LoginController { @GetMapping("/loginPage") public ModelAndView loginPage(ModelAndView modelAndView) { modelAndView.setViewName("ftl/index"); return modelAndView; } @GetMapping("/login") public ModelAndView login(String username, String password, ModelAndView modelAndView) { if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { modelAndView.setViewName("ftl/user"); modelAndView.addObject("username",username); return modelAndView; } else { modelAndView.setViewName("ftl/index"); return modelAndView; } } }
-
类路径下创建templates/ftl 文件夹。前端页面如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>首页</title> </head> <body> <div> <form action="/user/login" method="get"> <input type="text" value="username" name="username"><br> <input type="password" value="password" name="password"><br> <input type="submit" name="登录" value="登录"> </form> </div> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>首页</title> </head> <body> <p>欢迎${username}</p> <iframe id="play" src="" width="500" height="200"> </iframe> </body> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script type="text/javascript"> $(document).ready(function() { var socketUrl = "ws://localhost:8080/myWebSocketHandler?"+"userId="+"${username}"; var socket = new WebSocket(socketUrl); //打开事件 socket.onopen = function() { console.log("websocket已打开"); //socket.send("这是来自客户端的消息" + location.href + new Date()); }; //获得消息事件 socket.onmessage = function(msg) { console.log(msg.data); //发现消息进入 开始处理前端触发逻辑 document.getElementById("play").src = msg.data; }; //关闭事件 socket.onclose = function() { console.log("websocket已关闭"); }; //发生了错误事件 socket.onerror = function() { console.log("websocket发生了错误"); } }); </script> </html>
测试
- http://localhost:8080/user/loginPage
- 输入用户名、密码点击登陆查看页面效果和控制台输出。