SpringBoot + WebSocket 开发笔记
1. 服务端的实现,我尝试了两种方式:
- 第一种是用“@ServerEndPoint”注解来实现,实现简单;
- 第二种稍显麻烦,但是可以添加拦截器在WebSocket连接建立和断开前进行一些额外操作。
不管用哪种实现方式,都需要先导入jar包(如下),其中version根据实际springboot版本选择,避免冲突
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <!-- <version>1.3.5.RELEASE</version> --> </dependency>
1.1 第一种实现方法
(1)WebSocket 业务逻辑实现。参数传递采用路径参数的方法,通过以下方式获取参数:
-
@ServerEndpoint("/testWebSocket/{id}/{name}")
-
public void onOpen(Session session, @PathParam("id") long id, @PathParam("name") String name)
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RestController; @ServerEndpoint("/testWebSocket/{id}/{name}") @RestController public class TestWebSocket { // 用来记录当前连接数的变量 private static volatile int onlineCount = 0; // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象 private static CopyOnWriteArraySet<TestWebSocket> webSocketSet = new CopyOnWriteArraySet<TestWebSocket>(); // 与某个客户端的连接会话,需要通过它来与客户端进行数据收发 private Session session; private static final Logger LOGGER = LoggerFactory.getLogger(TestWebSocket.class);
@OnOpen public void onOpen(Session session, @PathParam("id") long id, @PathParam("name") String name) throws Exception { this.session = session; System.out.println(this.session.getId()); webSocketSet.add(this); LOGGER.info("Open a websocket. id={}, name={}", id, name); } @OnClose public void onClose() { webSocketSet.remove(this); LOGGER.info("Close a websocket. "); } @OnMessage public void onMessage(String message, Session session) { LOGGER.info("Receive a message from client: " + message); } @OnError public void onError(Session session, Throwable error) { LOGGER.error("Error while websocket. ", error); } public void sendMessage(String message) throws Exception { if (this.session.isOpen()) { this.session.getBasicRemote().sendText("Send a message from server. "); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { TestWebSocket.onlineCount++; } public static synchronized void subOnlineCount() { TestWebSocket.onlineCount--; } }
(2)配置ServerEndpointExporter,配置后会自动注册所有“@ServerEndpoint”注解声明的Websocket Endpoint
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
1.2 第二种实现方法
(1)WebSocket 业务逻辑实现。参数传递采用类似GET请求的方式传递,服务端的参数在拦截器中获取之后通过attributes传递给WebSocketHandler。
import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; @RestController public class TestWebSocketController implements WebSocketHandler { private static AtomicInteger onlineCount = new AtomicInteger(0); private static final ArrayList<WebSocketSession> sessions = new ArrayList<>(); private final Logger LOGGER = LoggerFactory.getLogger(TestWebSocketController.class); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); int onlineNum = addOnlineCount(); LOGGER.info("Oprn a WebSocket. Current connection number: " + onlineNum); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session); int onlineNum = subOnlineCount(); LOGGER.info("Close a webSocket. Current connection number: " + onlineNum); } @Override public void handleMessage(WebSocketSession wsSession, WebSocketMessage<?> message) throws Exception { LOGGER.info("Receive a message from client: " + message.toString()); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { LOGGER.error("Exception occurs on webSocket connection. disconnecting...."); if (session.isOpen()) { session.close(); } sessions.remove(session); subOnlineCount(); } /* * 是否支持消息拆分发送:如果接收的数据量比较大,最好打开(true), 否则可能会导致接收失败。 * 如果出现WebSocket连接接收一次数据后就自动断开,应检查是否是这里的问题。 */ @Override public boolean supportsPartialMessages() { return true; } public static int getOnlineCount() { return onlineCount.get(); } public static int addOnlineCount() { return onlineCount.incrementAndGet(); } public static int subOnlineCount() { return onlineCount.decrementAndGet(); } }
(2)HandShake 拦截器实现
import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; 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; public class TestHandShakeInterceptor extends HttpSessionHandshakeInterceptor { private final Logger LOGGER = LoggerFactory.getLogger(TestHandShakeInterceptor.class); /* * 在WebSocket连接建立之前的操作,以鉴权为例 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { LOGGER.info("Handle before webSocket connected. "); // 获取url传递的参数,通过attributes在Interceptor处理结束后传递给WebSocketHandler // WebSocketHandler可以通过WebSocketSession的getAttributes()方法获取参数 ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request; String id = serverRequest.getServletRequest().getParameter("id"); String name = serverRequest.getServletRequest().getParameter("name"); if (tokenValidation.validateSign()) { LOGGER.info("Validation passed. WebSocket connecting.... "); attributes.put("id", id); attributes.put("name", name); return super.beforeHandshake(request, response, wsHandler, attributes); } else { LOGGER.error("Validation failed. WebSocket will not connect. "); return false; } } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { // 省略 } }
(3)WebSocket 配置类实现(注册WebSocket实现类,绑定接口,同时将实现类和拦截器绑定)
import org.springframework.beans.factory.annotation.Autowired; 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 TestWebSocketController; import TestHandShakeInterceptor; @Configuration @EnableWebMvc @EnableWebSocket public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer { @Autowired private TestWebSocketController testWebSocketController; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(TestWebSocketController, "/testWebSocket") .addInterceptors(new TestHandShakeInterceptor()).setAllowedOrigins("*"); } }
1.3 补充说明
(1)在WebSocket实现过程中,尤其是通过“@ServerEndpoint”实现的时候,可能会出现注入失败的问题,即注入的Bean为null的问题。可以通过手动注入的方式来解决,需要改造实现类和SpringBoot启动类,如下:
@ServerEndpoint("testWebsocket") @RestController public class WebSocketController { private TestService testService; private static ApplicationContext applicationContext; @OnOpen public void onOpen(Session session) { testService = applicationContext.getBean(TestService.class); } @OnClose public void onClose() {} @OnMessage public void onMessage(String message, Session session) {} @OnError public void onError(Session session, Throwable error) {} public static void setApplicationContext(ApplicationContext applicationContext) { WebSocketController.applicationContext = applicationContext; } }
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import WebSocketController; @SpringBootApplication public class Application { public static void main(String[] args) { // SpringApplication.run(Application.class, args); SpringApplication springApplication = new SpringApplication(Application.class); ConfigurableApplicationContext configurableApplicationContext = springApplication.run(args); WebSocketController.setApplicationContext(configurableApplicationContext); // 解决WebSocket不能注入的问题 } }
2. 客户端的实现,我尝试了html和java WebSocketClient两种方式
2.1 html实现
<!DOCTYPE html> <html> <head> <title>WebSocket示例</title> <meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport' /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <input id="text" type="text"/> <button onclick="send()">发送消息</button> <hr/> <button onclick="closeWebSocket()">关闭WebSocket连接</button> <hr/> <div id="message"></div> </body> <script type="text/javascript"> var websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { // 不带参数的写法 websocket = new WebSocket("ws://127.0.0.1:18080/testWebsocket"); // 通过路径传递参数的方法(服务端采用第一种方法"@ServerEndpoint"实现) websocket = new WebSocket("ws://127.0.0.1:18080/testWebsocket/23/Lebron"); // 通过类似GET请求方式传递参数的方法(服务端采用第二种方法"WebSocketHandler"实现) websocket = new WebSocket("ws://127.0.0.1:18080/testWebsocket?id=23&name=Lebron"); } else { alert('当前浏览器 Not support websocket') } //连接发生错误的回调方法 websocket.onerror = function () { setMessageInnerHTML("WebSocket连接发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function () { setMessageInnerHTML("WebSocket连接成功"); } //接收到消息的回调方法 websocket.onmessage = function (event) { setMessageInnerHTML(event.data); } //连接关闭的回调方法 websocket.onclose = function () { setMessageInnerHTML("WebSocket连接关闭"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { closeWebSocket(); } //将消息显示在网页上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //关闭WebSocket连接 function closeWebSocket() { websocket.close(); } //发送消息 function send() { var message = document.getElementById('text').value; websocket.send(message); } </script> </html>
2.2 Java WebSocketClient实现
(1)WebSocketClient 实现类
import java.net.URI; import org.java_websocket.client.WebSocketClient; import org.java_websocket.drafts.Draft; import org.java_websocket.handshake.ServerHandshake; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TestWebSocketClient extends WebSocketClient { private final Logger LOGGER = LoggerFactory.getLogger(TestWebSocketClient.class); public TestWebSocketClient(URI serverUri) { super(serverUri); } public TestWebSocketClient(URI serverUri, Draft protocolDraft) { super(serverUri, protocolDraft); } @Override public void onOpen(ServerHandshake serverHandshake) { LOGGER.info("Open a WebSocket connection on client. "); } @Override public void onClose(int arg0, String arg1, boolean arg2) { LOGGER.info("Close a WebSocket connection on client. "); } @Override public void onMessage(String msg) { LOGGER.info("WebSocketClient receives a message: " + msg); } @Override public void onError(Exception exception) { LOGGER.error("WebSocketClient exception. ", exception); } }
(2)WebSocketClient 发送数据
String serverUrl = "ws://127.0.0.1:18080/testWebsocket" URI recognizeUri = new URI(serverUrl); client = new TestWebSocketClient(recognizeUri, new Draft_6455()); client.connect(); client.send("This is a message from client. ");