Skip to content

Commit

Permalink
Fixes #9552 - Jetty 12 - Rewrite of the Jetty WebSocket APIs. (#9652)
Browse files Browse the repository at this point in the history
* Removed unnecessary classes, among which `BatchMode`, `CloseStatus`, etc.
* Coalesced all listener interfaces into `Session.Listener`.
* Moved `RemoteEndpoint` functionality to `Session`.
* Renamed `WebSocketPolicy` to `Configurable`.
* Renamed `WriteCallback` to just `Callback`, as it is now also used for some listener methods.
* Renamed `@OnWebSocketConnect` to `@OnWebSocketOpen`
* Renamed `Session.Listener.onWebSocketConnect()` to `onWebSocketOpen()`.
* Removed `@WebSocket` annotation attributes, because they were just a subset of the configurable ones and they can be configured directly on the Session instance.
* Removed `Session.suspend()` and `SuspendToken`, and introduced `Session.demand()`.
* Introduced `@WebSocket.autoDemand` and `Session.Listener.AutoDemanding` to support automatic demand upon exit of WebSocket handler methods.
* Removed `FrameHandler.isAutoDemanding()` and `CoreSession.isAutoDemanding()`.
* Changed the responsibility of demand from `WebSocketCoreSession` to `FrameHandler`, which in turn may delegate to `MessageSink`.
* Updated MessageInputStream to fail if an exception is thrown by the application.

Signed-off-by: Simone Bordet <[email protected]>
  • Loading branch information
sbordet authored May 2, 2023
1 parent 7275bf1 commit adf5754
Show file tree
Hide file tree
Showing 215 changed files with 3,202 additions and 4,067 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The Maven artifact coordinates are the following:
[[pg-client-websocket-start]]
==== Starting WebSocketClient

The main class is `org.eclipse.jetty.ee9.websocket.client.WebSocketClient`; you instantiate it, configure it, and then start it like may other Jetty components.
The main class is `org.eclipse.jetty.websocket.client.WebSocketClient`; you instantiate it, configure it, and then start it like may other Jetty components.
This is a minimal example:

[source,java,indent=0]
Expand Down Expand Up @@ -124,7 +124,7 @@ In code:
include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=connectHTTP11]
----

`WebSocketClient.connect()` links the client-side WebSocket _endpoint_ to a specific server URI, and returns a `CompletableFuture` of an `org.eclipse.jetty.ee9.websocket.api.Session`.
`WebSocketClient.connect()` links the client-side WebSocket _endpoint_ to a specific server URI, and returns a `CompletableFuture` of an `org.eclipse.jetty.websocket.api.Session`.

The endpoint offers APIs to _receive_ WebSocket data (or errors) from the server, while the session offers APIs to _send_ WebSocket data to the server.

Expand All @@ -133,7 +133,7 @@ The endpoint offers APIs to _receive_ WebSocket data (or errors) from the server

Initiating a WebSocket communication with a server using HTTP/1.1 is detailed in link:https://tools.ietf.org/html/rfc8441[RFC 8441].

A WebSocket client establishes a TCP connection to the server or reuses an existing one currently used for HTTP/2, then sends an HTTP/2 _connect_ request over an HTTP/2 stream.
A WebSocket client establishes a TCP connection to the server or reuses an existing one currently used for HTTP/2, then sends an HTTP/2 _CONNECT_ request over an HTTP/2 stream.

If the server supports upgrading to WebSocket, it responds with HTTP status code `200`, then switches the communication over that stream, either incoming or outgoing, to happen using HTTP/2 `DATA` frames wrapping WebSocket frames.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,44 +35,48 @@ A WebSocket endpoint is the entity that receives WebSocket events.

The WebSocket events are the following:

* The _connect_ event.
* The _open_ event.
This event is emitted when the WebSocket communication has been successfully established.
Applications interested in the connect event receive the WebSocket _session_ so that they can use it to send data to the remote peer.
Applications interested in the open event receive the WebSocket _session_ so that they can use it to send data to the remote peer.
* The _close_ event.
This event is emitted when the WebSocket communication has been closed.
Applications interested in the close event receive a WebSocket status code and an optional close reason message.
* The _error_ event.
This event is emitted when the WebSocket communication encounters a fatal error, such as an I/O error (for example, the network connection has been broken), or a protocol error (for example, the remote peer sends an invalid WebSocket frame).
Applications interested in the error event receive a `Throwable` that represent the error.
* The _message_ event.
The message event is emitted when a WebSocket message is received.
Only one thread at a time will be delivering a message event to the `onMessage` method; the next message event will not be delivered until the previous call to the `onMessage` method has exited.
Endpoints will always be notified of message events in the same order they were received over the network.
* The _frame_ events.
The frame events are emitted when a WebSocket frame is received, either a control frame such as PING, PONG or CLOSE, or a data frame such as BINARY or TEXT.
One or more data frames of the same type define a _message_.
* The _message_ events.
The message event are emitted when a WebSocket message is received.
The message event can be of two types:
** Textual message event.
** TEXT.
Applications interested in this type of messages receive a `String` representing the UTF-8 bytes received.
** Binary message event.
Applications interested in this type of messages receive a `byte[]` representing the raw bytes received.
** BINARY.
Applications interested in this type of messages receive a `ByteBuffer` representing the raw bytes received.

Only one thread at a time will be delivering frame or message events to the corresponding methods; the next frame or message event will not be delivered until the previous call to the corresponding method has exited, and if there is demand for it.
Endpoints will always be notified of message events in the same order they were received over the network.

[[pg-websocket-endpoints-listener]]
===== Listener Endpoints

A WebSocket endpoint may implement the `org.eclipse.jetty.ee9.websocket.api.WebSocketListener` interface to receive WebSocket events:
A WebSocket endpoint may implement the `org.eclipse.jetty.websocket.api.Session.Listener` interface to receive WebSocket events:

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=listenerEndpoint]
----
<1> Your listener class implements `WebSocketListener`.
<1> Your listener class implements `Session.Listener`.

====== Message Streaming Reads

If you need to deal with large WebSocket messages, you may reduce the memory usage by streaming the message content.
For large WebSocket messages, the memory usage may be large due to the fact that the text or the bytes must be accumulated until the message is complete before delivering the message event.

To stream textual or binary messages, you must implement interface `org.eclipse.jetty.ee9.websocket.api.WebSocketPartialListener` instead of `WebSocketListener`.
To stream textual or binary messages, you override either `org.eclipse.jetty.websocket.api.Session.Listener.onWebSocketPartialText(\...)` or `org.eclipse.jetty.websocket.api.Session.Listener.onWebSocketPartialBinary(\...)`.

Interface `WebSocketPartialListener` exposes one method for textual messages, and one method to binary messages that receive _chunks_ of, respectively, text and bytes that form the whole WebSocket message.
These methods that receive _chunks_ of, respectively, text and bytes that form the whole WebSocket message.

You may accumulate the chunks yourself, or process each chunk as it arrives, or stream the chunks elsewhere, for example:

Expand All @@ -84,15 +88,15 @@ include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=s
[[pg-websocket-endpoints-annotated]]
===== Annotated Endpoints

A WebSocket endpoint may annotate methods with `org.eclipse.jetty.ee9.websocket.api.annotations.*` annotations to receive WebSocket events.
A WebSocket endpoint may annotate methods with `org.eclipse.jetty.websocket.api.annotations.*` annotations to receive WebSocket events.
Each annotated method may take an optional `Session` argument as its first parameter:

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=annotatedEndpoint]
----
<1> Use the `@WebSocket` annotation at the class level to make it a WebSocket endpoint.
<2> Use the `@OnWebSocketConnect` annotation for the _connect_ event.
<2> Use the `@OnWebSocketOpen` annotation for the _open_ event.
As this is the first event notified to the endpoint, you can configure the `Session` object.
<3> Use the `@OnWebSocketClose` annotation for the _close_ event.
The method may take an optional `Session` as first parameter.
Expand All @@ -103,20 +107,12 @@ The method may take an optional `Session` as first parameter.

[NOTE]
====
For binary messages, you may declare the annotated method with either or these two signatures:
For binary messages, you may declare the annotated method with this signature:
[source,java]
----
@OnWebSocketMessage
public void methodName(byte[] bytes, int offset, int length) { ... }
----
or
[source,java]
----
@OnWebSocketMessage
public void methodName(ByteBuffer buffer) { ... }
public void methodName(ByteBuffer buffer, Callback callback) { ... }
----
====

Expand Down Expand Up @@ -144,8 +140,8 @@ A WebSocket session is the entity that offers an API to send data to the remote
[[pg-websocket-session-configure]]
===== Configuring the Session

You may configure the WebSocket session behavior using the `org.eclipse.jetty.ee9.websocket.api.Session` APIs.
You want to do this as soon as you have access to the `Session` object, typically from the xref:pg-websocket-endpoints[_connect_ event] handler:
You may configure the WebSocket session behavior using the `org.eclipse.jetty.websocket.api.Session` APIs.
You want to do this as soon as you have access to the `Session` object, typically from the xref:pg-websocket-endpoints[_open_ event] handler:

[source,java,indent=0]
----
Expand Down Expand Up @@ -180,44 +176,17 @@ Please refer to the `Session` link:{javadoc-url}/org/eclipse/jetty/websocket/api
[[pg-websocket-session-send]]
===== Sending Data

To send data to the remote peer, you need to obtain the `RemoteEndpoint` object from the `Session`, and then use its API to send data.

`RemoteEndpoint` offers two styles of APIs to send data:

* Blocking APIs, where the call returns when the data has been sent, or throws an `IOException` if the data cannot be sent.
* Non-blocking APIs, where a callback object is notified when the data has been sent, or when there was a failure sending the data.

[[pg-websocket-session-send-blocking]]
====== Blocking APIs

`RemoteEndpoint` blocking APIs throw `IOException`:

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=sendBlocking]
----

Blocking APIs are simpler to use since they can be invoked one after the other sequentially.

[CAUTION]
====
Sending large messages to the remote peer may cause the sending thread to block, possibly exhausting the thread pool.
Consider using non-blocking APIs for large messages.
====

[[pg-websocket-session-send-non-blocking]]
====== Non-Blocking APIs

`RemoteEndpoint` non-blocking APIs have an additional callback parameter:
To send data to the remote peer, you can use the non-blocking APIs offered by `Session`.

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=sendNonBlocking]
----
<1> Non-blocking APIs require a `WriteCallback` parameter.
<1> Non-blocking APIs require a `Callback` parameter.
<2> Note how the second send must be performed from inside the callback.
<3> Sequential sends may throw `WritePendingException`.

// TODO: rewrite this section in light of maxOutgoingFrames.
Non-blocking APIs are more difficult to use since you are required to meet the following condition:

[IMPORTANT]
Expand Down Expand Up @@ -248,49 +217,42 @@ While non-blocking APIs are more difficult to use, they don't block the sender t

If you need to send large WebSocket messages, you may reduce the memory usage by streaming the message content.

Both blocking and non-blocking APIs offer `sendPartial*(...)` methods that allow you to send a chunk of the whole message at a time, therefore reducing the memory usage since it is not necessary to have the whole message `String` or `byte[]` in memory to send it.

Streaming sends using blocking APIs is quite simple:

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=streamSendBlocking]
----
The APIs offer `sendPartial*(\...)` methods that allow you to send a chunk of the whole message at a time, therefore reducing the memory usage since it is not necessary to have the whole message `String` or `ByteBuffer` in memory to send it.

Streaming sends using non-blocking APIs is more complicated, as you should wait (without blocking!) for the callbacks to complete.
The APIs for streaming the message content are non-blocking and therefore slightly more complicated to use, as you should wait (without blocking!) for the callbacks to complete.

Fortunately, Jetty provides you with the `IteratingCallback` utility class (described in more details xref:pg-arch-io-echo[in this section]) which greatly simplify the use of non-blocking APIs:
Fortunately, Jetty provides the `IteratingCallback` utility class (described in more details xref:pg-arch-io-echo[in this section]) which greatly simplify the use of non-blocking APIs:

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=streamSendNonBlocking]
----
<1> Implementing `WriteCallback` allows to pass `this` to `sendPartialBytes(...)`.
<2> The `process()` method is called iteratively when each `sendPartialBytes(...)` is completed.
<3> Send the message chunks.
<1> Implementing `Callback` allows to pass `this` to `sendPartialBinary(...)`.
<2> The `process()` method is called iteratively when each `sendPartialBinary(...)` is completed.
<3> Sends the message chunks.

[[pg-websocket-session-ping]]
===== Sending Ping/Pong

The WebSocket protocol defines two special frame, named `Ping` and `Pong` that may be interesting to applications for these use cases:
The WebSocket protocol defines two special frame, named `PING` and `PONG` that may be interesting to applications for these use cases:

* Calculate the round-trip time with the remote peer.
* Keep the connection from being closed due to idle timeout -- a heartbeat-like mechanism.

To handle `Ping`/`Pong` events, you may implement interface `org.eclipse.jetty.ee9.websocket.api.WebSocketPingPongListener`.
To handle `PING`/`PONG` events, you may implement methods `Session.Listener.onWebSocketPing(ByteBuffer)` and/or `Session.Listener.onWebSocketPong(ByteBuffer)`.

[NOTE]
====
`Ping`/`Pong` events are not supported when using annotations.
`PING`/`PONG` events are also supported when using annotations via the `OnWebSocketFrame` annotation.
====

`Ping` frames may contain opaque application bytes, and the WebSocket implementation replies to them with a `Pong` frame containing the same bytes:
`PING` frames may contain opaque application bytes, and the WebSocket implementation replies to them with a `PONG` frame containing the same bytes:

[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=pingPongListener]
----
<1> The WebSocket endpoint class must implement `WebSocketPingPongListener`
<1> The WebSocket endpoint class must implement `Session.Listener`

[[pg-websocket-session-close]]
===== Closing the Session
Expand Down
Loading

0 comments on commit adf5754

Please sign in to comment.