抛开别的语言不谈,使用 Java 实现的 WebSocket API 由 Oracle 制定,并已经成为 Java EE 7 的一部分,我们先使用该 API 简单实现一个 WebSocket 通信的 demo。
前置条件 JDK 需要大于等于 7、servlet-api 3.1、websocket-api 1.1 以及实现了 JSR356 规范的 Web 容器。
Tomcat 7 及以上版本都可以,Tomcat 7 以前的版本提供了 Tomcat 自己的 WebSocketServlet,在 Tomcat 7 中 deprecated
,在 Tomcat 8 中移除。
基于 JSR356 WebSocket API 的 demo 页面的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > index</title > </head > <body > <div > <textarea id ="txt" style ="width: 210px;height: 110px;" > </textarea > <br > <button id ="connect" > 新建连接</button > <button id ="send" > 发送消息</button > <button id ="close" > 断开连接</button > </div > <div id ="info" > </div > </body > <script type ="text/javascript" src ="/js/websocket.js" > </script > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 var $ = function (id ) { return document .querySelector (id); }; var ws = null ;var txt = $("#txt" );var connect = $("#connect" );var send = $("#send" );var close = $("#close" );var info = $("#info" );connect.addEventListener ("click" , function ( ) { ws = new WebSocket ("ws://localhost:8080/web/websocket" ); ws.addEventListener ("open" , function (open ) { info.innerHTML += "<p>" + new Date () + " 连接建立 </p>" ; }); ws.addEventListener ("message" , function (message ) { info.innerHTML += "<p>" + new Date () + " 服务器返回消息:" + message.data + "</p>" ; }); ws.addEventListener ("error" , function (error ) { info.innerHTML += "<p>" + new Date () + " 出错 </p>" ; }); ws.addEventListener ("close" , function (close ) { info.innerHTML += "<p>" + new Date () + " 连接关闭:" + close.code + "</p>" ; }); }); send.addEventListener ("click" , function ( ) { if (txt.value .length > 0 && ws) ws.send (txt.value ); }); close.addEventListener ("click" , function ( ) { if (ws) { ws.close (); } });
后端 Java 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.nekolr;import javax.websocket.*;import javax.websocket.server.ServerEndpoint;import java.io.IOException;@ServerEndpoint("/web/websocket") public class AnnotationEndpoint { @OnOpen public void onOpen (Session session) { System.out.println("------连接建立------" ); } @OnMessage public void onMessage (Session session, String text) { if (session.isOpen()) { try { session.getBasicRemote().sendText(text); } catch (IOException e) { e.printStackTrace(); try { session.close(); } catch (IOException e1) { e1.printStackTrace(); } } } } @OnClose public void onClose (Session session, CloseReason closeReason) { System.out.println("------连接关闭------" ); } }
除了使用注解的方式,也可以通过继承 Endpoint 来实现,在这之前,需要先写一个配置类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.nekolr;import javax.websocket.Endpoint;import javax.websocket.server.ServerApplicationConfig;import javax.websocket.server.ServerEndpointConfig;import java.util.HashSet;import java.util.Set;public class WebSocketConfig implements ServerApplicationConfig { @Override public Set<ServerEndpointConfig> getEndpointConfigs (Set<Class<? extends Endpoint>> set) { Set<ServerEndpointConfig> result = new HashSet <>(); for (Class<? extends Endpoint > ep : set) { result.add(ServerEndpointConfig.Builder.create(ep, "/web/websocket" ).build()); } return result; } @Override public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) { return set; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.nekolr;import javax.websocket.*;import java.io.IOException;public class CommonEndpoint extends Endpoint { @Override public void onOpen (Session session, EndpointConfig endpointConfig) { System.out.println("------连接建立------" ); session.addMessageHandler(new NekoMessageHandler (session.getBasicRemote())); } @Override public void onClose (Session session, CloseReason closeReason) { System.out.println("------连接关闭------" ); super .onClose(session, closeReason); } private static class NekoMessageHandler implements MessageHandler .Whole<String> { private final RemoteEndpoint.Basic remoteEndpointBasic; private NekoMessageHandler (RemoteEndpoint.Basic remoteEndpointBasic) { this .remoteEndpointBasic = remoteEndpointBasic; } @Override public void onMessage (String message) { if (remoteEndpointBasic != null ) { try { remoteEndpointBasic.sendText(message); } catch (IOException e) { e.printStackTrace(); } } } } }
Spring 的实现 Spring 从 4.0 版本后开始支持 JSR356 WebSocket API,同时也提供了一套 Spring 的 WebSocket API。
我们在使用 JSR356 WebSocket API 时,是通过 Servlet 容器扫描来部署 Endpoint 处理类的,现在我们使用 Spring 来处理这个过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.nekolr;import org.springframework.web.WebApplicationInitializer;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import org.springframework.web.servlet.DispatcherServlet;import javax.servlet.ServletRegistration;public class WebInitializer implements WebApplicationInitializer { @Override public void onStartup (javax.servlet.ServletContext servletContext) { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext (); ctx.register(MvcConfig.class, NekoEndpointConfig.class); ctx.setServletContext(servletContext); ServletRegistration.Dynamic servlet = servletContext.addServlet("mvc" , new DispatcherServlet (ctx)); servlet.addMapping("/" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.nekolr;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.HandlerMapping;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;@Configuration @EnableWebMvc @ComponentScan(value = {"com.nekolr"}) public class MvcConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/js/**" ).addResourceLocations("/js/" ); registry.addResourceHandler("/views/**" ).addResourceLocations("/WEB-INF/views/" ); } @Bean public HandlerMapping getHandlerMapping () { return super .resourceHandlerMapping(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.nekolr;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;import org.springframework.web.socket.server.standard.ServerEndpointRegistration;@Configuration public class NekoEndpointConfig { @Bean public ServerEndpointExporter endpointExporter () { return new ServerEndpointExporter (); } @Bean public ServerEndpointRegistration registration () { return new ServerEndpointRegistration ("/web/websocket" , NekoEndpoint.class); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.nekolr;import javax.websocket.*;import java.io.IOException;public class NekoEndpoint extends Endpoint { @Override public void onOpen (Session session, EndpointConfig endpointConfig) { session.addMessageHandler(new MessageHandler .Whole<String>() { @Override public void onMessage (String s) { try { session.getBasicRemote().sendText(s); } catch (IOException e) { e.printStackTrace(); } } }); } @Override public void onClose (Session session, CloseReason closeReason) { super .onClose(session, closeReason); } }
下面我们再使用 Spring 提供的一套 API 来实现,同时,我们在页面使用 SockJS,Spring 对 SockJS 有很好的支持。与 JSR356 WebSocket API 的 Endpoint 不同,使用 Spring WebSocket API,Endpoint 变成了 WebSocketHandler,同时也更容易理解了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <dependencies > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.1.0</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 4.3.23.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-web</artifactId > <version > 4.3.23.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 4.3.23.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-websocket</artifactId > <version > 4.3.23.RELEASE</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.8.6</version > <scope > runtime</scope > </dependency > </dependencies >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.nekolr;import org.springframework.web.socket.*;public class NekoWebSocketHandler implements WebSocketHandler { @Override public void afterConnectionEstablished (WebSocketSession webSocketSession) { System.out.println("---------连接建立------------" ); } @Override public void handleMessage (WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception { TextMessage returnMessage = new TextMessage (webSocketMessage.getPayload().toString()); System.out.println(webSocketSession.getHandshakeHeaders().getFirst("Cookie" )); webSocketSession.sendMessage(returnMessage); } @Override public void handleTransportError (WebSocketSession webSocketSession, Throwable throwable) throws Exception { if (webSocketSession.isOpen()) { webSocketSession.close(); } System.out.println("---------连接出错------------" ); } @Override public void afterConnectionClosed (WebSocketSession webSocketSession, CloseStatus closeStatus) { System.out.println("---------连接断开------------" ); } @Override public boolean supportsPartialMessages () { return false ; } }
同时还可以写拦截器来过滤握手请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.nekolr;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;import java.util.Map;public class NekoHandshakeInterceptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake (ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { if (request.getHeaders().containsKey("Sec-WebSocket-Extensions" )) { request.getHeaders().set("Sec-WebSocket-Extensions" , "permessage-deflate" ); } 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); } }
同时还需要写一个配置类来部署这些处理器类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.nekolr;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;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;import org.springframework.web.socket.server.HandshakeInterceptor;@Configuration @EnableWebSocket public class NekoWebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers (WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry.addHandler(nekoWebSocketHandler(), "/web/websocket" ) .addInterceptors(nekoHandshakeInterceptor()).withSockJS(); } @Bean public WebSocketHandler nekoWebSocketHandler () { return new NekoWebSocketHandler (); } @Bean public HandshakeInterceptor nekoHandshakeInterceptor () { return new NekoHandshakeInterceptor (); } }
由于没有使用 Spring 的 xml 配置文件(同时也没有使用 web.xml),所以要写配置类。如果 Spring 是通过 xml 文件来配置的话,可以参考下面的写法:
1 2 3 4 5 6 7 8 9 <websocket:handlers allowed-origins ="*" > <websocket:mapping path ="/web/websocket" handler ="nekoWebSocketHandler" /> <websocket:handshake-interceptors > <bean class ="com.nekolr.NekoHandshakeInterceptor" /> </websocket:handshake-interceptors > <websocket:sockjs /> </websocket:handlers > <bean id ="nekoWebSocketHandler" class ="com.nekolr.NekoWebSocketHandler" />
MvcConfig 与之前的写法一样,WebInitializer 需要将 NekoWebSocketConfig 加入并注册。与此同时,页面需要引入 sockjs.js 并将 new WebSocket
修改为 new SockJS
。
参考
JSR 356, Java API for WebSocket
websocket-spec
WebSocketServlet (Apache Tomcat 7.0.94 API Documentation)
spring4 websocket + sockjs 遇到的坑
Spring 4.0 系列 9 - websocket 简单应用