I'm trying to implement an async server application using Avro IPC. As far as I've researched, it is possible to make client async calls by calling the method of generated Callback protocol classes like this:
My generated protocol class:
#org.apache.avro.specific.AvroGenerated
public interface TestProtocol {
public static final org.apache.avro.Protocol PROTOCOL = /*ommited*/;
ResponseTest send(MessageTest TestMsg);
#org.apache.avro.specific.AvroGenerated
public interface Callback extends TestProtocol {
public static final org.apache.avro.Protocol PROTOCOL = TestProtocol.PROTOCOL;
void send(MessageTest TestMsg, org.apache.avro.ipc.Callback<ResponseTest> callback) throws java.io.IOException;
}
}
And implementation of this code
public final class TestProtocolImplAsync implements TestProtocol.Callback {
#Override
public #NotNull ResponseTest send(#NotNull MessageTest TestMsg) {
return new ResponseTest("Sync call");
}
#Override
public void send(#NotNull MessageTest TestMsg,
#NotNull org.apache.avro.ipc.Callback<ResponseTest> callback) {
callback.handleResult(new ResponseTest("Async call"));
}
}
The implementation of TestProtocol is binded to TestProtocolImpl on server side. However, while calling it on client side this way:
SpecificRequestor.getClient(TestProtocol.Callback.class, client).send(new MessageTest(/*params*/), new Callback<ResponseTest>() {
#Override
public void handleResult(#NotNull ResponseTest result) {
System.out.println(result.toString());
}
#Override
public void handleError(#NotNull Throwable error) {
//whatever
}
})
I keep getting sync server method called. I haven't found any info about this in documentation, but am I right that callback method is only to be implemented on async client side, not server, and it is impossible to process request on server asyncrously this way and call callback method from server side? Or am I missing something in my server settings?
Related
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 was wondering if there was a way to create a bi-direction stream (or allow multiple HTTP body sends over a single long-polled connection) in OKHTTP3 that does not require data to be constantly flowing between the client and the server.
For context, I am trying to implement a system where there can be intermittent data pushes can occur from either the client or the server over a persistent connection. The application is data-use sensitive, so I don't want the client sending requests to the server to see if there is data ready, I just want the server to push it.
A Websocket connection is the ideal solution to your problem. This creates a persistent connection between the client and the server and both parties can start sending data at any time.
in OKHTTP you can implement this by
adding the library to your build gradle file compile 'com.squareup.okhttp3:okhttp:3.6.0'
Create a class that implements the okhttp WebsocketListener interface
private final class MyWebSocketListener extends WebSocketListener {
private static final int CLOSE_STATUS = 1000;
#Override
public void onOpen(WebSocket webSocket, Response response) {
webSocket.send("Hello");
webSocket.close(CLOSE_STATUS, "Goodbye");
}
#Override
public void onMessage(WebSocket webSocket, String text) {
log(text);
}
#Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
log(bytes.hex());
}
#Override
public void onClosing(WebSocket webSocket, int code, String reason) {
webSocket.close(CLOSE_STATUS, null);
log("Closing");
}
#Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
log(t.getMessage());
}
}
Create a method to initiate the connection
private void connect() {
Request request = new Request.Builder().url("ws://my.websocket.url").build();
MyWebSocketListener listener = new MyWebSocketListener();
WebSocket ws = client.newWebSocket(request, listener);
\\ to shutdown the connection client.dispatcher().executorService().shutdown();
}
This should establish a connection with the server and should persist as long as the application is alive. I recommend reading more on websockets if you are the same person responsible for the backend.
I have a fairly simple setup to test Stomp support in Spring.
I was planning on using JS to send messages to the queue and receive and handle them in Spring app. However, it doesn't seem to work for some reason.
JS client:
var ws = new SockJS('/hello');
client = Stomp.over(ws);
...
client.subscribe('jms.queue.test', function(message) {}
...
client.send("jms.queue.test", {}, "message");
Spring config (mostly useless at the moment, since i don't use /app destination):
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("jms.topic", "jms.queue");
config.setApplicationDestinationPrefixes("/app");
config.setPathMatcher(new AntPathMatcher("."));
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hello").withSockJS();
}
}
Spring Boot App:
#Component
public class BusinessLogic implements CommandLineRunner {
#Autowired
hulloController controller;
#Override
public void run(String... args) throws Exception {
stuff(args);
}
public void stuff(String[] args) throws InterruptedException {
Thread.sleep(20000);//sleep while spring boot fully initializes
controller.waitForGreet();
}
}
Controller (not a real #Controller, i don't plan to use MVC):
#Component
public class hulloController {
private SimpMessagingTemplate template;
#Autowired
StompSessionHandler handler;
#Autowired
public hulloController(SimpMessagingTemplate template) {
this.template = template;
}
public void waitForGreet() {
System.out.println("Entered Listener");
WebSocketClient transport = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(transport);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.connect("ws://127.0.0.1:61613/stomp", handler);
}
}
And finally, the handler:
#Component
public class SessionHandler implements StompSessionHandler {
#Override
public void afterConnected(StompSession stompSession, StompHeaders stompHeaders) {
System.out.println("Connected!");
StompFrameHandler handler = new StompFrameHandler() {
#Override
public Type getPayloadType(StompHeaders stompHeaders) {
return null;
}
#Override
public void handleFrame(StompHeaders stompHeaders, Object o) {
System.out.println("received " + o.toString());
}
};
//StompSession.Subscription s = stompSession.subscribe("jms.queue.test", handler);
stompSession.send("jms.queue.test", "hello!");
}
...
}
If i comment the client.subscribe part, client.send works properly, message is received and rendered in JS, so the queue names and connection URL are fine. I also tried using SockJSClient as a Transport but it doesn't help.
When i send messages from JS, for about 1 or 2 minutes half of them isn't showing up (as it would be if the subscription would be working), then JS starts receiving 100% of the messages.
I've seen plenty of almost identical code on github today, and i just don't see what the issue might be here.
//StompSession.Subscription s = stompSession.subscribe("jms.queue.test", handler);
stompSession.send("jms.queue.test", "hello!");
The STOMP over WebSocket is a an async protocol.
You call there subscribe and got to the send. There is no guaranty that the subscription will happen already, when you start to send something to that destination.
Consider to use StompSession.Subscription and its addReceiptTask() to send messages to the destination after the confirmation for the subscription.
And you should enable StompSession.setAutoReceipt(true) to make that working with your STOMP Broker.
So, apparently switching from implementing StompSessionHandler to extending StompSessionHandlerAdapter for my handler did the trick.
I have absolutely no idea why, but i guess this passage from Spring docs for StompSessionHandler is there for a reason:
"Implementations of this interface should consider extending
StompSessionHandlerAdapter."
#Override
public Type getPayloadType(StompHeaders stompHeaders) {
return null;
}
because of this line: you supply unhandled converter - it throws exception. But it's not logged or anything. check out the code and paste proper Type instead of null
This looks weird but I ended up in this situation. Implemented Restful API call using Retrofit asynchronously. Now there is a sudden requirement change and have to call API one after the other (One at a time), so that in the second API call I have to send session token received from the previous response. One way is to make every API call as synchronous but it takes time to implement this change.
I have tried :
Used setExecutor(Executors.newSingleThreadExecutor(),new
MainThreadExecutor) for RestAdapter.Builder.This didn't work
since API calls are asynchronous and before getting response for the
previous API call second call is made. So the second request has
invalid session token.
In the class where I have implemented all Restful Web services,
used Executors.newSingleThreadExecutor() , this also didn't work
for the same reason.
Could anybody suggest how to resolve this with minimal changes.
Webservice Manager is as below, this is partial and there are many more api's like login:
public class WebServiceManager {
private static final String ROOT_PATH = Urls.REST_ROOT_URL;
RestAdapter restAdapter;
WebServiceInterface webServiceInterface;
private String requestKey;
private String sessionId;
private Context context;
public WebServiceManager(Context context) {
this.context = context;
initializeWebServiceAdapter();
}
private void initializeWebServiceAdapter() {
restAdapter = new RestAdapter.Builder()
.setEndpoint(ROOT_PATH)
.setLogLevel(RestAdapter.LogLevel.FULL)
.build();
webServiceInterface = restAdapter.create(WebServiceInterface.class);
}
private void setHeaderValues(BaseModel model) {
SessionManager sm= context.getApplicationContext().getSessionManager();
model.getRequestHeader().setRequestKey(sm.getRequestKey());
model.getRequestHeader().setSessionId(sm.getSessionId());
}
public void login(String emailID, String passwd, final WebServiceCallback loginModelWebServiceCallback) {
LoginModel model = RestRequest.getLoginModel(emailID, passwd);
setHeaderValues(model);
webServiceInterface.login(model, new Callback() {
#Override
public void success(LoginModel loginModel, Response response) {
if (loginModelWebServiceCallback != null)
{
SessionManager sm= context.getApplicationContext().getSessionManager();
sm.setSessionDetails(response.getRequestKey(),response.getSessionId());
loginModelWebServiceCallback.success(loginModel);
}
}
#Override
public void failure(RetrofitError error) {
if (loginModelWebServiceCallback != null)
loginModelWebServiceCallback.failure(error);
}
});
}
}
The Executor doesn't matter since you're always invoking the Retrofit service with the Callback argument, which makes it asynchronous. If you want your Retrofit call to be synchronous then the service call method needs a return type, not void. You can read this in the docs.
Once you make your API calls synchronous and ordered how you want, you can wrap them in a Runnable and let an Executor handle the threading for you.
A request can be made in the response from first API itself, when some parameter is expected for the second api call. Have a look at the sample :
public void login(String emailID, String passwd, final WebServiceCallback loginModelWebServiceCallback) {
LoginModel model = RestRequest.getLoginModel(emailID, passwd);
setHeaderValues(model);
webServiceInterface.login(model, new Callback() {
#Override
public void success(LoginModel loginModel, Response response) {
if (loginModelWebServiceCallback != null) {
makeSecondAPIcall();
}
}
#Override
public void failure(RetrofitError error) {
if (loginModelWebServiceCallback != null)
loginModelWebServiceCallback.failure(error);
}
});
}
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.