SpringBoot20-springboot的Web开发-WebSocket
来源:互联网 发布:网络电视直播pc版 编辑:程序博客网 时间:2024/06/09 23:59
一:什么是WebSocket
WebSocket为浏览器和服务端提供了双工异步通信的功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息。WebSocket需浏览器的支持,如ie10+,Chrome13+,Firefox6+,这对目前的浏览器来说不是什么问题了。
WebSocket是通过一个socket来实现双工异步通信的能力的。但是直接使用WebSocket(或者SockJS:WebSocket协议的模拟,增加了当浏览器不支持WebSocket的时候的兼容支持)协议开发程序显得特别繁琐,我们会使用它的子协议STOMP,它是一个更高级别的协议,STOMP协议使用一个基于帧(frame)的格式来定义消息,与HTTP的request和response类似(具有类似于@RequestMapping的@MessageMapping)
二:Spring Boot提供的自动配置
Spring Boot对内嵌的Tomcat(7或者8),Jetty9和Undertow使用WebSocket提供了支持。配置源码存于org.springframework.boot.autoconfig.websocket下,如下图:
Spring Boot为WebSocket提供的stater pom是spring-boot-starter-websocket
三:实战
1,准备
新建Spring Boot项目,选择Thymeleaf和Websocket依赖
2,广播式
广播式即服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器。
1)配置WebSocket,需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过继承AbstractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket。代码如下:
package com.jack;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.messaging.converter.MessageConverter;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;import java.util.List;@SpringBootApplication/** * 通过@EnableWebSocketMessageBroker注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器 * 支持使用@MessageMapping,就像使用@RequestMapping一样 */@EnableWebSocketMessageBrokerpublic class Springboot3websocketApplication extends AbstractWebSocketMessageBrokerConfigurer{public static void main(String[] args) {SpringApplication.run(Springboot3websocketApplication.class, args);}/** * 注册STOMP协议的节点(endpoint),并映射的指定的URL * * @param stompEndpointRegistry */@Overridepublic void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {//注册一个STOMP的endpoint,并指定使用SockJS协议stompEndpointRegistry.addEndpoint("/endpointWisely").withSockJS();}/** * 配置消息代理(MessageBroker) * @param registry */@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {//广播式应配置一个/topic消息代理registry.enableSimpleBroker("/topic");}}
2)浏览器向服务端发送消息用此类接收
package com.jack.pojo;/** * 浏览器向服务端发送的消息用此类接收 */public class WiselyMessage { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }}
3)服务端向浏览器发送的此类的消息
package com.jack.pojo;/** * 服务端向浏览器发送的此类的消息 */public class WiselyResponse { private String responseMessage; public WiselyResponse(String responseMessage) { this.responseMessage = responseMessage; } public String getResponseMessage() { return responseMessage; } public void setResponseMessage(String responseMessage) { this.responseMessage = responseMessage; }}
4)控制器,进行通信的控制器
package com.jack.controller;import com.jack.pojo.WiselyMessage;import com.jack.pojo.WiselyResponse;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.stereotype.Controller;/** * websocket控制器 */@Controllerpublic class WsController { /** * 当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping * @param message * @return * @throws Exception */ @MessageMapping("/welcome") /** * 当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息 */ @SendTo("/topic/getResponse") public WiselyResponse say(WiselyMessage message)throws Exception{ Thread.sleep(3000); return new WiselyResponse("Welcome," + message.getName() + "!"); }}
5)添加脚本。将stomp.min.js(STOMP协议的客户端脚本,下载地址:http://download.csdn.net/download/easternunbeaten/9749712),sockjs.min.js(SockJS的客户端脚本,下载地址:https://github.com/sockjs/sockjs-client/releases)以及jQuery放置在src/main/resources/static下。如下图:
6)演示页面。在src/main/resources/templates下新建ws.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-3.2.1.min.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() { //连接SockJS的endpoint名称为/endpointWisely var socket = new SockJS('/endpointWisely'); //使用STOMP子协议的WebSocket客户端 stompClient = Stomp.over(socket); //连接WebSocket服务端 stompClient.connect({},function (frame) { setConnected(true); console.log('Connected: '+frame); //通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息, //这个是在控制器的@SendTo中定义的 stompClient.subscribe('/topic/getResponse',function (responsse) { showResponse(JSON.parse(responsse.body).responseMessage); }); }); } function disconnect() { if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { var name = $('#name').val(); //通过stompClient.send向/welcome目标(destination)发送消息,这个是在控制器的 //@MesssageMapping中定义的 stompClient.send("/welcome",{},JSON.stringify({'name':name})); } function showResponse(message) { var response = $('#response'); response.html(message); }</script></body></html>
7)配置viewController,为ws.html提供便捷的路径映射
package com.jack.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;@Configurationpublic class WebMvcConfig extends WebMvcConfigurerAdapter{ @Override public void addViewControllers(ViewControllerRegistry registry) { //super.addViewControllers(registry); registry.addViewController("/ws").setViewName("/ws"); }}
application.yml的配置如下:
server: port: 9090
8)运行
我们预期的效果:当一个浏览器发送一个消息到服务端时,其他浏览器也能接收到服务端发送来的这个消息。
开启三个浏览器窗口,并访问:http://localhost:9090/ws,分别连接服务器。然后在一个浏览器中发送一条消息,其他浏览器接收消息。
连接服务端,如下:
一个浏览器发送消息,如下:
所有浏览器接收服务端发送的消息,如下:
在Chrome浏览器按f12,,观察一下STOMP的帧,如下所示:
连接服务器端的格式为:
连接成功的返回为:
订阅目标(destination)/topic/getResponse:
向目标(destination)/welcome发送消息的格式为:
从目标(destination)/topic/getResponse接收的格式为:
注意:跨域解决方法
stompEndpointRegistry.addEndpoint("/endpointWisely/*").setAllowedOrigins("*").withSockJS();
设置:setAllowedOrigins("*")
3,点对点式
广播式有自己的应用场景,但是广播式不能解决我们一个常见的场景,即消息由谁发送,由谁接收的问题。下面演示一个简单的聊天程序。例子中只有两个用户,互相发送消息给彼此,因需要用户相关的内容,所以先在这里引入最简单的Spring Security相关内容。
1)添加Spring Security的starter pom:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
2)Spring Security的简单配置
这里不对Spring Security做过多的解释,只解释对这个例子有帮助的部分:
package com.jack.config;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@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { //super.configure(http); http.authorizeRequests() .antMatchers("/", "/login").permitAll()//设置Spring Security对/和/"login"路径拦截 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login")//设置Spring Security的登入页面访问的路径为/login .defaultSuccessUrl("/chat")//登入成功后转向/chat路径 .permitAll() .and() .logout() .permitAll(); } /** * 在内存中分配两个用户jack1和jack2,密码和用户名一样角色是USER * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //super.configure(auth); auth.inMemoryAuthentication() .withUser("jack1"). password("jack1") .roles("USER") .and() .withUser("jack2") .password("jack2") .roles("USER"); } /** * /resources/static/目录下的静态资源,Spring Security不拦截 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { //super.configure(web); web.ignoring().antMatchers("/resources/static/**"); }}
3)配置WebSocket
package com.jack.config;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) { //注册一个STOMP的endpoint,并指定使用SockJS协议 registry.addEndpoint("/endpointWisely").withSockJS(); //注册一个名为/endpointChat的endpoint registry.addEndpoint("/endpointChat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //super.configureMessageBroker(registry); //点对点式增加消息代理 registry.enableSimpleBroker("/queue","/topic"); }}
4)控制器。在WsController内添加如下代码:
package com.jack.controller;import com.jack.pojo.WiselyMessage;import com.jack.pojo.WiselyResponse;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 java.security.Principal;/** * websocket控制器 */@Controllerpublic class WsController { /** * 通过SimpMessagingTemplate向浏览器发送消息 */ @Autowired private SimpMessagingTemplate messagingTemplate; /** * 当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping * * @param message * @return * @throws Exception */ @MessageMapping("/welcome") /** * 当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息 */ @SendTo("/topic/getResponse") public WiselyResponse say(WiselyMessage message) throws Exception { Thread.sleep(3000); return new WiselyResponse("Welcome," + message.getName() + "!"); } /** * 在Spring MVC,可以直接在参数中获得principal,pinciple中包含当前用户的的信息 * * @param principal * @param msg */ @MessageMapping("/chat") public void handleChat(Principal principal, String msg) { /** * 下面是一段硬编码,如果发送人是jack1则发送给jack2,如果发送人是jack2则发送给jack1, * 可以根据项目实际需要编写此处代码 */ if (principal.getName().equals("jack1")) { /** * 通过 messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户, * 第二个是浏览器订阅的地址,第三个是消息本身 */ messagingTemplate.convertAndSendToUser("jack2", "/queue/notifications", principal.getName() + "-send:" + msg); }else { messagingTemplate.convertAndSendToUser("jack1", "/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> <meta charset="UTF-8"/> <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> <meta charset="UTF-8"/> <title>Home</title> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery-3.2.1.min.js}"></script></head><body><p> 聊天室</p><form id="jackForm"> <textarea rows="4" cols="60" name="text"></textarea> <input type="submit"/></form><script th:inline="javascript"> $("#jackForm").submit(function (e) { e.preventDefault(); var text = $("#jackForm").find('textarea[name="text"]').val(); sendSpittle(text); }); /** * 连接endpoint名称为/endpointChat的endpoint * @type {SockJS} */ var sock = new SockJS("/endpointChat"); var stomp = Stomp.over(sock); stomp.connect('guest','guest',function (frame) { /** * 订阅/user/queue/notifications发送的消息,这里与在控制器的messagingTemplate.convertAndSendToUser * 中定义的订阅地址保持一致。这里多了一个/user,并且这个/user是必须的,使用了/user才会发送消息到指定的用户 */ stomp.subscribe("/user/queue/notifications",handleNotification) }); 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 com.jack.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;@Configurationpublic class WebMvcConfig extends WebMvcConfigurerAdapter{ @Override public void addViewControllers(ViewControllerRegistry registry) { //super.addViewControllers(registry); registry.addViewController("/ws").setViewName("/ws"); registry.addViewController("/login").setViewName("/login"); registry.addViewController("/chat").setViewName("/chat"); }}
8)运行
我们预期的效果是:用两个浏览器登入系统,可以互发消息。但是注意如果用一个浏览器,用户会话的session是共享的,需要在谷歌浏览器设置两个独立的用户,从而实现用户会话session隔离。这里我用的两个浏览器一个是谷歌,一个是火狐。
现在分别在两个用户下的浏览器访问:http://localhost:9090/login,并登入,如下:
jack1用户向jack2用户发送消息,如下:
jack2用户向jack1用户发送消息如下:
上面的一个点对点发送消息的流程是这样的,首先定义了两个权限用户作为登入的用户
/** * 在内存中分配两个用户jack1和jack2,密码和用户名一样角色是USER * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //super.configure(auth); auth.inMemoryAuthentication() .withUser("jack1"). password("jack1") .roles("USER") .and() .withUser("jack2") .password("jack2") .roles("USER"); }
然后定义了登录页面以及登录以后跳转到那个页面如下:
@Override protected void configure(HttpSecurity http) throws Exception { //super.configure(http); http.authorizeRequests() .antMatchers("/", "/login").permitAll()//设置Spring Security对/和/"login"路径拦截 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login")//设置Spring Security的登入页面访问的路径为/login .defaultSuccessUrl("/chat")//登入成功后转向/chat路径 .permitAll() .and() .logout() .permitAll(); }
由于需要用到一些静态资源,需要对静态进行过滤如下:
/** * /resources/static/目录下的静态资源,Spring Security不拦截 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { //super.configure(web); web.ignoring().antMatchers("/resources/static/**"); }
然后配置websocket的协议节点:
package com.jack.config;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) { //注册一个STOMP的endpoint,并指定使用SockJS协议 registry.addEndpoint("/endpointWisely").withSockJS(); //注册一个名为/endpointChat的endpoint registry.addEndpoint("/endpointChat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //super.configureMessageBroker(registry); //点对点式增加消息代理 registry.enableSimpleBroker("/queue","/topic"); }}
接下来是对浏览器发过来的消息,后台的处理:
/** * 在Spring MVC,可以直接在参数中获得principal,pinciple中包含当前用户的的信息 * * @param principal * @param msg */ @MessageMapping("/chat") public void handleChat(Principal principal, String msg) { /** * 下面是一段硬编码,如果发送人是jack1则发送给jack2,如果发送人是jack2则发送给jack1, * 可以根据项目实际需要编写此处代码 */ if (principal.getName().equals("jack1")) { /** * 通过 messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户, * 第二个是浏览器订阅的地址,第三个是消息本身 */ messagingTemplate.convertAndSendToUser("jack2", "/queue/notifications", principal.getName() + "-send:" + msg); }else { messagingTemplate.convertAndSendToUser("jack1", "/queue/notifications",principal.getName()+"-send:"+msg); } }
然后增加对页面的跳转控制:
@Override public void addViewControllers(ViewControllerRegistry registry) { //super.addViewControllers(registry); registry.addViewController("/ws").setViewName("/ws"); registry.addViewController("/login").setViewName("/login"); registry.addViewController("/chat").setViewName("/chat"); }
最后浏览器的websocket代码就在chat.html里面,有注释,比较容易理解
完整代码在github上:https://github.com/wj903829182/SpringCloudTwo/tree/master/springboot3websocket
另一种websocket实现方式的代码地址:https://github.com/wj903829182/SpringCloudTwo/tree/master/springboot4websocket
收集的websocket的信息:
http://www.cnblogs.com/xiaojf/p/6613822.html
http://www.cnblogs.com/akanairen/p/5616351.html
http://blog.csdn.net/blueblueskyhua/article/details/70807847
http://blog.csdn.net/zzhao114/article/details/60154017
https://segmentfault.com/a/1190000009038991
http://blog.csdn.net/haoyuyang/article/details/53364372
https://segmentfault.com/a/1190000007397316
- SpringBoot20-springboot的Web开发-WebSocket
- SpringBoot的web开发
- springboot加速你的web开发
- SpringBoot18-springboot的Web开发-Tomcat配置
- SpringBoot19-springboot的Web开发-Favicon配置
- WEB开发----springboot的登录拦截机制
- springboot websocket
- SpringBoot-Websocket
- SpringBoot + WebSocket
- SpringBoot Web开发体验
- springboot web 开发
- SpringBoot入门Web开发
- SpringBoot17-springboot的Web开发-Web相关配置
- 基于(ssm,websocket,mysql)开发的web聊天系统
- 基于 SpringBoot 和 webSocket 的匿名聊天室
- springboot中websocket的简单应用
- springboot使用websocket遇到的坑
- 关于springboot+websocket+mybatis的问题
- 远程来电流程分析---之二
- 笨办法学 Python · 续 练习 39:SQL 创建
- Spring-Service-事务中线程异常执行事务回滚的方式
- VC实现的全局键盘钩子
- Ext JS 继承
- SpringBoot20-springboot的Web开发-WebSocket
- Xilinx Altera FPGA中的逻辑资源(Slices VS LE)比较 前言 经常有朋友会问我,“我这个方案是用A家的FPGA还是X家的FPGA呢?他们的容量够不够呢?他们的容量怎么比较
- html5新特性:利用history的pushState等方法来解决使用ajax导致页面后退和前进的问题
- 平时学习时的记录||code blocks中“替换”的方法
- 并发编程(二):非线程安全集合类
- hdu 2846 Repository
- 基于mini2451开发板的裸机开发-电子相册
- 一周搞定9轴MPU9250(无华)(2)--STM32CUBEMX软件学习
- N个数依次入栈,出栈顺序有多少种