整合WebSocket
本篇来学习如何在SpringBoot中整合WebSocket,如果你之前没有使用过WebSocket,那么就有一个疑问,为什么需要使用WebSocket,以及它能做什么事情,这些当你阅读完本篇文章后,都会有一个较为清晰的答案。
为什么需要WebSocket
在HTTP协议中,所有的请求都是由客户端发起的,由服务端进行响应,服务端无法向客户端推送消息,但是在一些需要即时通信的应用中,又不可避免的需要服务端向客户端推送消息,传统的解决方案主要有如下几种:
1、轮询。轮询是最简单的一种解决方案,所谓轮询就是客户端在固定的时候间隔下不停地向服务端发送请求,查看服务段是否有最新的数据,若服务端有最新的数据,则返回给客户端;若服务端没有最新数据,则返回一个空的JSON或者XML文档。轮询对于开发人员而言实现起来非常方便,但是弊端也很明显:客户端每次都要新建HTTP请求,服务端要处理大量的无效请求,在高并发场景下会严重拖慢服务端的运行效率,同时服务端的资源被极大的浪费了,因此这种方式在实际开发中并不可取,不到万不得已不会使用这种方式。
2、长轮询。长轮询是对传统轮询的升级,开发人员看到传统轮询存在的弊端后,就开始解决这个弊端,于是就有了长轮询。前面说过传统轮询最大的弊端就是服务端要处理大量的无效请求,因此在长轮询中服务端不是每次都会立即响应客户端的请求,只有在服务端有最新数据的时候,才会立即响应客户端的请求,否则服务端会持有这个请求而不返回,直到有新数据时才返回。这种方式在一定程度上节省了网络资源和服务器资源,但是也存在一些弊端:(1)如果浏览器在服务器响应之前有新的数据需要发送,就只能创建一个新的并发请求,或者先尝试断掉当前请求,再创建新的请求。(2)TCP和HTTP规范中都有连接超时一说,所以所谓的长轮询并不能一直持续,服务端和客户端的连接需要定期的连接和关闭再连接,这又增加了开发人员的工作量,当然也有一些技术能够延长每次连接的时间,但毕竟是非主流的解决方案。
3、Applet和Flash。Applet 和Flash 都己经是明日黄花,不过在这两个技术存在的岁月里,除了可以让我们的HTML页面更加绚丽之外,还可以解决消息推送问题。开发者可以使用Applet和Flash来模拟全双工通信,
通过创建一个只有1个像素点大小的透明的Applet或者Flash,然后将之内嵌在网页中,再从Applet或者Flash的代码中创建一个Socket连接进行双向通信。这种连接方式消除了HTTP协议中的诸多限制,当服务器有消息发送到客户端的时候,开发者可以在Applet或者Flash中调用JavaScript函数将数据显示在页面上,当浏览器有数据要发送给服务器时也一样,通过Applet或者Flash来传递。这种方式真正地实现了全双工通信,不过也有问题,说明如下:(1)浏览器必须能够运行Java或者Flash;(2)无论是Applet 还是Flash都存在安全问题;(3)随着HTML5标准被各浏览器厂商广泛支持, Flash下架已经被提上日程(Adobe宣布2020年正式停止支持Flash)。
其实,传统的解决方案不止这三种,但是无论哪种解决方案都有自身的缺陷,于是就有了WebSocket的诞生。
WebSocket简介
WebSocket是一种在单个TCP连接上进行全双工通信的协议,目前已经是W3C标准。使用WebSocket可以使得客户端和服务器之间的数据交换变得更加简单,它允许服务端主动向客户端推送数据。在WebSocket协议中,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
WebSocket使用了HTTP/1.1的协议升级特性,一个WebSocket请求首先使用非正常的HTTP请求以特定的模式访问一个URL,这个URL有两种模式,分别是ws和wss,对应于HTTP协议中的HTTP和HTTPS,在请求头中有一个Connetion:Upgrade
字段,表示客户端想对协议进行升级,另外还有一个Upgrade:websocket
字段,表示客户端想将请求协议升级为WebSocket协议。这两个字段共同告诉服务器要将连接升级为WebSocket这样一种全双工协议。如果服务端同意协议升级,那么在握手完成之后,文本消息或者其他二进制消息就可以同时在两个方向上进行发送,而不需要关闭和重建连接。此时的客户端和服务端关系是对等的,它们可以互相向对方主动发送消息。和传统的解决方案相比,WebSocket主要有以下几个特点:(1)WebSocket使用是需要先创建连接,这使得WebSocket成为一种有状态的协议,在之后的通信过程中可以省略部分状态信息(如身份验证等);(2)WebSocket连接在端口80(ws)或者443(wss)上创建,与HTTP使用的端口相同,这样基本上所有的防火墙都不会阻止WebSocket的连接;(3)WebSocket使用HTTP协议进行握手,因此它可以自然而然地集成到网络浏览器和HTTP服务器中,而不需要额外的成本;(4)心跳消息(ping和pong)将被反复的发送,进而保持WebSocket连接一直处于活跃状态;(5)使用WebSocket协议时,当消息启动或者到达的时候,服务端和客户端都可以知道;(6)WebSocket连接关闭时将发送一个特殊的关闭消息;(7)WebSocket支持跨域,可以避免Ajax的限制;(8)HTTP规范要求浏览器将并发连接数限制为每个主机名两个连接,但是当我们使用WebSocket的时候,当握手完成之后,该限制就不存在了,因为此时的连接已经不再是HTTP连接了;(9)WebSocket协议支持扩展,用户可以扩展该协议,用来实现部分自定义的子协议;(10)WebSocket提供了更好的二进制支持以及更好的压缩效果。
WebSocket既然有上面提到的至少10个优势,其使用场景也是非常广泛的,如在线股票网站、即时聊天、多人在线游戏、应用集群通信等,在了解了这么多WebSocket的基本信息后,接下来学习如何在SpringBoot中使用WebSocket。
SpringBoot整合WebSocket
SpringBoot对WebSocket提供了非常友好的支持,可以方便开发者在项目中快速集成WebSocket功能,实现单人或者多人聊天等功能。
消息群发
第一步,创建项目并添加依赖。*使用spring Initializr
构建工具构建一个SpringBoot的Web应用,名称为websocketmessages
,然后在pom.xml文件中添加如下依赖:
1 | <!--添加webSocket依赖--> |
spring-boot-starter-websocket
依赖的是WebSocket相关依赖,其他的都是前端库,使用jar包的形式对这些前端库进行统一管理,使用webjar添加到项目中的前端库,在SpringBoot项目中已经默认添加了静态资源过滤,因此可以直接使用。
第二步,配置WebSocket。Spring框架提供了基于WebSocket的STOMP支持,STOMP是一个简单的可互操作的协议,通常被用于通过中间服务器在客户端之间进行异步信息传递。新建一个config包,然后在其中创建WebSocketConfig
类并实现WebSocketMessageBrokerConfigurer
接口中的configureMessageBroker
和registerStompEndpoints
方法,其中的代码为:
1 | @Configuration |
解释一下上述代码的含义:
- 自定义
WebSocketConfig
类实现了WebSocketMessageBrokerConfigurer
接口并进行WebSocket的设置,然后通过@EnableWebSocketMessageBroker
注解开启WebSocket消息代理。 registry.enableSimpleBroker("/topic")
表示设置消息代理的前缀,即如果消息的前缀是/topic
,那么就会将消息转发给消息代理(broker),再由消息代理将消息广播给当前连接的客户端。registry.setApplicationDestinationPrefixes("/app")
表示配置一个或多个前缀,通过这些前缀过滤出需要被注解方法处理的消息。如前缀为/app
的destination可以通过@MessageMapping
注解的方法处理,而其他的destination(如/topic
或者/queue
)将被直接交给broker处理。registry.addEndpoint("/chat").withSockJS()
则表示定义一个前缀为/chat
的endPoint,并开启sockjs支持,sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过这里配置的URL来建立WebSocket连接。
第三步,自定义Mesage实体类和Controller类。新建一个pojo包,并在其中新建Message实体类,其中的代码为:
1 | @Data |
接着新建一个controller包,并在其中新建GreetingController,其中的代码为:
1 | @Controller |
根据第二步的配置,@MessageMapping("/hello")
注解的方法将用来接收/app/hello
路径发送来的消息,在注解方法中对消息进行处理后,再将该消息转发到@SendTo
定义的路径上,而@SendTo
路径是一个前缀为/topic
的路径,因此该消息将被交给消息代理broker,再由broker进行广播。
第四步,搭建聊天页面。在resources/static
目录下创建chat.html
页面,作为聊天页面,其中的代码为:
1 | <!DOCTYPE html> |
注意前面四个script脚本引入的是外部的JS库,这些JS库在pom.xml文件中通过依赖加载进来。其中app.js是一个自定义的JS,里面的代码为:
1 | var stompClient = null; |
解释一下上述代码的含义:
- connect方法表示建立一个WebSocket连接,在建立WebSocket连接时,用户必须先输入用户名,然后才能建立连接。
- 接着首先使用SockJS建立连接,然后创建一个STOMP实例发起连接请求,在连接成功的回调方法中,首先调用
setConnected(true)
方法进行页面的设置,然后调用STOMP中的subscribe方法订阅服务端发送回来的消息,并将服务端发送来的消息通过showGreeting方法展示出来。 - 最后调用STOMP中的disconnect方法可以断开一个WebSocket连接。
第五步,测试。接下来启动SpringBoot项目进行测试,在浏览器地址栏中输入http://localhost:8080/chat.html
,显示结果如下图所示:
用户首先输入用户名,然后单击“连接”按钮,注意先不要在聊天框内填写任何信息:
然后换一个浏览器或者使用Chrome浏览器的多用户(注意不是多选项卡),重复刚才的步骤,这样就有两个用户连接上了(当然也可以有更多的用户连接上来),这样就可以开始群聊了,如下图所示:
消息点对点发送
在前面的消息发送中使用到了@SendTo
注解,该注解将方法处理过的消息转发到broker,再由broker进行消息广播。除了@SendTo
注解外,Spring还提供了SimpMessagingTemplate
类来让开发者更加灵活地发送消息,使用SimpMessagingTemplate
类可以对上述例子中的GreetingController
类进行改造:
1 | @Controller |
改造完成后,直接运行和前面是没有什么区别的。这里使用了SimpMessagingTemplate
进行消息的发送,在SpringBoot中,SimpMessagingTemplate
已经配置好,开发者可以直接注入进来即可。使用SimpMessagingTemplate
,开发者可以在任意地方发送消息到broker,也可以发送消息给某一个用户,这就是点对点的消息发送。接下来看看如何实现消息的点对点发送。(请注意本例子是在前面的基础上进行,但是考虑到以后复习的方便,此时我又重新创建了一个SpringBoot项目,名称为Websocketm2m
这个项目其实就是将前面的项目改了一个名字,顺便将GreetingController
进行了改造而已,其他的没有什么变动)。
第一步,添加依赖。既然是点对点发送,那么就应该有用户这个概念,因此需要在项目中添加Spring Security依赖:
1 | <!--添加Spring Security依赖--> |
第二步,配置Spring Security。简单起见,这里就不使用数据库了,而是直接在内存中进行验证,添加两个用户,同时配置所有地址都需要认证后才能访问。新建一个WebSecurityConfig
类,需要继承WebSecurityConfigurerAdapter
类并重写其中的configure(AuthenticationManagerBuilder auth)
和configure(HttpSecurity http)
方法,相应的代码为:
1 | @Configuration |
上述这些代码是常规配置,在学习Spring Security框架时就已经介绍了,因此这里就不再赘述了。
第三步,改造WebSocket配置。接下来对前面的WebSocketConfig
类进行改造,改造后的代码为:
1 | @Configuration |
所谓改造,只是在configureMessageBroker(MessageBrokerRegistry registry)
方法的enableSimpleBroker("/topic","/queue")
内新增了一个broker前缀/queue
,这样方便对群发消息和点对点消息进行管理。
第四步,自定义Chat实体类和修改Controller类。在pojo包内新建Chat实体类,其中的代码为:
1 | @Data |
接着继续对GreetingController
类进行进行改造,改造后的代码为:
1 | @Controller |
解释一下上述代码的含义:
- 群发消息依然使用
@SendTo
注解来实现,消息点对点发送则使用了SimpMessagingTemplate
来实现。 - 之后定义了一个新的消息处理接口,
@MessageMapping("/chat")
注解表示来自/app/chat
路径的消息将被chat方法所处理,chat方法第一个参数Principal可以用来获取当前登录用户的信息,第二个参数则是客户端发送过来的信息。 - 在chat方法中,首先需要获取当前的用户名,然后设置chat对象的from属性,再将消息发送出去,发送的目标用户就是chat对象的to属性的值。
- 消息发送使用的方法是
convertAndSendToUser
,该方法内部调用了convertAndSend
方法,并且对消息路径做了处理,部分源码如下:可以看到这个1
2
3
4
5
6
7
8
9private String destinationPrefix = "/user/";
......
public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Assert.notNull(user, "User must not be null");
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}destinationPrefix
的默认值是/user
,也就是说消息的最终发送路径是/user/用户名/queue/chat
。前面的chat是一个普通的JavaBean,to属性表示消息到哪里去(消息的目标用户),from表示消息从哪里来,content则是消息的主题内容。
第五步,创建在线聊天页面。在resources/static
目录下创建onlinechat.html
页面,作为在线聊天页面,其中的代码为:
1 | <!DOCTYPE html> |
这个页面和chat.html
页面基本相似,不同的是这里需要手动输入目标用户名。另外这里还有一个chat.js文件,其中的代码为:
1 | var stompClient = null; |
chat.js
文件基本上和前面的app.js
文件内容一致,但是也存在一些差异:(1)连接成功后,订阅地址为/user/queue/chat
,该地址比服务端配置的地址多了/user
前缀,这是因为SimpMessagingTemplate
类中自动添加了路径前缀;(2)聊天消息发送的路径为/app/chat
;(3)发送的消息内容中有一个to字段,该字段用来描述消息的目标用户。
第六步,开始测试。经过上述五个步骤后,一个点对点的聊天服务就搭建成功了,接下来直接在浏览器地址栏中输入http://localhost:8080/onlinechat.html
链接,首页页面会跳转到SpringSecurity的默认登录页面,分别使用一开始配置的两个用户admin/1234和envy/1234进行登录,登录成功后就可以开始在线聊天了,如下图所示:
整合WebSocket小结
本篇学习了SpringBoot整合WebSocket,总的来说,经过SpringBoot自动化配置后的WebSocket使用起来还是非常方便的。通过@MessageMapping
注解配置消息接口,通过@SendTo
或者SimpMessagingTemplate
进行消息转发,通过简单的几行配置就能实现点对点、点对面(群发)的消息发送。在企业信息管理系统中,一般即时通信,公告发布等功能都会使用到WebSocket。