Spring Boot整合WebSocket及Spring Security实例
一.为什么需要WebSocket
在HTTP协议中,所有请求都是由客户端发起的,由服务端进行响应,服务端无法向客户推送消息,但是在一些需要即时通信的应用中,有不可避免的需要服务端向客户端推送消息,传统的解决方案有如下几种
1.轮询
轮询是最简单的解决方案,其意义在于,客户端在固定的时间间隔下不停地向服务端发送请求,查看服务端是否有新的数据,若服务端有新的数据,则返回给客户端,若是服务端没有新的数据,则返回一个空的JSON或者XML文档,轮询对于开发者来说实现方便,但弊端明显:客户端每次都要建立新的HTTP请求,服务端要处理大量的无效请求,在高并发的情景下会严重拖慢服务端的运行效率,同时服务端的资源被极大地浪费了,因此,此种方式并不可取
2.长轮询
长轮询对于轮询存在的问题做了部分解决,在长轮询中,在服务端接收到客户端的请求后不会立即去响应客户端,而是会等到服务端有新的数据时才会立即响应客户端的请求,否则服务端会持有这个请求而不返回,直到有新数据时才返回,这种方式在一定程度上节省了服务端的资源,但是也存在一些问题,例如:
(1)如果浏览器在服务响应之前有新的数据要发送就只能创建一个新的并发请求,或者尝试先断掉当前请求,在创建新的请求
(2)TCP和HTTP规范中都有连接超时一说,所以所谓的长轮询并不能一直持续,服务端和客户端的连接需要定期的连接和关闭,这就增大了开发者的工作量,有技术可以延长连接时间,但是这并不是主流的解决方案
3.Applet和Flash(即将下架)
二.WebSocket简介
WebSocket是一种在单个TCP连接上进行双全工通信的协议,已被W3C定为标准,使用WebSocket可以使得客户端和服务器之间的数据交换变得更加简单,他允许服务端主动向客户端推送数据,在WebSocket协议中,浏览器和服务端只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输
WebSocket使用了HTTP/1.1的协议升级特性,一个WebSocket请求首先使用非正常的HTTP请求以特定的模式访问一个URL,这个URL有两种模式,分别是ws和wss,对应HTTP协议中的HTTP以及HTTPS,在请求头有一个Connection: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)使用该协议,当消息启动或者到达时,服务端和客户端都可以知道
(6)Websocket连接关闭时将发送一个特殊的关闭消息
(7)WebSocket支持跨域,可以避免Ajax的限制
(8)HTTP规范要求浏览器将并发连接限制为每个主机名两个连接,但是当我们使用WebSocket的时候,当握手完成后,该限制就不存在了,因为此时的连接已经不再是HTTP连接了
(9)WebSocket协议支持扩展,用户可以扩展协议,实现部分自定义的子协议
(10)更好的二进制支持以及更好的压缩效果
三.Spring Boot整合WebSocket
1.消息群发
创建项目:首先创建Spring Boot项目,添加如下依赖
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket org.webjars webjars-locator-core 0.35 org.webjars sockjs-client 1.1.2 org.webjars stomp-websocket 2.3.3-1 org.webjars jquery 3.3.1-1
Spring-boot-starter-websocket依赖时WebSocket相关依赖,其他的都是前端库,使用jar包的形式对这些前端库进行统一管理,使用webjsr添加到项目中的前端库,在Spring Boot项目中已经默认添'加了静态资源,因此可以直接使用
2.配置WebSocket
Spring框架提供了基于WebSocket的STOMP支持,STOMP是一个简单的可交互操作的协议,通常被用于通过中间服务器在客户端之间进行异步消息传递,WebSocket配置如下:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS(); }}
代码解释:
(1)自定义WebSocketConfig继承自WebSocketMessageBrokerConfigurer进行WebSocket配置,然后通过@EnableWebSocketMessageBroker注解开启了WebSocket消息代理
(2)registry.enableSimpleBroker("/topic")表示设置消息代理的前缀,即如果消息的前缀是"/topic",就会将消息转发给代理(broker),再由消息代理将消息广播给当前连接的客户端
(3)registry.setApplicationDestinationPrefixes("/app")表示配置一个或多个前缀,通过这些前缀过滤出需要被注解方法处理的消息
(4)registry.addEndpoint("/chat").withSockJS()表示定义一个前缀为"/chat"的edPoint,并开启sockjs支持,sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过这里配置的URL来建立WebSocket连接
3.定义Controller
定义Controller用来实现对消息的处理
@RestControllerpublic class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Message greeting(Message message)throws Exception{ return message; }}
根据第二部配置,@MessageMapping("/hello")注解的方法将用来接收"/app/hello"路径发送来的消息,在注解方法中对 对消息进行处理后,再将消息转发到@SendTo定义的路径上,而@SendTo的路径是一个以"/topic"的路径,因此该消息将被交给消息代理broker,再由broker进行广播
4.前台页面
单聊 群聊 群聊进行中... 请输入聊天内容: 目标用户:
5.页面以及websocket的js逻辑
var stompClient = null;function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); $("#chat").show(); } else { $("#conversation").hide(); $("#chat").hide(); } $("#greetings").html("");}function connect() { if (!$("#name").val()) { return; } var socket = new SockJS('/chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { setConnected(true); stompClient.subscribe('/topic/greetings', function(greeting) { showGreeting(JSON.parse(greeting.body)); }); });}function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false);}function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({ 'name' : $("#name").val(), 'content' : $("#content").val() }));}function showGreeting(message) { $("#greetings").append( "" + message.name + ":" + message.content + "");}$(function() { $("#connect").click(function() { connect(); }); $("#disconnect").click(function() { disconnect(); }); $("#send").click(function() { sendName(); });});
代码解释:
(1)connect方法表示建立一个WebSocket连接,在建立WebSocket连接时,用户必须先输入用户名,然后才能建立连接
(2)方法体中的意思:使用SockJS建立连接,然后创建一个STOMP实例发起请求,在连接成功回调方法中,首先调用setConnected(true);方法进行页面设置,然后调用STOPM中的subscribe方法订阅服务端发送回来的消息,并将服务端发送来的消息展示出来(使用showGreeting方法)
(3)调用STOMP中的disconnect方法可以断开一个WebScoket连接
5.实体类
public class Message { private String name; private String content;........getter,setter..............}
6.改造消息发送Controller
消息发送使用到了@SendTo注解,该注解讲方法处理过的消息转发到broker,再由broker进行广播,除了@SendTo注解外,Spring还提供了SimpMessagingTemplate类来让开发者更加灵活的发送消息,使用SimpMessagingTemlate可以对上面的Controller进行改造,改造结果如下:
@RestControllerpublic class GreetingController { @Autowired SimpMessagingTemplate simpMessagingTemplate; @MessageMapping("/hello") public void greeting(Message message)throws Exception{ simpMessagingTemplate.convertAndSend("/topic/greetings",message); }}
四.消息点对点发送
1.添加依赖
既然是点对点发送,就应该有用户的观念,因此首先在项目中加入Spring Security的依赖,代码如下:
org.springframework.boot spring-boot-starter-security
2.配置Spring Security
对spring security进行配置,添加两个用户,同时配置所有地址都认证后才能访问,代码如下:
@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 密码加密过:123 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("齐**") .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") .roles("admin") .and() .withUser("辛**") .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") .roles("user") .and() .withUser("李**") .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") .roles("user") .and() .withUser("岳**") .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") .roles("user") .and() .withUser("尚**") .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") .roles("user"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .authenticated() .and() .formLogin() .permitAll(); }}
3.改造WebSocket配置代码如下:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic","/queue"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS(); }}
代码解释:
这里修改了registry.enableSimpleBroker("/topic"),又增加了个前缀"/queue",方便对群发消息和点对点消息进行管理
4.配置Controller
对WebSocket的Controller进行改造,代码如下:
@RestControllerpublic class GreetingController { @Autowired SimpMessagingTemplate simpMessagingTemplate; /** * 消息群发 * @param message * @return * @throws Exception */ @MessageMapping("/hello") @SendTo("/topic/greetings") public Message greeting(Message message) throws Exception { return message; } /** * 点对点发送 * @param principal * @param chat * @throws Exception */ @MessageMapping("/chat") public void chat(Principal principal, Chat chat) throws Exception { String from=principal.getName(); chat.setFrom(from); simpMessagingTemplate.convertAndSendToUser(chat.getTo(), "/queue/chat", chat);; }}
代码解释:
(1)群发消息依然使用@SendTo来实现,点对点则用SimpMessagingTemplate来实现
(2)@MessageMapping("/chat")表示来自"/app/chat"路径的消息将被chat方法处理,chat方法的第一个参数Principal可以用来获取当前登录用户的信息,第二个参数则是客户端发送来的消息
(3)在chat方法中,首先获取当前登录用户的用户名,设置给chat对象的from属性,再将消息发送出去,发送的目标就是chat的to属性
(4)消息发送使用的方法是convertAndSendToUser,该方法内部调用了convertAndSend方法,并对消息路径做了处理
5.消息实体类:
public class Chat { private String to; private String from; private String content; ........getter,setter省略............}
代码解释:
Chat是一个普通的javaBean,to属性表示消息的目标用户,form表示消息从哪里来,content则是消息的主体内容
6.前台页面--在线聊天页面(创建在onlinechat.html)
单聊 请输入聊天内容: 目标用户:
5.页面以及websocket的js逻辑
var stompClient = null;function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); $("#chat").show(); } else { $("#conversation").hide(); $("#chat").hide(); } $("#greetings").html("");}function connect() { if (!$("#name").val()) { return; } var socket = new SockJS('/chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { setConnected(true); stompClient.subscribe('/topic/greetings', function(greeting) { showGreeting(JSON.parse(greeting.body)); }); });}function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false);}function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({ 'name' : $("#name").val(), 'content' : $("#content").val() }));}function showGreeting(message) { $("#greetings").append( "" + message.name + ":" + message.content + "");}$(function() { $("#connect").click(function() { connect(); }); $("#disconnect").click(function() { disconnect(); }); $("#send").click(function() { sendName(); });});
代码解释:
(1)连接成功后,订阅地址为"/user/queue/chat",该地址比服务端配置的地址多了"/user"前缀,这是因为,SimpMessagingTemplate类中自动添加了路径前缀
(2)聊天消息发送路径为"/app/chat"
(3)发送的消息内容中有一个to字段,该字段用来描述消息发送的目标用户