zoukankan      html  css  js  c++  java
  • 第18章-使用WebSocket和STOMP实现消息功能

    Spring 4.0为WebSocket通信提供了支持,包括:

    • 发送和接收消息的低层级API;
    • 发送和接收消息的高级API;
    • 用来发送消息的模板;
    • 支持SockJS,用来解决浏览器端、服务器以及代理不支持WebSocket的问题。

    1 使用Spring的低层级WebSocket API

    按照其最简单的形式,WebSocket只是两个应用之间通信的通道。位于WebSocket一端的应用发送消息,另外一端处理消息。因为它是全双工的,所以每一端都可以发送和处理消息。如图18.1所示。

    WebSocket通信可以应用于任何类型的应用中,但是WebSocket最常见的应用场景是实现服务器和基于浏览器的应用之间的通信。

    为了在Spring使用较低层级的API来处理消息,我们必须编写一个实现WebSocketHandler的类.WebSocketHandler需要我们实现五个方法。相比直接实现WebSocketHandler,更为简单的方法是扩展AbstractWebSocketHandler,这是WebSocketHandler的一个抽象实现。

    public class MarcoHandler extends AbstractWebSocketHandler {
    
        private static final Logger logger = LoggerFactory.getLogger(MarcoHandler.class);
    
        @Override
        protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
            logger.info("Received message: " + message.getPayload());
            Thread.sleep(2000);
            session.sendMessage(new TextMessage("Polo!"));
        }
    
    }

    除了重载WebSocketHandler中所定义的五个方法以外,我们还可以重载AbstractWebSocketHandler中所定义的三个方法:

    • handleBinaryMessage()
    • handlePongMessage()
    • handleTextMessage()
      这三个方法只是handleMessage()方法的具体化,每个方法对应于某一种特定类型的消息。

    另外一种方案,我们可以扩展TextWebSocketHandler或BinaryWebSocketHandler。TextWebSocketHandler是AbstractWebSocketHandler的子类,它会拒绝处理二进制消息。它重载了handleBinaryMessage()方法,如果收到二进制消息的时候,将会关闭WebSocket连接。与之类似,BinaryWebSocketHandler也是AbstractWeb-SocketHandler的子类,它重载了handleTextMessage()方法,如果接收到文本消息的话,将会关闭连接。

    现在,已经有了消息处理器类,我们必须要对其进行配置,这样Spring才能将消息转发给它。在Spring的Java配置中,这需要在一个配置类上使用@EnableWebSocket,并实现WebSocketConfigurer接口,如下面的程序清单所示。
    程序清单18.2 在Java配置中,启用WebSocket并映射消息处理器

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
    
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    //        registry.addHandler(marcoHandler(), "/marco").withSockJS();
            registry.addHandler(marcoHandler(), "/marco");
        }
    
        @Bean
        public MarcoHandler marcoHandler() {
            return new MarcoHandler();
        }
    
    }

    或XML配置:
    程序清单18.3 借助websocket命名空间以XML的方式配置WebSocket

    不管使用Java还是使用XML,这就是所需的配置。

    现在,我们可以把注意力转向客户端,它会发送“Marco!”文本消息到服务器,并监听来自服务器的文本消息。如下程序清单所展示的JavaScript代码开启了一个原始的WebSocket并使用它来发送消息给服务器。

    程序清单18.4 连接到“marco” WebSocket的JavaScript客户端

    通过发送“Marco!”,这个无休止的Marco Polo游戏就开始了,因为服务器端的MarcoHandler作为响应会将“Polo!”发送回来,当客户端收到来自服务器的消息后,onmessage事件会发送另外一个“Marco!”给服务器。这个过程会一直持续下去,直到连接关闭。

    2 应对不支持WebSocket的场景

    WebSocket是一个相对比较新的规范。虽然它早在2011年底就实现了规范化,但即便如此,在Web浏览器和应用服务器上依然没有得到一致的支持。Firefox和Chrome早就已经完整支持WebSocket了,但是其他的一些浏览器刚刚开始支持WebSocket。如下列出了几个流行的浏览器支持WebSocket功能的最低版本:

    • Internet Explorer:10.0
    • Firefox: 4.0(部分支持),6.0(完整支持)。
    • Chrome: 4.0(部分支持),13.0(完整支持)。
    • Safari: 5.0(部分支持),6.0(完整支持)。
    • Opera: 11.0(部分支持),12.10(完整支持)。
    • iOS Safari: 4.2(部分支持),6.0(完整支持)。
    • Android Browser: 4.4。

    服务器端对WebSocket的支持也好不到哪里去。GlassFish在几年前就开始支持一定形式的WebSocket,但是很多其他的应用服务器在最近的版本中刚刚开始支持WebSocket。例如,我在测试上述例子的时候,所使用的就是Tomcat 8的发布候选构建版本。

    即便浏览器和应用服务器的版本都符合要求,两端都支持WebSocket,在这两者之间还有可能出现问题。防火墙代理通常会限制所有除HTTP以外的流量。它们有可能不支持或者(还)没有配置允许进行WebSocket通信。

    幸好,提到WebSocket的备用方案,这恰是SockJS所擅长的。SockJS让我们能够使用统一的编程模型,就好像在各个层面都完整支持WebSocket一样,SockJS在底层会提供备用方案。

    例如,为了在服务端启用SockJS通信,我们在Spring配置中可以很简单地要求添加该功能。重新回顾一下程序清单18.2中的registerWebSocketHandlers()方法,稍微加一点内容就能启用SockJS:

        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            registry.addHandler(marcoHandler(), "/marco").withSockJS();
        }
    • XML完成相同的配置效果:

    要在客户端使用SockJS,需要确保加载了SockJS客户端库。具体的做法在很大程度上依赖于使用JavaScript模块加载器(如require.js或curl.js)还是简单地使用<script>标签加载JavaScript库。加载SockJS客户端库的最简单办法是使用<script>标签从SockJS CDN中进行加载,如下所示:

    <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>

    除了加载SockJS客户端库以外,在程序清单18.4中,要使用SockJS只需修改两行代码:

    var url = 'marco';
    var sock = new SocktJS(url);

    所做的第一个修改就是URL。SockJS所处理的URL是“http://”或“https://”模式,而不是“ws://”和“wss://”。即便如此,我们还是可以使用相对URL,避免书写完整的全限定URL。在本例中,如果包含JavaScript的页面位于“http://localhost:8080/websocket”路径下,那么给定的“marco”路径将会形成到“http://localhost:8080/websocket/marco”的连接。

    3 使用STOMP消息

    直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用。因为没有高层级的线路协议(wire protocol),因此就需要我们定义应用之间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。
    不过,好消息是我们并非必须要使用原生的WebSocket连接。就像HTTP在TCP套接字之上添加了请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。

    乍看上去,STOMP的消息格式非常类似于HTTP请求的结构。与HTTP请求和响应类似,STOMP帧由命令、一个或多个头信息以及负载所组成。例如,如下就是发送数据的一个STOMP帧:

    SEND
    destination:/app/marco
    content-length:20
    
    {"message":"Marco!"}

     3.1 启用STOMP消息功能

    在Spring MVC中为控制器方法添加@MessageMapping注解,使其处理STOMP消息,它与带有@RequestMapping注解的方法处理HTTP请求的方式非常类似。但是与@RequestMapping不同的是

    • @MessageMapping的功能无法通过@EnableWebMvc启用,而是@EnableWebSocketMessageBroker。
    • Spring的Web消息功能基于消息代理(message broker)构建,因此除了告诉Spring我们想要处理消息以外,还有其他的内容需要配置。
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer {
    
      @Override
      public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/marcopolo").withSockJS();
      }
    
      @Override
      public void configureMessageBroker(MessageBrokerRegistry registry) {
    //    registry.enableStompBrokerRelay("/queue", "/topic");
        registry.enableSimpleBroker("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
      }
    
    }

    上述配置,它重载了registerStompEndpoints()方法,将“/marcopolo”注册为STOMP端点。这个路径与之前发送和接收消息的目的地路径有所不同。这是一个端点,客户端在订阅或发布消息到目的地路径前,要连接该端点。

    WebSocketStompConfig还通过重载configureMessageBroker()方法配置了一个简单的消息代理。消息代理将会处理前缀为“/topic”和“/queue”的消息。除此之外,发往应用程序的消息将会带有“/app”前缀。图18.2展现了这个配置中的消息流。

    启用STOMP代理中继
    对于生产环境下的应用来说,你可能会希望使用真正支持STOMP的代理来支撑WebSocket消息,如RabbitMQ或ActiveMQ。这样的代理提供了可扩展性和健壮性更好的消息功能,当然它们也会完整支持STOMP命令。我们需要根据相关的文档来为STOMP搭建代理。搭建就绪之后,就可以使用STOMP代理来替换内存代理了,只需按照如下方式重载configureMessageBroker()方法即可:

      @Override
      public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
      }
    • 上述configureMessageBroker()方法的第一行代码启用了STOMP代理中继(broker relay)功能,并将其目的地前缀设置为“/topic”和“/queue”。这样的话,Spring就能知道所有目的地前缀为“/topic”或“/queue”的消息都会发送到STOMP代理中。

    • 在第二行的configureMessageBroker()方法中将应用的前缀设置为“/app”。所有目的地以“/app”打头的消息都将会路由到带有@MessageMapping注解的方法中,而不会发布到代理队列或主题中。

    默认情况下,STOMP代理中继会假设代理监听localhost的61613端口,并且客户端的username和password均为“guest”。如果你的STOMP代理位于其他的服务器上,或者配置成了不同的客户端凭证,那么我们可以在启用STOMP代理中继的时候,需要配置这些细节信息:

      @Override
      public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue", "/topic")
                .setRelayHost("rabbit.someotherserver")
                .setRelayPort(62623)
                .setClientLogin("marcopolo")
                .setClientPasscode("letmein01")
        registry.setApplicationDestinationPrefixes("/app");
      }

    3.2 处理来自客户端的STOMP消息

    Spring 4.0引入了@MessageMapping注解,它用于STOMP消息的处理,类似于Spring MVC的@RequestMapping注解。当消息抵达某个特定的目的地时,带有@MessageMapping注解的方法能够处理这些消息。

    @Controller
    public class MarcoController {
    
      private static final Logger logger = LoggerFactory
          .getLogger(MarcoController.class);
    
      @MessageMapping("/marco")
      public Shout handleShout(Shout incoming) {
        logger.info("Received message: " + incoming.getMessage());
    
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    
        Shout outgoing = new Shout();
        outgoing.setMessage("Polo!");
    
        return outgoing;
      }
    
    }

    示handleShout()方法能够处理指定目的地上到达的消息。在本例中,这个目的地也就是“/app/marco”(“/app”前缀是隐含的,因为我们将其配置为应用的目的地前缀)。

    • Shout类是个简单的JavaBean
    public class Shout {
    
      private String message;
    
      public String getMessage() {
        return message;
      }
    
      public void setMessage(String message) {
        this.message = message;
      }
    
    }

    因为我们现在处理的不是HTTP,所以无法使用Spring的HttpMessageConverter实现将负载转换为Shout对象。Spring 4.0提供了几个消息转换器,作为其消息API的一部分。表18.1描述了这些消息转换器,在处理STOMP消息的时候可能会用到它们。

    表18.1 Spring能够使用某一个消息转换器将消息负载转换为Java类型

    处理订阅
    @SubscribeMapping的主要应用场景是实现请求-回应模式。在请求-回应模式中,客户端订阅某一个目的地,然后预期在这个目的地上获得一个一次性的响应。
    例如,考虑如下@SubscribeMapping注解标注的方法:

      @SubscribeMapping({"/marco"})
      public Shout handleSubscription(){
        Shout outgoing = new Shout();
        outgoing.setMessage("Polo!");
        return outgoing;
      }

    可以看到,handleSubscription()方法使用了@SubscribeMapping注解,用这个方法来处理对“/app/marco”目的地的订阅(与@MessageMapping类似,“/app”是隐含的)。当处理这个订阅时,handleSubscription()方法会产生一个输出的Shout对象并将其返回。然后,Shout对象会转换成一条消息,并且会按照客户端订阅时相同的目的地发送回客户端。

    如果你觉得这种请求-回应模式与HTTP GET的请求-响应模式并没有太大差别的话,那么你基本上是正确的。但是,这里的关键区别在于HTTPGET请求是同步的,而订阅的请求-回应模式则是异步的,这样客户端能够在回应可用时再去处理,而不必等待。

    编写JavaScript客户端
    程序清单18.7 借助STOMP库,通过JavaScript发送消息

    在本例中,URL引用的是程序清单18.5中所配置的STOMP端点(不包括应用的上下文路径“/stomp”)。

    但是,这里的区别在于,我们不再直接使用SockJS,而是通过调用Stomp.over(sock)创建了一个STOMP客户端实例。这实际上封装了SockJS,这样就能在WebSocket连接上发送STOMP消息。

    3.3 发送消息到客户端

    WebSocket通常视为服务器发送数据给浏览器的一种方式,采用这种方式所发送的数据不必位于HTTP请求的响应中。使用Spring和WebSocket/STOMP的话,该如何与基于浏览器的客户端通信呢?
    Spring提供了两种发送数据给客户端的方法:

    • 作为处理消息或处理订阅的附带结果;
    • 使用消息模板。

    在处理消息之后,发送消息

    @MessageMapping("/marco")
      public Shout handleShout(Shout incoming) {
        logger.info("Received message: " + incoming.getMessage());
        Shout outgoing = new Shout();
        outgoing.setMessage("Polo!");
        return outgoing;
      }

    当@MessageMapping注解标示的方法有返回值的时候,返回的对象将会进行转换(通过消息转换器)并放到STOMP帧的负载中,然后发送给消息代理。

    默认情况下,帧所发往的目的地会与触发处理器方法的目的地相同,只不过会添加上“/topic”前缀。就本例而言,这意味着handleShout()方法所返回的Shout对象会写入到STOMP帧的负载中,并发布到“/topic/marco”目的地。不过,我们可以通过为方法添加@SendTo注解,重载目的地:

    @MessageMapping("/marco")
    @SendTo("/topic/shout")
      public Shout handleShout(Shout incoming) {
        logger.info("Received message: " + incoming.getMessage());
        Shout outgoing = new Shout();
        outgoing.setMessage("Polo!");
        return outgoing;
      }

    按照这个@SendTo注解,消息将会发布到“/topic/shout”。所有订阅这个主题的应用(如客户端)都会收到这条消息。
    按照类似的方式,@SubscribeMapping注解标注的方式也能发送一条消息,作为订阅的回应。

      @SubscribeMapping("/marco")
      public Shout handleSubscription(){
        Shout outgoing = new Shout();
        outgoing.setMessage("Polo!");
        return outgoing;
      }

    @SubscribeMapping的区别在于这里的Shout消息将会直接发送给客户端,而不必经过消息代理。如果你为方法添加@SendTo注解的话,那么消息将会发送到指定的目的地,这样会经过代理。

    在应用的任意地方发送消息
    @MessageMapping和@SubscribeMapping提供了一种很简单的方式来发送消息,这是接收消息或处理订阅的附带结果。不过,Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。

    我们不必要求用户刷新页面,而是让首页订阅一个STOMP主题,在Spittle创建的时候,该主题能够收到Spittle更新的实时feed。在首页中,我们需要添加如下的JavaScript代码块:

    Handlebars库将Spittle数据渲染为HTML并插入到列表中。Handlebars模板定义在一个单独的<script>标签中,如下所示:

    在服务器端,我们可以使用SimpMessagingTemplate将所有新创建的Spittle以消息的形式发布到“/topic/spittlefeed”主题上。如下程序清单展现的SpittleFeedServiceImpl就是实现该功能的简单服务:

    程序清单18.8 SimpMessagingTemplate能够在应用的任何地方发布消息

    @Service
    public class SpittleFeedServiceImpl implements SpittleFeedService {
    
        private SimpMessageSendingOperations messaging;
    
        @Autowired
        public SpittleFeedServiceImpl(SimpMessageSendingOperations messaging) {
            this.messaging = messaging;
        }
    
        public void broadcastSpittle(Spittle spittle) {
            messaging.convertAndSend("/topic/spittlefeed", spittle);
        }
    
    }

    在这个场景下,我们希望所有的客户端都能及时看到实时的Spittle feed,这种做法是很好的。但有的时候,我们希望发送消息给指定的用户,而不是所有的客户端。

    4 为目标用户发送消息

    但是,如果你知道用户是谁的话,那么就能处理与某个用户相关的消息,而不仅仅是与所有客户端相关联。好消息是我们已经了解了如何识别用户。通过使用与第9章相同的认证机制,我们可以使用Spring Security来认证用户,并为目标用户处理消息。

    在使用Spring和STOMP消息功能的时候,我们有三种方式利用认证用户:

    • @MessageMapping和@SubscribeMapping标注的方法能够使用Principal来获取认证用户;
    • @MessageMapping、@SubscribeMapping和@MessageException方法返回的值能够以消息的形式发送给认证用户;
    • SimpMessagingTemplate能够发送消息给特定用户。

    4.1 在控制器中处理用户的消息

    在控制器的@MessageMapping或@SubscribeMapping方法中,处理消息时有两种方式了解用户信息。在处理器方法中,通过简单地添加一个Principal参数,这个方法就能知道用户是谁并利用该信息关注此用户相关的数据。除此之外,处理器方法还可以使用@SendToUser注解,表明它的返回值要以消息的形式发送给某个认证用户的客户端(只发送给该客户端)。

      @MessageMapping("/spittle")
      @SendToUser("/queue/notifications")
      public Notification handleSpittle(Principal principal, SpittleForm form) {
          Spittle spittle = new Spittle(principal.getName(), form.getText(), new Date());
          spittleRepo.save(spittle);
          feedService.broadcastSpittle(spittle);
          return new Notification("Saved Spittle for user: " + principal.getName());
      }

    JavaScript客户端代码:

    stomp.subscribe("/user/queue/notifications", handleNotification);

    在内部,以“/user”作为前缀的目的地将会以特殊的方式进行处理。这种消息不会通过AnnotationMethodMessageHandler(像应用消息那样)来处理,也不会通过SimpleBrokerMessageHandler或StompBrokerRelayMessageHandler(像代理消息那样)来处理,以“/user”为前缀的消息将会通过UserDestinationMessageHandler进行处理,如图18.4所示。
    !!!

    4.2 为指定用户发送消息

    除了convertAndSend()以外,SimpMessagingTemplate还提供了convertAndSendToUser()方法。按照名字就可以判断出来,convertAndSendToUser()方法能够让我们给特定用户发送消息。

    为了阐述该功能,我们要在Spittr应用中添加一项特性,当其他用户提交的Spittle提到某个用户时,将会提醒该用户。例如,如果Spittle文本中包含“@jbauer”,那么我们就应该发送一条消息给使用“jbauer”用户名登录的客户端。如下程序清单中的broadcastSpittle()方法使用了convertAndSendToUser(),从而能够提醒所谈论到的用户。

    @Service
    public class SpittleFeedServiceImpl implements SpittleFeedService {
    
        private SimpMessagingTemplate messaging;
        private Pattern pattern = Pattern.compile("\@(\S+)");
    
        @Autowired
        public SpittleFeedServiceImpl(SimpMessagingTemplate messaging) {
            this.messaging = messaging;
        }
    
        public void broadcastSpittle(Spittle spittle) {
            messaging.convertAndSend("/topic/spittlefeed", spittle);
    
            Matcher matcher = pattern.matcher(spittle.getMessage());
            if (matcher.find()) {
                String username = matcher.group(1);
                messaging.convertAndSendToUser(username, "/queue/notifications",
                        new Notification("You just got mentioned!"));
            }
        }
    
    }

    在broadcastSpittle()中,如果给定Spittle对象的消息中包含了类似于用户名的内容(也就是以“@”开头的文本),那么一个新的Notification将会发送到名为“/queue/notifications”的目的地上。因此,如果Spittle中包含“@jbauer”的话,Notification将会发送到“/user/jbauer/queue/notifications”目的地上。

    5 处理消息异常

    源码

    https://github.com/myitroad/spring-in-action-4/tree/master/Chapter_18

  • 相关阅读:
    第三天 moyax
    mkfs.ext3 option
    write file to stroage trigger kernel warning
    download fomat install rootfs script
    custom usb-seriel udev relus for compatible usb-seriel devices using kermit
    Wifi Troughput Test using iperf
    learning uboot switch to standby system using button
    learning uboot support web http function in qca4531 cpu
    learngin uboot design parameter recovery mechanism
    learning uboot auto switch to stanbdy system in qca4531 cpu
  • 原文地址:https://www.cnblogs.com/myitroad/p/9334141.html
Copyright © 2011-2022 走看看