Spring Server-Sent Events

You are currently viewing Spring Server-Sent Events
Spring websockets with STOMP client

Spring provides Server-sent events apis ResponseBodyEmitter, SseEmitter and WebSockets for server to client communication and vice-versa. Server-sent events are used to handle asynchronous processing on the server side. Websockets connections allow both server as well as clients to send & receive messages over TCP layer. In this tutorial, we will create a Spring boot application to implement Server-Sent events and websockets for server client communication.

Server-Sent event is an HTTP standard, which enables a web application client to handle one-way directional event stream and receive events as updates whenever the server emits new events. WebSocket is a two-way communication connection between server and clients, enabling both to transmit & receive messages over TCP layer.

Server Push Messages

Server push is a mechanism to send messages from server to client. When a client connection is established with the server, then the server can send messages to the client. Server. There are many ways a server can communicate with clients, but following are the most popular communication standards.

  • Server-Sent Events
  • WebSockets

Server-Sent Events

A server sends some information or updates to the client browser, then this is known as an event and termed as server-sent events. A Server sent event occurs when a server and client connection is established, then server sends asynchronous events and browser reads the updates automatically.

The server-sent events are unidirectional events, as only the server can send events to the client, but the client can’t send back any message and only can receive the updates.

How Server-Sent Events work

Server-Sent events works in following way:

  1. Client requests a HTTP connection establishment request to the server over a particular url. If the server allows, connection gets established. 
  2. Url is basically a topic address and a topic is a category of messages. Server sends different types of events under different topics like news feed, funny quotes etc.
  3. When the server has some updates, the server pushes the messages as events under a topic.
  4. All the clients which have open connection upon that particular topic, receive the updates.
  5. Server can close the connection at any time, it can due to no more events possible, network issues etc.
  6. If a connection is closed due to server or network error, the client tries automatically to re-establish the connection.

Server-Sent events in Spring

In spring, we have Webflux streaming apis and event emitter apis. Since we are focusing on server-sent events, we are keeping streaming apis out of discussion. But if you want to read details about streaming data in spring, then you can refer to the guide here for streaming different types of data in spring.

In Spring we have sent server-events with two emitter class types. Infact one is the parent class and the other is a specialized case of the first one. Following are the two types:

  • ResponseBodyEmitter
  • SseEmitter

Gradle Dependency

The emitter class types are present in Spring 5, to use these in the Spring boot project. Add following dependency into you build.gradle file:

implementation 'org.springframework.boot:spring-boot-starter-web'

In the upcoming examples, You will see some additional dependencies. Emitter class types don’t depend upon these. To demonstrate the example as a complete web application, we have used Thymeleaf, webjars locator, bootstrap, jquery to create clients in JQuery & JS. Lombok is used to reduce the boilerplate code here, in your IDE you need to enable Annotation processing, in order to allow Lombok annotations to work at compile time.

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.11'
compile group: 'org.webjars', name: 'webjars-locator', version: '0.40'
compile group: 'org.webjars', name: 'bootstrap', version: '4.5.2'
compile group: 'org.webjars', name: 'jquery', version: '3.5.1'
compile 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

ResponseBodyEmitter

The ResponseBodyEmitter is a return type which is used to write one or more objects to the response for an asynchronous request processing. Earlier DeferredResult was mostly used for such purposes, but the limitation to produce a single result, was holding us back. A ResponseBodyEmitter return type can send multiple objects and each object can be written with a compatible HttpMessageConverter

ResponseBodyEmitter works very well with ResponseEntity that means if you wrap ResponseBodyEmitter within ResponseEntity then you can leverage all the features of ResponseEntity as well. In Simple words, you can say ResponseBodyEmitter is a kind of data holder to be written as response along with MediaType details to select a particular message converted.

ResponseBodyEmitter Example

Lets create a ResponseBodyEmitter example using a spring boot application. 

  1. Create a new project using Spring boot initialization. I prefer to choose a combination of Java 11 with Gradle.
  2. Create a rest controller and a rest endpoint which has return type as ResponseEntity<ResponseBodyEmitter>.
@RestController
@RequestMapping("/rbe")
@AllArgsConstructor
public class RbeContoller {
  private final RbeService rbeService;

  @GetMapping(value = "/receive")
  public ResponseEntity<ResponseBodyEmitter> getEmitter(){
     ResponseBodyEmitter emitter = rbeService.events();
     return new ResponseEntity(emitter, HttpStatus.OK);
  }
}
  1. We have one service class as RbeService, which holds the logic for ResponseBodyEmitter processing.
  2. To simulate the events after some type, I have added a sleep of 1 second. Which means that after every 1 second a new event will be generated.
public ResponseBodyEmitter events() {
  ResponseBodyEmitter emitter = new ResponseBodyEmitter();
     try {
        String message = "Only text message can be sent";
        for(int i=0; i<5; i++) {
           emitter.send(message + " "+ (i+1) , MediaType.TEXT_PLAIN);
           Thread.sleep(1000);
        }
        emitter.complete();
     } catch (Exception ex) {
        emitter.completeWithError(ex);
     }
  return emitter;
}
  1. Run the application and enter following url in browser or from curl command
http://127.0.0.1:8080/rbe/receive
  1. You will see that sample output of  5 server events are returned as API response.

SseEmitter

SseEmitter is a specialized form of ResponseBodyEmitter. ResponseBodyEmitter is mainly delivering the plain messages as text to client, whereas SseEmitter can deliver an object from server to client. SseEmitter can send events to clients asynchronously and can keep client connections open, until the server tells to close the connection. 

SseEmitter provides a lot of such additional benefits due to which for server-sent events, SseEmitter is the first choice to be used in Spring applications.

SseEmitter with multiple clients

For every client connection, you need to create a dedicated new object of SseEmitter. So, the questions arise, when we have multiple clients, then how to send the events to all clients.

  • The solution is very simple, keep a track of all client connections by having a list of all SseEmitter objects. 
  • For every client, when a new connection is established. You need to return a new object of SseEmitter type. Before returning from the method, add its reference to a list for later use, Otherwise you will not be able to track this newly created SseEmitter object.
  • When the server has an update to send as an event, then process the list and send the event to every emitter object for every client. 
  • If any SseEmitter object fails to send an event, that means the client connection is closed. So remove that SseEmitter object from the list.

SseEmitter Example

In this example, we have created a scheduled task to send one new event after every 5 seconds. If you are interested in going deeper into details then you refer to the guide of Spring schedule tasks.

  1. Create a service class which will hold a list of SseEmitter objects. I am using here CopyOnWriteArrayList type list, as it will help in reducing the code for clone operations. You can even use any type of list, it’s not necessary to have type only.
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
  1. Create a method to register new clients. This method will basically create a new object of SseEmitter type, will add to emitters list and setup the timeout and completion callbacks to complete & remove the emitter from master list emitters.
  public SseEmitter registerClient(){
     SseEmitter emitter = new SseEmitter();

     // Add client specific new emitter in global list
    emitters.add(emitter);

     // On Client connection completion, unregister client specific emitter
     emitter.onCompletion(() -> this.emitters.remove(emitter));

     // On Client connection timeout, unregister and mark complete client specific emitter
     emitter.onTimeout(() -> {
        emitter.complete();
        emitters.remove(emitter);
     });
     return emitter;
  }
  1. Create a POJO class, which can be sent to clients as an event. In this example, we will call this class a Notification class.
@Data
@Builder
public class Notification {

  private String user;
  private String message;
  @Builder.Default
  private LocalDateTime localDateTime = LocalDateTime.now();
}
  1. Create a method to construct the object to be sent as an event to the client. Let’s name this method as a process which will take the raw parameters (any parameters based upon your logic) and return an object of Notification class to be used in events. 
public void process(String message, String user) throws IOException {
  Notification notification = Notification.builder()
        .user(StringUtils.isBlank(user)?"Guest":user)
        .message(message)
        .build();

  sendEventToClients(notification);
}
  1. Now the next step is to send the events to clients using a list of emitters. In this method, if any emitter throws an exception, that represents that client connection is closed. Keep a track of failed emitters and after sending event to all emitters, remove the failed form global list
public void sendEventToClients(Notification notification){
  // Track which events could not be sent
  List<SseEmitter> deadEmitters = new ArrayList<>();
  // Send to all registered clients
  emitters.forEach(emitter -> {
     try {
        emitter.send(notification);
     } catch (Exception e) {
        // record failed ones
        deadEmitters.add(emitter);
     }
  });
  // remove the failed one, otherwise it will keep on waiting for client connection
  emitters.remove(deadEmitters);
}
  1. Overall your service class will look like this:
@Service
@Data
public class SseService {

  // Save all emitters from different connections, to send message to all
  private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();

  public SseEmitter registerClient() {
     SseEmitter emitter = new SseEmitter();

     // Add client specific new emitter in global list
     emitters.add(emitter);

     // On Client connection completion, unregister client specific emitter
     emitter.onCompletion(() -> emitters.remove(emitter));

     // On Client connection timeout, unregister and mark complete client specific emitter
     emitter.onTimeout(() -> {
        emitter.complete();
        emitters.remove(emitter);
     });
     return emitter;
  }

  public void process(String message, String user) throws IOException {
     Notification notification = Notification.builder()
           .user(StringUtils.isBlank(user) ? "Guest" : user)
           .message(message)
           .build();

     sendEventToClients(notification);
  }

  public void sendEventToClients(Notification notification) {
     // Track which events could not be sent
     List<SseEmitter> deadEmitters = new ArrayList<>();
     // Send to all registered clients
     emitters.forEach(emitter -> {
        try {
           emitter.send(notification);
        } catch (Exception e) {
           // record failed ones
           deadEmitters.add(emitter);
        }
     });
     // remove the failed one, otherwise it will keep on waiting for client connection
     emitters.remove(deadEmitters);
  }
}
  1. Now let’s create a scheduled task to generate & send events every 5 seconds. To enable this scheduled task, you need to add @EnableScheduling annotation in configuration class or main application class. We have already discussed the functionality of our process method.
@Scheduled(cron = "*/5 * * ? * *")
public void runJob() throws IOException {
  sseService.process("Scheduled job run", "Job");
}
  1. Let’s create one Rest endpoint as well to send a message to the server, which internally will be sent as an event to all emitters.
@GetMapping("/message")
public @ResponseBody void sendMessages(@RequestParam String message,
             @RequestParam(required = false) String user) throws IOException {
  sseService.process(message, user);
}
  1. Server side event generation logic and processing part is complete. We need to create a browser client to receive our events.
  2. Create an endpoint, from which emitter connection can be established. This method will return the emitter object to the client.
@GetMapping("/receive")
public @ResponseBody
SseEmitter getEmitter() {
  return sseService.registerClient();
}
  1. Create a webpage which will act as a client and will create a connection to the above endpoint. 
  2. You can even use a standalone html file alos. But I am using the thymeleaf html file to show this example. You can see in the below code snippet, we are creating an EventSource with relative url “/sse/receive”. On receiving the messages, we are parsing it as a JSON object and then accessing its child properties as an object within Javascript. After receiving the message, we are appending it as html text to the webpage, so it’s clearly visible. Name of following file will be SseClient.html
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:th="http://thymeleaf.org"
     xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{template}">
<body>
   <script src = "/webjars/jquery/3.5.1/jquery.min.js"></script>

   <div layout:fragment="content">
       <h3> SSE events - Client</h3>
       <div id="events"></div>
   </div>
   <script type="application/javascript">
       var subscribeEvents = function() {
           var key = $("#key").val();
           var eventSource = new EventSource('/sse/receive');

           eventSource.onmessage = function(e) {
               var notification = JSON.parse(e.data);
               if(key == notification.key){
                   var d = new Date(notification.localDateTime);
                   var html  = "<span><b>New event</b> on <i>" + d.toLocaleDateString()+" " + d.toLocaleTimeString() + "</i>: "
                       + "["+notification.user+"'s Action] "+ notification.message + "</span>";
                   document.getElementById("events").innerHTML = html + "<br/>" + document.getElementById("events").innerHTML;
               }
           };
       }
       window.onload = subscribeEvents;
       window.onbeforeunload = function() {
           eventSource.close();
       }
   </script>
</body>
</html>
  1. Create one endpoint to return this thymeleaf view.
@GetMapping("/client")
public String getCient() {
  return "sseClient";
}
  1. Everything is set up now. Run the application and add following url to web browser:
http://127.0.0.1:8080/sse/receive
  1. You will see the output on the web page after every 5 seconds. Try to use our custom message endpoint in another tab also. You will see it also appears as an event on a web-page.
http://127.0.0.1:8080/sse/message?message=sample%20Messages%20here
SseEmitter output
SseEmitter output

WebSocket

WebSocket is a thin & lightweight layer above TCP, which makes it suitable & flexible for using “subprotocols” to embed any type of messages. Websockets connections are basically full-duplex, bi-directional and persistent connections between a server and a web browser. Once a WebSocket connection is established, the connection remains open until the server or client terminates the connection.

In our example, we will create an interactive web application using STOMP messaging. STOMP is a Simple Text Orientated Messaging Protocol or can be called a streaming messaging protocol, which is basically a sub-protocol operating on top of the WebSocket’s lower level.

Spring 5 WebSockets can be used to send global notification to all users as well as to individual users. So we will discuss two different examples for Spring WebSockets

  • Spring WebSockets example
  • Spring WebSockets send to single user example

Gradle dependencies

Spring WebSockets require few dependencies. STOMP and SOCKJS are used to create clients for Spring WebSockets. Add following dependencies into your build.gradle file:

implementation 'org.springframework.boot:spring-boot-starter-websocket'
compile group: 'org.webjars', name: 'sockjs-client', version: '1.1.2'
compile group: 'org.webjars', name: 'stomp-websocket', version: '2.3.3-1'

Spring WebSockets example

In this example, we will create a web client where any user can send a message to the server and it will automatically get published to global topic. All the clients connected for that topic will receive the message and will show the message on webpage.

  1. Create an endpoint, and configure it to send messages automatically to the main topic. In our example, our clients will listen on the topic “/topic/news”. So either you can publish the message using code or annotation like following:
@SendTo("/topic/news")
  1. We need to receive the messages from clients. We need to create a mapping on which client can send messages to server. Any connected STOMP client can send a message to “/news” endpoint and the server can receive it, as shown below
@MessageMapping("/news")
  1. We can combine both annotations as well. This will mean that any message sent from a client will get published to a global topic. All the clients, connected over this topic will receive the messages.
@MessageMapping("/news")
@SendTo("/topic/news")
public @ResponseBody String broadcastNews(@Payload String message) {
  return message;
}
  1. We need a configuration class(let’s call it WebSocketConfig) for configuring Spring WebSockets via implementing WebSocketMessageBrokerConfigurer interface. This class will create STOMP endpoints automatically to publish topics as endpoints to be used in web client.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
   registry.addEndpoint("/technicalsand.com-websockets").withSockJS();
 }

 @Override
 public void configureMessageBroker(MessageBrokerRegistry config){
   config.enableSimpleBroker("/topic/", "/queue/");
   config.setApplicationDestinationPrefixes("/app");
 }
}
  1. Lets create a thymeleaf html file (websocketClient.html) as following:
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:th="http://thymeleaf.org"
     xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{template}">
<body>
   <div layout:fragment="content">
       <script src = "/webjars/jquery/3.5.1/jquery.min.js"></script>
       <script src = "/webjars/bootstrap/4.5.2/js/bootstrap.min.js"></script>
       <script src = "/webjars/sockjs-client/sockjs.min.js"></script>
       <script src = "/webjars/stomp-websocket/stomp.min.js"></script>
       <link href="/webjars/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">

       <h3> Websocket- Broadcast Message Example by <a href="https://technicalsand.com">Technicalsand</a> </h3>
       <br/>
       <div id="events"></div>
       <div id = "main-content" class = "container">
           <form>
               <div class="form-row">
                   <button id="connect" class="btn btn-success mb-2 " type="submit">Connect</button>
                   <button id="disconnect" class="btn btn-danger mx-sm-3 mb-2" type="submit" disabled="disabled">Disconnect</button>
               </div>
           </form>
           <form>
               <div class="form-row">
                   <label class="my-1 mr-2" for="name">Your Message:</label>
                   <div class="col-8">
                       <input type="text" class="form-control" id="name" placeholder="Enter message">
                   </div>
                   <button id = "send" class = "btn btn-primary" type = "submit" disabled>Send</button>
               </div>
           </form>
           <br/>
           <div class="row">
               <div class="col-md-12">
                   <table class="table table-bordered table-striped">
                       <thead class="thead-dark">
                       <tr>
                           <th>Messages</th>
                       </tr>
                       </thead>
                       <tbody id="messages"></tbody>
                   </table>
               </div>
           </div>
       </div>
   </div>
   <script type="application/javascript">
       var stompClient = null;

       function setConnected(connected) {
           $("#connect").prop("disabled", connected);
           $("#disconnect").prop("disabled", !connected);
           $("#send").prop("disabled", !connected);

           $("#messages").html("");
       }

       function connect() {
           var socket = new SockJS('/technicalsand.com-websockets');
           stompClient = Stomp.over(socket);
           stompClient.connect({}, function (frame) {
               setConnected(true);
               console.log('Connected: ' + frame);
               stompClient.subscribe('/topic/news', function (response) {
                   showGreeting(JSON.parse(response.body).content);
               });
           });
       }
       function disconnect() {
           if (stompClient !== null) {
               stompClient.disconnect();
           }
           setConnected(false);
           console.log("Disconnected");
       }
       function sendName() {
           stompClient.send("/app/news", {}, JSON.stringify({'content': $("#name").val()}));
       }
       function showGreeting(message) {
           $("#messages").append("<tr><td>" + message + "</td></tr>");
       }
       $(document).ready(function() {
           $( "form" ).on('submit', function (e) {e.preventDefault();});
           $( "#connect" ).click(function() { connect(); });
           $( "#disconnect" ).click(function() { disconnect(); });
           $( "#send" ).click(function() { sendName(); });
       });
   </script>

</body>
</html>
  1. I am skipping the explanation of HTML & JavaScript here, as it’s self-explanatory. In controller, lets create an endpoint to return this html as view.
@GetMapping("websocket/client")
public String getClient(){
  return "websocketClient";
}
  1. Now run the application and enter the following url in the browser. I recommend opening this url into multiple browser windows, so each browser window can act as a different client.
http://127.0.0.1:8080/websocket/client
  1. Click on the connect button in all browser windows. If the application is up and running, it will be connected. Without clicking the connect button, you will not receive any updates, as connection is yet to be established with the server on click of this button.
  2. Now from one of these opened windows, try to send some text message. You will see every client has received the message and shown it on the web page it-self. 
Websockets broadcast
Websockets broadcast

Steps for Spring WebSockets to send messages to single user:

Spring WebSockets can be used to send a message to specific users. To implement this, we need to follow a process to maintain connected clients connections. So that every user can be identified individually and messages can be sent to a particular user.

  • Maintain a global map of the user’s session id with username or some unique identifier. We can use ConcurrentMap to store username and session id, which will also help in concurrent modifications.
  • Listen to Spring’s SessionConnectEvent via implementing interface ApplicationListener<SessionConnectEvent> and for every new connection extract username and session id. Add this record on the map to use it later.
  • Listen to Spring’s SessionDisconnectEvent via implementing interface ApplicationListener<SessionDisconnectEvent> and remove user entry from global map, So we know which user is online and which user is offline.
  • In configuration class which implements WebSocketMessageBrokerConfigurer interface, configure the user destination prefix to identify user destinations. User destinations provide the ability for a user to subscribe to queue names unique to their session as well as for others to send messages to those user-specific unique queues.
  • Use SimpMessagingTemplate‘S method convertAndSendToUser to send the message to specific user using session id.

Spring WebSockets send to single user example

Following are the steps for a detailed example to create a  chat application in the Spring boot application. In this application, a user can see other online users and can send messages to any online user.

  1. Create a service class to hold global map userSessionsMap, which will hold the information for all online users. Username will be the key and session id will be the value. This class will hold a few methods to register new user, remove disconnected users. In the code snippet, comments are added which explains the purpose of each method, so skipping these details.
@Service
@NoArgsConstructor
public class SessionsStorage {

  //Save user sessions in a map as username, sessionid
  private final ConcurrentMap<String, String> userSessionsMap = new ConcurrentHashMap();
  private final Random random = new Random();

  // get session id from username
  public String getSessionId(String user) {
     return userSessionsMap.get(user);
  }

  // get details of all online users, whoever is connected has entry in map
  public ConcurrentMap<String, String> getAllSessionIds() {
     return userSessionsMap;
  }

  // Add new user entry with username and session id
  public void registerSessionId(String user, String sessionId) {
     Assert.notNull(user, "User must not be null");
     Assert.notNull(sessionId, "Session ID must not be null");
     userSessionsMap.put(user, sessionId);
  }

  // Once client connection is disconnected, we receive session id which is disconnected
  // find the entry in map from value set and remove it
  public void unregisterSessionId(String sessionId) {
     Assert.notNull(sessionId, "Session ID must not be null");
     userSessionsMap.entrySet().removeIf(entry -> StringUtils.equals(sessionId, entry.getValue()));
  }
 
  // One utility method to suggest random username
  public String getRandomUserName() {
     String userName = "user-" + random.nextInt(100);
     if (!userSessionsMap.containsKey(userName)) {
        return userName;
     }
     return getRandomUserName();
  }
}
  1. Create a listener for a new connection using SessionConnectEvent as shown in the below code snippet. From the client, we will send the username as “login” property which we are extracting here from headers. The class StompHeaderAccessor provides a method getSessionId() to extract the session id for any new connection established.
@Slf4j
@Service
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {
   @Autowired
   private SessionsStorage sessionsStorage;

   @Override
   public void onApplicationEvent(SessionConnectEvent event) {
       StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
       //login get from browser
       String agentId = sha.getNativeHeader("login").get(0);
       String sessionId = sha.getSessionId();
       log.info("agentId: "+ agentId + ", sessionId: " + sessionId);
       sessionsStorage.registerSessionId(agentId,sessionId);
   }
}
  1. Create a listener for a new connection using SessionDisconnectEvent as shown in the below code snippet. When a connection is disconnected, we get session id value, which is just disconnected. We need to remove this session id from the global session map, so we know which user is connected at any time and can receive messages.
@Slf4j
@Component
public class STOMPDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent> {
   @Autowired
   private SessionsStorage sessionsStorage;

   @Override
   @EventListener
   public void onApplicationEvent(SessionDisconnectEvent event) {
       StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
       //disconnect by sessionid
       String sessionId = sha.getSessionId();
       log.info("Disconnect sessionId: " + sessionId);
       sessionsStorage.unregisterSessionId(sessionId);
   }
}
  1. Enable user destination prefix for message broker registry to identify every user uniquely.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
   registry.addEndpoint("/technicalsand.com-websockets").withSockJS();
 }

 @Override
 public void configureMessageBroker(MessageBrokerRegistry config){
   config.enableSimpleBroker("/topic/", "/queue/");
   config.setApplicationDestinationPrefixes("/app");   
   config.setUserDestinationPrefix("/user");    
 }
}
  1. Create a POJO class, which will hold the information of the message, like name of sender, name of receiver, actual message, date & time of message. Let’s call this bean a Message class.
@Data
public class Message {

  private String content;
  private String toUser;
  private String fromUser;
  private LocalDateTime localDateTime;
}
  1. Now create a Rest endpoint where a web client can send a message and that message further can be passed to the client.
@MessageMapping("/websocket/message")
public void message(Message message) {
  String sessionId=sessionsStorage.getSessionId(message.getToUser());
  message.setLocalDateTime(LocalDateTime.now());
  SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
  headerAccessor.setSessionId(sessionId);
  headerAccessor.setLeaveMutable(true);
  template.convertAndSendToUser(sessionId,"/topic/message", message ,headerAccessor.getMessageHeaders());
}
  1. Create one endpoint to return all online users, basically all connected users are online users. Because we remove disconnected users automatically.
@GetMapping("websocket/users")
public @ResponseBody ResponseEntity<SortedSet<String>> getOnlineUsers(){
  SortedSet<String> users = new TreeSet(sessionsStorage.getAllSessionIds().keySet());
  return ResponseEntity.ok()
        .body(users);
}
  1. Create a Thymeleaf html file, most of the file will remain, as discussed in the previous example. We will discuss the new areas added to the html file. With the following JavaScript code, we will connect to the server via STOMP client.
function connect() {
   //Pass the user key value
   var login = $("#login").val();
   var socket = new SockJS('/technicalsand.com-websockets');
   stompClient = Stomp.over(socket);
   stompClient.connect({login:login}, function (frame) {
       setConnected(true);
       console.log('Connected: ' + frame);
       stompClient.subscribe('/user/topic/message', function (response) {
           showMessage(JSON.parse(response.body));
       });
   });
}
  1. Following code is responsible for sending the message to a selected user via STOMP client.
stompClient.send("/app/websocket/message", {},
   JSON.stringify({'toUser': $("#toUser").val(),
       'content': $("#content").val(),
       'fromUser': $("#login").val()
   }));
  1. Other sections of HTML file are for beautification purpose and full chat application experience. In the source code, you can refer to the full HTML file, if you want to create a chat application. 
  2. Name this HTML file and create one endpoint in the controller to return this as view.
@GetMapping("websocket/user")
public String getClient(ModelMap map){
  map.addAttribute("defaultUser", sessionsStorage.getRandomUserName());
  return "websocketClientSpecificUser";
}
  1. Now everything is set up. Run the application and enter the following url in multiple browser windows to simulate multiple online users.
http://127.0.0.1:8080/websocket/user
  1. A default random username will appear automatically, if you want you can change this name. This username will appear on other windows as an online user.
  1. Click on the connect button in each browser window. As soon as you click the connect button. You will see that the online users list will start showing all online users. If you click the disconnect button, that user will be automatically removed from the online user list.
  2. Along with names of all online users, there is a button for messages. Select the user to whom you want to send the message and click this button. On the extreme, right section, this user’s name will be automatically selected. Now enter the message and click the send button.
  3. The selected user’s window will show that message immediately. That user can reply to you in the same manner. 
Websockets single user message
WebSockets single user message
  1. You can extend this example in a number of ways. Like for each user, you can create a popup chat window. You can allow a user to do multiple logins with same name by replacing global map’s session id value with list of session id (all session ids for single username)

Source code for examples

You can download the source code for all this example from github repository.

Conclusion

I hope these examples will help you in understanding the server events in a better way. If you have any feedback or concern, please feel free to connect.