首页 > 代码库 > Spring Boot教程32——WebSocket

Spring Boot教程32——WebSocket

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;@Controllerpublic 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;@Configurationpublic 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@EnableWebSecuritypublic 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;@Controllerpublic 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;@Configurationpublic 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并分别用不同用户名登陆,然后互发消息

Spring Boot教程32——WebSocket