zoukankan      html  css  js  c++  java
  • Spring Boot实现STOMP协议的WebSocket

    关注公众号:锅外的大佬

    每日推送国外优秀的技术翻译文章,励志帮助国内的开发者更好地成长!
    在这里插入图片描述

    WebSocket协议是应用程序处理实时消息的方法之一。最常见的替代方案是长轮询(long polling)和服务器推送事件(server-sent events)。这些解决方案中的每个都有其优缺点。在本文中,我将向您展示如何使用Spring Boot实现WebSocket。我将介绍服务器端和客户端设置,使用WebSocket协议之上的STOMP进行相互通信。

    服务器端将完全用Java编码。但是,就客户端而言,我将展示用JavaJavaScript(SockJS)编写的片段,因为通常,WebSocket客户端嵌入在前端应用程序中。代码示例将演示如何使用pub-sub模型向多个用户广播消息以及如何仅向单个用户发送消息。在本文的另一部分中,我将简要讨论WebSocket安全问题以及如何确保即使环境不支持WebSocket协议,基于WebSocket的解决方案也能运行。

    注意,WebSocket安全话题仅在此处简要介绍,因为这是一个非常复杂的问题,可以单独撰写一篇文章。由于这个原因,以及我在文章最后一节WebSocket in production?中提及的因素,我建议在生产中先对安全设置进行修改,直到生产就绪,安全措施到位为止。

    1.WebSocket和STOMP协议

    WebSocket协议允许应用程序之间实现双向通信。重要的是要知道HTTP仅用于初始握手。初次握手之后,HTTP连接将升级为被WebSocket使用的新TCP/IP连接。

    WebSocket协议是一种相当低级的协议。它定义了如何将字节流转换为帧。帧可以包含文本或二进制消息。由于消息本身不提供有关如何路由或处理它的任何其他信息,因此很难在不编写其他代码的情况下实现更复杂的应用程序。幸运的是,WebSocket规范允许在更高的应用程序级别上使用子协议。STOMP是其中之一,由Spring Framework支持。

    STOMP是一种简单的基于文本的消息传递协议,最初是为RubyPythonPerl等脚本语言创建的,用于连接企业级消息代理。由于STOMP,使不同语言开发的客户端和代理可以相互发送和接收消息。WebSocket协议有时称为Web TCP。以此类推,STOMP被称为Web HTTP。它定义了一些映射到WebSocket帧的帧类型,例如CONNECTSUBSCRIBEUNSUBSCRIBEACKSEND。一方面,这些命令非常便于管理通信,另一方面,它们允许我们实现具有更复杂功能的解决方案,如消息确认。

    2.服务端:Spring Boot和WebSocket

    为了构建WebSocket服务器端,我们将利用Spring Boot框架,该框架使得在Java中开发独立程序和Web应用程序更快。 Spring Boot包含spring-WebSocket模块,该模块与Java WebSocket API标准(JSR-356)兼容。

    使用Spring Boot实现WebSocket服务器端并不是一项非常复杂的任务,只包含几个步骤,我们将逐一介绍。

    *步骤1:*首先,添加WebSocket库依赖项。

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

    如果计划使用JSON格式传输消息,则可能还需要包含GSONJackson依赖项。您还可能还需要一个安全框架,例如Spring Security

    *步骤2:*然后,可以配置Spring启用WebSocketSTOMP消息传递。

    Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
      @Override
      public void registerStompEndpoints(StompEndpointRegistry
       registry) {
        registry.addEndpoint("/mywebsockets")
            .setAllowedOrigins("mydomain.com").withSockJS();
      }
    
      @Override
      public void configureMessageBroker(MessageBrokerRegistry config){ 
        config.enableSimpleBroker("/topic/", "/queue/");
        config.setApplicationDestinationPrefixes("/app");
      }
    }
    

    configureMessageBroker主要做两件事情:

    • 创建内存中的消息代理,其中包含一个或多个用于发送和接收消息的目标。在上面的示例中,定义了两个目标地址前缀:topicqueue。它们遵循以下惯例:通过pub-sub模型将以topic为前缀的消息传递到所有订阅客户端的目标地址。另一方面,私有消息的目标地址通常以queue为前缀。
    • 定义前缀app,用于过滤目标地址,这些地址在Controller中被@MessageMapping修饰的方法处理。

    image

    服务器端如何处理消息

    回到上面的代码段 - 可能你已经注意到对方法withSockJS()的调用——它启用了SockJS后备选项。简而言之,即使互联网浏览器不支持WebSocket协议,它也会让我们的WebSockets工作。我将进一步详细讨论这个主题。

    还有一件事需要澄清——为什么我们在端点上调用setAllowedOrigins()方法。一般是必需的,因为WebSocketSockJS的默认行为是仅接受同源请求。因此,如果客户端和服务端处于不同的域,则需要调用此方法允许它们之间的通信。

    *步骤3:*实现处理用户请求的控制器
    它将向订阅特定主题的所有用户广播收到的消息。

    这是一个将消息发送到目标地址/topic/news的示例方法。

    @MessageMapping("/news")
    @SendTo("/topic/news")
    public void broadcastNews(@Payload String message) {
      return message;
    }
    

    也可以使用SimpMessagingTemplate而不是注解@SendTo,您可以在控制器内自动装配(Autowired)。

    @MessageMapping("/news")
    public void broadcastNews(@Payload String message) {
      this.simpMessagingTemplate.convertAndSend("/topic/news", message)
    }
    

    在后面的步骤中,可能需要添加一些其他类来保护端点,例如Spring Security框架中的ResourceServerConfigurerAdapterWebSecurityConfigurerAdapter。此外,实现消息模型通常是有益的,这样传输的JSON可以映射成对象。

    3.WebSocket客户端构建

    客户端实现是一项更简单的任务。

    *步骤1:*装配Spring STOMP客户端

    @Autowired
    private WebSocketStompClient stompClient;
    

    步骤2: 打开连接

    StompSessionHandler sessionHandler = new CustmStompSessionHandler();
    
    StompSession stompSession = stompClient.connect(loggerServerQueueUrl, 
    sessionHandler).get();
    

    此操作完成后,可以将消息发送到目的地。该消息将发送给所有订阅主题的用户。

    stompSession.send("topic/greetings", "Hello new user");
    

    以下方法也可以订阅消息

    session.subscribe("topic/greetings", this);
    
    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
        Message msg = (Message) payload;
        logger.info("Received : " + msg.getText()+ " from : " + 
        msg.getFrom());
    }
    

    有时需要向特定用户发送消息(例如,在实现聊天时)。然后,客户端和服务器端必须使用专用于此私人会话的单独目标地址。可以通过将唯一标识符附加到通用地址来创建目标地址的名称,例如/queue/chat-user123HTTP会话或STOMP会话标识符可用于此目的。

    Spring使发送私人消息变得更加容易。我们只需要使用@SendToUser注释Controller的方法。然后,目标地址将由UserDestinationMessageHandler处理,它依赖于会话标识符。在客户端,当客户端订阅以/user为前缀的目标地址时,此目标地址将转换为此用户唯一的目标地址。在服务器端,根据用户的Principal解析用户目标地址。

    服务器端使用@SendToUser注解示例代码:

    @MessageMapping("/greetings")
    @SendToUser("/queue/greetings")
    public String reply(@Payload String message,
       Principal user) {
     return  "Hello " + message;
    }
    

    或者可以使用SimpMessagingTemplate

    String username = ...
    this.simpMessagingTemplate.convertAndSendToUser();
       username, "/queue/greetings", "Hello " + username);
    

    现在让我们看看如何实现一个能够接收私有消息的JavaScript(SockJS)客户端,该客户端可以接收上面的示例中的Java代码发送的消息。值得一提的是,WebSocketsHTML5规范的一部分,并且受到大多数现代浏览器的支持(从版本10开始,Internet Explorer支持它们)。

    function connect() {
     var socket = new SockJS('/greetings');
     stompClient = Stomp.over(socket);
     stompClient.connect({}, function (frame) {
       stompClient.subscribe('/user/queue/greetings', function (greeting) {
         showGreeting(JSON.parse(greeting.body).name);
       });
     });
    }
    
    function sendName() {
     stompClient.send("/app/greetings", {}, $("#name").val());
    }
    

    您可能已经注意到,要接收私人消息,客户端需要订阅前缀为/user的目标地址/queue/greetings。它不必担心任何唯一标识符。但是,在客户端登录应用程序之前,服务器端必须初始化Principal对象。

    4.WebSocket安全

    许多Web应用程序使用基于cookie的身份验证,例如,我们可以使用Spring Security限制已登录的用户访问某些页面或控制器限制。然后,通过基于cookie的HTTP会话维护用户上下文安全,该会话稍后与为该用户创建的WebSocketSockJS会话相关联。 WebSocket端点可以像任何其他请求一样受到保护,例如,在SpringWebSecurityConfigurerAdapter中的实现。

    如今,Web应用程序通常使用REST API作为后端,使用OAuth/JWT令牌进行用户身份验证和授权。 WebSocket协议未描述服务器在HTTP握手期间如何对客户端进行身份验证。实际上,标准HTTP头(例如,授权)用于此目的。不幸的是,并非所有STOMP客户端都支持它。 SpringSTOMP客户端允许为握手设置标头:

    WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders();
    handshakeHeaders.add(principalRequestHeader, principalRequestValue);
    

    但是SockJS的JavaScript客户端不支持使用SockJS请求发送授权请求头(Authorization)。但是,它允许发送可用于传递令牌的查询参数。此方法需要在服务器端编写自定义代码,该代码将从查询参数中读取令牌并对其进行验证。特别重要的是确保令牌不与请求一起记录(或日志受到良好保护),因为这可能会导致严重的安全违规。

    5.SockJS后备选项

    WebSocket的集成可能并不总是尽如人意。某些浏览器(例如,IE 9)不支持WebSocket。更重要的是,限制性代理可能使HTTP升级变得不可能,或者它们切断了打开太久的连接。在这种情况下,SockJS就会伸出援手。

    SockJS传输分为三大类:WebSocketHTTP StreamingHTTP Long Polling。通信从SockJS发送GET /info以从服务器获取基本信息开始。SockJS根据响应决定使用的哪种传输方式。第一个选择是WebSocket。如果不支持,则尽可能使用Streaming。如果Streaming也不可用,则选择轮询作为传输方法。

    6.生产中使用WebSocket

    虽然这种设置有效,但它并不是“最佳”。Spring Boot允许您使用任何具有STOMP协议的完整消息系统(例如,ActiveMQ,RabbitMQ),并且外部代理可以支持更多STOMP操作(例如,确认,租借)而不是我们使用的简单代理。 STOMP Over WebSocket提供有关WebSocketSTOMP协议的信息。它列出了处理STOMP协议的消息传递系统,可能是在生产中使用的更好的解决方案。特别是由于请求数量很大,消息代理需要进行集群(Spring的简单消息代理不适合集群)。然后,不需要在WebSocketConfig中启用简单代理,而是需要启用Stomp代理中继,该中继将消息转发到外部消息代理和从外部消息代理转发消息。总而言之,外部消息代理可以帮助您构建更具伸缩性和可靠性的解决方案。

    作者:Tomasz Dąbrowski

    译者:Emma

  • 相关阅读:
    noip2012 同余方程
    bzoj1477 poj1061 青蛙的约会
    Nginx 从入门到放弃(五)
    Nginx 从入门到放弃(四)
    Nginx 从入门到放弃(三)
    Nginx 从入门到放弃(二)
    Nginx 从入门到放弃(一)
    python三大神器之fabric
    Docker 学习(一)
    linux就该这么学 第一天学习笔记
  • 原文地址:https://www.cnblogs.com/liululee/p/10947724.html
Copyright © 2011-2022 走看看