本文是WebSocket的故事系列第三篇第一节,将逐步深刻Spring源码进行介绍,本系列的干货也将陆续在后面的几篇文章中放出。WebSocket的故事系列计划分五大篇,旨在由浅入深的介绍WebSocket以及在Springboot中如何快速构建和使用WebSocket提供的能力。本系列计划包含以下几篇文章:html
第一篇,什么是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速构建WebSocket广播式消息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速构建点对点的消息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速构建点对点的消息模式(2)
第五篇,Springboot中,实现网页聊天室之自定义WebSocket消息代理
第六篇,Springboot中,实现更灵活的WebSocketjava
上一篇介绍Spring实现的最简单的STOMP的一种模式,经过@SendTo注解,将消息发送到指定消息代理,只要是订阅过该消息代理的客户端,都会收到这个消息。做为系列的第三篇,我会分三次来详细介绍实现细节,本篇将由@SendTo和@SendToUser开始,深刻Spring的WebSocket消息发送关键代码进行讲解。为下一篇点对点消息的讲解铺路。git
想要了解STOMP协议,Spring内部代码细节,以及如何使用Springboot搭建WebSocket服务的同窗。github
本篇的代码相对较多,我会尽可能细致讲解。bash
本篇咱们将详细介绍这两个注解背后的故事。服务器
上一篇中,咱们利用@SendTo
注解,使方法的返回值推送到消息代理器中,由消息代理器广播到订阅路径中去。但并无详细的介绍消息是怎样被Spring框架处理,最后发送广播出去的。先放上上节中的关键代码:session
@MessageMapping("/hello") //使用MessageMapping注解来标识全部发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理.
@SendTo("/topic/greetings") //使用SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic/greetings”.
//传入的参数Message为客户端发送过来的消息,是自动绑定的。
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // 模拟处理延时
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根据传入的信息,返回一个欢迎消息.
}
}
复制代码
上面方法中的返回值,会被广播到/topic/greetings
这个订阅路径中,只要客户端订阅了这个路径,都会接收到消息。Spring处理消息的主要类是SimpleBrokerMessageHandler
, 当须要发送广播消息时,最终会调用其中的sendMessageToSubscribers()
方法:app
Broker
的客户端
Session
,而后逐个发送消息。这里,入参
destination
就是
Broker
的地址,而
message
,就是咱们返回信息的封装,其余细节这里就不展开讲了。
那么若是我只是想用WebSocket向服务器发出查询请求,而后服务器你就把查询结果给我就好了,其余用户就不用你广播推送了,简单点,就是我请求,你就推送给我。这又该怎么办呢?是的,@SendToUser
就能解决这个问题。框架
先上代码片断:post
@MessageMapping("/hello") //使用MessageMapping注解来标识全部发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理.
@SendToUser("/topic/greetings") //使用SendToUser注解来标识这个方法返回的结果,都会被发送到请求它的用户的destination.
//传入的参数Message为客户端发送过来的消息,是自动绑定的。
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // 模拟处理延时
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根据传入的信息,返回一个欢迎消息.
}
}
复制代码
能够看到,这里我只是修改了注解,基于上节中咱们的示例代码,咱们启动程序,试验一下效果,结果发现并无收到返回信息,这是为何呢?让咱们深刻代码实现的关键节点来看看。
首先,在咱们查看代码细节以前,应该先静态分析一下。根据以前咱们介绍过的内容,很容易想到:
1.Spring WebSocket通道的创建最开始是源于Http协议的第一次握手,握手成功以后,就打开了客户端和服务器的WebSocket通道,即客户端与服务端经过一个
Session
来维持通讯。就像创建一条管道同样,你有内容就传给我,我有内容就传给你。
2.上面的greeting
方法,其实是框架提供给开发者一个处理客户端请求的一个时机,开发者能够根据业务须要,对信息处理加工后,返回给客户端须要的响应结果。那么当这个方法return
的时候,也就是响应信息由服务端向客户端返送的开始。
基于上述两个基本结论,咱们开始分析代码,首先就是从return
以后开始,看看代码跑到了哪里: AbstractMethodMessageHandler.java
中的handleMatch
方法
destination
来进行匹配,找到对应的处理类。在本例中,即根据
/hello
找到
GreetingController
(MessageMapping注解所在位置)。而后即经过
handleMatch
中的
invoke
方法,调用
GreetingController
中的
greeting
方法,
greeting
方法返回后,经过
handleRetureValue
处理其返回值,那么它对应的方法又是什么呢?咱们往下看:
顺着这个方法,咱们到了一个重要的类,SendToMethodReturnValueHandler.java
从类的名字就能够看出来,它是用来专门处理SendTo
相关注解的类。当用SendTo
注解的方法返回后,即调用此类中的handleReturnValue
方法来进行处理。代码流程很清晰,你们参考图片内的注释便可。
两个值得咱们继续追踪的点:
1.在
SendToUser
分支中,不管是广播仍是非广播消息,都用到了messagingTemplate
。这个messagingTemplate
是什么?
2.广播与非广播的消息发送,都调用了一样的方法,即convertAndSendToUser
。区别在于非广播时,多了一个sessionId
参数。这个方法以及这个参数该如何去理解呢?
带着这样的疑问继续追踪,仍是在SendToMethodReturnValueHandler.java
这个类中:
这里,咱们又接触到一个新类,SimpMessagingTemplate
。它实现了convertAndSendToUser
方法,咱们有必要详细介绍一下这个方法,它的代码量不大,但却相当重要:
public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Assert.notNull(user, "User must not be null");
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}
复制代码
介绍一下输入参数:
user
:用户标识,这里就是客户端与服务端连接的sessionId
destination
:这是SendToUser注解后括号内的参数值
payload
:Object
类型,它标识Controller
中定义的方法的返回值,这里就是GreetingController
类中greeting
方法的返回值
headers
:返回信息的消息头
postProcessor
:此处为Null
\
首先对入参进行校验和归一化,重点在最后一行,入参处作了字符串拼接,将原来的destination
拼接为/user/userID/topic/greetings
,userID
是客户端的SessionID
。拼接结果destination=“/user/au3ev44r/topic/greetings“
。好,接下来,咱们来看一下这个方法:
AbstractMessageSendingTemplate<D>.java
中:
public void convertAndSend(D destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Message<?> message = this.doConvert(payload, headers, postProcessor);
this.send(destination, message);
}
复制代码
它将要发送的Body
信息与Header
信息进行整合,获得Message
信息。以后,调用send方法发送。以后通过一系列加工方法的流转,最后到达了UserDestinationMessageHandler
类中的handleMessage
方法中。
resolveDestination
方法能识别带
/user
的订阅路径并作出处理,
此处将sourceDestination
转化成/topic/greetings-userau3ev44r
,userau3ev44r
中,user
是关键字,au3ev44r
是SessionID
,这样子就把用户和订阅路径惟一的匹配起来了。
targetDestinations
地址,调用了
SimpMessageTemplate
类中的send方法,最终又来到了
SimpleBrokerMessageHandler
类中,眼熟吧,没错,就是咱们在介绍
SendTo
注解时提到的,只不过,这时候它的目的地址,是
/topic/greetings-userau3ev44r
。至此,处理目的地址和封装消息的工做就完成了。以后,会走实际发送过程,客户端会收到返回的
greeting
消息。
上例中,咱们经过代码,详细讲解了一条客户端消息到达服务端后,是如何经过代码流转,找到下面两个关键参数的整个流程的。
欢迎持续关注
小铭出品,必属精品
欢迎关注xNPE技术论坛,更多原创干货每日推送。