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





阅读全文
0 1
原创粉丝点击