From dde6e04731611bd2344d8c65e8fff7f248b41777 Mon Sep 17 00:00:00 2001 From: Lachlan Date: Wed, 29 Mar 2023 04:42:44 +1100 Subject: [PATCH] Issue #7121 - Add documentation for websocket configuration. (#8931) * Improvements to WebSocket documentation. * Documented configuration parameters. * Documented and added a test case for PathSpec usage. Signed-off-by: Lachlan Roberts Signed-off-by: Simone Bordet Co-authored-by: Simone Bordet --- .../client/websocket/client-websocket.adoc | 7 ++- .../websocket/server-websocket-jetty.adoc | 25 ++++++++- .../websocket/server-websocket-standard.adoc | 2 +- .../server/websocket/server-websocket.adoc | 7 +-- .../asciidoc/programming-guide/websocket.adoc | 25 ++++++++- .../server/websocket/WebSocketServerDocs.java | 37 +++++++++++++ .../websocket/core/WebSocketConstants.java | 35 ++++++++++++ .../JettyWebSocketServletAttributeTest.java | 53 +++++++++++++++++++ 8 files changed, 182 insertions(+), 9 deletions(-) diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/websocket/client-websocket.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/websocket/client-websocket.adoc index ec756903efd..201c2590c07 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/websocket/client-websocket.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/websocket/client-websocket.adoc @@ -54,7 +54,12 @@ You may create multiple instances of `WebSocketClient`, but typically one instan Creating multiple instances may be necessary for example when you need to specify different configuration parameters for different instances. For example, you may need different instances when you need to configure the `HttpClient` differently: different transports, different proxies, different cookie stores, different authentications, etc. -WebSocket specific configuration may be typically be given a default value in `WebSocketClient` and then overridden more specifically, see for example xref:pg-websocket-session-configure[this section]. +The configuration that is not WebSocket specific (such as idle timeout, etc.) should be directly configured on the associated `HttpClient` instance. + +The WebSocket specific configuration can be configured directly on the `WebSocketClient` instance. +Configuring the `WebSocketClient` allows to give default values to various parameters, whose values may be overridden more specifically, as described in xref:pg-websocket-session-configure[this section]. + +Refer to the `WebSocketClient` link:{javadoc-url}/org/eclipse/jetty/websocket/client/WebSocketClient.html[javadocs] for the setter methods available to customize the WebSocket specific configuration. [[pg-client-websocket-stop]] ==== Stopping WebSocketClient diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-jetty.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-jetty.adoc index 81730c42346..144ddc3ea9e 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-jetty.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-jetty.adoc @@ -49,6 +49,8 @@ To configure correctly your WebSocket application based on the Jetty WebSocket A . Make sure that Jetty xref:pg-server-websocket-jetty-container[sets up] an instance of `JettyWebSocketServerContainer`. . Use the `JettyWebSocketServerContainer` APIs in your applications to xref:pg-server-websocket-jetty-endpoints[register your WebSocket endpoints] that implement your application logic. +You can read more about the xref:pg-websocket-architecture[Jetty WebSocket architecture], which is common to both client-side and server-side, to get familiar with the terminology used in the following sections. + [[pg-server-websocket-jetty-container]] ===== Setting up `JettyWebSocketServerContainer` @@ -112,10 +114,12 @@ You can also use this variant to set up the `JettyWebSocketServerContainer` and include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=jettyContainerAndEndpoints] ---- +In the call to `JettyWebSocketServerContainer.addMapping(\...)`, you can specify a _path spec_ (the first parameter) that can be configured as specified in xref:pg-server-websocket-jetty-pathspec[this section]. + When the `ServletContextHandler` is started, the `Configurator` lambda (the second parameter passed to `JettyWebSocketServletContainerInitializer.configure(\...)`) is invoked and allows you to explicitly configure the WebSocket endpoints using the Jetty WebSocket APIs provided by `JettyWebSocketServerContainer`. Under the hood, the call to `JettyWebSocketServerContainer.addMapping(\...)` installs the `org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter`, which is the component that intercepts HTTP requests to upgrade to WebSocket, described in xref:pg-server-websocket-standard-upgrade[this section]. -For more information about the `WebSocketUpgradeFilter` see also xref:pg-server-websocket-configure-filter[this section]. +For more information about the configuration of `WebSocketUpgradeFilter` see also xref:pg-server-websocket-configure-filter[this section]. One last alternative to register your WebSocket endpoints is to use a programmatic WebSocket upgrade via `JettyWebSocketServerContainer.upgrade(\...)`, which allows you to use a standard `HttpServlet` subclass (rather than a `JettyWebSocketServlet` as explained in xref:pg-server-websocket-jetty-endpoints-servlet[this section]) to perform a direct WebSocket upgrade when your application logic demands so: @@ -175,3 +179,22 @@ However, it is typically best to avoid mixing the use of `JettyWebSocketServerCo Using `JettyWebSocketServerContainer.addMapping(\...)` will install the `WebSocketUpgradeFilter` under the hood, which by default will intercepts all HTTP requests to upgrade to WebSocket. However, as explained in xref:pg-server-websocket-standard-upgrade[this section], if `WebSocketUpgradeFilter` does not find a matching WebSocket endpoint for the request URI path, then the HTTP request is passed to the Filter chain of your web application and may arrive to your `JettyWebSocketServlet` subclass, where it would be processed and possibly result in a WebSocket upgrade. + +[[pg-server-websocket-jetty-pathspec]] +====== Custom PathSpec Mappings + +The `JettyWebSocketServerContainer.addMapping(\...)` API maps a _path spec_ to a `JettyWebSocketCreator` instance (typically a lambda expression). +The path spec is matched against the WebSocket upgrade request URI to select the correspondent `JettyWebSocketCreator` to invoke. + +The path spec can have these forms: + +* Servlet syntax, specified with `servlet|`, where the `servlet|` prefix can be omitted if the path spec begins with `/` or `+*.+` (for example, `/ws`, `/ws/chat` or `+*.ws+`). +* Regex syntax, specified with `regex|`, where the `regex|` prefix can be omitted if the path spec begins with `^` (for example, `+^/ws/[0-9]++`). +* URI template syntax, specified with `uri-template|` (for example `+uri-template|/ws/chat/{room}+`). + +Within the `JettyWebSocketCreator`, it is possible to access the path spec and, for example in case of URI templates, extract additional information in the following way: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=uriTemplatePathSpec] +---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-standard.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-standard.adoc index 0ba2178d8f3..2560f299091 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-standard.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-standard.adoc @@ -91,7 +91,7 @@ Calling `JavaxWebSocketServletContainerInitializer.configure(\...)` must be done [[pg-server-websocket-standard-endpoints]] ===== Configuring Endpoints -Once you have xref:pg-server-websocket-standard-container[setup] the `ServerContainer`, you can configure your xref:pg-websocket-endpoints[WebSocket endpoints]. +Once you have xref:pg-server-websocket-standard-container[setup] the `ServerContainer`, you can configure your WebSocket endpoints. The WebSocket endpoints classes may be either annotated with the standard `javax.websocket` annotations, extend the `javax.websocket.Endpoint` abstract class, or implement the `javax.websocket.server.ServerApplicationConfig` interface. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket.adoc index d809963ab5d..8543d899d49 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket.adoc @@ -21,19 +21,16 @@ Jetty provides two API implementations of the WebSocket protocol: Using the standard `javax.websocket` APIs allows your applications to depend only on standard APIs, and your applications may be deployed in any compliant WebSocket Container that supports JSR 356. -The standard APIs provide few features that are not present in the Jetty WebSocket APIs: +The standard APIs provide these features that are not present in the Jetty WebSocket APIs: * Encoders and Decoders for automatic conversion of text or binary messages to objects. -* `Reader` and `InputStream` for simple, blocking, message streaming. -* Simple URI template matching. -On the other hand, the Jetty WebSocket APIs are more efficient and offer greater and more fine-grained control, and provide features that are not present in the standard APIs: +On the other hand, the Jetty WebSocket APIs are more efficient and offer greater and more fine-grained control, and provide these features that are not present in the standard APIs: * Suspend/resume to control backpressure. * Remote socket address (IP address and port) information. * WebSocket upgrade handling via Filter or Servlet. * Advanced URI matching with Servlet WebSocket upgrade. -* Control of the idle timeout. * Configuration of the network buffer capacity. If your application needs specific features that are not provided by the standard APIs, the Jetty WebSocket APIs may provide such features -- and if they do not, you may ask for these features by submitting an issue to the Jetty Project without waiting for the standard process to approve them. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/websocket.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/websocket.adoc index 9b84d051d43..f0ade7fcd86 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/websocket.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/websocket.adoc @@ -14,7 +14,7 @@ // Snippets of WebSocket documentation that are common between client and server. [[pg-websocket-architecture]] -==== WebSocket Architecture +==== Jetty WebSocket Architecture The Jetty WebSocket architecture is organized around the concept of a logical _connection_ between the client and the server. @@ -152,6 +152,29 @@ You want to do this as soon as you have access to the `Session` object, typicall include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=sessionConfigure] ---- +The settings that can be configured include: + +maxBinaryMessageSize:: +the maximum size in bytes of a binary message (which may be composed of multiple frames) that can be received. + +maxTextMessageSize:: +the maximum size in bytes of a text message (which may be composed of multiple frames) that can be received. + +maxFrameSize:: +the maximum payload size in bytes of any WebSocket frame that can be received. + +inputBufferSize:: +the input (read from network/transport layer) buffer size in bytes; it has no relationship with the WebSocket frame size or message size. + +outputBufferSize:: +the output (write to network/transport layer) buffer size in bytes; it has no relationship to the WebSocket frame size or message size. + +autoFragment:: +whether WebSocket frames are automatically fragmented to respect the maximum frame size. + +idleTimeout:: +the duration that a WebSocket connection may remain idle (that is, there is no network traffic, neither in read nor in write) before being closed by the implementation. + Please refer to the `Session` link:{javadoc-url}/org/eclipse/jetty/websocket/api/Session.html[javadocs] for the complete list of configuration APIs. [[pg-websocket-session-send]] diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java index a6f1552301c..50eee426146 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.docs.programming.server.websocket; import java.io.IOException; import java.util.List; +import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -24,6 +25,8 @@ import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.webapp.WebAppContext; @@ -348,4 +351,38 @@ public class WebSocketServerDocs private static class MyOtherJettyWebSocketEndPoint { } + + public void uriTemplatePathSpec() + { + Server server = new Server(8080); + + // tag::uriTemplatePathSpec[] + ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + + // Configure the JettyWebSocketServerContainer. + JettyWebSocketServletContainerInitializer.configure(handler, (servletContext, container) -> + { + container.addMapping("/ws/chat/{room}", (upgradeRequest, upgradeResponse) -> + { + // Retrieve the URI template. + UriTemplatePathSpec pathSpec = (UriTemplatePathSpec)upgradeRequest.getServletAttribute(PathSpec.class.getName()); + + // Match the URI template. + Map params = pathSpec.getPathParams(upgradeRequest.getRequestPath()); + String room = params.get("room"); + + // Create the new WebSocket endpoint with the URI template information. + return new MyWebSocketRoomEndPoint(room); + }); + }); + // end::uriTemplatePathSpec[] + } + + @WebSocket + private static class MyWebSocketRoomEndPoint + { + public MyWebSocketRoomEndPoint(String room) + { + } + } } diff --git a/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java b/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java index 80ec720d288..ccfc5918878 100644 --- a/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java +++ b/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java @@ -22,14 +22,49 @@ public final class WebSocketConstants public static final int SPEC_VERSION = 13; public static final String SPEC_VERSION_STRING = Integer.toString(SPEC_VERSION); + /** + * Max Binary Message Size - The maximum size of a binary message which can be received. + */ public static final int DEFAULT_MAX_BINARY_MESSAGE_SIZE = 64 * 1024; + + /** + * Max Text Message Size - The maximum size of a text message which can be received. + */ public static final int DEFAULT_MAX_TEXT_MESSAGE_SIZE = 64 * 1024; + + /** + * Max Frame Size - The maximum payload size of any WebSocket Frame which can be received. + */ public static final int DEFAULT_MAX_FRAME_SIZE = 64 * 1024; + + /** + * Output Buffer Size - The output (write to network layer) buffer size. This is the raw write operation buffer size and has no relationship to the websocket frame. + */ public static final int DEFAULT_INPUT_BUFFER_SIZE = 4 * 1024; + + /** + * Input Buffer Size - The input (read from network layer) buffer size. This is the raw read operation buffer size, before the parsing of the websocket frames. + */ public static final int DEFAULT_OUTPUT_BUFFER_SIZE = 4 * 1024; + + /** + * Max Outgoing Frames - Set the maximum number of data frames allowed to be waiting to be sent at any one time. + */ public static final int DEFAULT_MAX_OUTGOING_FRAMES = -1; + + /** + * Auto Fragment - If set to true, frames are automatically fragmented to respect the maximum frame size. + */ public static final boolean DEFAULT_AUTO_FRAGMENT = true; + + /** + * Idle Timeout - The duration that a websocket may be idle before being closed by the implementation. + */ public static final Duration DEFAULT_IDLE_TIMEOUT = Duration.ofSeconds(30); + + /** + * Write Timeout - The maximum time a frame may be waiting to be sent. + */ public static final Duration DEFAULT_WRITE_TIMEOUT = Duration.ZERO; /** diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletAttributeTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletAttributeTest.java index 0ef13177509..e86f2cb6da1 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletAttributeTest.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletAttributeTest.java @@ -13,12 +13,20 @@ package org.eclipse.jetty.websocket.tests; +import java.io.IOException; import java.net.URI; +import java.util.Map; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; @@ -28,6 +36,8 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class JettyWebSocketServletAttributeTest @@ -87,4 +97,47 @@ public class JettyWebSocketServletAttributeTest clientEndpoint.session.close(); assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); } + + @Test + public void testPathSpecAttribute() throws Exception + { + start((context, container) -> container.addMapping("uri-template|/{path}", (req, resp) -> + { + UriTemplatePathSpec pathSpec = (UriTemplatePathSpec)req.getServletAttribute(PathSpec.class.getName()); + assertNotNull(pathSpec); + + Map params = pathSpec.getPathParams(req.getRequestPath()); + String path = params.get("path"); + assertNotNull(path); + + return new ParamWebSocketEndPoint(path); + })); + + String param = "world"; + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/" + param); + EventSocket clientEndpoint = new EventSocket(); + try (Session session = client.connect(clientEndpoint, uri).get(5, TimeUnit.SECONDS)) + { + session.getRemote().sendString("hello", WriteCallback.NOOP); + String path = clientEndpoint.textMessages.poll(5, TimeUnit.SECONDS); + assertEquals(param, path); + } + } + + @WebSocket + public static class ParamWebSocketEndPoint + { + private final String param; + + public ParamWebSocketEndPoint(String param) + { + this.param = param; + } + + @OnWebSocketMessage + public void onText(Session session, String text) throws IOException + { + session.getRemote().sendString(param); + } + } }