In Spring Boot, when we try to send a Server Sent Event, it only sends an error event containing data: {"timeout":-1} when we try to connect, and the connection closes. The Spring Boot class is as follows
#RestController
#CrossOrigin(origins = "*")
public class SsePushNotificationRestController {
private static final Logger log = LoggerFactory.getLogger(SsePushNotificationRestController.class);
private SseEmitter emitter;
#GetMapping("/test")
public String getString(){
try {
emitter.send("User connected");
log.info("User connected");
emitter.complete();
} catch (Exception e) {
log.info("Error while sending message to client: " + e.getMessage());
}
return "placeholder";
}
#GetMapping("/emitter")
public SseEmitter eventEmitter(#RequestParam String userId) {
emitter = new SseEmitter(-1L);
return emitter;
}
}
And our client code is as follows:
const eventSource = new EventSource('http://localhost:8080/emitter?userId=testUser');
eventSource.addEventListener("message", (event) => {
console.log(event);
});
eventSource.addEventListener("open", (event) => {
console.log("connection opened");
});
eventSource.addEventListener("error", (e) => {
if (e.readyState === EventSource.CLOSED) {
console.log('closed');
}
else {
console.log(e);
}
e.target.close();
});
document.getElementById("btn").onclick = e => {
fetch('http://localhost:8080/test').then( data => console.log(data)).catch(data => console.log(data));
};
Immediately, an error is created before we can click the button to generate an event.
What could be wrong?
What does your Spring boot terminal say? I think I need that information to address your program's error. By the way allowing cross origin resources sharing for requests from any sources (using wildcard) is a very very bad practice.
One possible reason of error is something's wrong when you create an instance of SSEemitter. (new SSeEmitter(-1L))
SSeEmitter(Long timeout) is creating server side event with set timeout it says. So if timeout is -1, I guess it would immediately be timed out and return timeout response. So it wouldn't be error, just working as written
Related
I created a server with SignalR and SQLTableDependency. After that, I created a project with Vue and SignalR Javascript Client and everything works, the notification subscription in the server execute a SignalR method to send the object to all the Clients
private void Changed(object sender, RecordChangedEventArgs<Todo> eventArgs)
{
if(eventArgs.ChangeType != TableDependency.SqlClient.Base.Enums.ChangeType.None)
{
var changedEntity = eventArgs.Entity;
var mensaje = TipoCambios(eventArgs);
_hubContext.Clients.All.SendAsync("RegistrarTarea", changedEntity);
}
}
In JavaScript Client I made this:
coneccionTodo.on("RegistrarTarea", todos => {
this.$refs.alerta.Abrir(todos.cambio, "info", "Alerta");
console.log(todos);
});
coneccionTodo
.start()
.then(response => {
this.sinConexion = false;
})
.catch(error => {
console.log("Error Todo SignalR", error.toString());
});
The result of that is this:
And finally my C# Client made with .Net Core 2.1. This is not working
public static async Task Ejecutar() {
connection.On<List<dynamic>>("RegistrarTarea", (objects) => {
Console.WriteLine(objects);
});
try
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Conexión exitosa a {url}");
await connection.StartAsync();
//await connection.InvokeAsync("RegistrarTarea", "Consola", true);
}
catch (Exception ex)
{
SignalR_Exception(ex);
}
}
In void main Console app I call the Ejecutar method:
connection = new HubConnectionBuilder().WithUrl(url).Build();
connection.Closed += async (error) => {
await Task.Delay(new Random().Next(0, 5) * 1000);
await connection.StartAsync();
};
Task.Run(() => Ejecutar());
Console.ReadLine();
NOTE: In the server, the CORS are activated to allow anything.
Are you using Direct mode ? The direct mode does not function with this. Turn the Direct mode off.
Ok, in connection.on I use a List, but instead of that, I used a Class with the properties like the server send. So now, it's working:
connection.On<Result>("RegistrarTarea", (result) => {
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine(result.Cambio);
});
I find websocket will closed when my client(Chrome 65.0.3325.181 windows10 x64) offline,this is I expected. But is not close in my server(JavaWeb project, Tomcat 7.0.79 & 8.5.24).
How can I close the websocket in server when the client offline?
JS code:
var ws;
var wsUrl = 'wss://' + location.host + '/test-websocket';
window.onload = function () {
createWebSocket(wsUrl);
};
window.onbeforeunload = function () {
ws.close();
};
function createWebSocket(url) {
ws = new WebSocket(url);
initEventHandle();
}
function initEventHandle() {
ws.onerror = function () {
console.log("error");
};
ws.onclose = function () {
console.log("close");
};
ws.onopen = function () {
setInterval(function () {
var message = {
id: "ping"
};
sendMessage(message);
}, 5000);
};
ws.onmessage = function (message) {
console.info('Received message: ' + message.data);
}
}
function sendMessage(message) {
const jsonMessage = JSON.stringify(message);
console.log('Senging message: ' + jsonMessage);
ws.send(jsonMessage);
}
Java Code:
private static final Gson GSON = new GsonBuilder().create();
#OnMessage
public void onMessage(String message, final Session session) throws IOException {
JsonObject jsonMessage = GSON.fromJson(message, JsonObject.class);
System.out.println(jsonMessage);
session.getBasicRemote().sendText("pong");
}
#OnError
public void onError(Throwable error) {
error.printStackTrace();
}
#OnClose
public void onClose(CloseReason reason) {
System.out.println(reason.toString());
}
In general close, for example close webpage, I can see "CloseReason: code [1000], reason [null]" in server output.
But when client offline, the onclose will work in client, log "close" in console. But nothing will print in server output.
This is why I think websocket onclose not work in Java when client offline.
So, how can I close the websocket in server when the client offline?
I need to create a Websocket Server using spring which was easily completed by using the below code
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SocketHandler(), "/poll");
registry.addHandler(new SocketPushHandler(), "/push");
}
}
Where SocketHandler and SocketPushHandler are the handler classes for the websocket endpoints.
Till this very thing is good able to run the server and connect to the endpoint using normal socket = new WebSocket(websocketUrl) javascript code.
Now we need to have OAuth 2.0 Security implemented on the API endpoints which is done easily by importing some Spring security dependencies.\
Now the hard part is writing the client to connect the secure endpoint by passing the Oauth Authorization beaer <token> as part of header.
From documents came to know that we can't send headers to Web-socket endpoints.
So according to the information form this Is it possible to secure WebSocket APIs with OAuth 2.0? link I created a API gateway /open-ws Request type GET to which the client would connect and send the authorization headers and this endpoint internally on server-side will open an WebSocket Client connection passing the the headers as javax.websocket.WebSocketContainer supports websocket client with custom headers.
So now my javascript first makes an GET ajax call to the Gateway endpoint and on success makes an new websocket request.
Below is the Spring API mimicking as gateway
#RequestMapping(value = "/open-ws", method = RequestMethod.GET)
public void getOperatorTokenDefinition(#RequestHeader(value = HttpHeaders.AUTHORIZATION) String bearerToken,
#RequestHeader(value = "websocketURL") String websocketURL,
HttpServletRequest acquireTokenServletRequest, HttpServletResponse response) {
webSocketClient.connecttoserver(websocketURL, acquireTokenServletRequest.getRemoteHost(), bearerToken, response);
// ResponseEntity<String> responseEntity = new ResponseEntity<>("connected", HttpStatus.OK);
}
}
Below is my Spring side client.
#Component
public class WebSocketClient {
private Session client;
public void connecttoserver(String websocketURL,String host,String bearerAccessToken, HttpServletResponse response) {
final AtomicReference<String> message = new AtomicReference<>();
Endpoint endpoint = new Endpoint() {
#Override
public void onOpen(Session session, EndpointConfig config) {
System.out.println("WSS OPEN!!!!!");
try (OutputStream output = response.getOutputStream()) {
output.write(session.getId());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
ClientEndpointConfig.Configurator configurator = new ClientEndpointConfig.Configurator() {
#Override
public void beforeRequest(Map<String, List<String>> headers) {
List<String> connection = new ArrayList<>(1);
connection.add("Upgrade");
List<String> origin = new ArrayList<>(1);
origin.add(originURL);
List<String> upgradeWebsocket = new ArrayList<>(1);
upgradeWebsocket.add("WebSocket");
List<String> host = new ArrayList<>(1);
host.add(websocketURL);
List<String> contenttype = new ArrayList<>(1);
contenttype.add("application/json");
List<String> authorization = new ArrayList<>(1);
authorization.add("Bearer " + bearerAccessToken);
List<String> tenantId = new ArrayList<>(1);
tenantId.add(tenantID);
List<String> key = new ArrayList<>(1);
key.add("HcFOxrSD89ya65X2qMF9lQ==");
List<String> version = new ArrayList<>(1);
version.add("13");
headers.put("Connection", connection);
headers.put("Upgrade", upgradeWebsocket);
headers.put("Host", host);
headers.put("Origin", origin);
// headers.put("Content-Type", contenttype);
headers.put("Authorization", authorization);
headers.put("Sec-WebSocket-Key", key);
headers.put("Sec-WebSocket-Version", version);
}
};
ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().configurator(configurator).build();
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
try {
// if (!this.client.isOpen())
this.client = container.connectToServer(endpoint, clientConfig, URI.create(websocketURL));
client.addMessageHandler(new MessageHandler.Whole<String>() {
#Override
public void onMessage(String response) {
// TODO Auto-generated method stub
message.set(response);
// System.out.println("response>>>>>>>>>>>>>>>>>>> "+response.toString());// this dosent work
}
});
System.out.println("Response--------------------------------->" + message.get());
// client.close();
} catch (DeploymentException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Below is the JQuery code
$(document).ready(function(){
$("#Websocketconnect").click(function(){
$.ajax({
type: 'GET',
url: hostUrl.value,
headers: {
"Authorization":websocketToken.value,
"websocketURL":websocketUrl.value,
"Content-type":"text/plain"
},
success: function(result){
$("#readystatus").value = result;
webSocketCycleEvent(websocketUrl.value);
}});
});
});
function webSocketCycleEvent(websocketUrl){
socket = new WebSocket(websocketUrl);
socket.onerror = function(error) {
console.log('WebSocket Error: ' + error);
};
// Show a connected message when the WebSocket is opened.
socket.onopen = function(event) {
socketStatus.innerHTML = 'Connected to: ' + websocketUrl;
socketStatus.className = 'open';
};
socket.onmessage = function(event) {
var message = event.data;
messagesList.innerHTML += '<li class="received"><span>Received:</span>' +
message + '</li>';
};
socket.onclose = function(event) {
socketStatus.innerHTML = 'Disconnected from WebSocket.';
socketStatus.className = 'closed';
};
}
form.onsubmit = function(e) {
e.preventDefault();
// Retrieve the message from the textarea.
var message = messageField.value;
socket.send(message);
messagesList.innerHTML += '<li class="sent"><span>Sent:</span>' + message +
'</li>';
// Clear out the message field.
messageField.value = '';
return false;
};
I am not to connect to the websocket and send the socket.id as the server-side gives an
javax.websocket.DeploymentException: The HTTP response from the server [400] did not permit the HTTP upgrade to WebSocket
at org.apache.tomcat.websocket.WsWebSocketContainer.connectToServer(WsWebSocketContainer.java:343)
at com.example.simplewebsocketserver.WebSocketClient.connecttoserver(WebSocketClient.java:101)
at com.example.simplewebsocketserver.WebSocketController.getOperatorTokenDefinition(WebSocketController.java:31)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
So the Question arises is this the proper way of connecting the Oauth 2.0 Websocket, if yes how to deal with the above error, if no how to send headers to the authorization endpoint.
Note: No using Stomp because we are not yet confirmed whether the actual client i.e UI will allow/have stomp JS.
I've started learning Golang after writing in Node.js for a long time and I'm a bit curious as to how am I to implement a handler - I've opted to use Gorilla Websocket since I understood it's the most reliable package out there.
In socket.io for example you have the simple socket.on function that allows me to call a function based on the "name" parameter passed in JSON.
Gorilla websocket doesn't implement such a thing, so my question is am I to sort of implement the logic behind socket.io in order to achieve what I want ?
As in do a certain procedure based on the value transferred in the websocket ?
If so - I need to implement it both client (I'm using AngularJS on the front-end) and server side separately by my own - make a switch case statement based on a value I get in JSON both in AngularJS for the front-end and in Go for the back-end, and also - is that the most efficient way ?
Thanks !
If you've been using Javascript for a while it is really easy to implement your own version of socket.on and socket.emit here is one I made for my own projects but you can have it if you need,
// e.g.
// let socket = new Socket("ws://w/e");
// socket.on('connected', () => { console.log('Connected'); });
// socket.emit('lobby join', { data: { username: 'Boo' } });
// Using ES2015 with Babel
import {EventEmitter} from 'events';
class Socket {
constructor(wsurl, ee = new EventEmitter()) {
let ws = new WebSocket(wsurl);
this.ee = ee;
this.ws = ws;
ws.onmessage = this.message.bind(this);
ws.onopen = this.open.bind(this);
ws.onclose = this.close.bind(this);
}
on(name, fn) {
this.ee.on(name, fn);
}
off(name, fn) {
this.ee.removeListener(name, fn);
}
emit(name, data) {
const message = JSON.stringify({name, data});
this.ws.send(message);
}
message(e) {
try {
const msgData = JSON.parse(e.data);
this.ee.emit(msgData.name, msgData.data);
}
catch(err) {
let error = {
message: err
}
console.log(err)
this.ee.emit(error.message)
}
}
open() {
this.ee.emit('connected');
}
close() {
this.ee.emit('disconnected');
}
}
export default Socket
This will let you use your common socket.on('event', fn); and what not
As for handling it on the servers end:
For receiving messages, I personally just make a switch statement that will match an incoming string to a function, e.g.:
// readPump pumps messages from the websocket connection to the hub.
func (c *connection) readPump() {
defer c.ws.Close()
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
break
}
var incMessage interface{}
err = json.Unmarshal(message, &incMessage)
if err != nil {
log.Println(err)
}
incMessageMap := incMessage.(map[string]interface{})
switch incMessageMap["name"] {
case "lobby join":
// Do something to handle joining
case "lobby leave":
// Do something to handle leaving
}
}
}
For sending them I have a send channel on my connections that is stored in a map and when I need to emit I have a simple struct that takes a message name, and data, e.g.:
type wsMsg struct {
Name string `json:"name"`
Data map[string]interface{} `json:"data"`
}
c.send <- wsMsg{
"user joined",
map[string]interface{}{
"username": "Booh",
},
}
Then on the client side it would come as
socket.on('user joined', (msg) => {
console.log(msg) // { username: "Booh" }
});
I suggest looking at the examples on gorillas git: https://github.com/gorilla/websocket/tree/master/examples/chat
Here is an working example of golang websocket to stream video
https://github.com/interviewparrot/OpenAVStream
Let me know if it is good enough
Given a websocket configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/queue", "/topic");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/add").withSockJS();
}
}
and client javascript:
<script type="text/javascript">
console.log('begin javascript');
var stompClient = null;
function connect() {
var socket = new SockJS('/myapp/add');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected to STOMP: ' + frame);
stompClient.subscribe('/user/topic/abc', function(calResult) {
console.log('*** Got it ***');
});
});
}
connect();
</script>
and sending this message from the server:
messagingTemplate.convertAndSendToUser(username, "/topic/abc", "hello");
the callback never gets fired.
The javascript console shows that the connection is made:
Connected to STOMP: CONNECTED user-name:jschmoe heart-beat:0,0
version:1.1
SUBSCRIBE id:sub-0 destination:/user/topic/abc
and the tomcat console shows:
Processing SUBSCRIBE destination=/topic/abc-useryl3ovhr2
subscriptionId=sub-0 session=yl3ovhr2 user=jschmoe payload=byte[0]
and then when the message is sent:
Processing MESSAGE destination=/topic/abc-useryl3ovhr2 session=null
payload=hello
Seems like everything works except for the callback.
In my case the problem was caused by XML configuration, once I switched to Java config with #EnableWebSocketMessageBroker annotation I received the messages on client side.