WebSocket
一、Websocket介绍
WebSocket协议,RFC 6455,提供了一种标准化的方法,通过单个TCP连接在客户端和服务器之间建立一个全双工双向通信通道,它是一种不同于HTTP的TCP协议,但被设计用于HTTP之上,使用端口80和443并允许重用现有的防火墙规则。
WebSocket交互从HTTP请求开始,HTTP请求使用HTTP Upgrade header进行upgrade,或者在本例中切换到WebSocket协议,下面的示例展示了这种交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg== Sec-WebSocket-Protocol: v10.stomp, v11.stomp Sec-WebSocket-Version: 13 Origin: http://localhost:8080
- Upgrade: websocket => Upgrade header。
- Connection: Upgrade => 使用Upgrade连接。
支持WebSocket的服务器返回的输出与下面类似,而不是通常的200
状态代码:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0= Sec-WebSocket-Protocol: v10.stomp
- HTTP/1.1 101 Switching Protocols=> 协议转换。
在成功握手之后,在HTTP upgrade请求基础上的TCP socket仍然是开放的,以便客户端和服务器继续发送和接收消息。
关于WebSockets如何工作的完整介绍超出了本文的范围,参见RFC 6455,HTML5的WebSocket章节,或者Web上许多介绍和教程中的任何一个。
注意,如果WebSocket服务器运行在web服务器(例如nginx)之后,你可能需要将它配置为将WebSocket upgrade请求传递到WebSocket服务器,同样,如果应用程序在云环境中运行,请检查与WebSocket支持相关的云提供商的说明。
HTTP与WebSocket
尽管WebSocket被设计为与HTTP兼容并从HTTP请求开始,但重要的是要理解这两个协议导致了非常不同的体系结构和应用程序编程模型。
在HTTP和REST中,应用程序被建模为多个URL,为了与应用程序交互,客户端访问那些URL,请求-响应样式,服务器根据HTTP URL、方法和headers将请求路由到适当的处理程序。
相比之下,在WebSockets中,通常初始连接只有一个URL,随后,所有应用程序消息都在同一个TCP连接上流动,这指向了一个完全不同的异步、事件驱动的消息传递体系结构。
WebSocket也是一种低级别的传输协议,与HTTP不同的是,它没有对消息的内容规定任何语义,这意味着,除非客户端和服务器在消息语义上达成一致,否则无法路由或处理消息。
通过HTTP握手请求上的Sec-WebSocket-Protocolheader,WebSocket客户端和服务器可以协商使用更高级别的消息传递协议(例如STOMP),在没有这些的情况下,他们需要制定自己的约定。
Websocket的特点
服务端可以主动推送信息给客户端。
Websocket只需要一次HTTP交互,来进行协议上的切换,整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的无状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了服务端要反复解析HTTP请求头的问题。
数据格式比较轻量,性能开销小,通信高效。可以发送文本,也可以发送二进制数据。
没有同源限制,客户端可以与任意服务器通信。
何时使用WebSockets
WebSockets可以使一个web页面成为动态的和交互式的,然而,在许多情况下,Ajax和HTTP流或长轮询的组合可以提供简单而有效的解决方案。
例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没有问题,另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是决定性因素,如果消息量相对较低(例如,监视网络故障),HTTP流或轮询可以提供有效的解决方案,正是低延迟、高频率和高容量的组合,使WebSocket的使用成为最好的例子。
还要记住,在Internet上,超出你控制范围的限制性代理可能会妨碍WebSocket交互,要么是因为它们没有配置为传递Upgrade header,要么是因为它们关闭了看起来空闲的长连接,这意味着对于防火墙内的内部应用程序使用WebSocket比面向公共的应用程序更直接。
二、Websocket API
Spring Framework提供了一个WebSocket API,你可以使用它来编写处理WebSocket消息的客户端和服务器端应用程序。
WebSocketHandler
创建WebSocket服务器就像实现WebSocketHandler一样简单,或者更可能是扩展TextWebSocketHandler或BinaryWebSocketHandler,下面的示例使用TextWebSocketHandler:
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.TextMessage; public class MyHandler extends TextWebSocketHandler { @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // ... } }
有专门的WebSocket Java配置和XML命名空间支持将前面的WebSocket处理程序映射到特定的URL,如下面的示例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
本示例用于Spring MVC应用程序,并且应该包含在DispatcherServlet的配置中,然而,Spring的WebSocket支持不依赖于Spring MVC,在WebSocketHttpRequestHandler的帮助下,将WebSocketHandler集成到其他http服务环境中相对简单。
WebSocket握手
定制初始的HTTP WebSocket握手请求的最简单方法是通过HandshakeInterceptor
,它公开握手之前和之后的方法,你可以使用这样的拦截器来阻止握手或使WebSocketSession
的任何属性有效,下面的示例使用内置的拦截器将HTTP会话属性传递给WebSocket会话:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyHandler(), "/myHandler") .addInterceptors(new HttpSessionHandshakeInterceptor()); } }
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> <websocket:handshake-interceptors> <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/> </websocket:handshake-interceptors> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
更高级的选项是扩展DefaultHandshakeHandler,该处理程序执行WebSocket握手的步骤,包括验证客户端源、协商子协议和其他细节。如果应用程序需要配置定制的RequestUpgradeStrategy以适应WebSocket服务器引擎和还不支持的版本,那么它可能还需要使用这个选项(有关此主题的更多信息,请参阅部署),Java配置和XML命名空间都使配置自定义HandshakeHandler成为可能。
Spring提供了一个WebSocketHandlerDecorator基类,你可以使用它来修饰WebSocketHandler具有额外的行为。默认情况下,在使用WebSocket Java配置或XML命名空间时,会提供并添加日志记录和异常处理实现。ExceptionWebSocketHandlerDecorator捕获来自任何WebSocketHandler的所有未捕获的异常,并以状态1011关闭WebSocket会话,这表示服务器错误。
部署
Spring WebSocket API很容易集成到Spring MVC应用程序中,其中DispatcherServlet同时服务于HTTP WebSocket握手和其他HTTP请求,通过调用WebSocketHttpRequestHandler,很容易将其集成到其他HTTP处理场景中。这很方便,也很容易理解,但是,对于JSR-356运行时,需要特别考虑。
Java WebSocket API(JSR-356)提供了两种部署机制,第一个涉及Servlet容器类路径扫描(Servlet 3特性)在启动时,另一个是Servlet容器初始化时使用的注册API。这两种机制都无法为所有HTTP处理使用单个“前端控制器” - 包括WebSocket握手和所有其他HTTP请求 - 比如Spring MVC的DispatcherServlet。
这是JSR-356的一个重要限制,Spring的WebSocket支持地址具有特定于服务器的RequestUpgradeStrategy实现,即使在JSR-356运行时运行也是如此。对于Tomcat、Jetty、GlassFish、WebLogic、WebSphere和Undertow(和WildFly),目前存在这样的策略。
第二个需要考虑的问题是,具有JSR-356支持的Servlet容器预计将执行ServletContainerInitializer(SCI)扫描,这可能会减慢应用程序的启动速度 — 在某些情况下,会显著降低启动速度。如果在升级到支持JSR-356的Servlet容器版本后观察到重大影响,通过使用web.xml中的<absolute-ordering />元素,应该可以选择性地启用或禁用web片段(和SCI扫描),如下例所示:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <absolute-ordering/> </web-app>
然后可以根据名称选择性地启用web片段,比如Spring自己的SpringServletContainerInitializer,它为Servlet 3 Java初始化API提供支持,下面的示例展示了如何做到这一点:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <absolute-ordering> <name>spring_web</name> </absolute-ordering> </web-app>
服务器配置
每个基础WebSocket引擎都公开了控制运行时特性的配置属性,比如消息缓冲区大小、空闲超时等等。
对于Tomcat、WildFly和GlassFish,你可以将ServletServerContainerFactoryBean添加到WebSocket Java配置中,如下面的示例所示:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192); container.setMaxBinaryMessageBufferSize(8192); return container; } }
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <bean class="org.springframework...ServletServerContainerFactoryBean"> <property name="maxTextMessageBufferSize" value="8192"/> <property name="maxBinaryMessageBufferSize" value="8192"/> </bean> </beans>
对于客户端WebSocket配置,你应该使用WebSocketContainerFactoryBean(XML)或ContainerProvider.getWebSocketContainer()(Java配置)。
对于Jetty,需要提供预配置的Jetty WebSocketServerFactory,并通过WebSocket Java配置将其插入Spring的DefaultHandshakeHandler,下面的示例展示了如何做到这一点:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler()); } @Bean public DefaultHandshakeHandler handshakeHandler() { WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); policy.setInputBufferSize(8192); policy.setIdleTimeout(600000); return new DefaultHandshakeHandler( new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); } }
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers> <websocket:mapping path="/echo" handler="echoHandler"/> <websocket:handshake-handler ref="handshakeHandler"/> </websocket:handlers> <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler"> <constructor-arg ref="upgradeStrategy"/> </bean> <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy"> <constructor-arg ref="serverFactory"/> </bean> <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory"> <constructor-arg> <bean class="org.eclipse.jetty...WebSocketPolicy"> <constructor-arg value="SERVER"/> <property name="inputBufferSize" value="8092"/> <property name="idleTimeout" value="600000"/> </bean> </constructor-arg> </bean> </beans>
允许的源
在Spring Framework 4.1.5中,WebSocket和SockJS的默认行为是只接受相同源的请求,也可以允许所有或指定的源列表。这种检查主要是为浏览器客户端设计的,没有什么可以阻止其他类型的客户端修改Origin header值
三种可能的行为是:
- 只允许相同源的请求(默认):在这种模式下,当启用SockJS时,Iframe HTTP响应header X-Frame-Options被设置为SAMEORIGIN,并且JSONP传输被禁用,因为它不允许检查请求的源,因此,当启用该模式时,不支持IE6和IE7。
- 允许指定的源列表:每个允许的源必须以http://或https://开始,在这种模式下,当启用SockJS时,IFrame传输将被禁用,因此,当启用该模式时,将不支持IE6到IE9。
- 允许所有的源:要启用此模式,你应该提供*作为允许的源值,在这种模式下,所有的传输都是可用的。
你可以配置WebSocket和SockJS允许的源,如下面的例子所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("http://mydomain.com"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers allowed-origins="http://mydomain.com"> <websocket:mapping path="/myHandler" handler="myHandler" /> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
三、SockJS Fallback
在公共Internet上,在你控制之外的限制性代理可能会妨碍WebSocket交互,要么是因为它们没有配置为传递Upgrade header,要么是因为它们关闭了看起来空闲的长连接。
这个问题的解决方案是WebSocket模拟 - 也就是说,首先尝试使用WebSocket,然后转而使用基于http的技术来模拟WebSocket交互并公开相同的应用程序级别的API。
在Servlet堆栈上,Spring Framework为SockJS协议提供了服务器(以及客户端)支持。
概述
SockJS的目标是让应用程序使用WebSocket API,但如果在运行时有必要,可以回退到非WebSocket替代方案,而不需要修改应用程序代码。
SockJS包括:
- SockJS协议以可执行的叙述性测试的形式定义。
- SockJS JavaScript客户端 — 用于浏览器的客户端库。
- SockJS服务器实现,包括一个在Spring Framework spring-websocket模块。
- spring-websocket模块中的SockJS Java客户端(4.1版以来)。
SockJS是为浏览器设计的,它使用各种技术来支持各种浏览器版本。传输分为三大类:WebSocket、HTTP流媒体以及HTTP长轮询。
SockJS客户端首先发送GET /info以从服务器获取基本信息,在那之后,它必须决定使用哪种传输方式。如果可能的话,使用WebSocket,如果不是,在大多数浏览器中,至少有一个HTTP流媒体选项,如果还不是,则使用HTTP(长)轮询。
所有传输请求都具有以下URL结构:,有关这些类别的概述,请参阅这篇博客文章。
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
- {server-id}用于在集群中路由请求,但不用于其他用途。
- {session-id}关联属于SockJS会话的HTTP请求。
- {transport}指示传输类型(例如websocket、xhr-streaming和其他)。
WebSocket传输只需要一个HTTP请求来完成WebSocket握手,此后所有消息都在该socket上交换。
HTTP传输需要更多的请求,例如,Ajax/XHR流依赖于一个对服务器到客户端消息的长时间运行的请求,以及对客户端到服务器消息的额外HTTP POST请求。长轮询与此类似,只是它在每个服务器到客户端发送后结束当前请求。
SockJS添加了最少的消息框架,例如,服务器最初发送字母o(“open” frame),消息以["message1","message2"](json编码数组)的形式发送,如果在25秒内(默认情况下)没有消息流,则发送字母h("heartbeat" frame)和字母c("close" frame)来关闭会话。
要了解更多信息,请在浏览器中运行一个示例并观察HTTP请求,SockJS客户端允许修复传输列表,因此可以一次查看每个传输。SockJS客户端还提供了一个debug标志,它在浏览器控制台中启用有用的消息,在服务器端,你可以为org.springframework.web.socket启用TRACE日志记录。
启用SockJS
你可以通过Java配置启用SockJS,如下面的示例所示:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").withSockJS(); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> <websocket:sockjs/> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
前面的示例用于Spring MVC应用程序,应该包含在DispatcherServlet的配置中,然而,Spring的WebSocket和SockJS支持并不依赖于Spring MVC,在SockJsHttpRequestHandler的帮助下集成到其他HTTP服务环境中相对简单。
在浏览器端,应用程序可以使用sockjs-client(1.0.x版),它模拟W3C WebSocket API,并与服务器通信,根据运行它的浏览器选择最佳传输选项。请参阅sockjs-client页面和浏览器支持的传输类型列表。客户端还提供了一些配置选项 - 例如,指定要包含哪些传输。
IE 8和9
Internet Explorer 8和9仍在使用中,它们是有SockJS的关键原因,本节讨论在这些浏览器中运行的重要注意事项。
SockJS客户端使用微软的XDomainRequest支持IE 8和9中的Ajax/XHR流,这可以跨域工作,但不支持发送cookie。cookie对于Java应用程序来说通常是必不可少的,但是,由于SockJS客户端可以与许多服务器类型(不仅仅是Java类型)一起使用,因此需要知道cookie是否重要。如果是这样,SockJS客户端更喜欢使用Ajax/XHR来流媒体,否则,它依赖于基于iframe的技术。
来自SockJS客户端的第一个/info请求是对信息的请求,这些信息可能会影响客户端对传输的选择,其中一个细节是,服务器应用程序是否依赖于cookie(例如,用于身份验证还是使用具有粘性的会话进行集群),Spring的SockJS支持包括一个名为sessionCookieNeeded的属性,它是默认启用的,因为大多数Java应用程序都依赖于JSESSIONID cookie。如果你的应用程序不需要它,你可以关闭这个选项,然后SockJS客户端应该选择IE8和IE9中的xdr-streaming。
如果你确实使用基于iframe的传输,请记住,可以通过设置HTTP响应头X-Frame-Options为DENY、SAMEORIGIN或ALLOW-FROM <origin>来指示浏览器阻止在给定页面上使用IFrames,这是用来防止点击劫持。
Spring Security 3.2+支持在每个响应上设置X-Frame-Options,默认情况下,Spring Security Java配置将其设置为DENY,在3.2中,Spring Security XML命名空间默认不设置该header,但可以配置为这样做,将来,它可能会默认设置它。
如果你的应用程序添加了X-Frame-Options响应header(它应该这样做!)并依赖于基于iframe的传输,你需要将header值设置为SAMEORIGIN或ALLOW-FROM <origin>。Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的,默认情况下,iframe被设置为从CDN位置下载SockJS客户端,将此选项配置为使用与应用程序相同源的URL是个好主意。
下面的示例展示了如何在Java配置中实现这一点:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS() .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js"); } // ... }
XML命名空间通过<websocket:sockjs>元素提供了类似的选项。
在初始开发期间,启用SockJS客户端
devel
模式,以防止浏览器缓存SockJS请求(比如iframe),否则会被缓存。
心跳
SockJS协议要求服务器发送心跳消息,以防止代理断定连接挂起,Spring SockJS配置有一个名为heartbeatTime的属性,你可以使用它来定制频率。默认情况下,在25秒后发送心跳,假设在该连接上没有发送其他消息,这个25秒的值符合以下对公共互联网应用程序的IETF建议。
在WebSocket和SockJS上使用STOMP时,如果STOMP客户端和服务器协商交换心跳,那么SockJS心跳就被禁用了。
Spring SockJS支持还允许你配置TaskScheduler来调度心跳任务,任务调度程序由线程池支持,其默认设置基于可用处理器的数量,你应该考虑根据你的特定需要定制设置。
客户端端口连接
HTTP流和HTTP长轮询SockJS传输需要一个连接以保持比通常更长的开放时间。
在Servlet容器中,这是通过Servlet 3异步支持完成的,它允许退出Servlet容器线程,处理请求,并继续写入来自另一个线程的响应。
一个特定的问题是Servlet API没有为已经离开的客户端提供通知,查看eclipse-ee4j/servlet-api#44。但是,Servlet容器在后续尝试写入响应时引发异常,由于Spring的SockJS服务支持服务器发送心跳(默认情况下每25秒一次),这意味着客户端断开连接通常在这段时间内被检测到(或者更早,如果消息发送得更频繁)。
因此,网络I/O故障可能会发生,因为客户端断开连接,这会用不必要的堆栈跟踪填充日志。Spring尽最大努力识别代表客户端断开连接(特定于每个服务器)的网络故障并通过使用专用日志类别记录一条最小消息,DISCONNECTED_CLIENT_LOG_CATEGORY(定义在AbstractSockJsSession)。如果需要查看堆栈跟踪,可以将日志类别设置为TRACE。
SockJS和CORS
如果允许跨源请求,那么SockJS协议将使用CORS来支持XHR流和轮询传输中的跨域支持,因此,除非检测到响应中存在CORS headers,否则将自动添加CORS headers,因此,如果应用程序已经配置为提供CORS支持(例如,通过Servlet过滤器),Spring的SockJsService就跳过了这一部分。
还可以通过在Spring的SockJsService中设置suppressCors属性来禁用这些CORS headers的添加。
SockJS期望以下headers和值:
- Access-Control-Allow-Origin:从Origin请求header的值初始化。
- Access-Control-Allow-Credentials:总是设为true。
- Access-Control-Request-Headers:从等效请求header的值初始化。
- Access-Control-Allow-Methods:传输支持的HTTP方法(参见TransportType枚举)。
- Access-Control-Max-Age:设置为31536000(1年)。
有关确切的实现,请参阅源代码中AbstractSockJsService和TransportType枚举中的addCorsHeaders。
或者,如果CORS配置允许,考虑使用SockJS端点前缀排除URL,从而让Spring的SockJsService处理它。
SockJsClient
Spring提供了一个SockJS Java客户端来连接到远程SockJS端点,而无需使用浏览器,当需要通过公共网络在两台服务器之间进行双向通信时(也就是说,网络代理可能会阻止WebSocket协议的使用),这一点尤其有用。对于测试目的(例如,模拟大量并发用户),SockJS Java客户端也非常有用。
SockJS Java客户端支持websocket、xhr-streaming和xhr-polling传输,剩下的一个只有在浏览器中使用才有意义。
你可以配置WebSocketTransport使用:
- 在JSR-356运行时中的StandardWebSocketClient。
- 使用Jetty 9+原生WebSocket API的JettyWebSocketClient。
- Spring的WebSocketClient的任何实现。
根据定义,XhrTransport同时支持xhr-streaming和xhr-polling,从客户端角度来看,除了用于连接到服务器的URL之外,没有任何区别,目前有两种实现:
- RestTemplateXhrTransport使用Spring的RestTemplate用于HTTP请求。
- JettyXhrTransport使用Jetty的HttpClient用于HTTP请求。
下面的示例展示了如何创建SockJS客户端并连接到SockJS端点:
List<Transport> transports = new ArrayList<>(2); transports.add(new WebSocketTransport(new StandardWebSocketClient())); transports.add(new RestTemplateXhrTransport()); SockJsClient sockJsClient = new SockJsClient(transports); sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJS使用JSON格式的数组来处理消息,默认情况下,使用Jackson 2并需要在类路径中,或者,你可以配置
SockJsMessageCodec
的自定义实现,并在SockJsClient
上配置它。
要使用SockJsClient模拟大量并发用户,需要配置底层HTTP客户端(用于XHR传输),以允许足够数量的连接和线程,下面的例子展示了如何使用Jetty:
HttpClient jettyHttpClient = new HttpClient(); jettyHttpClient.setMaxConnectionsPerDestination(1000); jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
下面的示例显示了服务器端与SockJS相关的属性(有关详细信息,请参阅Javadoc),你还应该考虑自定义这些属性:
@Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/sockjs").withSockJS() .setStreamBytesLimit(512 * 1024) .setHttpMessageCacheSize(1000) .setDisconnectDelay(30 * 1000); } // ... }
- 将streamBytesLimit属性设置为512KB(默认为128KB => 128 * 1024)。
- 将httpMessageCacheSize属性设置为1000(默认为100)。
- 将disconnectDelay属性设置为30秒(默认为5秒 => 5 * 1000)。
四、WebSocket STOMP
WebSocket协议定义了两种类型的消息(文本和二进制),但是它们的内容没有定义。协议定义了一种机制,供客户端和服务器协商子协议(即更高级别的消息传递协议),以便在WebSocket上使用它来定义每个消息可以发送哪些类型、格式是什么、每个消息的内容等等。子协议的使用是可选的,但无论如何,客户端和服务器都需要就一些定义消息内容的协议达成一致。
概述
STOMP(简单的面向文本的消息传递协议)最初是为脚本语言(如Ruby、Python和Perl)创建的,用于连接到企业消息代理,它被设计用于处理常用消息传递模式的最小子集,STOMP可以用于任何可靠的双向流网络协议,如TCP和WebSocket,虽然STOMP是一个面向文本的协议,但消息payload可以是文本或二进制。
STOMP是基于帧的协议,其帧是基于HTTP建模的,下面的清单显示了一个STOMP帧的结构:
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用SEND或SUBSCRIBE命令发送或订阅消息,以及描述消息是关于什么并且谁应该接收它的destination header。这支持一种简单的发布-订阅机制,你可以使用该机制将消息通过代理发送到其他连接的客户端,或将消息发送到服务器,以请求执行某些工作。
当你使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理,消息被路由到@Controller消息处理方法或一个跟踪订阅并向订阅用户广播消息的简单的内存代理。你还可以将Spring配置为使用专用的STOMP代理(例如RabbitMQ、ActiveMQ和其他的)来实际广播消息,在这种情况下,Spring维护到代理的TCP连接,将消息传递给代理,并将消息从代理向下传递到连接的WebSocket客户端。因此,Spring web应用程序可以依赖于统一的基于http的安全性、公共验证和熟悉的消息处理编程模型。
下面的示例显示了订阅股票报价的客户端,服务器可能定期发出股票报价(例如,通过调度任务使用SimpMessagingTemplate向代理发送消息)。
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
下面的示例显示了发送交易请求的客户端,服务器可以通过@MessageMapping方法处理该请求:
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
执行之后,服务器可以向客户端广播交易确认消息和详细信息。
destination的含义在STOMP规范中故意保持不透明,它可以是任何字符串,完全由STOMP服务器来定义它们支持的destination的语义和语法。然而,destination通常是像路径字符串,其中包含/topic/..意味着发布-订阅(一对多)和/queue/意味着点对点(一对一)消息交换。
STOMP服务器可以使用MESSAGE命令向所有订阅者广播消息,下面的示例显示了服务器向订阅的客户端发送股票报价:
MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
服务器不能发送未经请求的消息,来自服务器的所有消息都必须响应特定的客户端订阅,服务器消息的subscription-id header必须与客户端订阅的id header匹配。
优点
使用STOMP作为子协议,可以让Spring Framework和Spring Security提供比使用原始WebSockets更丰富的编程模型,关于HTTP与原始TCP的区别,以及它如何让Spring MVC和其他web框架提供丰富的功能,可以提出同样的观点,以下是一些好处:
- 无需创建自定义消息传递协议和消息格式。
- STOMP客户端,包含一个Spring Framework中的Java客户端。
- 你可以(可选地)使用消息代理(如RabbitMQ、ActiveMQ等)来管理订阅和广播消息。
- 应用程序逻辑可以组织在任意数量的@Controller实例中,可以基于STOMP destination header将消息路由到它们,而不必针对给定连接使用单个WebSocketHandler处理原始WebSocket消息。
- 你可以使用Spring Security来基于STOMP destination和消息类型保护消息。
启用STOMP
spring-messaging和spring-websocket模块提供WebSocket支持的STOMP,一旦有了这些依赖项,就可以通过WebSocket使用SockJS Fallback公开STOMP端点,如下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.setApplicationDestinationPrefixes("/app"); config.enableSimpleBroker("/topic", "/queue"); } }
- /portfolio是WebSocket(或SockJS)客户端为WebSocket握手需要连接到的端点的HTTP URL。
- 以/app开头的destination header的STOMP消息被路由到@Controller类中的@MessageMapping方法。
- 使用内置的消息代理进行订阅和广播,并将destination header 以/topic或/queue开头的消息路由到代理。
下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:message-broker application-destination-prefix="/app"> <websocket:stomp-endpoint path="/portfolio"> <websocket:sockjs/> </websocket:stomp-endpoint> <websocket:simple-broker prefix="/topic, /queue"/> </websocket:message-broker> </beans>
对于内置的简单代理,/topic和/queue前缀没有任何特殊含义,它们只是区分发布-订阅和点对点消息传递(也就是说,许多订阅者和一个消费者)的一种约定,当你使用外部代理时,请检查代理的STOMP页面,以了解它支持哪种STOMP destination和前缀。
要从浏览器连接SockJS,可以使用sockjs-client,对于STOMP,许多应用程序已经使用了jmesnil/stomp-websocket库(也称为stomp.js),该库功能齐全,已经在生产中使用多年,但已不再维护。目前,JSteunou/webstomp-client是该库最积极维护和发展的继承者,下面的示例代码就是基于它编写的:
var socket = new SockJS("/spring-websocket-portfolio/portfolio"); var stompClient = webstomp.over(socket); stompClient.connect({}, function(frame) { }
或者,如果你通过WebSocket连接(没有SockJS),你可以使用以下代码:
var socket = new WebSocket("/spring-websocket-portfolio/portfolio"); var stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { }
注意,前面示例中的stompClient不需要指定login和passcode header,即使有,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。
消息流
一旦公开了STOMP端点,Spring应用程序就成为连接的客户端的STOMP代理,本节描述服务器端的消息流。
spring-messaging模块包含对源自Spring Integration的消息传递应用程序的基本支持,并且后来被提取并合并到Spring Framework中,以便在许多Spring项目和应用程序场景中得到更广泛的使用。下面的列表简要描述了一些可用的消息传递抽象:
- Message:消息的简单表示,包括header和payload。
- MessageHandler:处理消息的合约。
- MessageChannel:发送消息的合约,该消息支持生产者和消费者之间的松耦合。
- SubscribableChannel:带有MessageHandler的订阅者的MessageChannel。
- ExecutorSubscribableChannel:使用Executor来传递消息的SubscribableChannel。
Java配置(即@EnableWebSocketMessageBroker)和XML命名空间配置(即<websocket:message-broker>)都使用前面的组件组装消息工作流,下图显示了启用简单内置消息代理时使用的组件:
前面的图显示了三个消息通道:
- clientInboundChannel:传递从WebSocket客户端收到的消息
- clientOutboundChannel:用于向WebSocket客户端发送服务器消息。
- brokerChannel:用于从服务器端应用程序代码中向消息代理发送消息。
下一个图显示了配置外部代理(例如RabbitMQ)来管理订阅和广播消息时使用的组件:
前两个图的主要区别是使用“代理转播”将消息通过TCP传递到外部STOMP代理,并将消息从代理向下传递到订阅客户端。
当从WebSocket连接接收到消息时,它们被解码到STOMP帧,转换为Spring Message表示,并发送到clientInboundChannel进行进一步处理。例如,以/app开头的destination header的STOMP消息可以路由到带注解的控制器中的@MessageMapping方法,而/topic和/queue消息可以直接路由到消息代理。
处理来自客户端的STOMP消息的@Controller注解可以通过brokerChannel向消息代理发送消息,而代理通过clientOutboundChannel向匹配的订阅者广播消息。相同的控制器也可以对HTTP请求进行相同的响应,因此客户端可以执行HTTP POST,然后@PostMapping方法可以向消息代理发送消息,以便向订阅的客户端广播消息。
我们可以通过一个简单的示例跟踪流,考虑下面的示例,它设置了一个服务器:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); registry.enableSimpleBroker("/topic"); } } @Controller public class GreetingController { @MessageMapping("/greeting") { public String handle(String greeting) { return "[" + getTimestamp() + ": " + greeting; } }
前面的示例支持以下流:
- 客户端连接到http://localhost:8080/portfolio,一旦建立了WebSocket连接,STOMP帧就开始在其上流动。
- 客户端发送带有/topic/greeting的destination header的SUBSCRIBE帧,接收和解码后,消息被发送到clientInboundChannel,然后被路由到消息代理,消息代理存储客户端订阅。
- 客户端向/app/greeting发送一个SEND帧,/app前缀有助于将其路由到带注解的控制器。去掉/app前缀后,destination剩余的/greeting部分映射到GreetingController中的@MessageMapping方法。
- GreetingController返回的值被转换为一个Spring Message,其payload基于返回值和/topic/greeting的默认destination header(由/app替换为/topic的输入destination派生而来)。
- 消息代理找到所有匹配的订阅者,并通过clientOutboundChannel向每个订阅者发送MESSAGE帧,消息从该通道编码为STOMP帧并在WebSocket连接上发送。
带注解的控制器
应用程序可以使用带注解的@Controller类来处理来自客户端的消息,这些类可以声明@MessageMapping、@SubscribeMapping和@ExceptionHandler方法,如下面的主题所述:
@MessageMapping
你可以使用@MessageMapping来注解路由基于它们的destination消息的方法,在方法级别和类型级别都支持它。在类型级别,@MessageMapping用于表达控制器中所有方法之间的共享映射。
默认情况下,映射值是Ant风格的路径模式(例如/thing*、/thing/**),包括对模板变量的支持(例如/thing/{id}),这些值可以通过@DestinationVariable方法参数引用,应用程序还可以为映射切换到点分隔的destination约定,点作为分隔符章节进行了解释。
支持的方法参数
下表描述了方法参数:
方法参数 | 描述 |
---|---|
Message | 用于访问完整的消息 |
发送消息
如果希望从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向brokerChannel发送消息,最简单的方法是注入SimpMessagingTemplate并使用它发送消息,通常,你将按类型注入它,如下面的示例所示:
public class GreetingController { private SimpMessagingTemplate template; @Autowired public GreetingController(SimpMessagingTemplate template) { this.template = template; } @RequestMapping(path="/greetings", method=POST) public void greet(String greeting) { String text = "[" + getTimestamp() + "]:" + greeting; this.template.convertAndSend("/topic/greetings", text); } }
但是,如果存在另一个相同类型的bean,也可以通过它的名称(brokerMessagingTemplate)来限定它。
五、使用基本的javax.websocket-api
添加依赖
<!--webSocket依赖--> <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> <scope>provided</scope> </dependency>
Websocket服务端
import java.io.IOException; 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; /** * ~~~~~使用基本的 javax.websocket 开发服务端 ~~~~ * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端 */ @ServerEndpoint("/websocket/test") public class WebSocketTest { //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; /** * 连接建立成功调用的方法 * * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 */ @OnOpen public void onOpen(Session session) { this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在线数加1 System.out.println("有新连接加入!当前在线人数为" + getOnlineCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("来自客户端的消息:" + message); //群发消息 for (WebSocketTest item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); continue; } } } /** * 发生错误时调用 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { System.out.println("发生错误"); error.printStackTrace(); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 * * @param message * @throws IOException */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); //this.session.getAsyncRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketTest.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketTest.onlineCount--; } };
Websocket客户端
websocket.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> Welcome<br/><input id="text" type="text"/> <button onclick="send()">发送消息</button> <hr/> <button onclick="closeWebSocket()">关闭WebSocket连接</button> <hr/> <div id="message"></div> </body> <script> var websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8080/websocket/test"); } 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>
测试
访问websocket.jsp的Controller
@Controller public class WebSocketController { @GetMapping("/websocket") public String websocketPage(){ return "websocket"; } }
启动项目,localhost:8080/websocket
六、基于Spring的Websocket
添加依赖
<!--webSocket依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>5.1.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>5.1.6.RELEASE</version> </dependency>
配置Websocket
@Configuration @EnableWebSocket public class SpringWebSocketConfig implements WebSocketConfigurer{ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry.addHandler(webSocketHandler(), "/mysocket/test"); } @Bean public WebSocketHandler webSocketHandler() { return new CustomizeWebSocketHandler(); } }
Websocket服务端
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; /** * 使用Spring提供的websocket包 * WebSocketMessage * TextMessage * */ public class CustomizeWebSocketHandler extends TextWebSocketHandler { /** * 建立连接后触发的回调 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { TextMessage textMessage = new TextMessage("#####连接成功"); session.sendMessage(textMessage); } /** * 收到消息时触发的回调 */ @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { System.out.println("收到客户端信息:" + new String(message.asBytes())); session.sendMessage(new TextMessage(("服务端收到数据:" + new String(message.asBytes())))); } /** * 传输消息出错时触发的回调 */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { session.sendMessage(new TextMessage("服务端传输信息时出错了")); } /** * 断开连接后触发的回调 */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { } /** * 是否处理分片消息 */ @Override public boolean supportsPartialMessages() { return true; } }
Websocket客户端
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> Welcome<br/><input id="text" type="text"/> <button onclick="send()">发送消息</button> <hr/> <button onclick="closeWebSocket()">关闭WebSocket连接</button> <hr/> <div id="message"></div> </body> <script> var websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8080/mysocket/test"); } 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>
测试
websocket.jsp的controller
@Controller public class WebSocketController { @GetMapping("/websocket") public String websocketPage(){ return "websocket"; } }
启动项目,localhost:8080/websocket。