Monday, March 31, 2014

using spring boot, spring websockets and stomp to create a browser chat service

when automagical becomes autotragical...

I have a love/hate relationship with frameworks that are automagical.

On one hand, they can save you a lot of time and effort, preventing you from writing tons of boilerplate code, and just less code in general (every line of code is one you have to support, right?).

There's a dark side to this however. Often times automagical frameworks have rules, usually modeled by conventions. If you violate these, the framework will slap you across the face with a stacktrace. Sometimes the stacktrace is meaningful. Sometimes it's outright esoteric. Sometimes it tells you something that you can't accurately discern without digging into the bowels of the code that you're trying to leverage without creating bowels yourself.

In this case, Spring was guilty of said crimes on several occasions.

Don't get me wrong; I'm quite fond of Spring. However, in this case the solution involved going roughly shoulder deep into Spring's bowels to find simple, yet not obvious solutions to problems. Here's a list of some of the problems I faced, in the event that someone out on the intarwebs is searching for a way to make sense of it all:

  • Spring Boot, at least when I wrote my code, seems to despise Tomcat 7.0.51 (though this appears to have been resolved now). This is particularly important, because in 7.0.51 they fixed a bug that prevented pure websockets from working correctly with... Spring (among other things no doubt).
  • When using Spring's WebSocket API, their scheduling API ends up hijacked. If you need to add your own scheduled bean, you need to override things (as shown in my code).
  • If you have a queue for your messages, and you don't have the URI start with 'queue', you can't use user specific message queues. Regular queues will work just fine.
  • The STOMP heartbeat configuration doesn't work for browser clients since you have to deterministically configure which clients you have that will need to support a heartbeat on the backend. I had to roll my own here.
  • You can't inject the SimpMessagingTemplate into your configuration class or it's bootstrapping path if you're using constructor injection. This can be important if you're trying to set up a ChannelInterceptor to filter out messages before they hit a resource in your application. (Also, how important is it to shave off the 'le' exactly? It really can't just be called SimpleMessagingTemplate???)
  • When I was using Spring 4.0.0.RELEASE, none of this was working correctly. Switching to 4.0.1.RELEASE magically fixed all my problems. I won't ask how or why.

There were several other issues that I've long since repressed. I wrote this code a while ago, and became so irritated with the number of gotchas that I didn't feel motivated at all to blog about it.

All of that aside, Spring's implementation is still leaps and bounds better than what Jetty is offering. For reasons I can't comprehend, the people who write the Jetty API seem to think that every class should be polymorphic to every other class, making it ridiculously confusing to set up. I cannot emphasize enough how many times during my use of their API that I thought "wait... this class is an instance of that?"

if you just want a working demo to play with

Then feel free to grab my code on Github. Did I mention that every example I found online was broken out of the box?

getting your configurations built

We're going to use four different features: web sockets, security, scheduling, and auto configuration. Each one will have its own configuration class, as shown below:

auto configuration, aka spring boot

security, because we need to log in to chat right?

scheduling, because we have a heartbeat to schedule with the web socket api

web sockets, because obviously

There are a few things worth pointing out in this configuration. The /chat and /activeUsers paths will correspond to Spring MVC controllers, while the /queue and /topic paths will correspond to the messaging API's pub/sub model.

designing the application

Before we get into the implementation, I'd like to address the design and talk about some of the details of using STOMP here.

First, you may be wondering what STOMP is. In this case, it's not an off-broadway production; it stands for Simple (or Streaming) Text Oriented Messaging Protocol. There's more about this on Wikipedia and the STOMP website, but in a nutshell it's a command based protocol for sending messages, interoperable with any provider or client implementing the STOMP spec. In the case of this example, it can use web sockets or not; it doesn't matter since the spec works either way.

STOMP and Spring allow us to set up queues, and more interestingly user specific queues. Semantically this is what you'd want in a chat application: each user can subscribe to a queue of messages coming to them, and the front end can route them into the correct chat window. In this case, our front end will simply subscribe to /user/queue/messages, and on the back end Spring will create a dynamic URL to map those messages to that matches the session of the user. The nice thing here is that each user subscribes to the same queue, but they only get their own messages. Even more sophisticated is that Spring can map multiple sessions for one user, so you can be signed in at multiple locations and get the same chat. All of this maps up with Spring Security as well, so as long as you have that configured all of your sessions will be mapped to the correct user queue, all messages received by the client will show up in a chat window corresponding to the other person in chat.

STOMP and Spring also allow us to set up topics, where every subscriber will receive the same message. This is going to be very useful for tracking active users. In the UI, each user subscribes to a topic that reports back which users are active, and in our example that topic will produce a message every 2 seconds. The client will reply to every message containing a list of users with its own heartbeat, which then updates the message being sent to other clients. If a client hasn't checked in for more than 5 seconds (i.e. missed two heartbeats), we consider them offline. This gives us near real time resolution of users being available to chat. Users will appear in a box on the left hand side of the screen, clicking on a name will pull up a chat window for them, and names with an envelope next to them have new messages.

writing an implementation

Since we're writing a chat client, we need some way of modeling a message, as shown below:

We also need a place to send messages to; in this case a controller just like you would create in Spring MVC:

There's some interesting stuff happening here. First, we're injecting an instance of SimpMessagingTemplate into our controller. This class is what allows us to send messages to individual user queues via the method convertAndSendToUser. In our controller we're also assigning the sender ourselves based on the session information that Spring Security has identified, as allowing the client to specify who the sender was produces the same problem the email spec currently has. It's worth noting here that the message is sent to both the sender and the recipient, indicating that the message has passed through the server before being seen in the sender's client's chat window. We ignore the case of sending a message to the recipient in case you're messaging yourself for some reason so that you don't get double messaging (though I should probably just ditch that case altogether).

Lastly, I'd like to draw your attention to the @MessageMapping annotation, which binds the method the annotation is on to the path we set up in our configuration class earlier.

That's actually all we need server side to send and receive messages believe it or not. Next is determining who's signed in.

We're going to start with a controller that receives a heartbeat from users who are signed into chat:

In this case, we're receiving a message that contains information in the header about who the user is, and mark their most recent heartbeat in a class called the ActiveUserService, which can be seen below:

When we call the mark method, we set an updated time for when the user last checked in, which is stored in a Google Guava LoadingCache instance. We also have a method that will aggregate the user names of all the active users, based on the metric of them checking in within the last 5 seconds. This is the information we want to send back to our topic, which is handled with the code below:

In this class, we combine the service and the template to send a message to a topic every two seconds, letting all clients know which users are available to chat. In return, users can reply when receiving a message, telling the server they're still active, creating a round trip heartbeat.

putting it all together with a user interface

And here's where I step out into the realm of things I actively try to avoid: creating a user interface. I am using jQuery here, which makes the task far easier, but I'm still admittedly not great in this department.

Before we go down that path, it's worth calling out that I used two JavaScript files that were present in the Spring guide for using their messaging API with websockets: sock.js and stomp.js. The first sets up the connection to the server, while the second implements the STOMP spec. They're both available for download (and originally found) at Github.

Warning and full disclosure: I'm a lousy JavaScript developer. The code below is likely horribly inefficient and could probably be rewritten by a UI developer in about 10 lines. That said, it does work, and that's good enough for me for now. If anyone reading this would like to advise me on improvements I'm open to feedback. I'll probably try to clean it up a bit over time.

connecting

Before we can do anything, we need to connect (this fires when the document is ready):

In this method, we do three very important things:

  1. We capture the user name for the session (this is used for figuring out who we're chatting with and for coloring text in the chat window).
  2. We subscribe to a queue that's bound to our user, and call showMessage when we receive one.
  3. We subscribe to a topic that sends out all the active user names, and call showActive when we get an update.

There's something slightly peculiar here worth calling out: notice that we construct the SockJS instance with /chat as the URL, yet you may notice that the other JavaScript below sends messages to /app/chat. Spring seems to have some kind of handshake here that I don't fully understand. If you change the value in this constructor, it will fail saying that /app/chat/info doesn't exist, so Spring appears to expose the resource /chat/info here to facilitate a handshake with SockJS.

showing a message

Whenever we receive a message, we want to render it in the chat window for that user, which is done in the code below:

First, we figure out which chat window we should load based on comparing the recipient to our username. We then create a span with the new message, colored to indicate the sender, append it to the window, and scroll to the bottom of the window to show the latest message. If we're updating a chat window that's not currently visible to the user, we render an envelope character next to their name in the user list to indicate that there are pending messages. Below is the code to create the envelope:

getting the chat window

In the example above we obtain a chat window to update with message, but we have to create it if it doesn't already exist, as shown below:

We create a div to wrap everything, create a div for displaying message, and then create a textarea for typing in new messages. We automatically hide created chat windows if another one already exists, because otherwise we'd interrupt the user's current chat with another person. We create binds for two events: hitting 'enter' and clicking 'Submit'. In both cases, since the event references a DOM object that has a unique id referencing the user you want to send the message to, I'm capturing that and using it to route. JavaScript's variable scoping is something I often mix up in practice, so I'm relying on the DOM to tell me who to send the message to instead. The method for doing this is shown below:

We send the message to /app/chat, which routes to our MessageController class, which will then produce a message to the appropriate queues. After sending the message, we clear our the textarea for the next message.

showing active users

As I mentioned above, whenever the server tells us about the active users, the client reports back that it's active, allowing us to have a heartbeat. The first method in the code below sends the message back to the server, while the second renders the active users:

There's a lot going on here, so let me break it down:

  • First, capture who was previously selected.
  • Second, capture which users had pending messages. We'll want to re-render this with the new list.
  • Create a new div for the next user list.
  • As we process users, preserve the one that was previously selected by assigning the appropriate CSS class (.user-selected)
  • Bind a click event to each user that will hide the current chat window, removed the select class from all users and show them as unselected, remove the pending messages status, pull up the chat window for that user, and mark that user as selected.
  • If a user was added to the new list that previously had messages pending, redisplay the envelope icon.

There's really almost no HTML to this example; virtually everything is generated as messages come in. I opted for a lot of re-rendering of data to avoid an overabundance of conditional checking, though I don't know if this is good in practice or not: again, I'm not particularly good with JavaScript

try it out!

Like I mentioned, this is available on Github for you to play with. I know there's a lot covered in this blog post, and I'll probably update it several times to fill in gaps as a read it and/or get feedback. I hope this helps you get started using Spring and STOMP for doing some pretty sophisticated messaging between the browser and the server.

33 comments:

  1. Nice work.

    Thanks for sharing...

    ReplyDelete
  2. It's a wonderful article!! But, I just wonder how to implement external message broker(RabbitMQ) with this setup? I tried to use RabbitMQ by changing the broker registry in WebSocketConfig.java:
    // registry.enableSimpleBroker("/queue/", "/topic");
    registry.enableStompBrokerRelay("/queue/", "/topic").setRelayHost("localhost").setRelayPort(15672);
    But, with this I am getting the following exception:
    15:13:09.594 [MessageBrokerSockJS-1] ERROR o.s.s.s.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task.
    org.springframework.messaging.MessageDeliveryException: Message broker is not active.
    at org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler.handleMessageInternal(StompBrokerRelayMessageHandler.java:391) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler.handleMessage(AbstractBrokerMessageHandler.java:171) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.support.ExecutorSubscribableChannel.sendInternal(ExecutorSubscribableChannel.java:64) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:116) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:98) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.simp.SimpMessagingTemplate.doSend(SimpMessagingTemplate.java:125) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.simp.SimpMessagingTemplate.doSend(SimpMessagingTemplate.java:48) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:94) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.convertAndSend(AbstractMessageSendingTemplate.java:144) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.convertAndSend(AbstractMessageSendingTemplate.java:112) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.convertAndSend(AbstractMessageSendingTemplate.java:107) ~[spring-messaging-4.0.4.RELEASE.jar:4.0.4.RELEASE]
    at com.gchat.service.ActiveUserPinger.pingUsers(ActiveUserPinger.java:24) ~[ActiveUserPinger.class:na]

    Can you throw some light on this?

    ReplyDelete
    Replies
    1. Hi Gurminder,

      Thanks for the feedback!

      To be honest I really didn't get anywhere with wiring up an actual backing queue to the broker; everything I did was in the browser. I'd have to start messing around that a little bit to wrap my head around it. If I get some time to play with RabbitMQ I'll update you, but at the present I don't have a good answer for you. Sorry!

      --Ian

      Delete
    2. No problem! Actually I did it on my own. But honestly, it took quite an effort to do so. Anyways, thanks for this nice article and keep posting!!

      --Guru

      Delete
    3. Awesome, I'm glad to hear you got it working. I don't know if you have your own blog or not, but if you do and you post your solution let me know and I'd be happy to link to your post from mine :)

      Delete
    4. This comment has been removed by the author.

      Delete
    5. Sure, I have a blog and once I am done with the post I'll update you.

      --Guru

      Delete
    6. Hi Ian,
      I couldn't find the time to write a post on my Blog. But, in the hunt for resolving the issue, I posted a question on Stackoverflow and answered my own question. I think you might have a look into it. Here's the url:
      http://stackoverflow.com/questions/23557473/configure-external-brokerrabbitmq-in-spring4stompsockjs-application

      Delete
    7. Man, that's unfortunate that you don't have a layer of indirection to avoid exposing a username and password if you wanted to use an external broker as a simple broker in practice. There's something about putting a username and password in the view, even if it is guest/guest, that feels wrong to me.

      Delete
  3. Great Work! Keep sharing your posts.

    ReplyDelete
    Replies
    1. Thank you Sunny! I appreciate the compliment!

      Delete
  4. Ian- this post is incredible. I've been using Spring STOMP for a little over a day now and I'm looking forward to running your code locally. Thank you so much for sharing this.

    ReplyDelete
    Replies
    1. Hey, thanks Chris! Much appreciated. Let me know if you run into any issues!

      Delete
  5. Hi Ian - I've messed around with this app a few times and had some feedback:
    1. I'd mention that the end user should run: mvn spring-boot:run to start the app.
    2. They should open up 2 browsers and log in as different users in each to see the chats actually go through.
    3. Sendng a message to a specific user threw me for the biggest loop. I think would be helpful to state the Spring performs some destination resolution internally in determining who to send the message to.

    Per the javadoc on Spring's UserDestinationResolver, I learned:

    "For example when a user attempts to subscribe to "/user/queue/position-updates", the destination may be resolved to "/queue/position-updates-useri9oqdfzo" yielding a unique queue name that does not collide with any other user attempting to do the same. Subsequently when messages are sent to "/user/{username}/queue/position-updates", the destination is translated to "/queue/position-updates-useri9oqdfzo"."

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Your Spring Chat is great!
      I found only few issues with WebSocketConfig and SecurityConfig with new Spring Version. Here my modifications:

      @Configuration
      @EnableWebMvcSecurity
      @EnableGlobalMethodSecurity(prePostEnabled = true)
      public class SecurityConfig extends WebSecurityConfigurerAdapter {

      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
      auth.inMemoryAuthentication().withUser("mattia").password("1234").roles("USER");
      }

      }

      @Configuration
      @EnableWebSocketMessageBroker
      public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

      @Override
      public void configureMessageBroker(MessageBrokerRegistry config) {
      config.enableSimpleBroker("/queue", "/topic");
      config.setApplicationDestinationPrefixes("/app");
      }

      @Override
      public void registerStompEndpoints(StompEndpointRegistry registry) {
      registry.addEndpoint("/chat", "/activeUsers").withSockJS();
      }

      public void configureClientInboundChannel(
      ChannelRegistration channelRegistration) {
      }

      public void configureClientOutboundChannel(
      ChannelRegistration channelRegistration) {
      }

      @Override
      public boolean configureMessageConverters(List converters) {
      return true;
      }

      @Bean
      public ActiveUserService activeUserService() {
      return new ActiveUserService();
      }
      }

      Thank you men!

      Delete
  7. in latest version of spring you can replace

    public void greeting(Message Object message, @Payload ChatMessage chatMessage) throws Exception {
    Principal principal = message.getHeaders().get(SimpMessageHeaderAccessor.USER_HEADER, Principal.class);

    with

    public void greeting(Principal principal, @Payload ChatMessage chatMessage) throws Exception {

    ReplyDelete
  8. Fuck spring boot I hope not it will ends because its a piece of shit. For the pure spring solutions deployable and runnable by EAR OR WAR fuck spring boot forever!

    ReplyDelete
  9. Hi..
    I am trying to connect to App server from client server (both app and client are deployed in diff servers)
    I am getting a CORS issue even after adding registry.addEndpoint("/hello").setAllowedOrigins("*").withSockJS();

    ReplyDelete
  10. I'm struggling with understanding websockets. Specificially I don't understand why there must be a duplication between @MessageMapping("/chat") and register.endPoint("/chat"). I would expect to have one endpoint, e.g. "/mobile" and then extrapolate MessageMappings from that @MessageMapping("/mobile/chat"). Is there a reason for registering multiple endpoints? If you do have multiple endpoints registered, e.g. /chat and /activeUsers would you then have to connect separately within one app to both of them and would this create two connections? I would expect to just use one connection for let's say a mobile app for all purposes.

    I'm sorry if my questions are stupid. I find this topic confusing.

    ReplyDelete
  11. Great work.you explained very well.I have some doubts in designing.do you have a html code for it?I need it.

    ReplyDelete
  12. I get this error: http://localhost:8080/chat/info 404 (Not Found)

    ReplyDelete
  13. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  14. Hi,is it possible to connect an android application using Emiter in this websocket?

    ReplyDelete
  15. hi, great work you have done, but when i run this project it throws exception
    WARN 4696 --- [nio-8080-exec-7] o.s.c.s.ResourceBundleMessageSource: ResourceBundle [messages] not found for MessageSource: Can't find bundle for base name messages, locale en_US

    ReplyDelete
  16. I find you haven't replied for a long time,so , i not sure wheth you can reponse ,my question is rabbitmq create too much 'durable' queue when client resubscribe.

    ReplyDelete
  17. Any thoughts on the below stackoverflow link:
    https://stackoverflow.com/questions/54284576/simpmessagingtemplate-convertandsendtouser-lot-of-waiting-threads-blocking-other

    ReplyDelete
  18. nitrogen regulator with purge test settings exporter Digital gas flow detector (gas chromatograph) working principle for the micro bridge gas quality sensor element heat transmission principle, air or other gases flow through the sensor internal components, the heat taken away is equal to the flow through the gas (or quality), the final reflection in the output voltage of the sensor. The device is mainly used in gas chromatography system, used to do zuiJIA analysis parameter ratio (i.e. carrier gas flow) process, instead of the traditional soap foam flow meter, improve the analysis efficiency, instantly know the gas working parameters, easy to operate.

    ReplyDelete
  19. blood pressure watch professional in R&D wearable blood pressure monitor, in the innovative form of a wrist watch, proactively monitors your heart health by turning real-time heart data into heart.
    mason jar lids canning lids supplier. offer canning Lids in different style and size
    neck hammock We aim to create a healthy lifestyle that assists every individual to de-stress, relax. unwind and streamline a pain-free life and at the same time feel and look amazing.
    sex machine Choose Sex Machine to Get Orgasm and Enjoy Life
    moving dildo Enjoy sexual life for both men and women
    neon light for roomNeon Signs Light Is One-of-a-Kind Activities Designed and Hosted by Expert Locals. All Experiences are Vetted for Quality.
    dog training collarsPets Supplier mall offers the ultimate pet shop experience. We have all the pet supplies, pet food, toys and accessories you and your pet needs at great prices. Find all the best pets suppliers coupons, promotion.
    silk duragDiscover the latest and most interesting gadgets at a bargain price
    Automobile atmosphere lampHere you will find the latest and most fashionable car accessories

    ReplyDelete
  20. https://www.visualaidscentre.com/lasik-surgery-in-noida/ What are the risks involved with LASIK surgery. Not everyone experiences improved vision after LASIK. Find out what could go wrong!

    ReplyDelete