WebSocket为浏览器和服务端提供了双工异步通信功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息。WebSocket需要IE10+、Chrome13+、Firefox6+。
WebSocket是通过一个socket来实现双工异步通信能力的。但直接使用WebSocket协议开发程序比较繁琐,我们会使用它的子协议STOMP,它是一个更高级别的协议,使用一个基于帧(frame)的格式来定义消息,与Http的request和response类似(具有类似于@RequestMapping的@MessageMapping)。
Spring Boot对内嵌的Tomcat(7或者8)、Jetty9和Undertow使用WebSocket提供了支持。配置源码存于org.springframework.boot.autoconfigure.websocket下。
?
实战
1.新建Spring Boot项目
选择Thymeleaf和Websocket依赖
?
2.广播式
广播式即服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器。
1>.配置WebSocket
需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过继承AbsractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket。
package net.quickcodes.websocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker //1.通过@EnableWebSocketMessageBroker注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样。 public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{ @Override public void registerStompEndpoints(StompEndpointRegistry registry) {//2.注册STOMP协议的节点(endpoint),并映射为指定的URL registry.addEndpoint("/endpointQuickcodes").withSockJS();//3.注册一个STOMP的endpoint,并指定使用SockJS协议。 } @Override public void configureMessageBroker(MessageBrokerRegistry registry) {//4.配置消息代理(Message Broker) registry.enableSimpleBroker("/topic"); //5.广播式应配置一个/topic消息代理。 } }
2>.浏览器向服务端发送的消息用此类接受
package net.quickcodes.websocket.domain; public class QuickCodesMessage { private String name; public String getName(){ return name; } }
3>.服务端向浏览器发送的消息用此类接受
package net.quickcodes.websocket.domain; public class QuickCodesResponse { private String responseMessage; public QuickCodesResponse(String responseMessage){ this.responseMessage = responseMessage; } public String getResponseMessage(){ return responseMessage; } }
4>.演示控制器
package net.quickcodes.websocket.web; import java.security.Principal; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import net.quickcodes.websocket.domain.QuickCodesMessage; import net.quickcodes.websocket.domain.QuickCodesResponse; @Controller public class QcController { @MessageMapping("/welcome")//1.当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping @SendTo("/topic/getResponse")//2.当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息 public QuickCodesResponse say(QuickCodesMessage message) throws Exception{ Thread.sleep(3000); return new QuickCodesResponse("Welcome, "+message.getName() + "!"); } }
5>.添加脚本
将stomp.min.js(STOMP协议的客户端脚本)、sockjs.min.js(SockJS的客户端脚本)以及jQuery放置在src/main/resources/static下。
6>.演示页面
在src/main/resources/templates下新建qc.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Spring Boot+WebSocket+广播式</title> </head> <body onload="disconnect()"> <noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2></noscript> <div> <div> <button id="connect" onclick="connect();">连接</button> <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button> </div> <div id="conversationDiv"> <label>输入你的名字</label><input type="text" id="name" /> <button id="sendName" onclick="sendName();">发送</button> <p id="response"></p> </div> </div> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery.js}"></script> <script type="text/javascript"> var stompClient = null; function setConnected(connected) { document.getElementById(‘connect‘).disabled = connected; document.getElementById(‘disconnect‘).disabled = !connected; document.getElementById(‘conversationDiv‘).style.visibility = connected ? ‘visible‘ : ‘hidden‘; $(‘#response‘).html(); } function connect() { var socket = new SockJS(‘/endpointQuickcodes‘); //1.连接SockJS的endpoint名称为/endpointQuickcodes stompClient = Stomp.over(socket);//2.使用WebSocket子协议的STOMP客户端 stompClient.connect({}, function(frame) {//3.连接WebSocket服务端 setConnected(true); console.log(‘Connected: ‘ + frame); stompClient.subscribe(‘/topic/getResponse‘, function(respnose){ //4.通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息,这个是在控制器的@SendTo中定义的。 showResponse(JSON.parse(respnose.body).responseMessage); }); }); } function disconnect() { if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { var name = $(‘#name‘).val(); //5.通过stompClient.send向/welcome目标发送消息,这个是在控制器的@MessageMapping中定义的。 stompClient.send("/welcome", {}, JSON.stringify({ ‘name‘: name })); } function showResponse(message) { var response = $("#response"); response.html(message); } </script> </body> </html>
7>.配置viewController
为qc.html提供便捷的路径映射
package net.quickcodes.websocket; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; @Configuration public class WebMvcConfig extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter { @Override public void addViewControllers(ViewControllerRegistry registry){ registry.addViewController("/qc").setViewName("/qc"); } }
8>.运行
开启三个浏览器,并都访问http://localhost:8080/qc,分别连接服务器。然后在一个浏览器中发送一条消息,其他浏览器接收消息。
3.点对点式
广播式有自己的应用场景,但不能解决我们的一个常见场景,即消息由谁发送,就由谁接收的场景。
本例演示一个简单聊天室程序。例子中只有两个用户,互相发消息给彼此,因需要用户相关的内容,所以在这里引入最简单的Spring Security相关内容。
1>.在pom.xml添加Spring Security的starter pom:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2>.Spring Security的简单配置
package net.quickcodes.websocket; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/","/login").permitAll()//1根路径和/login路径不拦截 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") //2登陆页面 .defaultSuccessUrl("/chat") //3登陆成功转向该页面 .permitAll() .and() .logout() .permitAll(); } //4 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("manon").password("manon").roles("USER") .and() .withUser("qc").password("qc").roles("USER"); } //5忽略静态资源的拦截 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/resources/static/**"); } }
3>.配置WebSocket
package net.quickcodes.websocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBrokerpublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpointQuickcodes").withSockJS(); registry.addEndpoint("/endpointChat");//注册一个名为/endpointChat的endpoint } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue","/topic"); //增加一个/queue消息代理 } }
4>.控制器
package net.quickcodes.websocket.web; import java.security.Principal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; import net.quickcodes.websocket.domain.QuickCodesMessage; import net.quickcodes.websocket.domain.QuickCodesResponse; @Controller public class QcController { @MessageMapping("/welcome") @SendTo("/topic/getResponse") public QuickCodesResponse say(QuickCodesMessage message) throws Exception{ Thread.sleep(3000); return new QuickCodesResponse("Welcome, "+message.getName() + "!"); } @Autowired private SimpMessagingTemplate messagingTemplate;//1.通过SimpMessagingTemplate向浏览器发送消息 @MessageMapping("/chat") public void handleChat(Principal principal, String msg) { //2.在Spring MVC中,可以直接在参数中获得principal,principal中包含当前用户的信息 if (principal.getName().equals("qc")) {//3.这是一段硬编码,如果发送人是qc,则发送给manon;如果发送人是manon,则发送给qc,可根据项目实际需要改写此处代码 messagingTemplate.convertAndSendToUser("manon", "/queue/notifications", principal.getName() + "-send:" + msg);//4.通过messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户,第二个是浏览器订阅的地址见,第三个是消息本身。 } else { messagingTemplate.convertAndSendToUser("qc", "/queue/notifications", principal.getName() + "-send:" + msg); } } }
5>.登陆页面
src/main/resources/templates/login.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <meta charset="UTF-8" /> <head> <title>登陆页面</title> </head> <body> <div th:if="${param.error}"> 无效的账号和密码 </div> <div th:if="${param.logout}"> 你已注销 </div> <form th:action="@{/login}" method="post"> <div><label> 账号 : <input type="text" name="username"/> </label></div> <div><label> 密码: <input type="password" name="password"/> </label></div> <div><input type="submit" value="登陆"/></div> </form> </body> </html>
6>.聊天页面
src/main/resources/templates/chat.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8" /> <head> <title>Home</title> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery.js}"></script> </head> <body> <p> 聊天室 </p> <form id="wiselyForm"> <textarea rows="4" cols="60" name="text"></textarea> <input type="submit"/> </form> <script th:inline="javascript"> $(‘#wiselyForm‘).submit(function(e){ e.preventDefault(); var text = $(‘#wiselyForm‘).find(‘textarea[name="text"]‘).val(); sendSpittle(text); }); var sock = new SockJS("/endpointChat"); //1.连接名称为/endpointChat的endpoint var stomp = Stomp.over(sock); stomp.connect(‘guest‘, ‘guest‘, function(frame) { stomp.subscribe("/user/queue/notifications", handleNotification);//2.订阅/user/queue/notifications发送的消息,这里与控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致。这里多了一个/user,并且这个/user是必须的,使用了/user才会发送消息到指定的用户 }); function handleNotification(message) { $(‘#output‘).append("<b>Received: " + message.body + "</b><br/>") } function sendSpittle(text) { stomp.send("/chat", {}, text); } $(‘#stop‘).click(function() {sock.close()}); </script> <div id="output"></div> </body> </html>
7>.增加页面的viewController
package net.quickcodes.websocket; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter{ @Override public void addViewControllers(ViewControllerRegistry registry){ registry.addViewController("/qc").setViewName("/qc"); registry.addViewController("/login").setViewName("/login"); registry.addViewController("/chat").setViewName("/chat"); } }
8>.运行
分别在两个用户的浏览器下访问http://localhost:8080/login并分别用不同用户名登陆,然后互发消息