通过WebSocket和STOMP实现浏览器和服务器的消息通信

异步消息

异步消息有两个重要的概念, 消息代理(broker)和 目的地(destination)。


消息代理:当一个应用发送消息时,需要制定发送的目的地,然后将消息交给消息代理(类似邮局),消息代理会确保消息发送到指定的目的地。


目的地:不同的消息系统有不同的消息路由模式,但是有两种通用的目的地:队列(queue)和主题(topic),分别对应两种消息模型:点对点模型和发布/订阅模型


点对点模型

队列可以有多个接收者,但是消息只能被一个接收者取走。


发布-订阅消息模型


主题里的消息可以发送给多个订阅者。

WebSockt实现浏览器和服务端通信

WebSocket协议提供了通过套接字实现全双工、异步通信的功能。首先,浏览器的JavaScript客户端创建一个socket并连接到服务端,接下来,客户端和服务端之间可以通过这个通道发送和接收消息,服务端可以通过这个链接发送更新到客户端。

通过Spring的WebSocketAPI实现

客户端代码:

<html>
  <head>
    <title>Home</title>
    <script th:src="@{/webjars/sockjs-client/0.3.4/sockjs.min.js}"></script>
    <script th:src="@{/webjars/jquery/2.0.3/jquery.min.js}"></script>
  </head>
  <body>
    <button id="stop">Stop</button>
  
    <script th:inline="javascript">
      var sock = new SockJS([[@{/marco}]]);//SockJS是对WebSocket技术的一种模拟,在浏览器不支持WebSocket通信时,
      提供其他的通信方式。
      sock.onopen = function() {
    	  console.log('Opening');
    	  sayMarco();
      }
      
      sock.onmessage = function(e) {
    	  console.log('Received message: ', e.data);
    	  $('#output').append('Received "' + e.data + '"<br/>');
    	  setTimeout(function(){sayMarco()}, 2000);
      }
      
      sock.onclose = function() {
    	  console.log('Closing');
      }
      
      function sayMarco() {
    	  console.log('Sending Marco!');
    	  $('#output').append('Sending "Marco!"<br/>');
    	  sock.send("Marco!");
      }

      $('#stop').click(function() {sock.close()});
      </script>
    
    <div id="output"></div>
  </body>
</html>

上面代码中,sock创建的参数是/marco,这是一个路径,代表该客服端sock连接到服务端的路径是当前路径加上这个相对路径。而这个/marco其实对应的就是服务端的handler消息处理器。

服务端代码:

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!"));
	}
	
}   
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

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

}

上面两端代码中,@Configuration注解的是WebSocket的配置类。这个类指定了/marco对应的处理类,并且通过@Bean向Spring容器中注入了这个处理类。第一段代码则是MacroHandler类的实现,它继承了AbstractWebSock抽象类,实现了处理消息的方法。

总的来说,上述代码客户端首先和一个服务端名为“/macro”的应用建立了连接。通过这条连接,客户端给服务端发送一条消息“Macro”。服务端的handler会接收到这条消息,并通过这条通道(WebSocketSession)发送“polo”给客户端。客户端处理消息的方法onmessage()又会在接收到消息后调用sayMarco()。如此循环发送消息。这个发送消息的过程异步的,也就是说客户端给服务端发送消息后,不会等待服务端处理完成。所以说WebSocket建立的是一个异步全双工的通信通道。

通过Sping的STOMP协议实现消息机制

直接使用WebSocket(或者SockJS)类似于用TCP套接字编写Web应用。正如HTTP为TCP添加了请求相应模型,定义了发送内容的语义一样,STOMP协议定义在WEBSOCKET协议之上,为其定义了消息的语义。

STOMP的消息格式和HTTP请求或相应的结构类似:


SEND是命令,表示发送内容,接下来是两条头信息,destination是发送消息的目的地,content-length是负载大小。 接下来一个空行,下面是负载内容,这里是一个JSON字符串。

前端代码:

<html>
  <head>
    <title>Home</title>
    <script th:src="@{/webjars/sockjs-client/0.3.4/sockjs.min.js}"></script>
    <script th:src="@{/webjars/stomp-websocket/2.3.0/stomp.min.js}"></script>
    <script th:src="@{/webjars/jquery/2.0.3/jquery.min.js}"></script>
  </head>
  <body>
    <button id="stop">Stop</button>
  
    <script th:inline="javascript">
      var sock = new SockJS([[@{/marcopolo}]]);
      var stomp = Stomp.over(sock);

      stomp.connect('guest', 'guest', function(frame) {
        console.log('*****  Connected  *****');
        
        stomp.subscribe("/topic/marco", handlePolo);
        sayMarco();
      });
      
      function handleOneTime(message) {
        console.log('Received: ', message);
      }

      function handlePolo(message) {
        console.log('Received: ', message);
        $('#output').append("<b>Received: " + 
            JSON.parse(message.body).message + "</b><br/>")
        if (JSON.parse(message.body).message === 'Polo!') {
          setTimeout(function(){sayMarco()}, 2000);
        }
      }
      
      function handleErrors(message) {
        console.log('RECEIVED ERROR: ', message);
        $('#output').append("<b>GOT AN ERROR!!!: " + 
            JSON.parse(message.body).message + "</b><br/>")
      }

      function sayMarco() {
        console.log('Sending Marco!');
        stomp.send("/app/marco", {}, 
            JSON.stringify({ 'message': 'Marco!' }));
        $('#output').append("<b>Send: Marco!</b><br/>")
      }

      $('#stop').click(function() {sock.close()});
      </script>
    
    <div id="output"></div>
  </body>
</html>
      var sock = new SockJS([[@{/marcopolo}]]);

      var stomp = Stomp.over(sock);

    建立了到/marcopolo的WebSocket的连接之后,调用stomp.min.js库的Stomp对象的over()函数,在sock之上创建stomp客户端,实际是封装了SockJS。接下来建立stomp连接,订阅一个目的地/topic/macro,并把接收道德消息交给 handlepolo函数去处理。

服务端代码:

配置类:

@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");
  }

controller:

@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;
  }

}

可以看到,与前端对应,配置类中注册了endpoint:/marcopolo, 此外,在配置消息代理的函数中,启用了简单代理,简单代理会接收目的地前缀为/topic和/queue的消息,而目的地前缀为/app的消息不会交给代理,而是直接路由到带有@MessageMapping注解的控制器方法中。这个方法的返回值会经过消息代理发送到目的地:


发送消息到客户端

Spring提供了两种方式发送消息到客户端

  • 作为处理消息的附带结果
  • 使用消息模板SimpleMessagingTemplate

上面的服务端代码就是讲消息作为附带结果返回,返回的目的地是处理方法目的地默认是/macro加上一个/topic前缀,也正是前端subscribe()订阅的目的地。另外还可以通过@sendTo注解来重载目的地,相应的前端要订阅这个重载的目的地才能收到消息。


SimpleMessagingTemplate可以在应用的任意地方发送消息,不用以接收一条消息为前提,同时可以发给指定的用户,基于SpringSecurity。


@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!"));
		}
	}
	
}









猜你喜欢

转载自blog.csdn.net/wuqi_seu/article/details/80916077