使用spring的STOMP实现WebSocket


本文介绍如何利用WebSocket和STOMP实现消息功能

WebSocket是发送和接收消息的底层API,而SockJS是在WebSocket 之上的API,最后 STOMP(面向消息的简单文本协议)是基于SockJS 的高级API(简而言之,WebSocket 是底层协议SockJS 是WebSocket 的备选方案,也是底层协议,而STOMP是基于WebSocket(SockJS) 的上层协议

WebSocket
WebSocket协议提供了通过一个套接字实现全双工通信的功能。也能够实现web浏览器和server间的异步通信,全双工意味着server与浏览器间可以发送和接收消息。

使用spring来实现WebSocket有几种方式

方式一、使用spring的低层级WebSocket API

方式二、应对不支持WebSocket的场景(引入SockJS)

方式三、使用Spring的STOMP

1、如何理解 STOMP 与 WebSocket 的关系:
  1. 假设HTTP协议并不存在,只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情;TCP套接也是一种类似于Http的请求协议。
  2. 不过幸好,我们有HTTP协议,它解决了web 浏览器发起请求以及 web 服务器响应请求的细节;
  3. 直接使用 WebSocket(SockJS)就很类似于使用TCP套接字来编写web应用;因为没有高层协议,因此就需要我们定义应用间所发送消息的语义,还需要确保
    连接的两端都能遵循这些语义;
  4. 同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义;(干货——STOMP
    在 WebSocket 之上提供了一个基于 帧的线路格式层,用来定义消息语义)
2、STOMP帧的定义

该帧由命令,一个或多个头信息以及负载所组成。如下就是发送 数据的一个 STOMP帧:(引入了 STOMP帧格式)

SEND
destination:/app/marco
content-length:20

{"message":"Marco!"}

对以上代码的分析

  1. SEND:STOMP命令,表明会发送一些内容;
  2. destination:头信息,用来表示消息发送到哪里;
  3. content-length:头信息,用来表示负载内容的 大小;
  4. 空行:
  5. 帧内容(负载)内容:
3、启用STOMP消息功能

spring 的消息功能是基于消息代理构建的,因此我们必须要配置一个消息代理和一个消息目的地(spring 的消息功能是基于消息代理构建的

如下代码展现了如何通过java配置启用基于代理的的web 消息功能(@EnableWebSocketMessageBroker 注解的作用:能够在 WebSocket 上启用 STOMP)

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
      config.enableSimpleBroker("/topic", "/queue");
      config.setApplicationDestinationPrefixes("/app");
      //应用程序以/app为前缀,而代理目的地以/topic为前缀
  }
  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
      registry.addEndpoint("/hello").withSockJS();
      //在网页上我们就可以通过这个链接/server/hello来和服务器的WebSocket连接,比如http://ip:8080/hello注意,不是http://ip:8080/app/hello
  }
}

对以上代码的分析

  1. EnableWebSocketMessageBroker注解表明:这个配置类不仅配置了 WebSocket,还配置了基于代理的STOMP 消息;
  2. 它重载了 registerStompEndpoints() 方法:将 “/hello” 路径注册为STOMP端点。这个路径与之前发送和接收消息的目的路径有所不同,这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点,即用户发送请求 url=’/server/hello’ 与 STOMP server进行连接,之后再转发到订阅url;(端点的作用:客户端在订阅或发布消息到目的地址前,要连接该端点)
  3. 它重载了configureMessageBroker() 方法:配置了一个简单的消息代理。如果不重载,默认case下,会自动配置一个简单的内存消息代理,用来处理 "/topic"为前缀的消息。但经过重载后,消息代理将会处理前缀为 “/topic” and “/queue” 消息。
  4. 之外:发送应用程序的消息将会带有 “/app” 前缀,下图展现了 这个配置中的 消息流;

在这里插入图片描述

对上述处理step的分析

  1. 应用程序 目的地以 “/app” 为前缀,而代理的目的地以 “/topic” 和 “/queue” 作为前缀;
  2. 以应用程序为目的地的消息将会直接路由到带有 @MessageMapping 注解的控制器方法中;(@MessageMapping的作用)
  3. 而发送到代理上的消息,包括@MessageMapping注解方法的返回值所形成的消息,将会路由到代理上,并最终发送到订阅这些目的地客户端; (client 连接地址和发送地址是不同的,以本例为例,前者是/server/hello,后者是/server/app/XX,先连接后发送)
4、启用 STOMP 代理中继
  1. 在生成环境下,可能会希望使用真正支持STOMP的代理来支持WebSocket消息,如RabbitMQ或ActiveMQ。这样的代理提供了可扩展性和健壮性更好的消息功能,当然,他们也支持STOMP 命令;
5、处理来自客户端的 STOMP 消息

处理来自客户端的 STOMP 消息

  1. 借助@MessageMapping 注解能够在控制器中处理STOMP消息
@Controller
public class GreetingController {
	@MessageMapping("/hello")
	@SendTo("/topic/greetings")
	public Greeting greeting(HelloMessage message) throws Exception {
		System.out.println("receiving " + message.getName());
		System.out.println("connecting successfully.");
		return new Greeting("Hello, " + message.getName() + "!");
	}
}

对以上代码的分析

  1. @MessageMapping注解:表示 handleShout()方法能够处理指定目的地上到达的消息
  2. 这个目的地(消息发送目的地url)就是 “/server/app/hello”,其中 “/app” 是 隐含的,"/server" 是springmvc项目名称,类似http://localhost:8080
6、处理来自客户端的订阅(使用@SubscribeMapping注解)
  1. @SubscribeMapping注解的方法:当收到 STOMP 订阅消息的时候,带有@SubscribeMapping 注解的方法将会触发;其也是通过AnnotationMethodMessageHandler来接收消息的;
  2. @SubscribeMapping注解的应用场景:实现请求-回应模式。在请求-回应模式中,客户端订阅一个目的地,然后预期在这个目的地上获得一个一次性的响应(@SubsribeMapping注解实现请求-回应模式,客户端订阅之后一定会收到一个服务端的响应)
@SubscribeMapping({"/marco"})
public Shout handleSubscription() {
    Shout outgoing = new Shout();
    outgoing.setMessage("Polo!");
    return outgoing;
}

对以上代码的分析

  1. @SubscribeMapping注解的方法来处理 对 “/app/macro"目的地订阅(与@MessageMapping类似,”/app" 是隐含的 );
  2. 请求-回应模式与HTTP-GET的全球-响应模式差不多:关键区别在于,HTTP-GET请求是同步的,而订阅的全球-回应模式是异步的,这样客户端能够在回应可用时再去处理,而不必等待;(HTTP-GET请求是同步的,而订阅的请求-回应模式是异步的
7、编写 JavaScript 客户端

借助STOMP 库,通过 JavaScript发送消息

function connect() {
    //连接端点
    var socket = new SockJS("<c:url value='/hello'/>");
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function(frame) {
        //订阅/topic/greetings
        stompClient.subscribe('/topic/greetings', function(greeting){
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}

function sendName() {
    var name = document.getElementById('name').value;
    //发送消息/app/hello
    stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}

对以上代码的 分析

  1. 以上代码连接“/hello” 端点并发送 ”name“;
  2. stompClient.send("/app/hello", {}, JSON.stringify({‘name’:name}))。
    第一个参数:json 负载消息发送的目的地。
    第二个参数:是一个头信息的Map,它会包含在 STOMP 帧中。
    第三个参数:负载消息;(stomp client连接地址和发送地址不一样的。
    连接地址为/hello 类似于localhost:8080/springmvc_project_name/hello , 而发送地址为’/app/hello’,这里要当心)
8、发送消息到客户端

spring提供了两种发送数据到客户端的方法:

  1. 作为处理消息 或处理订阅的附带结果;
  2. 使用消息模板,主动给用户发送消息;
8.1、第一种收到订阅,做出响应。

在收到客户端发过来的消息,并处理消息后,给客户端一个响应

@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {

	return new Greeting("Hello, " + message.getName() + "!");
}

对以上代码的分析:

  1. 返回的对象将会进行转换(通过消息转换器)并放到STOMP帧的负载中(上面介绍了帧的负载中存的是要发送的内容),然后发送给消息代理(消息代理分为STOMP代理中继内存消息代理
  2. 默认情况下:帧所发往的目的地会与触发处理器方法的目的地相同。所以返回的对象会写入到STOMP帧的负载中,并发布到
    "/topic/stomp"目的地。不过可以通过@SendTo注解,重载目的地(@SendTo注解的作用)
  3. 消息将会发到/topic/greetings,所有订阅这个主题的应用都会收到这条消息;
  4. 如果为方法添加@SendTo注解的话,那么消息将会发送到指定的目的地,这样就会经过代理;(SubscribeMapping注解返回的消息直接发送到client,不经过代理,而@SendTo注解的路径,就会经过代理,然后再发送到目的地)
8.2、在应用的任意地方发送消息

spring的SimpMessagingTemplate能够在应用的任何地方发送消息,不必以接收一条消息为前提来发送,当前前提是要订阅。

看下面这个案例:让首页订阅一个STOMP主题,在Spittle创建的时候,该主题能够收到 Spittle更新时的feed

<script>
	var sock = new SockJS('spittr');
	var stomp = Stomp.over(sock);

	stomp.connect('guest', 'guest', function(frame) {
		stomp.subscribe("/topic/spittlefeed", handleSpittle);
	});

	function handleSpittle(incoming) {
		var spittle = JSON.parse(incoming.body);
		
		var source = $("#spittle-template").html();
		var template = Handlebars.compile(source);
		var spittleHtml = template(spittle);
		$('.spittleList').prepend(spittleHtml);
	}
</script>

对以上代码的分析

  1. 在连接到STMOP代理后,我们订阅了"/topic/spittlefeed"主题,并指定当消息到达后,由handleSpittle()函数来处理Spittle更新。

server端代码

使用SimpMessagingTemplate将所有新创建的Spittle以消息的形式发布到"/topic/feed"主题上

@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); //发送消息.
    }
}

对以上代码的分析:

  1. 配置spring支持stomp的一个附带功能是在spring应用上下文中已经包含了Simple
  2. 在发布消息给STOMP主题的时候,所有订阅该主题的客户端都会收到消息。但有的时候,我们希望将消息发送给指定用户;
8.2.1、为目标用户发送消息

1、在使用srping和STOMP消息功能的时候,有三种方式来认证用户:

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

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

例子:编写一个控制器方法,根据传入的消息创建新的Spittle对象,并发送一个回应,表明对象创建成功;(这种REST也可以实现,不过它是同步的,而这里是异步的)

代码如下:它会处理传入的消息并将其存储到Spittle

@MessageMapping("/spittle")
@SendToUser("/queue/notifications")
public Notification handleSpittle(Principal principal, SpittleForm form) {

    Spittle spittle = new Spittle(rincipal.getName(), form.getText(), new Date());
    spittleRepo.save(spittle);
    
    return new Notification("Saved Spittle");
}

上面代码分析

  1. 该方法最后返回一个新的Notificatino,表明对象保存成功;
  2. 该方法使用了@MessageMapping("/spittle")
    注解:所以当有发往"/app/spittle"目的地的消息到达时,该方法就会触发;如果用户已经认证的话,将会根据STOMP帧上的头信息得到Principal对象;
  3. @SendToUser注解:指定了Notification要发送的目的地"/queue/notifications";
  4. 表面上"/queue/notifications"并不会与特定用户相关联,但因为这里使用的是@SendToUser注解,而不是@SendTo,所以就会发生更多的事情了

看一下针对控制器方法发布的Notificatino对象的目的地,客户端该如何进行订阅。

看个例子:看如下的JavaScript代码,它订阅了一个用户特定的目的地

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

对以上代码的分析

这个目的地使用了"/user"作为前缀,在内部以"/user"为前缀的消息将会通过UserDestinationMessageHandler进行处理,而不是AnnotationMethodMessageHandlerSimpleBrokerMessageHandlerStompBrokerRelayMessageHandler

如下图所示:
在这里插入图片描述

注意:UserDestinationMessageHandler的主要任务:是将用户消息重新路由到某个用户独有的目的地上。在处理订阅的时候,它会将目标地址中的"/user"前缀去掉,并基于用户的会话添加一个后缀。如"/user/queue/notifications" 的订阅最后可能路由到 名为 "/queue/notifacations-user65a4sdfa"目的地上。

3、为指定用户发送消息

  1. SimpMessagingTemplate还提供了convertAndSendToUser()方法,该方法能够让我们给特定用户发送消息;
  2. 我们在web应用上添加一个特性:当其他用户提交的Spittle提到某个用户时,将会提醒该用户(类似于微博的@功能)

看个例子

如果Spittle文本中包含"@tangrong",那么我们就应该发送一条消息给使用tangrong用户名登录的client,代码实例如下

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

本文转自;https://blog.csdn.net/pacosonswjtu/article/details/51914567

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值