I did a simple web socket communication with spring 4, STOMP and sock.js, following this https://spring.io/guides/gs/messaging-stomp-websocket/
Now I want to upgrade it to simple chat. My problem is that when user subscribes to new chat room, he should get past messages. I don't know how to capture the moment when he subscribed to send him the list of the messages.
I tried using #MessageMapping annotation, but didn't reach any success:
#Controller
public class WebSocketController {
#Autowired
private SimpMessagingTemplate messagingTemplate;
#MessageMapping("/chat/{chatId}")
public void chat(ChatMessage message, #DestinationVariable String chatId) {
messagingTemplate.convertAndSend("/chat/" + chatId, new ChatMessage("message: " + message.getText()));
}
#SubscribeMapping("/chat")
public void chatInit() {
System.out.println("worked");
int chatId = 1; //for example
messagingTemplate.convertAndSend("/chat/" + chatId, new ChatMessage("connected"));
}
}
Then I created that:
#Controller
public class ApplicationEventObserverController implements ApplicationListener<ApplicationEvent> {
#Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println(applicationEvent);
}
}
It works, but captures all possible events, I don't think it is a good practice.
So, my question can be rephrased: how to send initial data when user subscried to sth?
You can return anything directly to a client when it subscribes to a destination using a #SubscribeMapping handler method. The returned object won't go to the broker but will be sent directly to the client:
#SubscribeMapping("/chat")
public Collection<ChatMessage> chatInit() {
...
return messages;
}
On the client side:
socket.subscribe("/app/chat", function(message) {
...
});
Check out the chat example on GitHub, which shows this exact scenario.
Related
I am building an app like slack.I've lots of clients running on both web and mobile.They connect to the websocket over Stomp.I want to detect which user is online and offline in realtime.Websocket server is running on spring framework.
spring accepts the requests as below.I set heartbeat incoming and outgoing values as 20000 ms.
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private List<StompPrincipal> onlineUsers = new ArrayList<>();
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.setHandshakeHandler(new CustomHandshakeHandler())
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/ws");
config.enableSimpleBroker("/ws")
.setTaskScheduler(new DefaultManagedTaskScheduler())
.setHeartbeatValue(new long[]{20000,20000});
}
}
To determine which user requests to Websocket, I added a handshake handler as below.
class CustomHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
String username=(String)attributes.get("name");
StompPrincipal user = new StompPrincipal(username)
onlineUsers.add(user);
return user;
}
}
class StompPrincipal implements Principal {
String name;
StompPrincipal(String name) {
this.name = name;
}
#Override
public String getName() {
return name;
}
}
As the code above, I can see user is added the onlineUser, after he request websocket server.
But the problem is, I can not determine if the user is offline after being online.Can you please suggest me to determine this.
Also determining online and offline users by this way is best practice? if not, waiting for your suggestions.many thanks for now.
After more search, I found a solution described here.I implemented ApplicationListener<SessionDisconnectEvent> and override the method below.
#Override
public void onApplicationEvent(SessionDisconnectEvent event) {
}
By this way, I can catch the which user is disconnected from websocket connection.
I am still waiting for your best practise suggestions..
You can also use org.springframework.messaging.simp.user.SimpUserRegistry;
inject it on your controller or service
#Autowired
SimpUserRegistry simpUserRegistry;
then create your logic in method in which you can invoke simpUserRegistry.findSubscriptions(matcher) like so:
public Set<SimpSubscription> onlineUsers(String chatroomId) {
if(null == chatroomId || StringUtils.isEmpty(chatroomId)) {
return findSubscriptions("");
}
Set<SimpSubscription> subscriptions = new HashSet<>();
List<String> list = chatRoomService.listUserIdsInChatRoom(chatroomId);
for (String userId: list) {
subscriptions.addAll(findSubscriptions(userId));
}
return subscriptions;
}
public Set<SimpSubscription> findSubscriptions(String userId) {
return simpUserRegistry.findSubscriptions(new SimpSubscriptionMatcher() {
#Override
public boolean match(SimpSubscription subscription) {
return StringUtils.isEmpty(userId) ? true : subscription.getDestination().contains(userId);
}
});
}
What this actually does, it gets all online users and match them with your specific userIds and if the chatroomId is empty, it will get you all the online users on all chatrooms.
Use the session Id to detect user
SocketJS
this.client = over(new SockJS(environment.SOCKET+'/end'));
this.client.connect({userId:'UserIdHere'}, () => {
});
Spring Boot
#Component
public class WebSocketEventListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);
#Autowired
private SimpMessageSendingOperations messagingTemplate;
#EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
StompHeaderAccessor stompAccessor = StompHeaderAccessor.wrap(event.getMessage());
#SuppressWarnings("rawtypes")
GenericMessage connectHeader = (GenericMessage) stompAccessor
.getHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER); // FIXME find a way to pass the username
// to the server
#SuppressWarnings("unchecked")
Map<String, List<String>> nativeHeaders = (Map<String, List<String>>) connectHeader.getHeaders()
.get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
String login = nativeHeaders.get("userId").get(0);
String sessionId = stompAccessor.getSessionId();
logger.info("Chat connection by user <{}> with sessionId <{}>", login, sessionId);
}
#EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor stompAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = stompAccessor.getSessionId();
logger.info("Chat connection by user <{}> with sessionId <{}>", "Nop", sessionId);
}
}
This is described in the spec under section
20.4.12 Listening To ApplicationContext Events and Intercepting Messages
https://docs.spring.io/spring-framework/docs/4.1.0.RC2/spring-framework-reference/html/websocket.html#websocket-stomp-appplication-context-events
SessionConnectedEvent — published shortly after a SessionConnectEvent when the broker has sent a STOMP CONNECTED frame in response to the CONNECT. At this point the STOMP session can be considered fully established.
SessionDisconnectEvent — published when a STOMP session ends. The DISCONNECT may have been sent from the client or it may also be automatically generated when the WebSocket session is closed. In some cases this event may be published more than once per session. Components should be idempotent to multiple disconnect events.
You can store online users in a cache if you need it, and stream the online status of relevant users over the websocket to the client as they come and go.
I am creating a task management application in Spring boot. Following are my models:
public class Task {
private String name;
private String description;
private User assignee;
//getters and setters
}
public class User {
private String name;
private String email;
private String password;
//getters and setters
}
I'm using spring security for the user. Now say there are three Users A, B and C. A creates a Task and assigns it to B. I am trying to send a notification only to B at this point using websocket. For this I created a WebSocketConfiguration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
The controller to assign this task:
#RestController
#RequestMapping("/api/task")
public class TaskController {
#PostMapping("/assign")
public void assign(#RequestBody Task task) {
taskService.assign(task);
}
}
And finally in the service, I have:
#Service
public class TaskService {
#Autowired
private SimpMessagingTemplate template;
#Override
public void assign(Task task) {
//logic to assign task
template.convertAndSendToUser(task.getAssignee().getEmail(), "/topic/notification",
"A task has been assigned to you");
}
}
In the client side, I'm using Angular and the subscribe portion looks like the following:
stompClient.subscribe('/topic/notification'+logged_user_email, notifications => {
console.log(notifications);
})
Currently, nothing is printed in the console for any of the users.
I followed this tutorial, which worked perfectly for broadcast message.
I've also used this answer as a reference for using the logged_user_email, but it doesn't work.
I've tried prefixing /user to subscribe to /user/topic/notification/ in client side as explained in this answer. I've also tried using queue instead of topic as explained in the same answer, but I have found no success.
Other answers I've found mention the use of #MessageMapping in the controller but I need to be able to send the notification from the service.
So the question is how do I distinguish which user the notification is intended for and how do I define this in both the server and client sides?
This may be a bit late but for anyone who's having the same problem, I made it working by doing the following:
Instead of sending the email of the user, I converted it to Base64 and sent.
#Service
public class TaskService {
#Autowired
private SimpMessagingTemplate template;
#Override
public void assign(Task task) {
//logic to assign task
String base64EncodedEmail = Base64.getEncoder().encodeToString(task.getAssignee().getEmail().getBytes(StandardCharsets.UTF_8));
template.convertAndSendToUser(base64EncodedEmail, "/topic/notification",
"A task has been assigned to you");
}
}
Then in the client side, in the WebSocketService of this tutorial, I replaced the connect method with the following:
public connect() {
const encodedEmail = window.btoa(email);
const socket = new SockJs(`http://localhost:8080/socket?token=${encodedEmail}`);
const stompClient = Stomp.over(socket);
return stompClient;
}
In the subscribe section, I did the following:
stompClient.subscribe("/user/topic/notification", notification => {
//logic to display notification
});
Everything else is same as in the tutotial.
I tried to create an API that send request via socket.
Code:
#Controller
public class GreetingController {
#GetMapping("/x")
public void send() {
greeting(new HelloMessage("Admin", "bla"));
}
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) {
return new Greeting(HtmlUtils.htmlEscape(message.getName() + ": " + message.getMsg()));
}
}
I don't understand how to send request via rest to websocket.
Can anyone explain why when I send a request to /x the websocket does not get new HelloMessage?
When you call another method from the same class, you just do that: call a method. This method call doesn't care whether the called method has annotations.
The greeting() method only sends a message when it's called from a websocket client posting a message to /hello.
To send a message programmatically, you use the SimpMessageTemplate, as documented:
What if you want to send messages to connected clients from any part of the application? Any application component can send messages to the brokerChannel. The easiest way to do so is to inject a SimpMessagingTemplate and use it to send messages. Typically, you would inject it by type, as the following example shows:
#Controller
public class GreetingController {
private SimpMessagingTemplate template;
#Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
#RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
In the Spring WebSocket docs I found this sentence:
It is important to know that a server cannot send unsolicited messages.
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html
(25.4.1)
However I tried this code:
#Controller
public class WebsocketTest {
#Autowired
public SimpMessageSendingOperations messagingTemplate;
#PostConstruct
public void init(){
ScheduledExecutorService statusTimerExecutor=Executors.newSingleThreadScheduledExecutor();
statusTimerExecutor.scheduleAtFixedRate(new Runnable() {
#Override
public void run() {
messagingTemplate.convertAndSend("/topic/greetings", new Object());
}
}, 5000,5000, TimeUnit.MILLISECONDS);
}
}
And the message is broadcasted every 5000ms as expected.
So why Spring docs says that a server cannot send unsollicited messages?
The next sentence might mean that in the stomp.js client you are required to set a subscription:
All messages from a server must be in response to a specific client
subscription
But this does not necessarily mean in response to a request. For example a web socket could send information to the following:
Javascript:
stompClient.subscribe('/return/analyze', function(data) {
generateTableData(JSON.parse(data.body));
});
Spring:
#Autowired
private SimpMessagingTemplate simpMessagingTemplate;
public void sendSetpoint(String data) throws Exception {
this.simpMessagingTemplate.convertAndSend("/return/analyze", data);
}
But it cannot send unsolicited messages to the client unless that subscription exists. If this is their intended point it is a little poorly worded.
Note: see update at the bottom of the question for what I eventually concluded.
I need to send multiple responses to a request over the web socket that sent the request message, the first one quickly, and the others after the data is verified (somewhere between 10 and 60 seconds later, from multiple parallel threads).
I am having trouble getting the later responses to stop broadcasting over all open web sockets. How do I get them to only send to the initial web socket? Or should I use something besides Spring STOMP (because, to be honest, all I want is the message routing to various functions, I don't need or want the ability to broadcast to other web sockets, so I suspect I could write the message distributor myself, even though it is reinventing the wheel).
I am not using Spring Authentication (this is being retrofitted into legacy code).
On the initial return message, I can use #SendToUser, and even though we don't have a user, Spring only sends the return value to the websocket that sent the message. (see this question).
With the slower responses, though, I think I need to use SimpMessagingTemplate.convertAndSendToUser(user, destination, message), but I can't, because I have to pass in the user, and I can't figure out what user the #SendToUser used. I tried to follow the steps in this question, but didn't get it to work when not authenticated (principal.getName() returns null in this case).
I've simplified this considerably for the prototype, so don't worry about synchronizing threads or anything. I just want the web sockets to work correctly.
Here is my controller:
#Controller
public class TestSocketController
{
private SimpMessagingTemplate template;
#Autowired
public TestSocketController(SimpMessagingTemplate template)
{
this.template = template;
}
// This doesn't work because I need to pass something for the first parameter.
// If I just use convertAndSend, it broacasts the response to all browsers
void setResults(String ret)
{
template.convertAndSendToUser("", "/user/topic/testwsresponse", ret);
}
// this only sends "Testing Return" to the browser tab hooked to this websocket
#MessageMapping(value="/testws")
#SendToUser("/topic/testwsresponse")
public String handleTestWS(String msg) throws InterruptedException
{
(new Thread(new Later(this))).start();
return "Testing Return";
}
public class Later implements Runnable
{
TestSocketController Controller;
public Later(TestSocketController controller)
{
Controller = controller;
}
public void run()
{
try
{
java.lang.Thread.sleep(2000);
Controller.setResults("Testing Later Return");
}
catch (Exception e)
{
}
}
}
}
For the record, here is the browser side:
var client = null;
function sendMessage()
{
client.send('/app/testws', {}, 'Test');
}
// hooked to a button
function test()
{
if (client != null)
{
sendMessage();
return;
}
var socket = new SockJS('/application-name/sendws/');
client = Stomp.over(socket);
client.connect({}, function(frame)
{
client.subscribe('/user/topic/testwsresponse', function(message)
{
alert(message);
});
sendMessage();
});
});
And here is the config:
#Configuration
#EnableWebSocketMessageBroker
public class TestSocketConfig extends AbstractWebSocketMessageBrokerConfigurer
{
#Override
public void configureMessageBroker(MessageBrokerRegistry config)
{
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/queue", "/topic");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry)
{
registry.addEndpoint("/sendws").withSockJS();
}
}
UPDATE: Due to the security issues involved with the possibility of information being sent over other websockets than the originating socket, I ended up recommending to my group that we do not use the Spring 4.0 implementation of STOMP over Web Sockets. I understand why the Spring team did it the way they did it, and it is more power then we needed, but the security restrictions on our project were severe enough, and the actual requirements were simple enough, that we decided to go a different way. That doesn't invalidate the answers below, so make your own decision based on your projects needs. At least we have hopefully all learned the limitations of the technology, for good or bad.
Why don't you use a separate topic for each client?
Client generates a session id.
var sessionId = Math.random().toString(36).substring(7);
Client subscribes to /topic/testwsresponse/{sessionId}, then sends a message to '/app/testws/{sessionId}'.
In your controller you use #MessageMapping(value="/testws/{sessionId}") and remove #SendToUser. You can use #DestinationVariable to access sessionId in your method.
The controller sends further responses to /topic/testwsresponse/{sessionId}.
Essentially Spring does a similar thing internally when you use user destinations. Since you don't use Spring Authentication you cannot rely on this mechanism but you can easily implement your own as I described above.
var client = null;
var sessionId = Math.random().toString(36).substring(7);
function sendMessage()
{
client.send('/app/testws/' + sessionId, {}, 'Test');
}
// hooked to a button
function test()
{
if (client != null)
{
sendMessage();
return;
}
var socket = new SockJS('/application-name/sendws/');
client = Stomp.over(socket);
client.connect({}, function(frame)
{
client.subscribe('/topic/testwsresponse/' + sessionId, function(message)
{
alert(message);
});
// Need to wait until subscription is complete
setTimeout(sendMessage, 1000);
});
});
Controller:
#Controller
public class TestSocketController
{
private SimpMessagingTemplate template;
#Autowired
public TestSocketController(SimpMessagingTemplate template)
{
this.template = template;
}
void setResults(String ret, String sessionId)
{
template.convertAndSend("/topic/testwsresponse/" + sessionId, ret);
}
#MessageMapping(value="/testws/{sessionId}")
public void handleTestWS(#DestinationVariable String sessionId, #Payload String msg) throws InterruptedException
{
(new Thread(new Later(this, sessionId))).start();
setResults("Testing Return", sessionId);
}
public class Later implements Runnable
{
TestSocketController Controller;
String sessionId;
public Later(TestSocketController controller, String sessionId)
{
Controller = controller;
this.sessionId = sessionId;
}
public void run()
{
try
{
java.lang.Thread.sleep(2000);
Controller.setResults("Testing Later Return", sessionId);
}
catch (Exception e)
{
}
}
}
}
Just tested it, works as expected.
This is not full answer. Just general consideration and suggestion.
You cannot do different stuff or type of connection via the same socket. Why not have different sockets for different work? Some with authentication and some without. Some for quick task and some for long execution.