Статьи

Spring Framework 4.0 M2: архитектура обмена сообщениями WebSocket


Следующий пост написал Россен Стоянчев

обзор

Как я  писал ранее , API-интерфейс WebSocket является лишь отправной точкой для приложений обмена сообщениями в стиле WebSocket. Многие практические проблемы остаются. Как недавно подумал один из пользователей рассылки Tomcat  :

мне действительно кажется, что веб-сокеты все еще не являются «готовыми к производству» (я не говорю о реализации Tomcat как таковой, но в более общем плане)… встроенная возможность веб-сокетов в IE доступна только с IE-10 и это решения, которые Позволить этому работать в более низких версиях IE немного «ненадежно» (полагается, например, на использование Adobe FlashPlayer). (Большинство наших клиентов — крупные корпорации, которые не собираются ни обновлять свои браузеры, ни открывать специальные порты в своих брандмауэрах, просто чтобы угодить нам).

Первой вехой в Spring Framework 4.0 стала поддержка SockJS на стороне  сервера , лучших и наиболее полных вариантов резервирования браузера WebSocket. Вам понадобятся альтернативные варианты в браузерах, которые не поддерживают WebSocket, и в ситуациях, когда сетевые прокси-серверы  не позволяют его использовать . Проще говоря, SockJS позволяет создавать приложения WebSocket сегодня и при необходимости полагаться на прозрачные альтернативные варианты.

Даже с запасными вариантами остаются большие проблемы. Сокет — это абстракция очень низкого уровня, и подавляющее большинство современных веб-приложений не программируют на сокеты. Вот почему протокол WebSocket определяет механизм суб-протокола, который, по сути, позволяет и поощряет использование протоколов более высокого уровня через WebSocket, подобно тому, как мы используем HTTP поверх TCP.

Второй этап Spring Framework 4.0 позволяет использовать протоколы обмена сообщениями более высокого уровня через WebSocket. Чтобы продемонстрировать это, мы собрали пример приложения.

Образец стокового портфолио

Пример приложения Stock Portfolio,  доступного на Github , загружает позиции портфеля пользователя, позволяет покупать и продавать акции, использовать котировки цен и отображать обновления позиций. Это достаточно простое приложение. Тем не менее, он обрабатывает ряд общих задач, которые могут возникнуть в браузерных приложениях обмена сообщениями.

Так, как мы собираем приложение как это? От HTTP и REST мы привыкли полагаться на URL вместе с глаголами HTTP для выражения того, что нужно сделать. Здесь у нас есть сокет и много сообщений. Как вы говорите, для кого предназначено сообщение и что оно означает?

Браузер и сервер должны согласовать общий формат сообщения, прежде чем такая семантика может быть выражена. Существует несколько протоколов, которые могут помочь. Мы выбрали  STOMP  для этого этапа благодаря его простоте и  широкой поддержке .

Простой / потоковый протокол обмена текстовыми сообщениями (STOMP)

STOMP  — это протокол обмена сообщениями, созданный с учетом простоты. Он основан на кадрах, смоделированных по HTTP. Кадр состоит из команды, необязательных заголовков и необязательного тела.

Например, приложение Stock Portfolio должно получать котировки акций, поэтому клиент отправляет  SUBSCRIBE фрейм, в котором  destination заголовок указывает, на что клиент хочет подписаться:

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

Когда биржевые котировки становятся доступными, сервер отправляет  MESSAGE фрейм с соответствующим адресатом назначения и идентификатором подписки, а также заголовком типа содержимого и телом:

MESSAGE
subscription:sub-1
message-id:wm2si1tj-4
content-type: application/json
destination:/topic/stocks.PRICE.STOCK.NASDAQ.EMC
{\"ticker\":\"EMC\",\"price\":24.19}

Чтобы сделать все это в браузере, мы используем  stomp.js  и  клиент SockJS :

varsocket = newSockJS('/spring-websocket-portfolio/portfolio');
varclient = Stomp.over(socket);
varonConnect = function() {
client.subscribe("/topic/price.stock.*", function(message) {
// process quote
});
};
client.connect('guest', 'guest', onConnect);

Это уже огромный выигрыш !! У нас есть стандартный формат сообщений и поддержка на стороне клиента.

Теперь мы можем переместить один на сторону сервера.

Message-Broker Solution

Один вариант на стороне сервера — это чисто решение для брокера сообщений, в котором сообщения отправляются непосредственно традиционному брокеру сообщений, такому как RabbitMQ, ActiveMQ и т. Д. Большинство, если не все брокеры, поддерживают STOMP через TCP, но все чаще поддерживают его и через WebSocket, в то время как RabbitMQ идет дальше и также поддерживает SockJS. Наша архитектура будет выглядеть так:

Это надежное и масштабируемое решение, но, возможно, не самое подходящее для рассматриваемой проблемы. Посредники сообщений обычно используются на предприятии. Экспонировать их напрямую через Интернет не идеально.

Если мы узнали что-то от REST, так это то, что мы не хотим раскрывать подробности о внутренних элементах нашей системы, таких как база данных или модель предметной области.

Кроме того, как разработчик Java, вы хотите применить безопасность, проверку и добавить логику приложения. В решении для брокера сообщений сервер приложений находится позади брокера сообщений, что является значительным отклонением от того, к чему привыкли большинство разработчиков веб-приложений.

Вот почему такая библиотека, как  socket.io  , популярна. Это просто и нацелено на потребности веб-приложений. С другой стороны, мы не должны игнорировать возможности брокеров сообщений обрабатывать сообщения, они действительно хороши в этом, и обмен сообщениями является сложной проблемой. Нам нужно лучшее из обоих.

Решение для брокеров приложений и сообщений

Другой подход заключается в том, чтобы приложение обрабатывало входящие сообщения и служило посредником между веб-клиентами и брокером сообщений. Сообщения от клиентов могут передаваться посреднику через приложение, и, наоборот, сообщения от посредника могут возвращаться клиентам через приложение. Это дает приложению возможность изучить тип входящего  сообщения  и заголовок «назначения» и решить, обрабатывать ли сообщение или передавать его посреднику.

Это подход, который мы выбрали. Чтобы проиллюстрировать лучше, вот несколько сценариев.

Загрузить позиции портфеля

  • Клиент запрашивает позиции портфеля
  • Приложение обрабатывает запрос путем загрузки и возврата данных в подписку.
  • Посредник сообщений не участвует в этом взаимодействии

Подписаться на котировки акций

  • Client sends subscription request for stock quotes
  • The application passes the message to the message broker
  • The message broker propagates the message to all subscribed clients

Receive stock quotes

  • QuoteService sends stock quote message to the message broker
  • The message broker propagates the message to all subscribed clients

Execute a trade

  • Client sends trade request
  • The application handles it, submits the trade for execution through the TradeService
  • The message broker is not involved in this interaction

Receive position update

  • Trade service sends a position update message to a queue on the message broker
  • The message broker sends the position update to the client
  • Sending messages to a specific user is covered in more detail further below

Strictly speaking the use of a message broker is optional. We provide an out-of-the-box «simple» alternative for getting-started. However the use of a message broker is recommended for scalability and for deployments with multiple application servers.

Code Snippets

Let’s see some examples of client and server-side code.

This is portfolio.js requesting portfolio positions:

stompClient.subscribe("/app/positions", function(message) {
self.portfolio().loadPositions(JSON.parse(message.body));
});

On the server side PortfolioController detects the request and returns portfolio positions demonstrating a request-reply interaction that is very common in web applications. Since we use Spring Security to protect HTTP requests, including the one leading to the WebSocket handshake, the principal method argument below is taken from the user principal Spring Security set on the HttpServletRequest.

@Controller
publicclassPortfolioController {
// ...
@SubscribeEvent("/app/positions")
publicList<PortfolioPosition> getPortfolios(Principal principal) {
String user = principal.getName();
Portfolio portfolio = this.portfolioService.findPortfolio(user);
returnportfolio.getPositions();
}
}

This is portfolio.js sending a trade request:

stompClient.send("/app/trade", {}, JSON.stringify(trade));

On the server side PortfolioController sends the trade for execution:

@Controller
publicclassPortfolioController {
// ...
@MessageMapping(value="/app/trade")
publicvoidexecuteTrade(Trade trade, Principal principal) {
trade.setUsername(principal.getName());
this.tradeService.executeTrade(trade);
}
}

PortfolioController can also handle unexpected exceptions by sending a message to the user.

@Controller
publicclassPortfolioController {
// ...
@MessageExceptionHandler
@ReplyToUser(value="/queue/errors")
publicString handleException(Throwable exception) {
returnexception.getMessage();
}
}

What about sending messages from within the application to subscribed clients? This is how the QuoteService sends quotes:

@Service
publicclassQuoteService {
privatefinalMessageSendingOperations<String> messagingTemplate;
@Scheduled(fixedDelay=1000)
publicvoidsendQuotes() {
for(Quote quote : this.quoteGenerator.generateQuotes()) {
String destination = "/topic/price.stock."+ quote.getTicker();
this.messagingTemplate.convertAndSend(destination, quote);
}
}
}

And this is how the TradeService sends position updates after a trade is executed:

@Service
publicclassTradeService {
// ...
@Scheduled(fixedDelay=1500)
publicvoidsendTradeNotifications() {
for(TradeResult tr : this.tradeResults) {
String queue = "/queue/position-updates";
this.messagingTemplate.convertAndSendToUser(tr.user, queue, tr.position);
}
}
}

And just in case you’re wondering… yes PortfolioController can also contain Spring MVC methods (e.g. @RequestMapping) as suggested in this ticket by a developer who previously built an online game application:

Yes, having [message] mappings and spring mvc mappings consolidated would be nice. There is no reason why they can’t be unified.

And just like the QuoteService and TradeService, Spring MVC controller methods can publish messages too.

Messaging Support For Spring Applications

For a long time Spring Integration has provided first-class abstractions for the well-knownEnterprise Integration patterns as well as lightweight messagings. While working on this milestone we realized the latter was exactly what we needed to build on.

As a result I’m pleased to announce we’ve moved a selection of Spring Integration types to the Spring Framework into a new module predictably called spring-messaging. Besides core abstractions such as MessageMessageChannelMessageHandler, and others, the new module contains all the annotations and classes to support the new features described in this post.

With that in mind we can now look at a diagram of the internal architecture of the Stock Portfolio application:

StompWebSocketHandler puts incoming client messages on the «dispatch» message channel. There are 3 subscribers to this channel. The first one delegates to annotated methods, the second relays messages to a STOMP message broker, while the third one handles messages to individual users by transforming the destination into a unique queue name to which the client is subscribed (more detail to come).

By default the application runs with a «simple» message broker provided as a getting-started option. As explained in the sample README, you can alternate between the «simple» and a full-featured message broker by activating and de-activating profiles.

Another possible configuration change is to switch from Executor to Reactor-based implementations of MessageChannel for message passing. The Reactor project that recently released a first milestone is also used to manage TCP connections between the application and the message broker.

You can see the full application configuration that also includes the new Spring Security Java configuration. You might also be interested in the improved STS support for Java configuration.

Sending Messages To a Single User

It is easy to see how messages can be broadcast to multiple subscribed clients, just publish a message to a topic. It is more difficult to see how to send a message to a specific user. For example you may catch an exception and would like to send an error message. Or you may have received a trade confirmation and would like to send it to the user.

In traditional messaging applications it is common to create a temporary queue and set a «reply-to» header on any message to which a reply is expected. This works but feels rather cumbersome in web applications. The client must remember to set the necessary header on all applicable messages and the server application may need to keep track and pass this around. Sometimes such information may simply not be readily available, e.g. while handling an HTTP POST as an alternative to passing messages.

To support this requirement, we send a unique queue suffix to every connected client. The suffix can then be appended to create unique queue names.

client.connect('guest', 'guest', function(frame) {
varsuffix = frame.headers['queue-suffix'];
client.subscribe("/queue/error"+ suffix, function(msg) {
// handle error
});
client.subscribe("/queue/position-updates"+ suffix, function(msg) {
// handle position update
});
});

Then on the server-side an @MessageExceptionHandler method (or any message-handling method) can add an @ReplyToUser annotation to send the return value as a message.

@MessageExceptionHandler
@ReplyToUser(value="/queue/errors")
publicString handleException(Throwable exception) {
// ...
}

All other classes, like the TradeService, can use a messaging template to achieve the same.

String user = "fabrice";
String queue = "/queue/position-updates";
this.messagingTemplate.convertAndSendToUser(user, queue, position);

In both cases internally we locate the user queue suffix (through the configuredUserQueueSuffixResolver) in order to reconstruct the correct queue name. At the moment there is only one simple resolver implementation. However, it would be easy to add a Redisimplementation that would support the same feature regardless of whether the user is connected to this or another application server.

Conclusion

Hopefully this has been a useful introduction of the new functionality. Rather than making the post longer, I encourage you to check the sample and consider what it means for applications you write or intend to write. It is a perfect time for feedback as we work towards a release candidate in early September.

To use Spring Framework 4.0.0.M2 add the http://repo.springsource.org/libs-milestone or the http://repo.springsource.org/milestone repositories to your configuration. The former includes transient dependencies as explained in our Repository FAQ.

SpringOne 2GX 2013 is around the corner

Book your place at SpringOne in Santa Clara soon. It’s simply the best opportunity to find out first hand all that’s going on and to provide direct feedback. Expect a number of significant new announcements this year. Check recent blog posts to see what I mean and there is more to come!