Skip to content

Latest commit

 

History

History
313 lines (245 loc) · 8.74 KB

File metadata and controls

313 lines (245 loc) · 8.74 KB

WebSockets

WebSockets are added using the javadoc:Router[ws] method:

WebSocket
{
  ws("/ws", (ctx, configurer) -> {             // (1)
    configurer.onConnect(ws -> {
      ws.send("Connected");                    // (2)
    });

    configurer.onMessage((ws, message) -> {
      ws.send("Got " + message.value());       // (3)
    });

    configurer.onClose((ws, statusCode) -> {
      // Clean up resources                      (4)
    });

    configurer.onError((ws, cause) -> {
      // Handle exceptions                       (5)
    });
  });
}
Kotlin
{
  ws("/ws") { ctx, configurer ->               // (1)
    configurer.onConnect { ws ->
      ws.send("Connected")                     // (2)
    }

    configurer.onMessage { ws, message ->
      ws.send("Got " + message.value())        // (3)
    }

    configurer.onClose { ws, statusCode ->
      // Clean up resources                      (4)
    }

    configurer.onError { ws, cause ->
      // Handle exceptions                       (5)
    }
  }
}
  1. Register a WebSocket handler.

  2. On connection (open), send a message back to the client. This is also a good place to initialize resources.

  3. On receiving a new message, send a response back to the client.

  4. The WebSocket is about to close. You must free/release any acquired resources here.

  5. The WebSocket encountered an exception. Useful for logging the error or providing an alternative response if the socket is still open.

You are free to access the HTTP context from the WebSocket configurer or callbacks, but it is forbidden to modify the HTTP context or produce an HTTP response from it.

Accessing Context
{
  ws("/ws/{key}", (ctx, configurer) -> {
    String key = ctx.path("key").value();           // (1)
    String foo = ctx.session().get("foo").value();  // (2)
    // ...
  });
}
Kotlin
{
  ws("/ws/{key}") { ctx, configurer ->
    val key = ctx.path("key").value()               // (1)
    val foo = ctx.session().get("foo").value()      // (2)
    // ...
  }
}
  1. Access a path variable (key).

  2. Access a session variable (foo).

Structured Data

Structured data (like JSON) is supported using the Value API and the javadoc:WebSocket[render, java.lang.Object] method.

To use structured messages, you need a registered javadoc:MessageDecoder[] and javadoc:MessageEncoder[]. In the following example, both are provided by the Jackson2Module.

JSON Example
import io.jooby.jackson.Jackson2Module;

{
  install(new Jackson2Module());                        // (1)

  ws("/ws", (ctx, configurer) -> {
    configurer.onMessage((ws, message) -> {
      MyObject myobject = message.to(MyObject.class);  // (2)
      ws.render(myobject);                             // (3)
    });
  });
}
Kotlin
import io.jooby.jackson.Jackson2Module

{
  install(Jackson2Module())                             // (1)

  ws("/ws") { ctx, configurer ->
    configurer.onMessage { ws, message ->
      val myobject = message.to<MyObject>()            // (2)
      ws.render(myobject)                              // (3)
    }
  }
}
  1. Install the Jackson module (required for JSON decoding/encoding).

  2. Parse and decode the incoming message to a MyObject.

  3. Encode myobject as JSON and send it to the client.

Alternatively, you can explicitly tell the WebSocket which decoder/encoder to use by specifying the consumes and produces attributes:

Explicit Content Types
import io.jooby.jackson.Jackson2Module;

{
  install(new Jackson2Module());                        // (1)

  ws("/ws", (ctx, configurer) -> {
    configurer.onMessage((ws, message) -> {
      MyObject myobject = message.to(MyObject.class);  // (2)
      ws.render(myobject);                             // (3)
    });
  })
  .consumes(MediaType.json)
  .produces(MediaType.json);
}
Kotlin
import io.jooby.jackson.Jackson2Module

{
  install(Jackson2Module())                             // (1)

  ws("/ws") { ctx, configurer ->
    configurer.onMessage { ws, message ->
      val myobject = message.to<MyObject>()            // (2)
      ws.render(myobject)                              // (3)
    }
  }.consumes(MediaType.json)
   .produces(MediaType.json)
}

Declarative definition

You can implement the same WebSocket as above using annotated classes in declarative style. Ensure that jooby-apt is in the annotation processor path, annotate the class with javadoc:annotation.Path[], and mark methods with javadoc:annotation.ws.OnConnect[], javadoc:annotation.ws.OnMessage[], javadoc:annotation.ws.OnClose[], and javadoc:annotation.ws.OnError[]. Compile code to generate an extension javadoc:Extension[] and register it by calling javadoc:Jooby[ws, io.jooby.Extension].

When a lifecycle method returns a value, that value is written to the client automatically: plain text or binary for String, byte[], and ByteBuffer, and structured values (for example JSON) using the same encoders as in Structured Data. Alternatively, use a void method and send with ws.send(…​) on the javadoc:WebSocket[] argument.

Java
@Path("/chat/{room}")                                        // (1)
public class ChatSocket {

  @OnConnect
  public String onConnect(WebSocket ws, Context ctx) {       // (2)
    return "welcome";
  }

  @OnMessage
  public Map<String, String> onMessage(WebSocket ws, Context ctx, WebSocketMessage message) { // (3)
    return Map.of("echo", message.value());
    // ws.send(message.value());                             // (4)
  }

  @OnClose
  public void onClose(WebSocket ws, Context ctx, WebSocketCloseStatus status) {}

  @OnError
  public void onError(WebSocket ws, Context ctx, Throwable cause) {}
}

// Application startup:
{
  ws(new ChatSocketWs_());                                   // (5)
}
Kotlin
@Path("/chat/{room}")                                        // (1)
class ChatSocket {

  @OnConnect
  fun onConnect(ws: WebSocket, ctx: Context): String {       // (2)
    return "welcome"
  }

  @OnMessage
  fun onMessage(ws: WebSocket, ctx: Context, message: WebSocketMessage): Map<String, String> { // (3)
    return mapOf("echo" to message.value())
    // ws.send(message.value())                              // (4)
  }

  @OnClose
  fun onClose(ws: WebSocket, ctx: Context, status: WebSocketCloseStatus) {}

  @OnError
  fun onError(ws: WebSocket, ctx: Context, cause: Throwable) {}
}

// Application startup:
{
  ws(ChatSocketWs_())                                         // (5)
}
  1. WebSocket route patterns for this handler.

  2. Returning a value sends it to the client without calling send.

  3. Return a value for automatic encoding (see Structured Data)

  4. You still can use ws.send(…​) if method return type is void.

  5. Register the generated extension with javadoc:Jooby[ws, io.jooby.Extension].

@OnMessage handlers also support parsing messages into structured data, similar to MVC methods:

Java
@Path("/chat/{room}")
public class ChatSocket {

  record ChatMessage(String username, String message, String type) {}

  @OnMessage
  public Map<String, ChatMessage> onMessage(ChatMessage message) { // (1)
    return Map.of("echo", message);
  }

  ...
}
Kotlin
@Path("/chat/{room}")
class ChatSocket {

  data class ChatMessage(
      val username: String,
      val message: String,
      val type: String
  )

  @OnMessage
  fun onMessage(message: ChatMessage): Map<String, ChatMessage> { // (1)
    return mapOf("echo" to message)
  }

  ...
}
  1. WebSocket message is automatically decoded into ChatMessage structure.

Options

Connection Timeouts

Jooby automatically times out idle connections that have no activity after 5 minutes. You can control this behavior by setting the websocket.idleTimeout property in your configuration file:

application.conf
websocket.idleTimeout = 1h

See the Typesafe Config documentation for the supported duration format.

Max Size

The maximum message size is set to 128K by default. You can override it using the websocket.maxSize property:

application.conf
websocket.maxSize = 128K

See the Typesafe Config documentation for the supported size in bytes format.