SpringBoot整合WebSocket

SpringBoot整合WebSocket

      • 一、为什么需要WebSocket
      • 二、WebSocket简介
      • 三、整合WebSocket
      • 四、消息点对点发送
      • 五、总结

一、为什么需要WebSocket

在HTTP协议中,所有的请求都是由客户端发起的,由服务端进行响应,服务端无法向客户端推送消息,但是在一些需要即时通信的应用中,又不可避免地需要服务端像客户端推送消息,传统的解决方案主要由如下几种:

  • 轮询
    轮询是简单的一种解决方案,所谓轮询,就是客户端在固定的时间间隔下不停地向服务端发送请求,查看服务端是否由最新的数据,若服务端有最新的数据,则返回给客户端,若服务端没有,则返回一个空的JSON或者XML文档。轮询对开发人员而言实现方便,但是弊端也是明显:客户端需要每次都要新建HTTP请求,服务端需要处理大量的无效请求,在高并发场景下会严重拖慢服务端的运行效率,同时服务端的资源被极大的浪费了。因此这种方式并不可取。
  • 长轮询
    长轮询是传统轮询的升级版,不同于传统轮询,在长轮询中,服务端不是每次都会立即响应客户端的请求,只有在服务端有最新数据的时候才会立即响应客户端的请求,否则服务端会持有这个请求而不返回,直到有服务端有最新数据时才返回。这种方式可以在一定程度上节省网络资源和服务器资源,但是也存在一些问题,例如:
  1. 如果浏览器在服务器响应之前有新数据要发送,就只能创建一个新的并发请求,或者先尝试断掉当前请求,再创建新的请求。
  2. TCP和HTTP规范中都有连接超时一说,所以所谓的长轮询并不能一直持续,服务端和客户端的连接需要定期的连接和关闭再连接,这又增大开发人员的工作量,当然也有一些技术能够延长每次连接时间,但毕竟是非主流解决方案。
  • Applet和Flash
    Applet和Flash都已经不在继续支持了,它们也可以解决消息推送问题。可以使用Applet和Flase来模拟比全双工通信,通过创建一个只有1个像素点大小的透明的Applet或者Flash,然后将之内嵌再网页中,再从Applet或者Flash的代码中创建一个Socket连接进行双向通信。这种连接方式消除了HTTP协议中的诸多限制,当服务器又消息发送到客户端的时候,开发者可以在Applet或者Flash中调用JavaScript函数将数据显示在页面上,当浏览器有数据要发送给服务器时也一样,通过Applet或者Flash来传递,这种方式真正地实现了全双工通信,不过也有问题,说明如下:
  1. 浏览器必须能够运行Java或者Flash
    2.无论是Applet还是Flash都存在安全问题
    3.随着HTML5标准被各浏览器厂商广泛支持,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主要有如下特点:

  • WebSocket使用时需要先创建连接,这使得WebSocket成为一种有状态的协议,在之后的通信过程中可以省略部分状态信息(例如身份认证等)。
  • WebSocket连接在端口80(ws)或者443(wss)上创建,与HTTP使用的端口相同,这样,基本上所有的防火墙都不会阻止WebSocket连接。
  • WebSocket使用HTTP协议进行握手,因此它可以自然而然地集成到网络浏览器和HTTP服务器中,而不需要额外的成本。
  • 心跳消息(ping 和 pong) 将被反复的发送,进而保持WebSocket连接一直处于活跃状态。
  • 使用该协议,当消息启动或者到达的时候,服务端和客户端都可以知道
  • WebSocket连接关闭时将发送一个特殊的关闭消息
  • WebSocket连接支持跨域,可以避免Ajax的限制
  • HTTP规范要求支持浏览器将并发连接数限制为每个主机名两个连接,但是当我们使用WebSocket的时候,当握手完成之后,该限制就不存在了,因为此时的连接已经不再是HTTP连接了。
  • WebSocket协议支持扩展,用户可以扩展协议,实现部分自定义的子协议。
  • 更好的二进制支持以及更好的压缩效果。

三、整合WebSocket

Stomp
websocket使用socket实现双工异步通信能力。但是如果直接使用websocket协议开发程序比较繁琐,我们可以使用它的子协议Stomp
SockJS
sockjs是websocket协议的实现,增加了对浏览器不支持websocket的时候的兼容支持
SockJS的支持的传输的协议有3类: WebSocket, HTTP Streaming, and HTTP Long Polling。默认使用websocket,如果浏览器不支持websocket,则使用后两种的方式。
SockJS使用”Get /info”从服务端获取基本信息。然后客户端会决定使用哪种传输方式。如果浏览器使用websocket,则使用websocket。如果不能,则使用Http Streaming,如果还不行,则最后使用 HTTP Long Polling

STOMP作用:
提供消息体的格式,允许STOMP客户端(Endpoints)与任意STOMP消息代理(message broker)进行交互,实现客户端之间进行异步消息传送

SpringBoot对WebSocket提供了非常友好的支持,可以方便开发者项目中快速集成WebSocket功能,实现单聊或者群聊。

  • 创建项目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cjw</groupId>
    <artifactId>websocket01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>websocket01</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.1.2</version>
        </dependency>


        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

**spring-boot-starter-websocket**依赖是WebSocket相关依赖,其他的都是前端库,使用jar包的形式对这些前端库进行统一管理,使用webjar添加到项目中的前端库,在``SpringBoot项目中已经默认添加了静态资源过滤,因此可以直接使用。

  • 配置WebSocket
    Spring框架提供了基于WebSocket的STOMP支持,STOMP是一个简单的可互操作的协议,通常被用于通过中间服务器在客户端之间进行异步消息传递
    STOMPSimple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议可以建立在WebSocket之上,也可以建立在其他应用层协议之上。
    WebSocket配置如下:
public interface WebSocketMessageBrokerConfigurer {
// 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务,也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs
    default void registerStompEndpoints(StompEndpointRegistry registry) {
    }
 // 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间
    default void configureWebSocketTransport(WebSocketTransportRegistration registry) {
    }
   // 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
    default void configureClientInboundChannel(ChannelRegistration registration) {
    }
 // 设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
    default void configureClientOutboundChannel(ChannelRegistration registration) {
    }
// 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法
    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    }
 // 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法
    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
    }
// 添加自定义的消息转换器,spring 提供多种默认的消息转换器,返回false,
//不会添加消息转换器,返回true,会添加默认的消息转换器,
//当然也可以把自己写的消息转换器添加到转换链中
    default boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        return true;
    }
  // 配置消息代理,哪种路径的消息会进行代理处理
    default void configureMessageBroker(MessageBrokerRegistry registry) {
    }
}

服务端和浏览器的版本要求
WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持。当前支持websocket的版本:Tomcat 7.0.47+, Jetty 9.1+, GlassFish 4.1+, WebLogic 12.1.3+, and Undertow 1.0+ (and WildFly 8.0+).

浏览器的支持版本:
查看所有支持websocket浏览器的连接:

SpringBoot整合WebSocket_第1张图片

/**
 * 自定义类WebSocketConfig继承WebSocketMessageBrokerConfigurer
 * 进行WebSocket配置
 * 通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理
 */
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息
// 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样

@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    //配置消息代理(Message Broker)
     public void configureMessageBroker(MessageBrokerRegistry registry) {
         /*
         *设置消息代理前缀,即如果消息的前缀是“/topic”,就会将消息转发给消息代理(broker),
         *再由消息代理将消息广播给当前连接的客户端。(topic路径交给broker处理)
         * */
         //点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理
         registry.enableSimpleBroker("/topic");
         /*表示配置一个或者多个前缀,通过这些前缀过滤出需要被注解方法处理的消息
         * 例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理
         * 而其他destination(例如“/topic” “/queue”)将被直接交给broker处理
         * (app路径交给@MessageMapping注解的方法处理)
         * */
         //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
         registry.setApplicationDestinationPrefixes("/app");
    }
    //注册STOMP协议的节点(endpoint),并映射指定的url
     public void registerStompEndpoints(StompEndpointRegistry registry) {
         /*表示定义一个前缀为"/chat"的endPoint,并开启sockjs支持
         * sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过
         * 这里配置的URL建立WebSocket连接
         * ()
         * */
         //注册一个STOMP的endpoint,并指定使用SockJS协议
         registry.addEndpoint("/chat").withSockJS();
    }
}
  1. 定义Controller
@Controller
public class GreetingController {

    /*
    *  @MessageMapping("/hello")注解的方法将用来接收
    * ”/app/hello“路径发送来的消息,在注解方法中对消息进行处理后,
    * 再将消息转发到@SendTo定义的路径上,而@SendTo路径
    * 是一个前缀为“topic”的路径,因此该消息将被交给消息代理broker
    * 再由broker进行广播
    * */
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Message greeting(Message message) {
        return message;
    }
}
  1. chat.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>群聊</title>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<div>
    <label for="name">请输入用户名:</label>
    <input id="name" placeholder="用户名">
</div>
<div>
    <button id="connect" type="button">连接</button>
    <button id="disconnect" type="button" disabled>断开连接</button>
</div>
<div id="chat" style="display: none;"></div>
<div>
    <label for="name">请输入聊天内容:</label>
    <input id="content" placeholder="聊天内容">
</div>
<div id="greetings">
</div>
<button id="send" type="button">发送</button>
<div id="conversation" stype="display:none">聊天进行中。。。。</div>
</body>
</html>

  1. app.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() {
    console.log("connect-------------->start");
    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) {
            console.log("message========" + greeting);
            showGreeting(JSON.parse(greeting.body));
        });
    });
    console.log("connect-------------->end");
}

function disconnect() {
    console.log("disconnect-------------------> start");
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("disconnect-----------------> end");
}

function sendName () {
    console.log("sendName--------------------->start");
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val(),
    'content':$("#content").val()}));
    console.log("sendName--------------------->end");
}

function showGreeting(message) {
    $("#greetings").append(
        "
" + message.name + ":" + message.content + "
"
); } $(function () { $("#content").click(function () {connect();}); $("#disconnect").click(function () {disconnect()}); $("#send").click(function () {sendName();}); });

四、消息点对点发送

  • 配置用户
    引入Spring Security配置
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  • 配置SecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String password = bCryptPasswordEncoder.encode("123");
        System.out.println("password = " + password);
        return new BCryptPasswordEncoder();
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("$2a$10$SUfihxUrF1gn4XoZuZygAuMb9VzT.n7n6Rh1kA1SlP5TXe4Kvxb8S")
                .roles("admin")
                .and()
                .withUser("sang")
                .password("$2a$10$SUfihxUrF1gn4XoZuZygAuMb9VzT.n7n6Rh1kA1SlP5TXe4Kvxb8S")
                .roles("user");
    }
}
  • 改造WebSocket配置
/**
 * 自定义类WebSocketConfig继承WebSocketMessageBrokerConfigurer
 * 进行WebSocket配置
 * 通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理
 */
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息
// 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样

@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    //配置消息代理(Message Broker)
     public void configureMessageBroker(MessageBrokerRegistry registry) {
         /*
         *设置消息代理前缀,即如果消息的前缀是“/topic”,就会将消息转发给消息代理(broker),
         *再由消息代理将消息广播给当前连接的客户端。(topic路径交给broker处理)
         * */
         //点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理
         /*为了方便群发消息和点对点发送消息添加了queue*/
         registry.enableSimpleBroker("/topic1", "/queue");
         /*表示配置一个或者多个前缀,通过这些前缀过滤出需要被注解方法处理的消息
         * 例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理
         * 而其他destination(例如“/topic” “/queue”)将被直接交给broker处理
         * (app路径交给@MessageMapping注解的方法处理)
         * */
         //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
         registry.setApplicationDestinationPrefixes("/app");
    }
    //注册STOMP协议的节点(endpoint),并映射指定的url
     public void registerStompEndpoints(StompEndpointRegistry registry) {
         /*表示定义一个前缀为"/chat"的endPoint,并开启sockjs支持
         * sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过
         * 这里配置的URL建立WebSocket连接
         * ()
         * */
         //注册一个STOMP的endpoint,并指定使用SockJS协议
         registry.addEndpoint("/chat").withSockJS();
    }
}
  • 配置Controller
@Controller
public class GreetingController {

    /*
    * SimpMessagingTemplate进行消息的发送,在SpringBoot中,已经配置好了
    * 直接注入进来即可,使用SimpMessageTemplate,
    * 可以在任意地方发送消息到broker,也可以发送消息给某一个用户
    * (点对点发送)
    * */
    @Autowired
    SimpMessagingTemplate simpMessageingTemplate;
    /*
    *  @MessageMapping("/hello")注解的方法将用来接收
    * ”/app/hello“路径发送来的消息,在注解方法中对消息进行处理后,
    * 再将消息转发到@SendTo定义的路径上,而@SendTo路径
    * 是一个前缀为“topic”的路径,因此该消息将被交给消息代理broker
    * 再由broker进行广播
    * */
    @MessageMapping("/hello")
    @SendTo("/topic1/greetings")
    public Message greeting(Message message) {
        return message;
    }

    /*
    *  @MessageMapping("/chat")表示来自/app/chat的路径消息将被chat处理
    * Principal获取登录信息
    * Chat 客户端发送来的消息
    *
    * */
    @MessageMapping("/chat")
    public void chat (Principal principal, Chat chat) {
        /*获取当前用户信息*/
        String from = principal.getName();
        /*将消息存储进来*/
        chat.setFrom(from);
        /*将消息发送出去*/
        /*
        *
        *  public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
        Assert.notNull(user, "User must not be null");
        Assert.isTrue(!user.contains("%2F"), "Invalid sequence \"%2F\" in user name: " + user);
        user = StringUtils.replace(user, "/", "%2F");
        destination = destination.startsWith("/") ? destination : "/" + destination;
        super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
                 }
                 * destinationPrefix:private String destinationPrefix = "/user/";
    *
    * */
        simpMessageingTemplate.convertAndSendToUser(chat.getTo(),
                "/queue/chat", chat);
    }

    public static  class Chat {
        private String to;
        private String from;
        private String content;

        public String getTo() {
            return to;
        }

        public void setTo(String to) {
            this.to = to;
        }

        public String getFrom() {
            return from;
        }

        public void setFrom(String from) {
            this.from = from;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }
    }
}
  • 创建聊天界面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>单聊</title>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/chat.js"></script>
</head>
<body>
<div id="chat">
    <div id="chatsContent">

    </div>
    <div>
        请输入聊天内容:
        <input id="content" placeholder="聊天内容">目标用户:
        <input id="to" placeholder="目标用户">
        <button id="send" type="button">发送</button>
    </div>
</div>
</body>
</html>
  • chat.js
var stompClient = null;
function connect() {
    console.log("connetct--------------------->");
    var socket = new SockJS('/chat');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame){
        stompClient.subscribe('/user/queue/chat', function (chat) {
            console.log("chat---------------->");
                console.log(chat);
           showGreeting(JSON.parse(chat.body))
        });
    })
}

function sendMsg() {
    stompClient.send('/app/chat', {},
        JSON.stringify({'content':$("#content").val(),
        'to':$('#to').val()}));
}

function showGreeting(message) {
    console.log(message);
    $("#chatsContent").append("
" + message.from + ":" + message.content + "
"
); } $(function() { connect(); $("#send").click(function () {sendMsg();}); })

五、总结

经过SpringBoot自动化配置之后WebSocket使用起来还是非常方便的。通过@MessageMapping注解配置消息接口,通过@SendTo或者SimpMessagingTemplate进行消息转发,通过简单的配置,就能实现点对点、点对面的消息发送。一般在及时通信、通告发布等功能都会用到WebSocket。

你可能感兴趣的:(SpringBoot,websocket,spring,boot,json)