异步消息
异步消息有两个重要的概念, 消息代理(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!"));
}
}
}