Fixes #9552 - Jetty 12 - Rewrite of the Jetty WebSocket APIs. (#9652)

* 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 <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-05-02 16:42:40 +02:00 committed by GitHub
parent 7275bf15a9
commit adf5754836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
215 changed files with 3299 additions and 4164 deletions

View File

@ -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]
@ -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.
@ -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.

View File

@ -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:
@ -84,7 +88,7 @@ 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]
@ -92,7 +96,7 @@ Each annotated method may take an optional `Session` argument as its first param
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.
@ -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) { ... }
----
====
@ -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]
----
@ -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]
@ -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.
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 blocking APIs is quite simple:
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.
[source,java,indent=0]
----
include::{doc_code}/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=streamSendBlocking]
----
Streaming sends using non-blocking APIs is more complicated, 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

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.docs.programming;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
@ -22,17 +21,13 @@ import java.time.Duration;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPartialListener;
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@SuppressWarnings("unused")
@ -40,14 +35,14 @@ public class WebSocketDocs
{
@SuppressWarnings("InnerClassMayBeStatic")
// tag::listenerEndpoint[]
public class ListenerEndPoint implements WebSocketListener // <1>
public class ListenerEndPoint implements Session.Listener // <1>
{
private Session session;
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
// The WebSocket connection is established.
// The WebSocket endpoint has been opened.
// Store the session to be able to send data to the remote peer.
this.session = session;
@ -56,13 +51,13 @@ public class WebSocketDocs
session.setMaxTextMessageSize(16 * 1024);
// You may immediately send a message to the remote peer.
session.getRemote().sendString("connected", WriteCallback.NOOP);
session.sendText("connected", Callback.NOOP);
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
// The WebSocket connection is closed.
// The WebSocket endpoint has been closed.
// You may dispose resources.
disposeResources();
@ -71,7 +66,7 @@ public class WebSocketDocs
@Override
public void onWebSocketError(Throwable cause)
{
// The WebSocket connection failed.
// The WebSocket endpoint failed.
// You may log the error.
cause.printStackTrace();
@ -87,11 +82,11 @@ public class WebSocketDocs
// You may echo it back if it matches certain criteria.
if (message.startsWith("echo:"))
session.getRemote().sendString(message.substring("echo:".length()), WriteCallback.NOOP);
session.sendText(message.substring("echo:".length()), Callback.NOOP);
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int length)
public void onWebSocketBinary(ByteBuffer payload, Callback callback)
{
// A WebSocket binary message is received.
@ -99,17 +94,18 @@ public class WebSocketDocs
byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'};
for (int i = 0; i < pngBytes.length; ++i)
{
if (pngBytes[i] != payload[offset + i])
if (pngBytes[i] != payload.get(i))
return;
}
savePNGImage(payload, offset, length);
savePNGImage(payload);
callback.succeed();
}
}
// end::listenerEndpoint[]
@SuppressWarnings("InnerClassMayBeStatic")
// tag::streamingListenerEndpoint[]
public class StreamingListenerEndpoint implements WebSocketPartialListener
public class StreamingListenerEndpoint implements Session.Listener
{
private Path textPath;
@ -121,10 +117,12 @@ public class WebSocketDocs
}
@Override
public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin)
public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin, Callback callback)
{
// Save chunks to file.
appendToFile(payload, fin);
// Complete the callback.
callback.succeed();
}
}
// end::streamingListenerEndpoint[]
@ -136,10 +134,10 @@ public class WebSocketDocs
{
private Session session;
@OnWebSocketConnect // <2>
public void onConnect(Session session)
@OnWebSocketOpen // <2>
public void onOpen(Session session)
{
// The WebSocket connection is established.
// The WebSocket endpoint has been opened.
// Store the session to be able to send data to the remote peer.
this.session = session;
@ -148,13 +146,13 @@ public class WebSocketDocs
session.setMaxTextMessageSize(16 * 1024);
// You may immediately send a message to the remote peer.
session.getRemote().sendString("connected", WriteCallback.NOOP);
session.sendText("connected", Callback.NOOP);
}
@OnWebSocketClose // <3>
public void onClose(int statusCode, String reason)
{
// The WebSocket connection is closed.
// The WebSocket endpoint has been closed.
// You may dispose resources.
disposeResources();
@ -163,7 +161,7 @@ public class WebSocketDocs
@OnWebSocketError // <4>
public void onError(Throwable cause)
{
// The WebSocket connection failed.
// The WebSocket endpoint failed.
// You may log the error.
cause.printStackTrace();
@ -179,11 +177,11 @@ public class WebSocketDocs
// You may echo it back if it matches certain criteria.
if (message.startsWith("echo:"))
session.getRemote().sendString(message.substring("echo:".length()), WriteCallback.NOOP);
session.sendText(message.substring("echo:".length()), Callback.NOOP);
}
@OnWebSocketMessage // <5>
public void onBinaryMessage(byte[] payload, int offset, int length)
public void onBinaryMessage(ByteBuffer payload, Callback callback)
{
// A WebSocket binary message is received.
@ -191,10 +189,10 @@ public class WebSocketDocs
byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'};
for (int i = 0; i < pngBytes.length; ++i)
{
if (pngBytes[i] != payload[offset + i])
if (pngBytes[i] != payload.get(i))
return;
}
savePNGImage(payload, offset, length);
savePNGImage(payload);
}
}
// end::annotatedEndpoint[]
@ -222,10 +220,10 @@ public class WebSocketDocs
@SuppressWarnings("InnerClassMayBeStatic")
// tag::sessionConfigure[]
public class ConfigureEndpoint implements WebSocketListener
public class ConfigureEndpoint implements Session.Listener
{
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
// Configure the max length of incoming messages.
session.setMaxTextMessageSize(16 * 1024);
@ -236,38 +234,6 @@ public class WebSocketDocs
}
// end::sessionConfigure[]
@SuppressWarnings("InnerClassMayBeStatic")
// tag::sendBlocking[]
@WebSocket
public class BlockingSendEndpoint
{
@OnWebSocketMessage
public void onText(Session session, String text)
{
// Obtain the RemoteEndpoint APIs.
RemoteEndpoint remote = session.getRemote();
try
{
// Send textual data to the remote peer.
remote.sendString("data");
// Send binary data to the remote peer.
ByteBuffer bytes = readImageFromFile();
remote.sendBytes(bytes);
// Send a PING frame to the remote peer.
remote.sendPing(ByteBuffer.allocate(8).putLong(NanoTime.now()).flip());
}
catch (IOException x)
{
// No need to rethrow or close the session.
System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send data", x);
}
}
}
// end::sendBlocking[]
@SuppressWarnings("InnerClassMayBeStatic")
// tag::sendNonBlocking[]
@WebSocket
@ -276,27 +242,24 @@ public class WebSocketDocs
@OnWebSocketMessage
public void onText(Session session, String text)
{
// Obtain the RemoteEndpoint APIs.
RemoteEndpoint remote = session.getRemote();
// Send textual data to the remote peer.
remote.sendString("data", new WriteCallback() // <1>
session.sendText("data", new Callback() // <1>
{
@Override
public void writeSuccess()
public void succeed()
{
// Send binary data to the remote peer.
ByteBuffer bytes = readImageFromFile();
remote.sendBytes(bytes, new WriteCallback() // <2>
session.sendBinary(bytes, new Callback() // <2>
{
@Override
public void writeSuccess()
public void succeed()
{
// Both sends succeeded.
}
@Override
public void writeFailed(Throwable x)
public void fail(Throwable x)
{
System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send binary data", x);
}
@ -304,53 +267,18 @@ public class WebSocketDocs
}
@Override
public void writeFailed(Throwable x)
public void fail(Throwable x)
{
// No need to rethrow or close the session.
System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send textual data", x);
}
});
// remote.sendString("wrong", WriteCallback.NOOP); // May throw WritePendingException! <3>
// remote.sendString("wrong", Callback.NOOP); // May throw WritePendingException! <3>
}
}
// end::sendNonBlocking[]
@SuppressWarnings("InnerClassMayBeStatic")
// tag::streamSendBlocking[]
@WebSocket
public class StreamSendBlockingEndpoint
{
@OnWebSocketMessage
public void onText(Session session, String text)
{
try
{
RemoteEndpoint remote = session.getRemote();
while (true)
{
ByteBuffer chunk = readChunkToSend();
if (chunk == null)
{
// No more bytes, finish the WebSocket message.
remote.sendPartialBytes(ByteBuffer.allocate(0), true);
break;
}
else
{
// Send the chunk.
remote.sendPartialBytes(chunk, false);
}
}
}
catch (IOException x)
{
x.printStackTrace();
}
}
}
// end::streamSendBlocking[]
@SuppressWarnings("InnerClassMayBeStatic")
// tag::streamSendNonBlocking[]
@WebSocket
@ -359,18 +287,17 @@ public class WebSocketDocs
@OnWebSocketMessage
public void onText(Session session, String text)
{
RemoteEndpoint remote = session.getRemote();
new Sender(remote).iterate();
new Sender(session).iterate();
}
private class Sender extends IteratingCallback implements WriteCallback // <1>
private class Sender extends IteratingCallback implements Callback // <1>
{
private final RemoteEndpoint remote;
private final Session session;
private boolean finished;
private Sender(RemoteEndpoint remote)
private Sender(Session session)
{
this.remote = remote;
this.session = session;
}
@Override
@ -383,27 +310,27 @@ public class WebSocketDocs
if (chunk == null)
{
// No more bytes, finish the WebSocket message.
remote.sendPartialBytes(ByteBuffer.allocate(0), true, this); // <3>
session.sendPartialBinary(ByteBuffer.allocate(0), true, this); // <3>
finished = true;
return Action.SCHEDULED;
}
else
{
// Send the chunk.
remote.sendPartialBytes(ByteBuffer.allocate(0), false, this); // <3>
session.sendPartialBinary(ByteBuffer.allocate(0), false, this); // <3>
return Action.SCHEDULED;
}
}
@Override
public void writeSuccess()
public void succeed()
{
// When the send succeeds, succeed this IteratingCallback.
succeeded();
}
@Override
public void writeFailed(Throwable x)
public void fail(Throwable x)
{
// When the send fails, fail this IteratingCallback.
failed(x);
@ -420,14 +347,14 @@ public class WebSocketDocs
@SuppressWarnings("InnerClassMayBeStatic")
// tag::pingPongListener[]
public class RoundTripListenerEndpoint implements WebSocketPingPongListener // <1>
public class RoundTripListenerEndpoint implements Session.Listener // <1>
{
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
// Send to the remote peer the local nanoTime.
ByteBuffer buffer = ByteBuffer.allocate(8).putLong(NanoTime.now()).flip();
session.getRemote().sendPing(buffer, WriteCallback.NOOP);
session.sendPing(buffer, Callback.NOOP);
}
@Override
@ -451,7 +378,7 @@ public class WebSocketDocs
public void onText(Session session, String text)
{
if ("close".equalsIgnoreCase(text))
session.close(StatusCode.NORMAL, "bye");
session.close(StatusCode.NORMAL, "bye", Callback.NOOP);
}
}
// end::sessionClose[]
@ -476,7 +403,7 @@ public class WebSocketDocs
{
}
private static void savePNGImage(byte[] payload, int offset, int length)
private static void savePNGImage(ByteBuffer byteBuffer)
{
}

View File

@ -152,13 +152,12 @@ public interface CoreSession extends OutgoingFrames, IncomingFrames, Configurati
void abort();
/**
* Manage flow control by indicating demand for handling Frames. A call to
* {@link FrameHandler#onFrame(Frame, Callback)} will only be made if a
* corresponding demand has been signaled. It is an error to call this method
* if {@link FrameHandler#isAutoDemanding()} returns true.
* <p>Manages flow control by indicating demand for WebSocket frames.</p>
* <p>A call to {@link FrameHandler#onFrame(Frame, Callback)} will only
* be made if there is demand.</p>
*
* @param n The number of frames that can be handled (in sequential calls to
* {@link FrameHandler#onFrame(Frame, Callback)}). May not be negative.
* @param n the number of frames that can be handled in sequential calls to
* {@link FrameHandler#onFrame(Frame, Callback)}, must be positive.
*/
void demand(long n);
@ -235,7 +234,8 @@ public interface CoreSession extends OutgoingFrames, IncomingFrames, Configurati
@Override
public ByteBufferPool getByteBufferPool()
{
return null;
WebSocketComponents components = getWebSocketComponents();
return components != null ? components.getByteBufferPool() : null;
}
@Override

View File

@ -16,102 +16,113 @@ package org.eclipse.jetty.websocket.core;
import org.eclipse.jetty.util.Callback;
/**
* Interface for local WebSocket Endpoint Frame handling.
*
* <p>
* This is the receiver of Parsed Frames. It is implemented by the Application (or Application API layer or Framework)
* as the primary API to/from the Core websocket implementation. The instance to be used for each websocket connection
* is instantiated by the application, either:
* </p>
* <p>Handles incoming WebSocket frames for a given endpoint.</p>
* <p>FrameHandler is the receiver of parsed WebSocket frames.
* It is implemented by application code as the primary API to
* interact with the WebSocket implementation.</p>
* <p>The FrameHandler instance to be used for each WebSocket
* connection is instantiated by the application, either:</p>
* <ul>
* <li>On the server, the application layer must provide a {@code org.eclipse.jetty.websocket.core.server.WebSocketNegotiator} instance
* to negotiate and accept websocket connections, which will return the FrameHandler instance to use from
* {@code org.eclipse.jetty.websocket.core.server.WebSocketNegotiator#negotiate(Negotiation)}.</li>
* <li>On the client, the application returns the FrameHandler instance to user from the {@code ClientUpgradeRequest}
* instance that it passes to the {@code org.eclipse.jetty.websocket.core.client.WebSocketCoreClient#connect(ClientUpgradeRequest)} method/</li>
* <li>On the server, the application must provide a
* {@code WebSocketNegotiator} to negotiate and accept WebSocket
* connections, which will return the FrameHandler instance from
* {@code WebSocketNegotiator#negotiate(Negotiation)}.</li>
* <li>On the client, the application returns the FrameHandler
* instance from the {@code CoreClientUpgradeRequest} instance
* passed to {@code WebSocketCoreClient#connect(ClientUpgradeRequest)}.</li>
* </ul>
* <p>
* Once instantiated the FrameHandler follows is used as follows:
* </p>
* <p>Once instantiated the FrameHandler is used as follows:</p>
* <ul>
* <li>The {@link #onOpen(CoreSession, Callback)} method is called when negotiation of the connection is completed. The passed {@link CoreSession} instance is used
* to obtain information about the connection and to send frames</li>
* <li>Every data and control frame received is passed to {@link #onFrame(Frame, Callback)}.</li>
* <li>Received Control Frames that require a response (eg Ping, Close) are first passed to the {@link #onFrame(Frame, Callback)} to give the
* Application an opportunity to send the response itself. If an appropriate response has not been sent when the callback passed is completed, then a
* response will be generated.</li>
* <li>If an error is detected or received, then {@link #onError(Throwable, Callback)} will be called to inform the application of the cause of the problem.
* The connection will then be closed or aborted and the {@link #onClosed(CloseStatus, Callback)} method called.</li>
* <li>The {@link #onClosed(CloseStatus, Callback)} method is always called once a websocket connection is terminated, either gracefully or not. The error code
* will indicate the nature of the close.</li>
* <li>The {@link #onOpen(CoreSession, Callback)} method is called
* when negotiation of the connection is completed.
* The {@link CoreSession} argument is used to configure the connection,
* to obtain information about the connection, and to send frames</li>
* <li>Every data and control frame received is passed to
* {@link #onFrame(Frame, Callback)}.</li>
* <li>Received control frames that require a response (e.g. PING, CLOSE)
* are first passed to {@link #onFrame(Frame, Callback)} to give the
* application an opportunity to respond explicitly. If a response
* has not been sent when the callback argument is completed, then
* the implementation will generate a response.</li>
* <li>If an error is detected or received, then
* {@link #onError(Throwable, Callback)} will be called to inform
* the application of the cause of the problem.
* The connection will then be closed or aborted and then
* {@link #onClosed(CloseStatus, Callback)} will be called.</li>
* <li>The {@link #onClosed(CloseStatus, Callback)} method is always
* called once a WebSocket connection is terminated, either gracefully
* or not. The error code will indicate the nature of the close.</li>
* </ul>
* <p>FrameHandler is responsible to manage the demand for more
* WebSocket frames, either directly by calling {@link CoreSession#demand(long)}
* or by delegating the demand management to other components.</p>
*/
public interface FrameHandler extends IncomingFrames
{
/**
* Async notification that Connection is being opened.
* <p>
* FrameHandler can write during this call, but can not receive frames until the callback is succeeded.
* </p>
* <p>
* If the FrameHandler succeeds the callback we transition to OPEN state and can now receive frames if auto-demanding,
* or can now call {@link CoreSession#demand(long)} to receive frames if it is not auto-demanding.
* If the FrameHandler fails the callback a close frame will be sent with {@link CloseStatus#SERVER_ERROR} and
* the connection will be closed. <br>
* </p>
* <p>Invoked when the WebSocket connection is opened.</p>
* <p>It is allowed to send WebSocket frames via
* {@link CoreSession#sendFrame(Frame, Callback, boolean)}.
* <p>WebSocket frames cannot be received until a call to
* {@link CoreSession#demand(long)} is made.</p>
* <p>If the callback argument is failed, the implementation
* sends a CLOSE frame with {@link CloseStatus#SERVER_ERROR},
* and the connection will be closed.</p>
*
* @param coreSession the session associated with this connection.
* @param callback the callback to indicate success in processing (or failure)
* @param callback the callback to indicate success or failure of
* the processing of this event.
*/
void onOpen(CoreSession coreSession, Callback callback);
/**
* Receiver of all Frames.
* This method will never be called in parallel for the same session and will be called
* sequentially to satisfy all outstanding demand signaled by calls to
* {@link CoreSession#demand(long)}.
* Control and Data frames are passed to this method.
* Close frames may be responded to by the handler, but if an appropriate close response is not
* sent once the callback is succeeded, then a response close will be generated and sent.
* <p>Invoked when a WebSocket frame is received.</p>
* <p>This method will never be called concurrently for the
* same session; will be called sequentially to satisfy the
* outstanding demand signaled by calls to
* {@link CoreSession#demand(long)}.</p>
* <p>Both control and data frames are passed to this method.</p>
* <p>CLOSE frames may be responded from this method, but if
* they are not responded, then the implementation will respond
* when the callback is completed.</p>
* <p>The callback argument must be completed to indicate
* that the buffers associated with the frame can be recycled.</p>
* <p>Additional WebSocket frames (of any type, including CLOSE
* frames) cannot be received until a call to
* {@link CoreSession#demand(long)} is made.</p>
*
* @param frame the raw frame
* @param callback the callback to indicate success in processing frame (or failure)
* @param frame the WebSocket frame.
* @param callback the callback to indicate success or failure of
* the processing of this event.
*/
void onFrame(Frame frame, Callback callback);
/**
* An error has occurred or been detected in websocket-core and being reported to FrameHandler.
* A call to onError will be followed by a call to {@link #onClosed(CloseStatus, Callback)} giving the close status
* derived from the error. This will not be called more than once, {@link #onClosed(CloseStatus, Callback)}
* <p>Invoked when an error has occurred or has been detected.</p>
* <p>A call to this method will be followed by a call to
* {@link #onClosed(CloseStatus, Callback)} with the close status
* derived from the error.</p>
* <p>This method will not be called more than once, {@link #onClosed(CloseStatus, Callback)}
* will be called on the callback completion.
*
* @param cause the reason for the error
* @param callback the callback to indicate success in processing (or failure)
* @param cause the error cause
* @param callback the callback to indicate success or failure of
* the processing of this event.
*/
void onError(Throwable cause, Callback callback);
/**
* This is the Close Handshake Complete event.
* <p>
* The connection is now closed, no reading or writing is possible anymore.
* Implementations of FrameHandler can cleanup their resources for this connection now.
* This method will be called only once.
* </p>
* <p>Invoked when a WebSocket close event happened.</p>
* <p>The WebSocket connection is closed, no reading or writing
* is possible anymore.</p>
* <p>Implementations of this method may cleanup resources
* that have been allocated.</p>
* <p>This method will not be called more than once.</p>
*
* @param closeStatus the close status received from remote, or in the case of abnormal closure from local.
* @param callback the callback to indicate success in processing (or failure)
* @param closeStatus the close status received from the remote peer,
* or generated locally in the case of abnormal closures.
* @param callback the callback to indicate success or failure of
* the processing of this event.
*/
void onClosed(CloseStatus closeStatus, Callback callback);
/**
* Does the FrameHandler manage it's own demand?
*
* @return true if demand will be managed by an automatic call to demand(1) after every succeeded callback passed to
* {@link #onFrame(Frame, Callback)}. If false the FrameHandler will need to manage its own demand by calling
* {@link CoreSession#demand(long)} when it is willing to receive new Frames.
*/
default boolean isAutoDemanding()
{
return true;
}
}

View File

@ -27,7 +27,7 @@ public interface OutgoingFrames
* layers and extensions present in the implementation.
* <p>
* If you are implementing a mutation, you are obliged to handle
* the incoming WriteCallback appropriately.
* the incoming Callback appropriately.
*
* @param frame the frame to eventually write to the network layer.
* @param callback the callback to notify when the frame is written.

View File

@ -55,7 +55,6 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
private final WebSocketSessionState sessionState = new WebSocketSessionState();
private final FrameHandler handler;
private final Negotiated negotiated;
private final boolean autoDemanding;
private final Flusher flusher = new Flusher(this);
private final ExtensionStack extensionStack;
@ -80,7 +79,6 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
this.handler = handler;
this.behavior = behavior;
this.negotiated = negotiated;
this.autoDemanding = handler.isAutoDemanding();
extensionStack = negotiated.getExtensions();
extensionStack.initialize(new IncomingAdaptor(), new OutgoingAdaptor(), this);
}
@ -113,14 +111,6 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
}
}
/**
* @return True if the sessions handling is demanding.
*/
public boolean isAutoDemanding()
{
return autoDemanding;
}
public ExtensionStack getExtensionStack()
{
return negotiated.getExtensions();
@ -382,21 +372,18 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
if (LOG.isDebugEnabled())
LOG.debug("ConnectionState: Transition to CONNECTED");
Callback openCallback = Callback.from(
() ->
{
sessionState.onOpen();
if (LOG.isDebugEnabled())
LOG.debug("ConnectionState: Transition to OPEN");
if (autoDemanding)
autoDemand();
},
x ->
{
if (LOG.isDebugEnabled())
LOG.debug("Error during OPEN", x);
processHandlerError(new CloseException(CloseStatus.SERVER_ERROR, x), NOOP);
});
Callback openCallback = Callback.from(() ->
{
sessionState.onOpen();
if (LOG.isDebugEnabled())
LOG.debug("ConnectionState: Transition to OPEN");
},
x ->
{
if (LOG.isDebugEnabled())
LOG.debug("Error during OPEN", x);
processHandlerError(new CloseException(CloseStatus.SERVER_ERROR, x), NOOP);
});
try
{
@ -417,16 +404,9 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
@Override
public void demand(long n)
{
if (autoDemanding)
throw new IllegalStateException("FrameHandler is not demanding: " + this);
getExtensionStack().demand(n);
}
public void autoDemand()
{
getExtensionStack().demand(1);
}
@Override
public boolean isRsv1Used()
{
@ -655,13 +635,7 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
// Handle inbound frame
if (frame.getOpCode() != OpCode.CLOSE)
{
Callback handlerCallback = !isAutoDemanding() ? callback : Callback.from(() ->
{
callback.succeeded();
autoDemand();
}, callback::failed);
handle(() -> handler.onFrame(frame, handlerCallback));
handle(() -> handler.onFrame(frame, callback));
return;
}
@ -677,22 +651,21 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
}
else
{
closeCallback = Callback.from(
() ->
closeCallback = Callback.from(() ->
{
if (sessionState.isOutputOpen())
{
if (sessionState.isOutputOpen())
{
CloseStatus closeStatus = CloseStatus.getCloseStatus(frame);
if (LOG.isDebugEnabled())
LOG.debug("ConnectionState: sending close response {}", closeStatus);
close(closeStatus == null ? CloseStatus.NO_CODE_STATUS : closeStatus, callback);
}
else
{
callback.succeeded();
}
},
x -> processHandlerError(x, callback));
CloseStatus closeStatus = CloseStatus.getCloseStatus(frame);
if (LOG.isDebugEnabled())
LOG.debug("ConnectionState: sending close response {}", closeStatus);
close(closeStatus == null ? CloseStatus.NO_CODE_STATUS : closeStatus, callback);
}
else
{
callback.succeeded();
}
},
x -> processHandlerError(x, callback));
}
handler.onFrame(frame, closeCallback);
@ -783,7 +756,7 @@ public class WebSocketCoreSession implements CoreSession, Dumpable
@Override
public String toString()
{
return String.format("WSCoreSession@%x{%s,%s,%s,af=%b,i/o=%d/%d,fs=%d}->%s",
return String.format("WebSocketCoreSession@%x{%s,%s,%s,af=%b,i/o=%d/%d,fs=%d}->%s",
hashCode(),
behavior,
sessionState,

View File

@ -32,11 +32,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A utility implementation of FrameHandler that defragments
* text frames into a String message before calling {@link #onText(String, Callback)}.
* Flow control is by default automatic, but an implementation
* may extend {@link #isAutoDemanding()} to return false and then explicitly control
* demand with calls to {@link CoreSession#demand(long)}.
* <p>A utility implementation of FrameHandler that aggregates TEXT frames
* into a String message before calling {@link #onText(String, Callback)}.
*/
public class MessageHandler implements FrameHandler
{
@ -120,6 +117,7 @@ public class MessageHandler implements FrameHandler
this.coreSession = coreSession;
callback.succeeded();
coreSession.demand(1);
}
@Override
@ -130,30 +128,26 @@ public class MessageHandler implements FrameHandler
switch (frame.getOpCode())
{
case OpCode.CLOSE:
onCloseFrame(frame, callback);
break;
case OpCode.PING:
onPingFrame(frame, callback);
break;
case OpCode.PONG:
onPongFrame(frame, callback);
break;
case OpCode.TEXT:
case OpCode.CLOSE -> onCloseFrame(frame, callback);
case OpCode.PING -> onPingFrame(frame, callback);
case OpCode.PONG -> onPongFrame(frame, callback);
case OpCode.TEXT ->
{
dataType = OpCode.TEXT;
onTextFrame(frame, callback);
break;
case OpCode.BINARY:
}
case OpCode.BINARY ->
{
dataType = OpCode.BINARY;
onBinaryFrame(frame, callback);
break;
case OpCode.CONTINUATION:
}
case OpCode.CONTINUATION ->
{
onContinuationFrame(frame, callback);
if (frame.isFin())
dataType = OpCode.UNDEFINED;
break;
default:
callback.failed(new IllegalStateException());
}
default -> callback.failed(new IllegalStateException());
}
}
@ -199,7 +193,8 @@ public class MessageHandler implements FrameHandler
long currentSize = frame.getPayload().remaining() + textBuffer.length();
if (currentSize > maxSize)
throw new MessageTooLargeException("Message larger than " + maxSize + " bytes");
textBuffer.append(frame.getPayload());
else
textBuffer.append(frame.getPayload());
}
if (frame.isFin())
@ -211,8 +206,11 @@ public class MessageHandler implements FrameHandler
{
if (textBuffer.hasCodingErrors())
throw new BadPayloadException("Invalid UTF-8");
callback.succeeded();
else
callback.succeeded();
}
coreSession.demand(1);
}
catch (Throwable t)
{
@ -232,8 +230,8 @@ public class MessageHandler implements FrameHandler
long currentSize = frame.getPayload().remaining() + binaryBuffer.size();
if (currentSize > maxSize)
throw new MessageTooLargeException("Message larger than " + maxSize + " bytes");
BufferUtil.writeTo(frame.getPayload(), binaryBuffer);
else
BufferUtil.writeTo(frame.getPayload(), binaryBuffer);
}
if (frame.isFin())
@ -245,6 +243,8 @@ public class MessageHandler implements FrameHandler
{
callback.succeeded();
}
coreSession.demand(1);
}
catch (Throwable t)
{
@ -256,27 +256,21 @@ public class MessageHandler implements FrameHandler
{
switch (dataType)
{
case OpCode.BINARY:
onBinaryFrame(frame, callback);
break;
case OpCode.TEXT:
onTextFrame(frame, callback);
break;
default:
throw new IllegalStateException();
case OpCode.BINARY -> onBinaryFrame(frame, callback);
case OpCode.TEXT -> onTextFrame(frame, callback);
default -> throw new IllegalStateException();
}
}
protected void onPingFrame(Frame frame, Callback callback)
{
coreSession.sendFrame(new Frame(OpCode.PONG, true, frame.getPayload()), callback, false);
coreSession.sendFrame(new Frame(OpCode.PONG, true, frame.getPayload()), Callback.from(() -> coreSession.demand(1), callback), false);
}
protected void onPongFrame(Frame frame, Callback callback)
{
callback.succeeded();
coreSession.demand(1);
}
protected void onCloseFrame(Frame frame, Callback callback)
@ -346,7 +340,7 @@ public class MessageHandler implements FrameHandler
int i = 0;
@Override
protected Action process() throws Throwable
protected Action process()
{
if (i + 1 > parts.length)
return Action.SUCCEEDED;
@ -398,7 +392,7 @@ public class MessageHandler implements FrameHandler
int i = 0;
@Override
protected Action process() throws Throwable
protected Action process()
{
if (i + 1 > parts.length)
return Action.SUCCEEDED;

View File

@ -18,14 +18,81 @@ import java.util.Objects;
import org.eclipse.jetty.websocket.core.CoreSession;
/**
* <p>Abstract implementation of {@link MessageSink}.</p>
* <p>Management of demand for WebSocket frames may either be entirely managed
* by the {@link MessageSink} implementation ({@code autoDemand==true}); or
* it may be managed collaboratively between the application and the
* {@link MessageSink} implementation ({@code autoDemand==true}).</p>
* <p>{@link MessageSink} implementations must handle the demand for WebSocket
* frames in this way:</p>
* <ul>
* <li>If {@code autoDemand==false}, the {@link MessageSink} manages the
* demand until the conditions to invoke the application function are met;
* when the {@link MessageSink} invokes the application function, then the
* application is responsible to demand for more WebSocket frames.</li>
* <li>If {@code autoDemand==true}, only the {@link MessageSink} manages the
* demand for WebSocket frames. If the {@link MessageSink} invokes the application
* function, the {@link MessageSink} must demand for WebSocket frames after the
* invocation of the application function returns successfully.</li>
* </ul>
* <p>Method {@link #autoDemand()} helps to manage the demand after the
* invocation of the application function returns successfully.</p>
*/
public abstract class AbstractMessageSink implements MessageSink
{
protected final CoreSession session;
protected final MethodHandle methodHandle;
private final CoreSession session;
private final MethodHandle methodHandle;
private final boolean autoDemand;
public AbstractMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link MessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke
* @param autoDemand whether this {@link MessageSink} manages demand automatically
* as explained in {@link AbstractMessageSink}
*/
public AbstractMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
this.session = Objects.requireNonNull(session, "CoreSession");
this.methodHandle = Objects.requireNonNull(methodHandle, "MethodHandle");
this.autoDemand = autoDemand;
}
/**
* @return the WebSocket session
*/
public CoreSession getCoreSession()
{
return session;
}
/**
* @return the application function
*/
public MethodHandle getMethodHandle()
{
return methodHandle;
}
/**
* @return whether this {@link MessageSink} automatically demands for more
* WebSocket frames after the invocation of the application function has returned.
*/
public boolean isAutoDemand()
{
return autoDemand;
}
/**
* <p>If {@link #isAutoDemand()} then demands for one more WebSocket frame
* via {@link CoreSession#demand(long)}; otherwise it is a no-operation,
* because the demand is explicitly managed by the application function.</p>
*/
protected void autoDemand()
{
if (isAutoDemand())
getCoreSession().demand(1);
}
}

View File

@ -25,22 +25,31 @@ import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.MessageTooLargeException;
/**
* <p>A {@link MessageSink} implementation that accumulates BINARY frames
* into a message that is then delivered to the application function
* passed to the constructor in the form of a {@code byte[]}.</p>
*/
public class ByteArrayMessageSink extends AbstractMessageSink
{
private static final byte[] EMPTY_BUFFER = new byte[0];
private ByteBufferCallbackAccumulator out;
private ByteBufferCallbackAccumulator accumulator;
public ByteArrayMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link ByteArrayMessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke when a new message has been assembled
* @param autoDemand whether this {@link MessageSink} manages demand automatically
*/
public ByteArrayMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
// This uses the offset length byte array signature not supported by jakarta websocket.
// The jakarta layer instead uses decoders for whole byte array messages instead of this message sink.
MethodType onMessageType = MethodType.methodType(Void.TYPE, byte[].class, int.class, int.class);
if (methodHandle.type().changeReturnType(void.class) != onMessageType.changeReturnType(void.class))
{
throw InvalidSignatureException.build(onMessageType, methodHandle.type());
}
}
@Override
@ -48,62 +57,59 @@ public class ByteArrayMessageSink extends AbstractMessageSink
{
try
{
long size = (out == null ? 0 : out.getLength()) + frame.getPayloadLength();
long maxBinaryMessageSize = session.getMaxBinaryMessageSize();
if (maxBinaryMessageSize > 0 && size > maxBinaryMessageSize)
long size = (accumulator == null ? 0 : accumulator.getLength()) + frame.getPayloadLength();
long maxSize = getCoreSession().getMaxBinaryMessageSize();
if (maxSize > 0 && size > maxSize)
{
throw new MessageTooLargeException(
String.format("Binary message too large: (actual) %,d > (configured max binary message size) %,d", size, maxBinaryMessageSize));
}
// If we are fin and no OutputStream has been created we don't need to aggregate.
if (frame.isFin() && (out == null))
{
if (frame.hasPayload())
{
byte[] buf = BufferUtil.toArray(frame.getPayload());
methodHandle.invoke(buf, 0, buf.length);
}
else
methodHandle.invoke(EMPTY_BUFFER, 0, 0);
callback.succeeded();
session.demand(1);
callback.failed(new MessageTooLargeException(String.format("Binary message too large: %,d > %,d", size, maxSize)));
return;
}
// Aggregate the frame payload.
if (frame.hasPayload())
ByteBuffer payload = frame.getPayload();
if (frame.isFin() && accumulator == null)
{
ByteBuffer payload = frame.getPayload();
if (out == null)
out = new ByteBufferCallbackAccumulator();
out.addEntry(payload, callback);
byte[] buf = BufferUtil.toArray(payload);
getMethodHandle().invoke(buf, 0, buf.length);
callback.succeeded();
autoDemand();
return;
}
// If the methodHandle throws we don't want to fail callback twice.
callback = Callback.NOOP;
if (!frame.isFin() && !frame.hasPayload())
{
callback.succeeded();
getCoreSession().demand(1);
return;
}
if (accumulator == null)
accumulator = new ByteBufferCallbackAccumulator();
accumulator.addEntry(payload, callback);
if (frame.isFin())
{
byte[] buf = out.takeByteArray();
methodHandle.invoke(buf, 0, buf.length);
// Do not complete twice the callback if the invocation fails.
callback = Callback.NOOP;
byte[] buf = accumulator.takeByteArray();
getMethodHandle().invoke(buf, 0, buf.length);
autoDemand();
}
else
{
getCoreSession().demand(1);
}
session.demand(1);
}
catch (Throwable t)
{
if (out != null)
out.fail(t);
if (accumulator != null)
accumulator.fail(t);
callback.failed(t);
}
finally
{
if (frame.isFin())
{
// reset
out = null;
}
accumulator = null;
}
}
}

View File

@ -16,32 +16,46 @@ package org.eclipse.jetty.websocket.core.messages;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.ByteBuffer;
import java.util.Objects;
import org.eclipse.jetty.io.ByteBufferCallbackAccumulator;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.MessageTooLargeException;
/**
* <p>A {@link MessageSink} implementation that accumulates BINARY frames
* into a message that is then delivered to the application function
* passed to the constructor in the form of a {@link ByteBuffer}.</p>
*/
public class ByteBufferMessageSink extends AbstractMessageSink
{
private ByteBufferCallbackAccumulator out;
private ByteBufferCallbackAccumulator accumulator;
public ByteBufferMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link ByteBufferMessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke when a new message has been assembled
* @param autoDemand whether this {@link MessageSink} manages demand automatically
*/
public ByteBufferMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
this(session, methodHandle, autoDemand, true);
}
// Validate onMessageMethod
Objects.requireNonNull(methodHandle, "MethodHandle");
MethodType onMessageType = MethodType.methodType(Void.TYPE, ByteBuffer.class);
if (methodHandle.type() != onMessageType)
protected ByteBufferMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand, boolean validateSignature)
{
super(session, methodHandle, autoDemand);
if (validateSignature)
{
throw InvalidSignatureException.build(onMessageType, methodHandle.type());
MethodType onMessageType = MethodType.methodType(Void.TYPE, ByteBuffer.class);
if (methodHandle.type() != onMessageType)
throw InvalidSignatureException.build(onMessageType, methodHandle.type());
}
}
@ -50,69 +64,64 @@ public class ByteBufferMessageSink extends AbstractMessageSink
{
try
{
long size = (out == null ? 0 : out.getLength()) + frame.getPayloadLength();
long maxBinaryMessageSize = session.getMaxBinaryMessageSize();
if (maxBinaryMessageSize > 0 && size > maxBinaryMessageSize)
long size = (accumulator == null ? 0 : accumulator.getLength()) + frame.getPayloadLength();
long maxSize = getCoreSession().getMaxBinaryMessageSize();
if (maxSize > 0 && size > maxSize)
{
throw new MessageTooLargeException(String.format("Binary message too large: (actual) %,d > (configured max binary message size) %,d",
size, maxBinaryMessageSize));
}
// If we are fin and no OutputStream has been created we don't need to aggregate.
if (frame.isFin() && (out == null))
{
if (frame.hasPayload())
methodHandle.invoke(frame.getPayload());
else
methodHandle.invoke(BufferUtil.EMPTY_BUFFER);
callback.succeeded();
session.demand(1);
callback.failed(new MessageTooLargeException(String.format("Binary message too large: %,d > %,d", size, maxSize)));
return;
}
// Aggregate the frame payload.
if (frame.hasPayload())
if (frame.isFin() && accumulator == null)
{
ByteBuffer payload = frame.getPayload();
if (out == null)
out = new ByteBufferCallbackAccumulator();
out.addEntry(payload, callback);
invoke(getMethodHandle(), frame.getPayload(), callback);
autoDemand();
return;
}
// If the methodHandle throws we don't want to fail callback twice.
callback = Callback.NOOP;
if (!frame.isFin() && !frame.hasPayload())
{
callback.succeeded();
getCoreSession().demand(1);
return;
}
if (accumulator == null)
accumulator = new ByteBufferCallbackAccumulator();
accumulator.addEntry(frame.getPayload(), callback);
if (frame.isFin())
{
ByteBufferPool bufferPool = session.getByteBufferPool();
RetainableByteBuffer buffer = bufferPool.acquire(out.getLength(), false);
ByteBufferPool bufferPool = getCoreSession().getByteBufferPool();
RetainableByteBuffer buffer = bufferPool.acquire(accumulator.getLength(), false);
ByteBuffer byteBuffer = buffer.getByteBuffer();
out.writeTo(byteBuffer);
try
{
methodHandle.invoke(byteBuffer);
}
finally
{
buffer.release();
}
accumulator.writeTo(byteBuffer);
callback = Callback.from(buffer::release);
invoke(getMethodHandle(), byteBuffer, callback);
autoDemand();
}
else
{
// Did not call the application so must explicitly demand here.
getCoreSession().demand(1);
}
session.demand(1);
}
catch (Throwable t)
{
if (out != null)
out.fail(t);
if (accumulator != null)
accumulator.fail(t);
callback.failed(t);
}
finally
{
if (frame.isFin())
{
out = null;
}
accumulator = null;
}
}
protected void invoke(MethodHandle methodHandle, ByteBuffer byteBuffer, Callback callback) throws Throwable
{
methodHandle.invoke(byteBuffer);
callback.succeeded();
}
}

View File

@ -14,6 +14,8 @@
package org.eclipse.jetty.websocket.core.messages;
import java.io.Closeable;
import java.io.InputStream;
import java.io.Reader;
import java.lang.invoke.MethodHandle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@ -24,144 +26,89 @@ import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
/**
* Centralized logic for Dispatched Message Handling.
* <p>
* A Dispatched MessageSink can consist of 1 or more {@link #accept(Frame, Callback)} calls.
* <p>
* The first {@link #accept(Frame, Callback)} in a message will trigger a dispatch to the
* function specified in the constructor.
* <p>
* The completion of the dispatched function call is the sign that the next message is suitable
* for processing from the network. (The connection fillAndParse should remain idle for the
* NEXT message until such time as the dispatched function call has completed)
* </p>
* <p>
* There are a few use cases we need to handle.
* </p>
* <p>
* <em>1. Normal Processing</em>
* </p>
* <pre>
* Connection Thread | DispatchedMessageSink | Thread 2
* TEXT accept()
* - dispatch - function.read(stream)
* CONT accept() stream.read()
* CONT accept() stream.read()
* CONT=fin accept() stream.read()
* EOF stream.read EOF
* IDLE
* exit method
* RESUME(NEXT MSG)
* </pre>
* <p>
* <em>2. Early Exit (with no activity)</em>
* </p>
* <pre>
* Connection Thread | DispatchedMessageSink | Thread 2
* TEXT accept()
* - dispatch - function.read(stream)
* CONT accept() exit method (normal return)
* IDLE
* TIMEOUT
* </pre>
* <p>
* <em>3. Early Exit (due to exception)</em>
* </p>
* <pre>
* Connection Thread | DispatchedMessageSink | Thread 2
* TEXT accept()
* - dispatch - function.read(stream)
* CONT accept() exit method (throwable)
* callback.fail()
* endpoint.onError()
* close(error)
* </pre>
* <p>
* <em>4. Early Exit (with Custom Threading)</em>
* </p>
* <pre>
* Connection Thread | DispatchedMessageSink | Thread 2 | Thread 3
* TEXT accept()
* - dispatch - function.read(stream)
* thread.new(stream) stream.read()
* exit method
* CONT accept() stream.read()
* CONT accept() stream.read()
* CONT=fin accept() stream.read()
* EOF stream.read EOF
* RESUME(NEXT MSG)
* </pre>
* <p>A partial implementation of {@link MessageSink} for methods that consume WebSocket
* messages using blocking stream APIs, typically via {@link InputStream} or {@link Reader}.</p>
* <p>The first call to {@link #accept(Frame, Callback)} triggers the application function
* specified in the constructor to be invoked in a different thread.</p>
* <p>Subsequent calls to {@link #accept(Frame, Callback)} feed a nested {@link MessageSink}
* that in turns feeds the {@link InputStream} or {@link Reader} stream.</p>
* <p>Implementations of this class must manage the demand for WebSocket frames, and
* therefore must always be auto-demanding.</p>
* <p>Upon return from the application function, the stream is closed.
* This means that the stream must be consumed synchronously within the invocation of the
* application function.</p>
* <p>The demand for the next WebSocket message is performed when both the application
* function has returned and the last frame has been consumed (signaled by completing the
* callback associated with the frame).</p>
* <p>Throwing from the application function results in the WebSocket connection to be
* closed.</p>
*/
public abstract class DispatchedMessageSink extends AbstractMessageSink
{
private CompletableFuture<Void> dispatchComplete;
private MessageSink typeSink;
private final Executor executor;
private volatile CompletableFuture<Void> dispatchComplete;
private MessageSink typeSink;
public DispatchedMessageSink(CoreSession session, MethodHandle methodHandle)
public DispatchedMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
if (!autoDemand)
throw new IllegalArgumentException("%s must be auto-demanding".formatted(getClass().getSimpleName()));
executor = session.getWebSocketComponents().getExecutor();
}
public abstract MessageSink newSink(Frame frame);
public abstract MessageSink newMessageSink();
public void accept(Frame frame, final Callback callback)
{
if (typeSink == null)
{
typeSink = newSink(frame);
typeSink = newMessageSink();
dispatchComplete = new CompletableFuture<>();
// Dispatch to end user function (will likely start with blocking for data/accept).
// If the MessageSink can be closed do this after invoking and before completing the CompletableFuture.
// Call the endpoint method in a different
// thread, since it will use blocking APIs.
executor.execute(() ->
{
try
{
methodHandle.invoke(typeSink);
if (typeSink instanceof Closeable)
IO.close((Closeable)typeSink);
getMethodHandle().invoke(typeSink);
if (typeSink instanceof Closeable closeable)
IO.close(closeable);
dispatchComplete.complete(null);
}
catch (Throwable throwable)
{
if (typeSink instanceof Closeable)
IO.close((Closeable)typeSink);
typeSink.fail(throwable);
dispatchComplete.completeExceptionally(throwable);
}
});
}
Callback frameCallback;
Callback frameCallback = callback;
if (frame.isFin())
{
// This is the final frame we should wait for the frame callback and the dispatched thread.
Callback.Completable finComplete = Callback.Completable.from(callback);
frameCallback = finComplete;
CompletableFuture.allOf(dispatchComplete, finComplete).whenComplete((aVoid, throwable) ->
// Wait for both the frame callback and the dispatched thread.
Callback.Completable frameComplete = Callback.Completable.from(callback);
frameCallback = frameComplete;
CompletableFuture.allOf(dispatchComplete, frameComplete).whenComplete((result, failure) ->
{
typeSink = null;
dispatchComplete = null;
if (throwable == null)
session.demand(1);
// The nested MessageSink manages the demand until the last
// frame, while this MessageSink manages the demand when both
// the last frame and the dispatched thread are completed.
if (failure == null)
autoDemand();
});
}
else
{
frameCallback = new Callback.Nested(callback)
{
@Override
public void succeeded()
{
super.succeeded();
session.demand(1);
}
};
}
typeSink.accept(frame, frameCallback);
}
public boolean isDispatched()
{
return dispatchComplete != null;
}
}

View File

@ -16,18 +16,17 @@ package org.eclipse.jetty.websocket.core.messages;
import java.lang.invoke.MethodHandle;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
public class InputStreamMessageSink extends DispatchedMessageSink
{
public InputStreamMessageSink(CoreSession session, MethodHandle methodHandle)
public InputStreamMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
}
@Override
public MessageSink newSink(Frame frame)
public MessageSink newMessageSink()
{
return new MessageInputStream();
return new MessageInputStream(getCoreSession());
}
}

View File

@ -17,14 +17,15 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -38,43 +39,59 @@ import org.slf4j.LoggerFactory;
public class MessageInputStream extends InputStream implements MessageSink
{
private static final Logger LOG = LoggerFactory.getLogger(MessageInputStream.class);
private static final Entry EOF = new Entry(BufferUtil.EMPTY_BUFFER, Callback.NOOP);
private static final Entry CLOSED = new Entry(BufferUtil.EMPTY_BUFFER, Callback.NOOP);
private static final Entry EOF = new Entry(null, Callback.NOOP);
private static final Entry CLOSED = new Entry(null, Callback.NOOP);
private static final Entry FAILED = new Entry(null, Callback.NOOP);
private final AutoLock lock = new AutoLock();
private final BlockingArrayQueue<Entry> buffers = new BlockingArrayQueue<>();
private boolean closed = false;
private final AutoLock.WithCondition lock = new AutoLock.WithCondition();
private final ArrayDeque<Entry> buffers = new ArrayDeque<>();
private final CoreSession session;
private Entry currentEntry;
private Throwable failure;
private boolean closed;
private long timeoutMs = -1;
public MessageInputStream(CoreSession session)
{
this.session = session;
}
@Override
public void accept(Frame frame, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("accepting {}", frame);
boolean succeed = false;
try (AutoLock l = lock.lock())
if (!frame.isFin() && !frame.hasPayload())
{
// If closed or we have no payload, request the next frame.
if (closed || (!frame.hasPayload() && !frame.isFin()))
callback.succeeded();
session.demand(1);
return;
}
Runnable action = null;
try (AutoLock.WithCondition l = lock.lock())
{
if (failure != null)
{
succeed = true;
Throwable cause = failure;
action = () -> callback.failed(cause);
}
else if (closed)
{
action = callback::succeeded;
}
else
{
if (frame.hasPayload())
buffers.add(new Entry(frame.getPayload(), callback));
else
succeed = true;
buffers.offer(new Entry(frame, callback));
if (frame.isFin())
buffers.add(EOF);
buffers.offer(EOF);
}
l.signal();
}
if (succeed)
callback.succeeded();
if (action != null)
action.run();
}
@Override
@ -93,7 +110,7 @@ public class MessageInputStream extends InputStream implements MessageSink
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException
public int read(byte[] b, int off, int len) throws IOException
{
ByteBuffer buffer = ByteBuffer.wrap(b, off, len).slice();
BufferUtil.clear(buffer);
@ -106,6 +123,9 @@ public class MessageInputStream extends InputStream implements MessageSink
if (LOG.isDebugEnabled())
LOG.debug("currentEntry = {}", currentEntry);
if (currentEntry == FAILED)
throw IO.rethrow(getFailure());
if (currentEntry == CLOSED)
throw new IOException("Closed");
@ -116,49 +136,79 @@ public class MessageInputStream extends InputStream implements MessageSink
return -1;
}
// We have content.
int fillLen = BufferUtil.append(buffer, currentEntry.buffer);
if (!currentEntry.buffer.hasRemaining())
ByteBuffer payload = currentEntry.frame.getPayload();
if (currentEntry.frame.isFin() && !payload.hasRemaining())
{
succeedCurrentEntry();
// Recurse to avoid returning 0, as now EOF will be found.
return read(buffer);
}
int length = BufferUtil.append(buffer, payload);
if (!payload.hasRemaining())
succeedCurrentEntry();
// Return number of bytes actually copied into buffer.
// Return number of bytes copied into the buffer.
if (LOG.isDebugEnabled())
LOG.debug("filled {} bytes from {}", fillLen, currentEntry);
return fillLen;
LOG.debug("filled {} bytes from {}", length, currentEntry);
return length;
}
@Override
public void close() throws IOException
public void fail(Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("fail()", failure);
ArrayList<Entry> entries = new ArrayList<>();
try (AutoLock.WithCondition l = lock.lock())
{
if (this.failure != null)
return;
this.failure = failure;
drainInto(entries);
buffers.offer(FAILED);
l.signal();
}
entries.forEach(e -> e.callback.failed(failure));
}
@Override
public void close()
{
if (LOG.isDebugEnabled())
LOG.debug("close()");
ArrayList<Entry> entries = new ArrayList<>();
try (AutoLock l = lock.lock())
try (AutoLock.WithCondition l = lock.lock())
{
if (closed)
return;
closed = true;
if (currentEntry != null)
{
entries.add(currentEntry);
currentEntry = null;
}
// Clear queue and fail all entries.
entries.addAll(buffers);
buffers.clear();
drainInto(entries);
buffers.offer(CLOSED);
l.signal();
}
// Succeed all entries as we don't need them anymore (failing would close the connection).
for (Entry e : entries)
entries.forEach(e -> e.callback.succeeded());
}
private void drainInto(ArrayList<Entry> entries)
{
assert lock.isHeldByCurrentThread();
if (currentEntry != null)
{
e.callback.succeeded();
entries.add(currentEntry);
currentEntry = null;
}
super.close();
// Drain the queue.
entries.addAll(buffers);
buffers.clear();
}
public void setTimeout(long timeoutMs)
@ -169,47 +219,44 @@ public class MessageInputStream extends InputStream implements MessageSink
private void succeedCurrentEntry()
{
Entry current;
try (AutoLock l = lock.lock())
try (AutoLock ignored = lock.lock())
{
current = currentEntry;
currentEntry = null;
}
if (current != null)
{
current.callback.succeeded();
if (!current.frame.isFin())
session.demand(1);
}
}
private Entry getCurrentEntry() throws IOException
{
try (AutoLock l = lock.lock())
try (AutoLock.WithCondition l = lock.lock())
{
if (currentEntry != null)
return currentEntry;
}
try
{
long timeout = timeoutMs;
if (LOG.isDebugEnabled())
LOG.debug("Waiting {} ms to read", timeoutMs);
LOG.debug("Waiting {} ms to read", timeout);
Entry result;
if (timeoutMs < 0)
while (true)
{
// Wait forever until a buffer is available.
result = buffers.take();
}
else
{
// Wait at most for the given timeout.
result = buffers.poll(timeoutMs, TimeUnit.MILLISECONDS);
if (result == null)
throw new IOException(String.format("Read timeout: %,dms expired", timeoutMs));
result = buffers.poll();
if (result != null)
break;
if (timeout < 0)
l.await();
else if (!l.await(timeout, TimeUnit.MILLISECONDS))
throw new IOException(String.format("Read timeout: %,dms expired", timeout));
}
try (AutoLock l = lock.lock())
{
currentEntry = result;
return currentEntry;
}
return currentEntry = result;
}
catch (InterruptedException e)
{
@ -218,21 +265,15 @@ public class MessageInputStream extends InputStream implements MessageSink
}
}
private static class Entry
private Throwable getFailure()
{
public ByteBuffer buffer;
public Callback callback;
public Entry(ByteBuffer buffer, Callback callback)
try (AutoLock ignored = lock.lock())
{
this.buffer = Objects.requireNonNull(buffer);
this.callback = callback;
}
@Override
public String toString()
{
return String.format("Entry[%s,%s]", BufferUtil.toDetailString(buffer), callback.getClass().getSimpleName());
return failure;
}
}
private record Entry(Frame frame, Callback callback)
{
}
}

View File

@ -24,8 +24,8 @@ import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.WebSocketConstants;
import static java.nio.charset.StandardCharsets.UTF_8;
@ -36,49 +36,45 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/
public class MessageReader extends Reader implements MessageSink
{
private static final int BUFFER_SIZE = WebSocketConstants.DEFAULT_INPUT_BUFFER_SIZE;
private final ByteBuffer buffer;
private final MessageInputStream stream;
private final CharsetDecoder utf8Decoder = UTF_8.newDecoder()
.onUnmappableCharacter(CodingErrorAction.REPORT)
.onMalformedInput(CodingErrorAction.REPORT);
public MessageReader()
public MessageReader(CoreSession coreSession)
{
this(BUFFER_SIZE);
}
public MessageReader(int bufferSize)
{
this.stream = new MessageInputStream();
this.buffer = BufferUtil.allocate(bufferSize);
this.stream = new MessageInputStream(coreSession);
this.buffer = BufferUtil.allocate(coreSession.getInputBufferSize());
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException
public int read(char[] chars, int off, int len) throws IOException
{
CharBuffer charBuffer = CharBuffer.wrap(cbuf, off, len);
boolean endOfInput = false;
CharBuffer charBuffer = CharBuffer.wrap(chars, off, len);
boolean eof;
while (true)
{
int read = stream.read(buffer);
if (read == 0)
eof = read < 0;
if (eof || read == 0)
break;
if (read < 0)
{
endOfInput = true;
break;
}
}
CoderResult result = utf8Decoder.decode(buffer, charBuffer, endOfInput);
CoderResult result = utf8Decoder.decode(buffer, charBuffer, eof);
if (result.isError())
result.throwException();
if (endOfInput && (charBuffer.position() == 0))
if (eof && charBuffer.position() == off)
return -1;
return charBuffer.position();
return charBuffer.position() - off;
}
@Override
public void fail(Throwable failure)
{
stream.fail(failure);
}
@Override

View File

@ -14,19 +14,38 @@
package org.eclipse.jetty.websocket.core.messages;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.FrameHandler;
/**
* Sink consumer for messages (used for multiple frames with continuations,
* and also to allow for streaming APIs)
* <p>A consumer of WebSocket data frames (either BINARY or TEXT).</p>
* <p>{@link FrameHandler} delegates the processing of data frames
* to {@link MessageSink}, including the processing of the demand
* for the next frames.</p>
*/
public interface MessageSink
{
/**
* Consume the frame payload to the message.
* <p>Consumes the WebSocket frame, possibly asynchronously
* when this method has returned.</p>
* <p>The callback argument must be completed when the frame
* payload is consumed.</p>
* <p>The demand for more frames must be explicitly invoked,
* or arranged to be invoked asynchronously, by the implementation
* of this method, by calling {@link CoreSession#demand(long)}.</p>
*
* @param frame the frame, its payload (and fin state) to append
* @param callback the callback for how the frame was consumed
* @param frame the frame to consume
* @param callback the callback to complete when the frame is consumed
*/
void accept(Frame frame, Callback callback);
/**
* <p>Fails this {@link MessageSink} with the given cause.</p>
*
* @param failure the cause of the failure
*/
default void fail(Throwable failure)
{
}
}

View File

@ -20,13 +20,23 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
/**
* <p>A {@link MessageSink} implementation that delivers BINARY frames
* to the application function passed to the constructor in the form
* of a {@code byte[]}.</p>
*/
public class PartialByteArrayMessageSink extends AbstractMessageSink
{
private static byte[] EMPTY_BUFFER = new byte[0];
public PartialByteArrayMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link PartialByteArrayMessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke when a new frame has arrived
* @param autoDemand whether this {@link MessageSink} manages demand automatically
*/
public PartialByteArrayMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
}
@Override
@ -36,12 +46,16 @@ public class PartialByteArrayMessageSink extends AbstractMessageSink
{
if (frame.hasPayload() || frame.isFin())
{
byte[] buffer = frame.hasPayload() ? BufferUtil.toArray(frame.getPayload()) : EMPTY_BUFFER;
methodHandle.invoke(buffer, frame.isFin());
byte[] buffer = BufferUtil.toArray(frame.getPayload());
getMethodHandle().invoke(buffer, frame.isFin());
callback.succeeded();
autoDemand();
}
else
{
callback.succeeded();
getCoreSession().demand(1);
}
callback.succeeded();
session.demand(1);
}
catch (Throwable t)
{

View File

@ -14,16 +14,29 @@
package org.eclipse.jetty.websocket.core.messages;
import java.lang.invoke.MethodHandle;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
/**
* <p>A {@link MessageSink} implementation that delivers BINARY frames
* to the application function passed to the constructor in the form
* of a {@link ByteBuffer}.</p>
*/
public class PartialByteBufferMessageSink extends AbstractMessageSink
{
public PartialByteBufferMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link PartialByteBufferMessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke when a new frame has arrived
* @param autoDemand whether this {@link MessageSink} manages demand automatically
*/
public PartialByteBufferMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
}
@Override
@ -32,14 +45,25 @@ public class PartialByteBufferMessageSink extends AbstractMessageSink
try
{
if (frame.hasPayload() || frame.isFin())
methodHandle.invoke(frame.getPayload(), frame.isFin());
callback.succeeded();
session.demand(1);
{
invoke(getMethodHandle(), frame.getPayload(), frame.isFin(), callback);
autoDemand();
}
else
{
callback.succeeded();
getCoreSession().demand(1);
}
}
catch (Throwable t)
{
callback.failed(t);
}
}
protected void invoke(MethodHandle methodHandle, ByteBuffer byteBuffer, boolean fin, Callback callback) throws Throwable
{
methodHandle.invoke(byteBuffer, fin);
callback.succeeded();
}
}

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.websocket.core.messages;
import java.lang.invoke.MethodHandle;
import java.util.Objects;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Utf8StringBuilder;
@ -22,14 +21,25 @@ import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.exception.BadPayloadException;
/**
* <p>A {@link MessageSink} implementation that delivers TEXT frames
* to the application function passed to the constructor in the form
* of a {@link String}.</p>
*/
public class PartialStringMessageSink extends AbstractMessageSink
{
private Utf8StringBuilder out;
private Utf8StringBuilder accumulator;
public PartialStringMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link PartialStringMessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke when a new frame has arrived
* @param autoDemand whether this {@link MessageSink} manages demand automatically
*/
public PartialStringMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
Objects.requireNonNull(methodHandle, "MethodHandle");
super(session, methodHandle, autoDemand);
}
@Override
@ -37,28 +47,34 @@ public class PartialStringMessageSink extends AbstractMessageSink
{
try
{
if (out == null)
out = new Utf8StringBuilder(session.getInputBufferSize());
if (accumulator == null)
accumulator = new Utf8StringBuilder(getCoreSession().getInputBufferSize());
accumulator.append(frame.getPayload());
out.append(frame.getPayload());
if (frame.isFin())
{
String complete = out.takeCompleteString(() -> new BadPayloadException("Invalid UTF-8"));
methodHandle.invoke(complete, true);
out = null;
String complete = accumulator.takeCompleteString(() -> new BadPayloadException("Invalid UTF-8"));
getMethodHandle().invoke(complete, true);
}
else
{
String partial = out.takePartialString(() -> new BadPayloadException("Invalid UTF-8"));
methodHandle.invoke(partial, false);
String partial = accumulator.takePartialString(() -> new BadPayloadException("Invalid UTF-8"));
getMethodHandle().invoke(partial, false);
}
callback.succeeded();
session.demand(1);
autoDemand();
}
catch (Throwable t)
{
callback.failed(t);
}
finally
{
if (frame.isFin())
accumulator = null;
}
}
}

View File

@ -16,18 +16,17 @@ package org.eclipse.jetty.websocket.core.messages;
import java.lang.invoke.MethodHandle;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
public class ReaderMessageSink extends DispatchedMessageSink
{
public ReaderMessageSink(CoreSession session, MethodHandle methodHandle)
public ReaderMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
}
@Override
public MessageReader newSink(Frame frame)
public MessageReader newMessageSink()
{
return new MessageReader(session.getInputBufferSize());
return new MessageReader(getCoreSession());
}
}

View File

@ -22,14 +22,26 @@ import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.exception.BadPayloadException;
import org.eclipse.jetty.websocket.core.exception.MessageTooLargeException;
/**
* <p>A {@link MessageSink} implementation that accumulates TEXT frames
* into a message that is then delivered to the application function
* passed to the constructor in the form of a {@link String}.</p>
*/
public class StringMessageSink extends AbstractMessageSink
{
private Utf8StringBuilder out;
private int size;
public StringMessageSink(CoreSession session, MethodHandle methodHandle)
/**
* Creates a new {@link StringMessageSink}.
*
* @param session the WebSocket session
* @param methodHandle the application function to invoke when a new message has been assembled
* @param autoDemand whether this {@link MessageSink} manages demand automatically
*/
public StringMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle);
super(session, methodHandle, autoDemand);
this.size = 0;
}
@ -39,23 +51,29 @@ public class StringMessageSink extends AbstractMessageSink
try
{
size += frame.getPayloadLength();
long maxTextMessageSize = session.getMaxTextMessageSize();
if (maxTextMessageSize > 0 && size > maxTextMessageSize)
long maxSize = getCoreSession().getMaxTextMessageSize();
if (maxSize > 0 && size > maxSize)
{
throw new MessageTooLargeException(String.format("Text message too large: (actual) %,d > (configured max text message size) %,d",
size, maxTextMessageSize));
callback.failed(new MessageTooLargeException(String.format("Text message too large: %,d > %,d", size, maxSize)));
return;
}
if (out == null)
out = new Utf8StringBuilder(session.getInputBufferSize());
out = new Utf8StringBuilder(getCoreSession().getInputBufferSize());
out.append(frame.getPayload());
if (frame.isFin())
{
methodHandle.invoke(out.takeCompleteString(() -> new BadPayloadException("Invalid UTF-8")));
getMethodHandle().invoke(out.takeCompleteString(() -> new BadPayloadException("Invalid UTF-8")));
callback.succeeded();
autoDemand();
}
else
{
callback.succeeded();
getCoreSession().demand(1);
}
callback.succeeded();
session.demand(1);
}
catch (Throwable t)
{
@ -65,7 +83,6 @@ public class StringMessageSink extends AbstractMessageSink
{
if (frame.isFin())
{
// reset
size = 0;
out = null;
}

View File

@ -74,13 +74,13 @@ public class AutoFragmentTest
// Turn off fragmentation on the server.
assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS));
serverHandler.coreSession.setMaxFrameSize(0);
serverHandler.coreSession.setAutoFragment(false);
serverHandler.getCoreSession().setMaxFrameSize(0);
serverHandler.getCoreSession().setAutoFragment(false);
// Set the client to fragment to the maxFrameSize.
int maxFrameSize = 30;
clientHandler.coreSession.setMaxFrameSize(maxFrameSize);
clientHandler.coreSession.setAutoFragment(true);
clientHandler.getCoreSession().setMaxFrameSize(maxFrameSize);
clientHandler.getCoreSession().setAutoFragment(true);
// Send a message which is too large.
int size = maxFrameSize * 2;
@ -88,7 +88,7 @@ public class AutoFragmentTest
Arrays.fill(array, 0, size, (byte)'X');
ByteBuffer message = BufferUtil.toBuffer(array);
Frame sentFrame = new Frame(OpCode.BINARY, BufferUtil.copy(message));
clientHandler.coreSession.sendFrame(sentFrame, Callback.NOOP, false);
clientHandler.getCoreSession().sendFrame(sentFrame, Callback.NOOP, false);
// We should not receive any frames larger than the max frame size.
// So our message should be split into two frames.
@ -121,20 +121,20 @@ public class AutoFragmentTest
connect.get(5, TimeUnit.SECONDS);
// Turn off fragmentation on the client.
clientHandler.coreSession.setMaxFrameSize(0);
clientHandler.coreSession.setAutoFragment(false);
clientHandler.getCoreSession().setMaxFrameSize(0);
clientHandler.getCoreSession().setAutoFragment(false);
// Set the server should fragment to the maxFrameSize.
int maxFrameSize = 30;
assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS));
serverHandler.coreSession.setMaxFrameSize(maxFrameSize);
serverHandler.coreSession.setAutoFragment(true);
serverHandler.getCoreSession().setMaxFrameSize(maxFrameSize);
serverHandler.getCoreSession().setAutoFragment(true);
// Send a message which is too large.
int size = maxFrameSize * 2;
byte[] message = new byte[size];
Arrays.fill(message, 0, size, (byte)'X');
clientHandler.coreSession.sendFrame(new Frame(OpCode.BINARY, BufferUtil.toBuffer(message)), Callback.NOOP, false);
clientHandler.getCoreSession().sendFrame(new Frame(OpCode.BINARY, BufferUtil.toBuffer(message)), Callback.NOOP, false);
// We should not receive any frames larger than the max frame size.
// So our message should be split into two frames.
@ -166,14 +166,14 @@ public class AutoFragmentTest
connect.get(5, TimeUnit.SECONDS);
// Turn off fragmentation on the client.
clientHandler.coreSession.setMaxFrameSize(0);
clientHandler.coreSession.setAutoFragment(false);
clientHandler.getCoreSession().setMaxFrameSize(0);
clientHandler.getCoreSession().setAutoFragment(false);
// Set a small maxFrameSize on the server.
int maxFrameSize = 10;
assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS));
serverHandler.coreSession.setMaxFrameSize(maxFrameSize);
serverHandler.coreSession.setAutoFragment(true);
serverHandler.getCoreSession().setMaxFrameSize(maxFrameSize);
serverHandler.getCoreSession().setAutoFragment(true);
// Generate a large random payload.
int payloadSize = 1000;
@ -187,7 +187,7 @@ public class AutoFragmentTest
BufferUtil.flipToFlush(payload, 0);
// Send the large random payload which should be fragmented on the server.
clientHandler.coreSession.sendFrame(new Frame(OpCode.BINARY, BufferUtil.copy(payload)), Callback.NOOP, false);
clientHandler.getCoreSession().sendFrame(new Frame(OpCode.BINARY, BufferUtil.copy(payload)), Callback.NOOP, false);
// Assemble the message from the fragmented frames.
ByteBuffer message = BufferUtil.allocate(payloadSize * 2);
@ -219,14 +219,14 @@ public class AutoFragmentTest
connect.get(5, TimeUnit.SECONDS);
// Turn off fragmentation on the client.
clientHandler.coreSession.setMaxFrameSize(0);
clientHandler.coreSession.setAutoFragment(false);
clientHandler.getCoreSession().setMaxFrameSize(0);
clientHandler.getCoreSession().setAutoFragment(false);
// Set a small maxFrameSize on the server.
int maxFrameSize = 1024;
assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS));
serverHandler.coreSession.setMaxFrameSize(maxFrameSize);
serverHandler.coreSession.setAutoFragment(true);
serverHandler.getCoreSession().setMaxFrameSize(maxFrameSize);
serverHandler.getCoreSession().setAutoFragment(true);
// Highly compressible payload.
byte[] data = new byte[512 * 1024];
@ -234,7 +234,7 @@ public class AutoFragmentTest
ByteBuffer payload = ByteBuffer.wrap(data);
// Send the payload which should be fragmented on the server.
clientHandler.coreSession.sendFrame(new Frame(OpCode.BINARY, BufferUtil.copy(payload)), Callback.NOOP, false);
clientHandler.getCoreSession().sendFrame(new Frame(OpCode.BINARY, BufferUtil.copy(payload)), Callback.NOOP, false);
// Assemble the message from the fragmented frames.
ByteBuffer message = BufferUtil.allocate(payload.remaining() * 2);
@ -282,13 +282,13 @@ public class AutoFragmentTest
connect.get(5, TimeUnit.SECONDS);
// Turn off fragmentation on the client.
clientHandler.coreSession.setMaxFrameSize(0);
clientHandler.coreSession.setAutoFragment(false);
clientHandler.getCoreSession().setMaxFrameSize(0);
clientHandler.getCoreSession().setAutoFragment(false);
// Set maxFrameSize and autoFragment on the server.
assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS));
serverHandler.coreSession.setMaxFrameSize(maxFrameSize);
serverHandler.coreSession.setAutoFragment(true);
serverHandler.getCoreSession().setMaxFrameSize(maxFrameSize);
serverHandler.getCoreSession().setAutoFragment(true);
// Send the payload which should be fragmented by the server permessage-deflate.
ByteBuffer sendPayload = BufferUtil.copy(payload);

View File

@ -83,7 +83,6 @@ public class DemandTest
public void onError(Throwable cause, Callback callback)
{
callback.succeeded();
_coreSession.demand(1);
}
@Override
@ -91,12 +90,6 @@ public class DemandTest
{
callback.succeeded();
}
@Override
public boolean isAutoDemanding()
{
return false;
}
}
@Test

View File

@ -33,8 +33,7 @@ public class DemandingIncomingFramesCapture extends IncomingFramesCapture
}
finally
{
if (_coreSession.isAutoDemanding())
_coreSession.autoDemand();
_coreSession.demand(1);
}
}
}

View File

@ -55,5 +55,7 @@ public class EchoFrameHandler extends TestAsyncFrameHandler
{
callback.succeeded();
}
coreSession.demand(1);
}
}

View File

@ -85,8 +85,8 @@ public class FrameBufferTest extends WebSocketTester
TestFrameHandler clientHandler = new TestFrameHandler();
client.connect(clientHandler, server.getUri()).get(5, TimeUnit.SECONDS);
serverHandler.open.await(5, TimeUnit.SECONDS);
clientHandler.coreSession.setAutoFragment(false);
serverHandler.coreSession.setAutoFragment(false);
clientHandler.getCoreSession().setAutoFragment(false);
serverHandler.getCoreSession().setAutoFragment(false);
int payloadLen = 32 * 1024;
byte[] array = new byte[payloadLen];

View File

@ -46,7 +46,6 @@ public class MessageHandlerTest
static byte[] fourByteUtf8Bytes = fourByteUtf8String.getBytes(StandardCharsets.UTF_8);
static byte[] nonUtf8Bytes = {0x7F, (byte)0xFF, (byte)0xFF};
boolean autoDemanding;
CoreSession coreSession;
List<String> textMessages = new ArrayList<>();
List<ByteBuffer> binaryMessages = new ArrayList<>();
@ -55,10 +54,8 @@ public class MessageHandlerTest
MessageHandler handler;
@BeforeEach
public void beforeEach() throws Exception
public void beforeEach()
{
autoDemanding = true;
coreSession = new CoreSession.Empty()
{
private final ByteBufferPool bufferPool = new ArrayByteBufferPool();
@ -92,12 +89,6 @@ public class MessageHandlerTest
binaryMessages.add(message);
callbacks.add(callback);
}
@Override
public boolean isAutoDemanding()
{
return autoDemanding;
}
};
handler.onOpen(coreSession, NOOP);

View File

@ -51,6 +51,7 @@ public class TestAsyncFrameHandler implements FrameHandler
LOG.debug("[{}] onOpen {}", name, coreSession);
this.coreSession = coreSession;
callback.succeeded();
coreSession.demand(1);
openLatch.countDown();
}
@ -61,6 +62,7 @@ public class TestAsyncFrameHandler implements FrameHandler
LOG.debug("[{}] onFrame {}", name, frame);
receivedFrames.offer(Frame.copy(frame));
callback.succeeded();
coreSession.demand(1);
}
@Override

View File

@ -56,6 +56,7 @@ public class TestFrameHandler implements SynchronousFrameHandler
if (LOG.isDebugEnabled())
LOG.debug("onOpen {}", coreSession);
this.coreSession = coreSession;
demand();
open.countDown();
}
@ -65,6 +66,12 @@ public class TestFrameHandler implements SynchronousFrameHandler
if (LOG.isDebugEnabled())
LOG.debug("onFrame: " + OpCode.name(frame.getOpCode()) + ":" + BufferUtil.toDetailString(frame.getPayload()));
receivedFrames.offer(Frame.copy(frame));
demand();
}
protected void demand()
{
coreSession.demand(1);
}
@Override

View File

@ -27,7 +27,6 @@ public class TestMessageHandler extends MessageHandler
{
protected static final Logger LOG = LoggerFactory.getLogger(TestMessageHandler.class);
public CoreSession coreSession;
public BlockingQueue<String> textMessages = new BlockingArrayQueue<>();
public BlockingQueue<ByteBuffer> binaryMessages = new BlockingArrayQueue<>();
public CloseStatus closeStatus;
@ -40,7 +39,6 @@ public class TestMessageHandler extends MessageHandler
public void onOpen(CoreSession coreSession, Callback callback)
{
super.onOpen(coreSession, callback);
this.coreSession = coreSession;
openLatch.countDown();
}

View File

@ -546,8 +546,8 @@ public class WebSocketCloseTest extends WebSocketTester
LOG.debug("onOpen {}", coreSession);
this.coreSession = coreSession;
state = this.coreSession.toString();
opened.countDown();
callback.succeeded();
opened.countDown();
}
@Override
@ -582,11 +582,5 @@ public class WebSocketCloseTest extends WebSocketTester
state = coreSession.toString();
callback.succeeded();
}
@Override
public boolean isAutoDemanding()
{
return false;
}
}
}

View File

@ -484,7 +484,7 @@ public class WebSocketNegotiationTest extends WebSocketTester
// The list of Extensions on the client contains the internal Extensions.
StringBuilder negotiatedExtensions = new StringBuilder();
List<Extension> extensions = ((WebSocketCoreSession)clientHandler.coreSession).getExtensionStack().getExtensions();
List<Extension> extensions = ((WebSocketCoreSession)clientHandler.getCoreSession()).getExtensionStack().getExtensions();
for (int i = 0; i < extensions.size(); i++)
{
negotiatedExtensions.append(extensions.get(i).getConfig().getParameterizedName());

View File

@ -16,14 +16,12 @@ package org.eclipse.jetty.websocket.core;
import java.net.Socket;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.BiConsumer;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.util.Callback.NOOP;
import static org.hamcrest.MatcherAssert.assertThat;
@ -40,10 +38,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class WebSocketOpenTest extends WebSocketTester
{
private static final Logger LOG = LoggerFactory.getLogger(WebSocketOpenTest.class);
private WebSocketServer server;
private DemandingAsyncFrameHandler serverHandler;
private NonDemandingAsyncFrameHandler serverHandler;
private Socket client;
@AfterEach
@ -53,9 +49,9 @@ public class WebSocketOpenTest extends WebSocketTester
server.stop();
}
public void setup(BiFunction<CoreSession, Callback, Void> onOpen) throws Exception
public void setup(BiConsumer<CoreSession, Callback> onOpen) throws Exception
{
serverHandler = new DemandingAsyncFrameHandler(onOpen);
serverHandler = new NonDemandingAsyncFrameHandler(onOpen);
server = new WebSocketServer(serverHandler);
server.start();
client = newClient(server.getLocalPort());
@ -70,7 +66,6 @@ public class WebSocketOpenTest extends WebSocketTester
s.sendFrame(new Frame(OpCode.TEXT, "Hello"), NOOP, false);
c.succeeded();
s.demand(1);
return null;
});
Frame.Parsed frame = receiveFrame(client.getInputStream());
assertThat(frame.getPayloadAsUTF8(), is("Hello"));
@ -87,13 +82,12 @@ public class WebSocketOpenTest extends WebSocketTester
@Test
public void testFailureInOnOpen() throws Exception
{
try (StacklessLogging stackless = new StacklessLogging(WebSocketCoreSession.class))
try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class))
{
setup((s, c) ->
{
assertThat(s.toString(), containsString("CONNECTED"));
c.failed(new Exception("Test Exception in onOpen"));
return null;
});
assertTrue(serverHandler.closeLatch.await(5, TimeUnit.SECONDS));
@ -115,7 +109,6 @@ public class WebSocketOpenTest extends WebSocketTester
{
assertThat(s.toString(), containsString("CONNECTED"));
s.close(CloseStatus.SHUTDOWN, "Test close in onOpen", c);
return null;
});
Frame.Parsed frame = receiveFrame(client.getInputStream());
@ -144,7 +137,6 @@ public class WebSocketOpenTest extends WebSocketTester
{
throw new RuntimeException(e);
}
return null;
});
CoreSession coreSession = sx.exchange(null);
@ -182,11 +174,11 @@ public class WebSocketOpenTest extends WebSocketTester
assertThat(new CloseStatus(frame).getCode(), is(CloseStatus.NORMAL));
}
static class DemandingAsyncFrameHandler extends TestAsyncFrameHandler
static class NonDemandingAsyncFrameHandler extends TestAsyncFrameHandler
{
private final BiFunction<CoreSession, Callback, Void> onOpen;
private final BiConsumer<CoreSession, Callback> onOpen;
DemandingAsyncFrameHandler(BiFunction<CoreSession, Callback, Void> onOpen)
NonDemandingAsyncFrameHandler(BiConsumer<CoreSession, Callback> onOpen)
{
this.onOpen = onOpen;
}
@ -197,14 +189,17 @@ public class WebSocketOpenTest extends WebSocketTester
if (LOG.isDebugEnabled())
LOG.debug("[{}] onOpen {}", name, coreSession);
this.coreSession = coreSession;
onOpen.apply(coreSession, callback);
onOpen.accept(coreSession, callback);
openLatch.countDown();
}
@Override
public boolean isAutoDemanding()
public void onFrame(Frame frame, Callback callback)
{
return false;
if (LOG.isDebugEnabled())
LOG.debug("[{}] onFrame {}", name, frame);
receivedFrames.offer(Frame.copy(frame));
callback.succeeded();
}
}
}

View File

@ -177,7 +177,7 @@ public class CoreAutobahnClient
if (!echoHandler.closeLatch.await(5, TimeUnit.MINUTES))
{
LOG.warn("could not close {}, aborting session", echoHandler);
echoHandler.coreSession.abort();
echoHandler.getCoreSession().abort();
}
}
}

View File

@ -104,6 +104,7 @@ public class WebSocketClientServerTest
else
{
callback.succeeded();
getCoreSession().demand(1);
}
}
};

View File

@ -82,7 +82,7 @@ public class ExtensionTool
byte[] net;
// Simulate initial demand from onOpen().
coreSession.autoDemand();
coreSession.demand(1);
for (int i = 0; i < parts; i++)
{
@ -102,8 +102,7 @@ public class ExtensionTool
public void succeeded()
{
super.succeeded();
if (coreSession.isAutoDemanding())
coreSession.autoDemand();
coreSession.demand(1);
}
};
ext.onFrame(frame, callback);

View File

@ -59,7 +59,7 @@ public class FragmentExtensionTest extends AbstractExtensionTest
ext.setNextIncomingFrames(capture);
// Simulate initial demand from onOpen().
coreSession.autoDemand();
coreSession.demand(1);
// Quote
List<String> quote = new ArrayList<>();
@ -131,7 +131,7 @@ public class FragmentExtensionTest extends AbstractExtensionTest
ext.setNextIncomingFrames(capture);
// Simulate initial demand from onOpen().
coreSession.autoDemand();
coreSession.demand(1);
String payload = "Are you there?";
Frame ping = new Frame(OpCode.PING).setPayload(payload);

View File

@ -295,7 +295,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest
ext.setNextIncomingFrames(capture);
// Simulate initial demand from onOpen().
coreSession.autoDemand();
coreSession.demand(1);
String payload = "Are you there?";
Frame ping = new Frame(OpCode.PING).setPayload(payload);
@ -333,7 +333,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest
ext.setNextIncomingFrames(capture);
// Simulate initial demand from onOpen().
coreSession.autoDemand();
coreSession.demand(1);
// Quote
List<String> quote = new ArrayList<>();
@ -386,7 +386,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest
ext.setNextIncomingFrames(capture);
// Simulate initial demand from onOpen().
coreSession.autoDemand();
coreSession.demand(1);
Frame ping = new Frame(OpCode.TEXT);
ping.setRsv1(true);

View File

@ -185,11 +185,5 @@ public class PermessageDeflateDemandTest
{
callback.succeeded();
}
@Override
public boolean isAutoDemanding()
{
return false;
}
}
}

View File

@ -21,6 +21,7 @@ import java.util.concurrent.CountDownLatch;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
@ -45,22 +46,22 @@ class WebSocketProxy
FAILED
}
private final Object lock = new Object();
private final WebSocketCoreClient client;
private final URI serverUri;
private final WebSocketCoreClient proxyClient;
private final URI serverURI;
public Client2Proxy client2Proxy = new Client2Proxy();
public Server2Proxy server2Proxy = new Server2Proxy();
public Proxy2Server proxy2Server = new Proxy2Server();
public WebSocketProxy(WebSocketCoreClient client, URI serverUri)
public WebSocketProxy(WebSocketCoreClient proxyClient, URI serverURI)
{
this.client = client;
this.serverUri = serverUri;
this.proxyClient = proxyClient;
this.serverURI = serverURI;
}
class Client2Proxy implements FrameHandler
{
private CoreSession client;
private final AutoLock lock = new AutoLock();
private CoreSession client2ProxySession;
private State state = State.NOT_OPEN;
private Callback closeCallback;
@ -71,7 +72,7 @@ class WebSocketProxy
public State getState()
{
synchronized (this)
try (AutoLock ignored = lock.lock())
{
return state;
}
@ -81,16 +82,16 @@ class WebSocketProxy
public void onOpen(CoreSession coreSession, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onOpen {}", toString(), coreSession);
LOG.debug("[{}] onOpen {}", this, coreSession);
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
case NOT_OPEN:
state = State.CONNECTING;
client = coreSession;
client2ProxySession = coreSession;
break;
default:
@ -102,16 +103,16 @@ class WebSocketProxy
if (failure != null)
callback.failed(failure);
else
server2Proxy.connect(Callback.from(() -> onOpenSuccess(callback), (t) -> onOpenFail(callback, t)));
proxy2Server.connect(Callback.from(() -> onOpenSuccess(callback), (t) -> onOpenFail(callback, t)));
}
private void onOpenSuccess(Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onOpenSuccess", toString());
LOG.debug("[{}] onOpenSuccess", this);
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -130,18 +131,23 @@ class WebSocketProxy
}
if (failure != null)
server2Proxy.fail(failure, callback);
{
proxy2Server.fail(failure, callback);
}
else
{
callback.succeeded();
client2ProxySession.demand(1);
}
}
private void onOpenFail(Callback callback, Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onOpenFail {}", toString(), t);
LOG.debug("[{}] onOpenFail", this, t);
Throwable failure = t;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -168,12 +174,13 @@ class WebSocketProxy
public void onFrame(Frame frame, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onFrame {}", toString(), frame);
LOG.debug("[{}] onFrame {}", this, frame);
receivedFrames.offer(Frame.copy(frame));
boolean demand = false;
Callback sendCallback = callback;
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -181,11 +188,14 @@ class WebSocketProxy
if (frame.getOpCode() == OpCode.CLOSE)
{
state = State.ISHUT;
// the callback is saved until a close response comes in sendFrame from Server2Proxy
// the callback is saved until a close response comes in sendFrame from Proxy2Server
// if the callback was completed here then core would send its own close response
closeCallback = callback;
sendCallback = Callback.from(() ->
{}, callback::failed);
sendCallback = Callback.from(() -> {}, callback::failed);
}
else
{
demand = true;
}
break;
@ -205,19 +215,35 @@ class WebSocketProxy
}
if (failure != null)
{
callback.failed(failure);
}
else
server2Proxy.send(frame, sendCallback);
{
if (demand)
{
Callback c = sendCallback;
proxy2Server.send(frame, Callback.from(() ->
{
c.succeeded();
client2ProxySession.demand(1);
}, c::failed));
}
else
{
proxy2Server.send(frame, sendCallback);
}
}
}
@Override
public void onError(Throwable failure, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onError {}", toString(), failure);
LOG.debug("[{}] onError", this, failure);
boolean failServer2Proxy;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -235,7 +261,7 @@ class WebSocketProxy
}
if (failServer2Proxy)
server2Proxy.fail(failure, callback);
proxy2Server.fail(failure, callback);
else
callback.failed(failure);
}
@ -243,10 +269,10 @@ class WebSocketProxy
public void fail(Throwable failure, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] fail {}", toString(), failure);
LOG.debug("[{}] fail", this, failure);
Callback sendCallback = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -268,7 +294,7 @@ class WebSocketProxy
}
if (sendCallback != null)
client.close(CloseStatus.SHUTDOWN, failure.getMessage(), sendCallback);
client2ProxySession.close(CloseStatus.SHUTDOWN, failure.getMessage(), sendCallback);
else
callback.failed(failure);
}
@ -277,10 +303,10 @@ class WebSocketProxy
public void onClosed(CloseStatus closeStatus, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onClosed {}", toString(), closeStatus);
LOG.debug("[{}] onClosed {}", this, closeStatus);
boolean abnormalClose = false;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -294,7 +320,7 @@ class WebSocketProxy
}
if (abnormalClose)
server2Proxy.fail(new ClosedChannelException(), Callback.NOOP);
proxy2Server.fail(new ClosedChannelException(), Callback.NOOP);
closed.countDown();
callback.succeeded();
@ -303,11 +329,11 @@ class WebSocketProxy
public void send(Frame frame, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] send {}", toString(), frame);
LOG.debug("[{}] send {}", this, frame);
Callback sendCallback = callback;
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -337,7 +363,7 @@ class WebSocketProxy
if (failure != null)
callback.failed(failure);
else
client.sendFrame(frame, sendCallback, false);
client2ProxySession.sendFrame(frame, sendCallback, false);
}
@Override
@ -347,9 +373,10 @@ class WebSocketProxy
}
}
class Server2Proxy implements FrameHandler
class Proxy2Server implements FrameHandler
{
private CoreSession server;
private final AutoLock lock = new AutoLock();
private CoreSession proxy2ServerSession;
private State state = State.NOT_OPEN;
private Callback closeCallback;
@ -360,7 +387,7 @@ class WebSocketProxy
public State getState()
{
synchronized (this)
try (AutoLock ignored = lock.lock())
{
return state;
}
@ -369,10 +396,10 @@ class WebSocketProxy
public void connect(Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] connect", toString());
LOG.debug("[{}] connect", this);
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -380,7 +407,7 @@ class WebSocketProxy
try
{
state = State.CONNECTING;
client.connect(this, serverUri).whenComplete((s, t) ->
proxyClient.connect(this, serverURI).whenComplete((s, t) ->
{
if (t != null)
onConnectFailure(t, callback);
@ -415,10 +442,10 @@ class WebSocketProxy
private void onConnectSuccess(CoreSession coreSession, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onConnectSuccess {}", toString(), coreSession);
LOG.debug("[{}] onConnectSuccess {}", this, coreSession);
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -438,18 +465,23 @@ class WebSocketProxy
}
if (failure != null)
{
coreSession.close(CloseStatus.SHUTDOWN, failure.getMessage(), Callback.from(callback, failure));
}
else
{
callback.succeeded();
coreSession.demand(1);
}
}
private void onConnectFailure(Throwable t, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onConnectFailure {}", toString(), t);
LOG.debug("[{}] onConnectFailure", this, t);
Throwable failure = t;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -477,16 +509,16 @@ class WebSocketProxy
public void onOpen(CoreSession coreSession, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onOpen {}", toString(), coreSession);
LOG.debug("[{}] onOpen {}", this, coreSession);
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
case CONNECTING:
state = State.OPEN;
server = coreSession;
proxy2ServerSession = coreSession;
break;
case FAILED:
@ -509,12 +541,13 @@ class WebSocketProxy
public void onFrame(Frame frame, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onFrame {}", toString(), frame);
LOG.debug("[{}] onFrame {}", this, frame);
receivedFrames.offer(Frame.copy(frame));
boolean demand = false;
Callback sendCallback = callback;
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -523,8 +556,11 @@ class WebSocketProxy
{
state = State.ISHUT;
closeCallback = callback;
sendCallback = Callback.from(() ->
{}, callback::failed);
sendCallback = Callback.from(() -> {}, callback::failed);
}
else
{
demand = true;
}
break;
@ -544,19 +580,35 @@ class WebSocketProxy
}
if (failure != null)
{
callback.failed(failure);
}
else
client2Proxy.send(frame, sendCallback);
{
if (demand)
{
Callback c = sendCallback;
client2Proxy.send(frame, Callback.from(() ->
{
c.succeeded();
proxy2ServerSession.demand(1);
}, c::failed));
}
else
{
client2Proxy.send(frame, sendCallback);
}
}
}
@Override
public void onError(Throwable failure, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onError {}", toString(), failure);
LOG.debug("[{}] onError", this, failure);
boolean failClient2Proxy = false;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -582,10 +634,10 @@ class WebSocketProxy
public void onClosed(CloseStatus closeStatus, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] onClosed {}", toString(), closeStatus);
LOG.debug("[{}] onClosed {}", this, closeStatus);
boolean abnormalClose = false;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -608,10 +660,10 @@ class WebSocketProxy
public void fail(Throwable failure, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] fail {}", toString(), failure);
LOG.debug("[{}] fail", this, failure);
Callback sendCallback = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -633,7 +685,7 @@ class WebSocketProxy
}
if (sendCallback != null)
server.close(CloseStatus.SHUTDOWN, failure.getMessage(), sendCallback);
proxy2ServerSession.close(CloseStatus.SHUTDOWN, failure.getMessage(), sendCallback);
else
callback.failed(failure);
}
@ -641,11 +693,11 @@ class WebSocketProxy
public void send(Frame frame, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("[{}] send {}", toString(), frame);
LOG.debug("[{}] send {}", this, frame);
Callback sendCallback = callback;
Throwable failure = null;
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
switch (state)
{
@ -675,13 +727,13 @@ class WebSocketProxy
if (failure != null)
callback.failed(failure);
else
server.sendFrame(frame, sendCallback, false);
proxy2ServerSession.sendFrame(frame, sendCallback, false);
}
@Override
public String toString()
{
return "Server2Proxy:" + getState();
return "Proxy2Server:" + getState();
}
}
}

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.websocket.core.proxy;
import java.net.URI;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -146,7 +145,7 @@ public class WebSocketProxyTest
_server.stop();
}
public void awaitProxyClose(WebSocketProxy.Client2Proxy client2Proxy, WebSocketProxy.Server2Proxy server2Proxy) throws Exception
public void awaitProxyClose(WebSocketProxy.Client2Proxy client2Proxy, WebSocketProxy.Proxy2Server server2Proxy) throws Exception
{
if (client2Proxy != null && !client2Proxy.closed.await(5, TimeUnit.SECONDS))
throw new TimeoutException("client2Proxy close timeout");
@ -160,7 +159,7 @@ public class WebSocketProxyTest
{
TestAsyncFrameHandler clientFrameHandler = new TestAsyncFrameHandler("CLIENT");
WebSocketProxy.Client2Proxy proxyClientSide = proxy.client2Proxy;
WebSocketProxy.Server2Proxy proxyServerSide = proxy.server2Proxy;
WebSocketProxy.Proxy2Server proxyServerSide = proxy.proxy2Server;
CoreClientUpgradeRequest upgradeRequest = CoreClientUpgradeRequest.from(_client, proxyUri, clientFrameHandler);
upgradeRequest.setConfiguration(defaultCustomizer);
@ -168,6 +167,20 @@ public class WebSocketProxyTest
response.get(5, TimeUnit.SECONDS);
clientFrameHandler.sendText("hello world");
Frame frame = proxyClientSide.receivedFrames.poll(5, TimeUnit.SECONDS);
assertNotNull(frame);
assertThat(frame.getPayloadAsUTF8(), is("hello world"));
frame = serverFrameHandler.receivedFrames.poll(5, TimeUnit.SECONDS);
assertNotNull(frame);
assertThat(frame.getPayloadAsUTF8(), is("hello world"));
frame = proxyServerSide.receivedFrames.poll(5, TimeUnit.SECONDS);
assertNotNull(frame);
assertThat(frame.getPayloadAsUTF8(), is("hello world"));
frame = clientFrameHandler.receivedFrames.poll(5, TimeUnit.SECONDS);
assertNotNull(frame);
assertThat(frame.getPayloadAsUTF8(), is("hello world"));
clientFrameHandler.close(CloseStatus.NORMAL, "standard close");
assertTrue(clientFrameHandler.closeLatch.await(5, TimeUnit.SECONDS));
assertTrue(serverFrameHandler.closeLatch.await(5, TimeUnit.SECONDS));
@ -176,11 +189,6 @@ public class WebSocketProxyTest
assertThat(proxyClientSide.getState(), is(WebSocketProxy.State.CLOSED));
assertThat(proxyServerSide.getState(), is(WebSocketProxy.State.CLOSED));
assertThat(Objects.requireNonNull(proxyClientSide.receivedFrames.poll()).getPayloadAsUTF8(), is("hello world"));
assertThat(Objects.requireNonNull(serverFrameHandler.receivedFrames.poll()).getPayloadAsUTF8(), is("hello world"));
assertThat(Objects.requireNonNull(proxyServerSide.receivedFrames.poll()).getPayloadAsUTF8(), is("hello world"));
assertThat(Objects.requireNonNull(clientFrameHandler.receivedFrames.poll()).getPayloadAsUTF8(), is("hello world"));
assertThat(CloseStatus.getCloseStatus(proxyClientSide.receivedFrames.poll()).getReason(), is("standard close"));
assertThat(CloseStatus.getCloseStatus(serverFrameHandler.receivedFrames.poll()).getReason(), is("standard close"));
assertThat(CloseStatus.getCloseStatus(proxyServerSide.receivedFrames.poll()).getReason(), is("standard close"));
@ -197,7 +205,7 @@ public class WebSocketProxyTest
{
testHandler.blockServerUpgradeRequests();
WebSocketProxy.Client2Proxy proxyClientSide = proxy.client2Proxy;
WebSocketProxy.Server2Proxy proxyServerSide = proxy.server2Proxy;
WebSocketProxy.Proxy2Server proxyServerSide = proxy.proxy2Server;
TestAsyncFrameHandler clientFrameHandler = new TestAsyncFrameHandler("CLIENT");
try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class))
@ -237,7 +245,7 @@ public class WebSocketProxyTest
}
};
WebSocketProxy.Client2Proxy proxyClientSide = proxy.client2Proxy;
WebSocketProxy.Server2Proxy proxyServerSide = proxy.server2Proxy;
WebSocketProxy.Proxy2Server proxyServerSide = proxy.proxy2Server;
try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class))
{
@ -269,7 +277,7 @@ public class WebSocketProxyTest
{
serverFrameHandler.throwOnFrame();
WebSocketProxy.Client2Proxy proxyClientSide = proxy.client2Proxy;
WebSocketProxy.Server2Proxy proxyServerSide = proxy.server2Proxy;
WebSocketProxy.Proxy2Server proxyServerSide = proxy.proxy2Server;
TestAsyncFrameHandler clientFrameHandler = new TestAsyncFrameHandler("CLIENT");
CoreClientUpgradeRequest upgradeRequest = CoreClientUpgradeRequest.from(_client, proxyUri, clientFrameHandler);
@ -299,7 +307,7 @@ public class WebSocketProxyTest
frame = serverFrameHandler.receivedFrames.poll();
assertNull(frame);
// Server2Proxy
// Proxy2Server
frame = proxyServerSide.receivedFrames.poll();
assertNotNull(frame);
closeStatus = CloseStatus.getCloseStatus(frame);
@ -328,7 +336,7 @@ public class WebSocketProxyTest
{
serverFrameHandler.throwOnFrame();
WebSocketProxy.Client2Proxy proxyClientSide = proxy.client2Proxy;
WebSocketProxy.Server2Proxy proxyServerSide = proxy.server2Proxy;
WebSocketProxy.Proxy2Server proxyServerSide = proxy.proxy2Server;
TestAsyncFrameHandler clientFrameHandler = new TestAsyncFrameHandler("CLIENT")
{
@ -365,7 +373,7 @@ public class WebSocketProxyTest
assertThat(frame.getPayloadAsUTF8(), is("hello world"));
assertNull(serverFrameHandler.receivedFrames.poll());
// Server2Proxy
// Proxy2Server
frame = proxyServerSide.receivedFrames.poll();
closeStatus = CloseStatus.getCloseStatus(frame);
assertThat(closeStatus.getCode(), is(CloseStatus.SERVER_ERROR));
@ -382,7 +390,7 @@ public class WebSocketProxyTest
assertNull(proxyClientSide.receivedFrames.poll());
assertThat(proxyClientSide.getState(), is(WebSocketProxy.State.FAILED));
// Server2Proxy is failed by the Client2Proxy
// Proxy2Server is failed by the Client2Proxy
assertNull(proxyServerSide.receivedFrames.poll());
assertThat(proxyServerSide.getState(), is(WebSocketProxy.State.FAILED));
}

View File

@ -94,9 +94,9 @@ public class WebSocketServerTest extends WebSocketTester
TestFrameHandler serverHandler = new TestFrameHandler()
{
@Override
public boolean isAutoDemanding()
protected void demand()
{
return false;
// Demand is explicitly performed by the test code.
}
};
@ -147,17 +147,10 @@ public class WebSocketServerTest extends WebSocketTester
@Override
public void onOpen(CoreSession coreSession, Callback callback)
{
super.onOpen(coreSession);
callback.succeeded();
super.onOpen(coreSession, callback);
coreSession.demand(1);
}
@Override
public boolean isAutoDemanding()
{
return false;
}
@Override
public void onFrame(Frame frame, Callback callback)
{
@ -254,17 +247,10 @@ public class WebSocketServerTest extends WebSocketTester
@Override
public void onOpen(CoreSession coreSession, Callback callback)
{
super.onOpen(coreSession);
callback.succeeded();
super.onOpen(coreSession, callback);
coreSession.demand(3);
}
@Override
public boolean isAutoDemanding()
{
return false;
}
@Override
public void onFrame(Frame frame, Callback callback)
{
@ -272,6 +258,12 @@ public class WebSocketServerTest extends WebSocketTester
receivedFrames.offer(frame);
receivedCallbacks.offer(callback);
}
@Override
protected void demand()
{
// Demand is explicitly performed by the test code.
}
};
server = new WebSocketServer(serverHandler);
@ -325,12 +317,6 @@ public class WebSocketServerTest extends WebSocketTester
super.onClosed(closeStatus);
}
@Override
public boolean isAutoDemanding()
{
return false;
}
@Override
public void onFrame(Frame frame, Callback callback)
{
@ -392,12 +378,6 @@ public class WebSocketServerTest extends WebSocketTester
coreSession.demand(2);
}
@Override
public boolean isAutoDemanding()
{
return false;
}
@Override
public void onFrame(Frame frame, Callback callback)
{

View File

@ -24,6 +24,7 @@ import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.core.messages.MessageReader;
@ -37,7 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class MessageReaderTest
{
private final MessageReader reader = new MessageReader();
private final MessageReader reader = new MessageReader(new CoreSession.Empty());
private final CompletableFuture<String> message = new CompletableFuture<>();
private boolean first = true;

View File

@ -51,7 +51,7 @@ public class PartialStringMessageSinkTest
@BeforeEach
public void before() throws Exception
{
messageSink = new PartialStringMessageSink(coreSession, endpoint.getMethodHandle());
messageSink = new PartialStringMessageSink(coreSession, endpoint.getMethodHandle(), true);
}
@Test

View File

@ -44,7 +44,7 @@ public class StringMessageSinkTest
@Test
public void testMaxMessageSize() throws Exception
{
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle());
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle(), true);
ByteBuffer utf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0xF0, (byte)0x90, (byte)0x8D, (byte)0x88});
FutureCallback callback = new FutureCallback();
@ -60,7 +60,7 @@ public class StringMessageSinkTest
@Test
public void testValidUtf8() throws Exception
{
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle());
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle(), true);
ByteBuffer utf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0xF0, (byte)0x90, (byte)0x8D, (byte)0x88});
FutureCallback callback = new FutureCallback();
@ -73,7 +73,7 @@ public class StringMessageSinkTest
@Test
public void testUtf8Continuation() throws Exception
{
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle());
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle(), true);
ByteBuffer firstUtf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0xF0, (byte)0x90});
ByteBuffer continuationUtf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0x8D, (byte)0x88});
@ -91,7 +91,7 @@ public class StringMessageSinkTest
@Test
public void testInvalidSingleFrameUtf8() throws Exception
{
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle());
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle(), true);
ByteBuffer invalidUtf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0xF0, (byte)0x90, (byte)0x8D});
FutureCallback callback = new FutureCallback();
@ -106,7 +106,7 @@ public class StringMessageSinkTest
@Test
public void testInvalidMultiFrameUtf8() throws Exception
{
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle());
StringMessageSink messageSink = new StringMessageSink(coreSession, endpoint.getMethodHandle(), true);
ByteBuffer firstUtf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0xF0, (byte)0x90});
ByteBuffer continuationUtf8Payload = BufferUtil.toBuffer(new byte[]{(byte)0x8D});

View File

@ -1,42 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* The possible batch modes when enqueuing outgoing frames.
*/
public enum BatchMode
{
/**
* Implementers are free to decide whether to send or not frames
* to the network layer.
*/
AUTO,
/**
* Implementers must batch frames.
*/
ON,
/**
* Implementers must send frames to the network layer.
*/
OFF;
public static BatchMode max(BatchMode one, BatchMode two)
{
// Return the BatchMode that has the higher priority, where AUTO < ON < OFF.
return one.ordinal() < two.ordinal() ? two : one;
}
}

View File

@ -0,0 +1,105 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* Callback for Write events.
* <p>
* NOTE: We don't expose org.eclipse.jetty.util.Callback here as that would complicate matters with the WebAppContext's classloader isolation.
*/
public interface Callback
{
Callback NOOP = new Callback()
{
};
/**
* Creates a callback from the given success and failure lambdas.
*
* @param success called when the callback succeeds
* @param failure called when the callback fails
* @return a new callback
*/
static Callback from(Runnable success, Consumer<Throwable> failure)
{
return new Callback()
{
@Override
public void succeed()
{
success.run();
}
@Override
public void fail(Throwable x)
{
failure.accept(x);
}
};
}
/**
* <p>
* Callback invoked when the write succeeds.
* </p>
*
* @see #fail(Throwable)
*/
default void succeed()
{
}
/**
* <p>
* Callback invoked when the write fails.
* </p>
*
* @param x the reason for the write failure
*/
default void fail(Throwable x)
{
}
class Completable extends CompletableFuture<Void> implements Callback
{
public static Completable with(Consumer<Completable> consumer)
{
Completable completable = new Completable();
consumer.accept(completable);
return completable;
}
@Override
public void succeed()
{
complete(null);
}
@Override
public void fail(Throwable x)
{
completeExceptionally(x);
}
public Completable compose(Consumer<Completable> consumer)
{
Completable completable = new Completable();
thenAccept(ignored -> consumer.accept(completable));
return completable;
}
}
}

View File

@ -1,50 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
public class CloseStatus
{
private static final int MAX_CONTROL_PAYLOAD = 125;
public static final int MAX_REASON_PHRASE = MAX_CONTROL_PAYLOAD - 2;
private final int code;
private final String phrase;
/**
* Creates a reason for closing a web socket connection with the given code and reason phrase.
*
* @param closeCode the close code
* @param reasonPhrase the reason phrase
* @see StatusCode
*/
public CloseStatus(int closeCode, String reasonPhrase)
{
this.code = closeCode;
this.phrase = reasonPhrase;
if (reasonPhrase.length() > MAX_REASON_PHRASE)
{
throw new IllegalArgumentException("Phrase exceeds maximum length of " + MAX_REASON_PHRASE);
}
}
public int getCode()
{
return code;
}
public String getPhrase()
{
return phrase;
}
}

View File

@ -16,12 +16,10 @@ package org.eclipse.jetty.websocket.api;
import java.time.Duration;
/**
* Settings for WebSocket operations.
* <p>Implementations allow to configure WebSocket parameters.</p>
*/
public interface WebSocketPolicy
public interface Configurable
{
WebSocketBehavior getBehavior();
/**
* The duration that a websocket may be idle before being closed by the implementation
*
@ -29,6 +27,13 @@ public interface WebSocketPolicy
*/
Duration getIdleTimeout();
/**
* The duration that a websocket may be idle before being closed by the implementation
*
* @param duration the timeout duration (may not be null or negative)
*/
void setIdleTimeout(Duration duration);
/**
* The input (read from network layer) buffer size.
* <p>
@ -39,6 +44,13 @@ public interface WebSocketPolicy
*/
int getInputBufferSize();
/**
* The input (read from network layer) buffer size.
*
* @param size the size in bytes
*/
void setInputBufferSize(int size);
/**
* The output (write to network layer) buffer size.
* <p>
@ -49,6 +61,13 @@ public interface WebSocketPolicy
*/
int getOutputBufferSize();
/**
* The output (write to network layer) buffer size.
*
* @param size the size in bytes
*/
void setOutputBufferSize(int size);
/**
* Get the maximum size of a binary message during parsing.
* <p>
@ -64,6 +83,16 @@ public interface WebSocketPolicy
*/
long getMaxBinaryMessageSize();
/**
* The maximum size of a binary message during parsing/generating.
* <p>
* Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
* </p>
*
* @param size the maximum allowed size of a binary message.
*/
void setMaxBinaryMessageSize(long size);
/**
* Get the maximum size of a text message during parsing.
* <p>
@ -79,51 +108,6 @@ public interface WebSocketPolicy
*/
long getMaxTextMessageSize();
/**
* The maximum payload size of any WebSocket Frame which can be received.
*
* @return the maximum size of a WebSocket Frame.
*/
long getMaxFrameSize();
/**
* If true, frames are automatically fragmented to respect the maximum frame size.
*
* @return whether to automatically fragment incoming WebSocket Frames.
*/
boolean isAutoFragment();
/**
* The duration that a websocket may be idle before being closed by the implementation
*
* @param duration the timeout duration (may not be null or negative)
*/
void setIdleTimeout(Duration duration);
/**
* The input (read from network layer) buffer size.
*
* @param size the size in bytes
*/
void setInputBufferSize(int size);
/**
* The output (write to network layer) buffer size.
*
* @param size the size in bytes
*/
void setOutputBufferSize(int size);
/**
* The maximum size of a binary message during parsing/generating.
* <p>
* Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
* </p>
*
* @param size the maximum allowed size of a binary message.
*/
void setMaxBinaryMessageSize(long size);
/**
* The maximum size of a text message during parsing/generating.
* <p>
@ -133,6 +117,13 @@ public interface WebSocketPolicy
*/
void setMaxTextMessageSize(long size);
/**
* The maximum payload size of any WebSocket Frame which can be received.
*
* @return the maximum size of a WebSocket Frame.
*/
long getMaxFrameSize();
/**
* The maximum payload size of any WebSocket Frame which can be received.
* <p>
@ -143,10 +134,39 @@ public interface WebSocketPolicy
*/
void setMaxFrameSize(long maxFrameSize);
/**
* If true, frames are automatically fragmented to respect the maximum frame size.
*
* @return whether to automatically fragment incoming WebSocket Frames.
*/
boolean isAutoFragment();
/**
* If set to true, frames are automatically fragmented to respect the maximum frame size.
*
* @param autoFragment whether to automatically fragment incoming WebSocket Frames.
*/
void setAutoFragment(boolean autoFragment);
/**
* Get the maximum number of data frames allowed to be waiting to be sent at any one time.
* The default value is -1, this indicates there is no limit on how many frames can be
* queued to be sent by the implementation. If the limit is exceeded, subsequent frames
* sent are failed with a {@link java.nio.channels.WritePendingException} but
* the connection is not failed and will remain open.
*
* @return the max number of frames.
*/
int getMaxOutgoingFrames();
/**
* Set the maximum number of data frames allowed to be waiting to be sent at any one time.
* The default value is -1, this indicates there is no limit on how many frames can be
* queued to be sent by the implementation. If the limit is exceeded, subsequent frames
* sent are failed with a {@link java.nio.channels.WritePendingException} but
* the connection is not failed and will remain open.
*
* @param maxOutgoingFrames the max number of frames.
*/
void setMaxOutgoingFrames(int maxOutgoingFrames);
}

View File

@ -1,196 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
public interface RemoteEndpoint
{
/**
* Send a binary message, returning when all bytes of the message has been transmitted.
* <p>
* Note: this is a blocking call
*
* @param data the message to be sent
* @throws IOException if unable to send the bytes
*/
void sendBytes(ByteBuffer data) throws IOException;
/**
* Initiates the asynchronous transmission of a binary message. This method returns before the message is transmitted.
* Developers may provide a callback to be notified when the message has been transmitted or resulted in an error.
*
* @param data the data being sent
* @param callback callback to notify of success or failure of the write operation
*/
void sendBytes(ByteBuffer data, WriteCallback callback);
/**
* Send a binary message in pieces, blocking until all of the message has been transmitted.
* The runtime reads the message in order. Non-final pieces are
* sent with isLast set to false. The final piece must be sent with isLast set to true.
*
* @param fragment the piece of the message being sent
* @param isLast true if this is the last piece of the partial bytes
* @throws IOException if unable to send the partial bytes
*/
void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException;
/**
* Initiates the asynchronous transmission of a partial binary message. This method returns before the message is
* transmitted.
* The runtime reads the message in order. Non-final pieces are sent with isLast
* set to false. The final piece must be sent with isLast set to true.
* Developers may provide a callback to be notified when the message has been transmitted or resulted in an error.
*
* @param fragment the data being sent
* @param isLast true if this is the last piece of the partial bytes
* @param callback callback to notify of success or failure of the write operation
*/
void sendPartialBytes(ByteBuffer fragment, boolean isLast, WriteCallback callback);
/**
* Send a text message, blocking until all bytes of the message has been transmitted.
* <p>
* Note: this is a blocking call
*
* @param text the message to be sent
* @throws IOException if unable to send the text message
*/
void sendString(String text) throws IOException;
/**
* Initiates the asynchronous transmission of a text message. This method may return before the message is
* transmitted. Developers may provide a callback to
* be notified when the message has been transmitted or resulted in an error.
*
* @param text the text being sent
* @param callback callback to notify of success or failure of the write operation
*/
void sendString(String text, WriteCallback callback);
/**
* Send a text message in pieces, blocking until all of the message has been transmitted. The runtime reads the
* message in order. Non-final pieces are sent
* with isLast set to false. The final piece must be sent with isLast set to true.
*
* @param fragment the piece of the message being sent
* @param isLast true if this is the last piece of the partial bytes
* @throws IOException if unable to send the partial bytes
*/
void sendPartialString(String fragment, boolean isLast) throws IOException;
/**
* Initiates the asynchronous transmission of a partial text message.
* This method may return before the message is transmitted.
* The runtime reads the message in order. Non-final pieces are sent with isLast
* set to false. The final piece must be sent with isLast set to true.
* Developers may provide a callback to be notified when the message has been transmitted or resulted in an error.
*
* @param fragment the text being sent
* @param isLast true if this is the last piece of the partial bytes
* @param callback callback to notify of success or failure of the write operation
*/
void sendPartialString(String fragment, boolean isLast, WriteCallback callback);
/**
* Send a Ping message containing the given application data to the remote endpoint, blocking until all of the
* message has been transmitted.
* The corresponding Pong message may be picked up using the MessageHandler.Pong handler.
*
* @param applicationData the data to be carried in the ping request
* @throws IOException if unable to send the ping
*/
void sendPing(ByteBuffer applicationData) throws IOException;
/**
* Asynchronously send a Ping message containing the given application data to the remote endpoint.
* The corresponding Pong message may be picked up using the MessageHandler.Pong handler.
*
* @param applicationData the data to be carried in the ping request
* @param callback callback to notify of success or failure of the write operation
*/
void sendPing(ByteBuffer applicationData, WriteCallback callback);
/**
* Allows the developer to send an unsolicited Pong message containing the given application data
* in order to serve as a unidirectional heartbeat for the session, this will block until
* all of the message has been transmitted.
*
* @param applicationData the application data to be carried in the pong response.
* @throws IOException if unable to send the pong
*/
void sendPong(ByteBuffer applicationData) throws IOException;
/**
* Allows the developer to asynchronously send an unsolicited Pong message containing the given application data
* in order to serve as a unidirectional heartbeat for the session.
*
* @param applicationData the application data to be carried in the pong response.
* @param callback callback to notify of success or failure of the write operation
*/
void sendPong(ByteBuffer applicationData, WriteCallback callback);
/**
* @return the batch mode with which messages are sent.
* @see #flush()
*/
BatchMode getBatchMode();
/**
* Set the batch mode with which messages are sent.
*
* @param mode the batch mode to use
* @see #flush()
*/
void setBatchMode(BatchMode mode);
/**
* Get the maximum number of data frames allowed to be waiting to be sent at any one time.
* The default value is -1, this indicates there is no limit on how many frames can be
* queued to be sent by the implementation. If the limit is exceeded, subsequent frames
* sent are failed with a {@link java.nio.channels.WritePendingException} but
* the connection is not failed and will remain open.
*
* @return the max number of frames.
*/
int getMaxOutgoingFrames();
/**
* Set the maximum number of data frames allowed to be waiting to be sent at any one time.
* The default value is -1, this indicates there is no limit on how many frames can be
* queued to be sent by the implementation. If the limit is exceeded, subsequent frames
* sent are failed with a {@link java.nio.channels.WritePendingException} but
* the connection is not failed and will remain open.
*
* @param maxOutgoingFrames the max number of frames.
*/
void setMaxOutgoingFrames(int maxOutgoingFrames);
/**
* Get the SocketAddress for the established connection.
*
* @return the SocketAddress for the established connection.
*/
SocketAddress getRemoteAddress();
/**
* Flushes messages that may have been batched by the implementation.
*
* @throws IOException if the flush fails
*/
void flush() throws IOException;
}

View File

@ -14,184 +14,349 @@
package org.eclipse.jetty.websocket.api;
import java.io.Closeable;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
/**
* Session represents an active link of communications with a Remote WebSocket Endpoint.
* <p>{@link Session} represents an active link of
* communication with a remote WebSocket endpoint.</p>
* <p>{@link Session} APIs can be used to configure
* the various parameters that control the behavior
* of the WebSocket communication, such as
* {@link #setMaxTextMessageSize(long)}, and to send
* WebSocket frames or messages to the other peer.</p>
* <p>The passive link of communication that receives
* WebSocket events is {@link Listener}.</p>
*/
public interface Session extends WebSocketPolicy, Closeable
public interface Session extends Configurable, Closeable
{
/**
* Request a close of the current conversation with a normal status code and no reason phrase.
* <p>
* This will enqueue a graceful close to the remote endpoint.
* <p>Explicitly demands for WebSocket events.</p>
* <p>This method should be called only when the WebSocket endpoint is not
* demanding automatically, as defined by {@link WebSocket#autoDemand()}
* and {@link Listener.AutoDemanding}.</p>
* <p>In general, invoking this method results in a listener method or
* an annotated method to be called when the corresponding event is
* ready to be delivered.</p>
* <p>For WebSocket endpoints that wants to receive frame events
* (for example by overriding {@link Listener#onWebSocketFrame(Frame, Callback)}),
* invoking this method will result in a frame event being delivered to
* the listener/annotated method when a new frame is received.</p>
* <p>For WebSocket endpoints that want to receive whole <em>message</em>
* events (for example by overriding {@link Listener#onWebSocketText(String)}),
* invoking this method will result in a message event being delivered to
* the listener/annotated method when a new message is received.
* The implementation will automatically demand for more frames until a
* whole message is assembled and then deliver the whole message as event.</p>
* <p>Note that even when the WebSocket endpoint is interested in whole
* messages, calling this method is necessary not only to possibly receive
* the next whole message, but also to receive control frames (such as
* PING or CLOSE frames).
* Failing to call this method after receiving a whole message results
* in the CLOSE frame event to not be processed, and therefore for the
* endpoint to not notice when the other peer closed the WebSocket
* communication.</p>
*
* @see #close(CloseStatus)
* @see #close(int, String)
* @throws IllegalStateException if the WebSocket endpoint is auto-demanding
*/
void demand();
/**
* <p>Initiates the asynchronous send of a BINARY message, notifying
* the given callback when the message send is completed, either
* successfully or with a failure.</p>
*
* @param buffer the message bytes to send
* @param callback callback to notify when the send operation is complete
*/
void sendBinary(ByteBuffer buffer, Callback callback);
/**
* <p>Initiates the asynchronous send of a BINARY frame, possibly part
* of a larger binary message, notifying the given callback when the frame
* send is completed, either successfully or with a failure.</p>
* <p>Non-final frames must be sent with the parameter {@code last=false}.
* The final frame must be sent with {@code last=true}.</p>
*
* @param buffer the frame bytes to send
* @param last whether this is the last frame of the message
* @param callback callback to notify when the send operation is complete
*/
void sendPartialBinary(ByteBuffer buffer, boolean last, Callback callback);
/**
* <p>Initiates the asynchronous send of a TEXT message, notifying
* the given callback when the message send is completed, either
* successfully or with a failure.</p>
*
* @param text the message text to send
* @param callback callback to notify when the send operation is complete
*/
void sendText(String text, Callback callback);
/**
* <p>Initiates the asynchronous send of a TEXT frame, possibly part
* of a larger binary message, notifying the given callback when the frame
* send is completed, either successfully or with a failure.</p>
* <p>Non-final frames must be sent with the parameter {@code last=false}.
* The final frame must be sent with {@code last=true}.</p>
*
* @param text the frame text to send
* @param last whether this is the last frame of the message
* @param callback callback to notify when the send operation is complete
*/
void sendPartialText(String text, boolean last, Callback callback);
/**
* <p>Initiates the asynchronous send of a PING frame, notifying the given
* callback when the frame send is completed, either successfully or with
* a failure.</p>
*
* @param applicationData the data to be carried in the PING frame
* @param callback callback to notify when the send operation is complete
*/
void sendPing(ByteBuffer applicationData, Callback callback);
/**
* <p>Initiates the asynchronous send of a PONG frame, notifying the given
* callback when the frame send is completed, either successfully or with
* a failure.</p>
*
* @param applicationData the data to be carried in the PONG frame
* @param callback callback to notify when the send operation is complete
*/
void sendPong(ByteBuffer applicationData, Callback callback);
/**
* <p>Equivalent to {@code close(StatusCode.NORMAL, null, Callback.NOOP)}.</p>
*
* @see #close(int, String, Callback)
* @see #disconnect()
*/
@Override
void close();
/**
* Request Close the current conversation, giving a reason for the closure. Note the websocket spec defines the acceptable uses of status codes and reason
* phrases.
* <p>
* This will enqueue a graceful close to the remote endpoint.
*
* @param closeStatus the reason for the closure
* @see #close()
* @see #close(int, String)
* @see #disconnect()
*/
void close(CloseStatus closeStatus);
/**
* Send a websocket Close frame, with status code.
* <p>
* This will enqueue a graceful close to the remote endpoint.
*
* @param statusCode the status code
* @param reason the (optional) reason. (can be null for no reason)
* @see StatusCode
* @see #close()
* @see #close(CloseStatus)
* @see #disconnect()
*/
void close(int statusCode, String reason);
/**
* Send a websocket Close frame, with status code.
* <p>
* This will enqueue a graceful close to the remote endpoint.
*
* @param statusCode the status code
* @param reason the (optional) reason. (can be null for no reason)
* @param callback the callback to track close frame sent (or failed)
* @see StatusCode
* @see #close()
* @see #close(CloseStatus)
* @see #disconnect()
*/
default void close(int statusCode, String reason, WriteCallback callback)
default void close()
{
try
{
close(statusCode, reason);
callback.writeSuccess();
}
catch (Throwable t)
{
callback.writeFailed(t);
}
close(StatusCode.NORMAL, null, Callback.NOOP);
}
/**
* Issue a harsh disconnect of the underlying connection.
* <p>
* This will terminate the connection, without sending a websocket close frame.
* <p>
* Once called, any read/write activity on the websocket from this point will be indeterminate.
* <p>
* Once the underlying connection has been determined to be closed, the various onClose() events (either
* {@link WebSocketListener#onWebSocketClose(int, String)} or {@link OnWebSocketClose}) will be called on your
* websocket.
* <p>Sends a websocket CLOSE frame, with status code and reason, notifying
* the given callback when the frame send is completed, either successfully
* or with a failure.</p>
*
* @param statusCode the status code
* @param reason the (optional) reason
* @param callback callback to notify when the send operation is complete
* @see StatusCode
* @see #close()
* @see #close(CloseStatus)
* @see #close(int, String)
* @see #disconnect()
*/
void close(int statusCode, String reason, Callback callback);
/**
* <p>Abruptly closes the WebSocket connection without sending a CLOSE frame.</p>
*
* @see #close(int, String, Callback)
*/
void disconnect();
/**
* The Local Socket Address for the active Session
* <p>
* Do not assume that this will return a {@link InetSocketAddress} in all cases.
* Use of various proxies, and even UnixSockets can result a SocketAddress being returned
* without supporting {@link InetSocketAddress}
* </p>
*
* @return the SocketAddress for the local connection, or null if not supported by Session
* @return the local SocketAddress for the connection, if available
*/
SocketAddress getLocalAddress();
SocketAddress getLocalSocketAddress();
/**
* Access the (now read-only) {@link WebSocketPolicy} in use for this connection.
*
* @return the policy in use
* @return the remote SocketAddress for the connection, if available
*/
default WebSocketPolicy getPolicy()
{
return this;
}
SocketAddress getRemoteSocketAddress();
/**
* Returns the version of the websocket protocol currently being used. This is taken as the value of the Sec-WebSocket-Version header used in the opening
* handshake. i.e. "13".
* <p>Returns the version of the WebSocket protocol currently being used.</p>
* <p>This is taken as the value of the {@code Sec-WebSocket-Version} header
* used in the {@link #getUpgradeRequest() upgrade request}.
*
* @return the protocol version
* @return the WebSocket protocol version
*/
String getProtocolVersion();
/**
* Return a reference to the RemoteEndpoint object representing the other end of this conversation.
*
* @return the remote endpoint
*/
RemoteEndpoint getRemote();
/**
* The Remote Socket Address for the active Session
* <p>
* Do not assume that this will return a {@link InetSocketAddress} in all cases.
* Use of various proxies, and even UnixSockets can result a SocketAddress being returned
* without supporting {@link InetSocketAddress}
* </p>
*
* @return the SocketAddress for the remote connection, or null if not supported by Session
*/
SocketAddress getRemoteAddress();
/**
* Get the UpgradeRequest used to create this session
*
* @return the UpgradeRequest used to create this session
*/
UpgradeRequest getUpgradeRequest();
/**
* Get the UpgradeResponse used to create this session
*
* @return the UpgradeResponse used to create this session
*/
UpgradeResponse getUpgradeResponse();
/**
* Return true if and only if the underlying socket is open.
*
* @return whether the session is open
*/
boolean isOpen();
/**
* Return true if and only if the underlying socket is using a secure transport.
*
* @return whether its using a secure transport
* @return whether the underlying socket is using a secure transport
*/
boolean isSecure();
/**
* Suspend the delivery of incoming WebSocket frames.
* <p>
* If this is called from inside the scope of the message handler the suspend takes effect immediately.
* If suspend is called outside the scope of the message handler then the call may take effect
* after 1 more frame is delivered.
* </p>
*
* @return the suspend token suitable for resuming the reading of data on the connection.
* <p>The passive link of communication with a remote WebSocket endpoint.</p>
* <p>Applications provide WebSocket endpoints that implement this interface
* to receive WebSocket events from the remote peer, and can use
* {@link Session} for configuration and to send WebSocket frames or messages
* to the other peer.</p>
*/
SuspendToken suspend();
interface Listener
{
/**
* <p>A WebSocket {@link Session} has opened successfully and is ready to be used.</p>
* <p>Applications can store the given {@link Session} as a field so it can be used
* to send messages back to the other peer.</p>
*
* @param session the WebSocket session
*/
default void onWebSocketOpen(Session session)
{
}
/**
* <p>A WebSocket frame has been received.</p>
* <p>The received frames may be control frames such as PING, PONG or CLOSE,
* or data frames either BINARY or TEXT.</p>
*
* @param frame the received frame
*/
default void onWebSocketFrame(Frame frame, Callback callback)
{
}
/**
* <p>A WebSocket PING frame has been received.</p>
*
* @param payload the PING payload
*/
default void onWebSocketPing(ByteBuffer payload)
{
}
/**
* <p>A WebSocket PONG frame has been received.</p>
*
* @param payload the PONG payload
*/
default void onWebSocketPong(ByteBuffer payload)
{
}
/**
* <p>A WebSocket BINARY (or associated CONTINUATION) frame has been received.</p>
* <p>The {@code ByteBuffer} is read-only, and will be recycled when the {@code callback}
* is completed.</p>
*
* @param payload the BINARY frame payload
* @param last whether this is the last frame
* @param callback the callback to complete when the payload has been processed
*/
default void onWebSocketPartialBinary(ByteBuffer payload, boolean last, Callback callback)
{
}
/**
* <p>A WebSocket TEXT (or associated CONTINUATION) frame has been received.</p>
*
* @param payload the text message payload
* <p>
* Note that due to framing, there is a above average chance of any UTF8 sequences being split on the
* border between two frames will result in either the previous frame, or the next frame having an
* invalid UTF8 sequence, but the combined frames having a valid UTF8 sequence.
* <p>
* The String being provided here will not end in a split UTF8 sequence. Instead this partial sequence
* will be held over until the next frame is received.
* @param last whether this is the last frame
*/
default void onWebSocketPartialText(String payload, boolean last)
{
}
/**
* <p>A WebSocket BINARY message has been received.</p>
*
* @param payload the raw payload array received
*/
default void onWebSocketBinary(ByteBuffer payload, Callback callback)
{
}
/**
* <p>A WebSocket TEXT message has been received.</p>
*
* @param message the text payload
*/
default void onWebSocketText(String message)
{
}
/**
* <p>A WebSocket error has occurred during the processing of WebSocket frames.</p>
* <p>Usually errors occurs from bad or malformed incoming packets, for example
* text frames that do not contain UTF-8 bytes, frames that are too big, or other
* violations of the WebSocket specification.</p>
* <p>The WebSocket {@link Session} will be closed, but applications may
* explicitly {@link Session#close(int, String, Callback) close} the
* {@link Session} providing a different status code or reason.</p>
*
* @param cause the error that occurred
*/
default void onWebSocketError(Throwable cause)
{
}
/**
* <p>The WebSocket {@link Session} has been closed.</p>
*
* @param statusCode the close {@link StatusCode status code}
* @param reason the optional reason for the close
*/
default void onWebSocketClose(int statusCode, String reason)
{
}
/**
* <p>Tag interface that signals that the WebSocket endpoint
* is demanding for WebSocket frames automatically.</p>
*
* @see WebSocket#autoDemand()
*/
interface AutoDemanding extends Session.Listener
{
}
abstract class Abstract implements Listener
{
private volatile Session session;
@Override
public void onWebSocketOpen(Session session)
{
this.session = session;
}
public Session getSession()
{
return session;
}
public boolean isOpen()
{
Session session = this.session;
return session != null && session.isOpen();
}
}
abstract class AbstractAutoDemanding extends Abstract implements AutoDemanding
{
}
}
}

View File

@ -1,25 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* Connection suspend token
*/
public interface SuspendToken
{
/**
* Resume a previously suspended connection.
*/
void resume();
}

View File

@ -1,77 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* Default implementation of the {@link WebSocketListener}.
* <p>
* Convenient abstract class to base standard WebSocket implementations off of.
*/
public class WebSocketAdapter implements WebSocketListener
{
private volatile Session session;
private RemoteEndpoint remote;
public RemoteEndpoint getRemote()
{
return remote;
}
public Session getSession()
{
return session;
}
public boolean isConnected()
{
Session sess = this.session;
return (sess != null) && (sess.isOpen());
}
public boolean isNotConnected()
{
return !isConnected();
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
/* do nothing */
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
/* do nothing */
}
@Override
public void onWebSocketConnect(Session sess)
{
this.session = sess;
this.remote = sess.getRemote();
}
@Override
public void onWebSocketError(Throwable cause)
{
/* do nothing */
}
@Override
public void onWebSocketText(String message)
{
/* do nothing */
}
}

View File

@ -1,26 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* Behavior for how the WebSocket should operate.
* <p>
* This dictated by the <a href="https://tools.ietf.org/html/rfc6455">RFC 6455</a> spec in various places, where certain behavior must be performed depending on
* operation as a <a href="https://tools.ietf.org/html/rfc6455#section-4.1">CLIENT</a> vs a <a href="https://tools.ietf.org/html/rfc6455#section-4.2">SERVER</a>
*/
public enum WebSocketBehavior
{
CLIENT,
SERVER
}

View File

@ -1,58 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* Core WebSocket Connection Listener
*/
public interface WebSocketConnectionListener
{
/**
* A Close Event was received.
* <p>
* The underlying Connection will be considered closed at this point.
*
* @param statusCode the close status code. (See {@link StatusCode})
* @param reason the optional reason for the close.
*/
default void onWebSocketClose(int statusCode, String reason)
{
}
/**
* A WebSocket {@link Session} has connected successfully and is ready to be used.
* <p>
* Note: It is a good idea to track this session as a field in your object so that you can write messages back via the {@link RemoteEndpoint}
*
* @param session the websocket session.
*/
default void onWebSocketConnect(Session session)
{
}
/**
* A WebSocket exception has occurred.
* <p>
* This is a way for the internal implementation to notify of exceptions occured during the processing of websocket.
* <p>
* Usually this occurs from bad / malformed incoming packets. (example: bad UTF8 data, frames that are too big, violations of the spec)
* <p>
* This will result in the {@link Session} being closed by the implementing side.
*
* @param cause the error that occurred.
*/
default void onWebSocketError(Throwable cause)
{
}
}

View File

@ -1,27 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* WebSocket Frame Listener interface for incoming WebSocket frames.
*/
public interface WebSocketFrameListener extends WebSocketConnectionListener
{
/**
* A WebSocket frame has been received.
*
* @param frame the immutable frame received
*/
void onWebSocketFrame(Frame frame);
}

View File

@ -1,40 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* Basic WebSocket Listener interface for incoming WebSocket message events.
*/
public interface WebSocketListener extends WebSocketConnectionListener
{
/**
* A WebSocket binary frame has been received.
*
* @param payload the raw payload array received
* @param offset the offset in the payload array where the data starts
* @param len the length of bytes in the payload
*/
default void onWebSocketBinary(byte[] payload, int offset, int len)
{
}
/**
* A WebSocket Text frame was received.
*
* @param message the message
*/
default void onWebSocketText(String message)
{
}
}

View File

@ -1,53 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
import java.nio.ByteBuffer;
/**
* WebSocket Partial Message Listener interface for incoming WebSocket TEXT/BINARY/CONTINUATION frames.
*/
public interface WebSocketPartialListener extends WebSocketConnectionListener
{
/**
* A WebSocket BINARY (or associated CONTINUATION) frame has been received.
* <p>
* <b>Important Note</b>: The payload {@code ByteBuffer} cannot be modified, and the ByteBuffer object itself
* will be recycled on completion of this method call, make a copy of the data contained within if you want to
* retain it between calls.
*
* @param payload the binary message frame payload
* @param fin true if this is the final frame, false otherwise
*/
default void onWebSocketPartialBinary(ByteBuffer payload, boolean fin)
{
}
/**
* A WebSocket TEXT (or associated CONTINUATION) frame has been received.
*
* @param payload the text message payload
* <p>
* Note that due to framing, there is a above average chance of any UTF8 sequences being split on the
* border between two frames will result in either the previous frame, or the next frame having an
* invalid UTF8 sequence, but the combined frames having a valid UTF8 sequence.
* <p>
* The String being provided here will not end in a split UTF8 sequence. Instead this partial sequence
* will be held over until the next frame is received.
* @param fin true if this is the final frame, false otherwise
*/
default void onWebSocketPartialText(String payload, boolean fin)
{
}
}

View File

@ -1,40 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
import java.nio.ByteBuffer;
/**
* WebSocket PING/PONG Listener interface for incoming WebSocket PING/PONG frames.
*/
public interface WebSocketPingPongListener extends WebSocketConnectionListener
{
/**
* A WebSocket PING has been received.
*
* @param payload the ping payload
*/
default void onWebSocketPing(ByteBuffer payload)
{
}
/**
* A WebSocket PONG has been received.
*
* @param payload the pong payload
*/
default void onWebSocketPong(ByteBuffer payload)
{
}
}

View File

@ -1,62 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.api;
/**
* Callback for Write events.
* <p>
* NOTE: We don't expose org.eclipse.jetty.util.Callback here as that would complicate matters with the WebAppContext's classloader isolation.
*/
public interface WriteCallback
{
WriteCallback NOOP = new WriteCallback()
{
};
/**
* <p>
* Callback invoked when the write fails.
* </p>
*
* @param x the reason for the write failure
*/
default void writeFailed(Throwable x)
{
}
/**
* <p>
* Callback invoked when the write succeeds.
* </p>
*
* @see #writeFailed(Throwable)
*/
default void writeSuccess()
{
}
@Deprecated
class Adaptor implements WriteCallback
{
@Override
public void writeFailed(Throwable x)
{
}
@Override
public void writeSuccess()
{
}
}
}

View File

@ -19,23 +19,17 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.eclipse.jetty.websocket.api.Session;
/**
* Annotation for tagging methods to receive connection close events.
* <p>
* Acceptable method patterns.<br>
* Note: {@code methodName} can be any name you want to use.
* <p>Annotation for methods to receive WebSocket close events.</p>
* <p>Acceptable method patterns:</p>
* <ol>
* <li>{@code public void methodName(int statusCode, String reason)}</li>
* <li><code>public void methodName({@link Session} session, int statusCode, String reason)</code></li>
* <li>{@code public void <methodName>(int statusCode, String reason)}</li>
* <li>{@code public void <methodName>(Session session, int statusCode, String reason)}</li>
* </ol>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value =
{ElementType.METHOD})
@Target(ElementType.METHOD)
public @interface OnWebSocketClose
{
/* no config */
}

View File

@ -19,23 +19,17 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.eclipse.jetty.websocket.api.Session;
/**
* Annotation for receiving websocket errors (exceptions) that have occurred internally in the websocket implementation.
* <p>
* Acceptable method patterns.<br>
* Note: {@code methodName} can be any name you want to use.
* <p>Annotation for methods to receive WebSocket errors.</p>
* <p>Acceptable method patterns:</p>
* <ol>
* <li><code>public void methodName({@link Throwable} error)</code></li>
* <li><code>public void methodName({@link Session} session, {@link Throwable} error)</code></li>
* <li>{@code public void <methodName>(Throwable cause)}</li>
* <li>{@code public void <methodName>(Session session, Throwable cause)}</li>
* </ol>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value =
{ElementType.METHOD})
@Target(ElementType.METHOD)
public @interface OnWebSocketError
{
/* no config */
}

View File

@ -20,23 +20,20 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.eclipse.jetty.websocket.api.Frame;
import org.eclipse.jetty.websocket.api.Session;
/**
* (ADVANCED) Annotation for tagging methods to receive frame events.
* <p>
* Acceptable method patterns.<br>
* Note: {@code methodName} can be any name you want to use.
* <p>Annotation for methods to receive WebSocket frame events.</p>
* <p>Acceptable method patterns:</p>
* <ol>
* <li><code>public void methodName({@link Frame} frame)</code></li>
* <li><code>public void methodName({@link Session} session, {@link Frame} frame)</code></li>
* <li>{@code public void <methodName>(Frame frame)}</li>
* <li>{@code public void <methodName>(Session session, Frame frame)}</li>
* </ol>
*
* @see Frame
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value =
{ElementType.METHOD})
@Target(ElementType.METHOD)
public @interface OnWebSocketFrame
{
/* no config */
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.websocket.api.annotations;
import java.io.InputStream;
import java.io.Reader;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@ -20,15 +21,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketPartialListener;
/**
* Annotation for tagging methods to receive Binary or Text Message events.
* <p>
* Acceptable method patterns.<br>
* Note: {@code methodName} can be any name you want to use.
* <p>
* <p>Annotation for methods to receive BINARY or TEXT WebSocket events.</p>
* <p>Acceptable method patterns:</p>
* <u>Text Message Versions</u>
* <ol>
* <li>{@code public void methodName(String text)}</li>
@ -36,32 +31,38 @@ import org.eclipse.jetty.websocket.api.WebSocketPartialListener;
* <li>{@code public void methodName(Reader reader)}</li>
* <li>{@code public void methodName(Session session, Reader reader)}</li>
* </ol>
* <p>Note: that the {@link Reader} in this case will always use UTF-8 encoding/charset (this is dictated by the RFC 6455 spec for Text Messages. If you need to
* use a non-UTF-8 encoding/charset, you are instructed to use the binary messaging techniques.</p>
* <p>NOTE</p>
* <p>Method that takes a {@link Reader} must have
* {@link WebSocket#autoDemand()} set to {@code true}.</p>
* <p>NOTE</p>
* <p>The {@link Reader} argument will always use the UTF-8 charset,
* (as dictated by RFC 6455). If you need to use a different charset,
* you must use BINARY messages.</p>
* <u>Binary Message Versions</u>
* <ol>
* <li>{@code public void methodName(ByteBuffer message)}</li>
* <li>{@code public void methodName(Session session, ByteBuffer message)}</li>
* <li>{@code public void methodName(byte[] buf, int offset, int length)}</li>
* <li>{@code public void methodName(Session session, byte[] buf, int offset, int length)}</li>
* <li>{@code public void methodName(ByteBuffer message, Callback callback)}</li>
* <li>{@code public void methodName(Session session, ByteBuffer message, Callback callback)}</li>
* <li>{@code public void methodName(InputStream stream)}</li>
* <li>{@code public void methodName(Session session, InputStream stream)}</li>
* </ol>
* <p>NOTE</p>
* <p>Method that takes a {@link InputStream} must have
* {@link WebSocket#autoDemand()} set to {@code true}.</p>
* <u>Partial Message Variations</u>
* <p>These are used to receive partial messages without aggregating them into a complete WebSocket message. Instead the a boolean
* argument is supplied to indicate whether this is the last segment of data of the message. See {@link WebSocketPartialListener}
* interface for more details on partial messages.</p>
* <p>These are used to receive individual frames (and therefore partial
* messages) without aggregating the frames into a complete WebSocket message.
* A {@code boolean} parameter is supplied to indicate whether the frame is
* the last segment of data of the message.</p>
* <ol>
* <li>{@code public void methodName(ByteBuffer payload, boolean last)}</li>
* <li>{@code public void methodName(ByteBuffer payload, boolean last, Callback callback)}</li>
* <li>{@code public void methodName(Session session, ByteBuffer payload, boolean last, Callback callback)}</li>
* <li>{@code public void methodName(String payload, boolean last)}</li>
* <li>{@code public void methodName(Session session, String payload, boolean last)}</li>
* </ol>
* <p>Note: Similar to the signatures above these can all be used with an optional first {@link Session} parameter.</p>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value =
{ElementType.METHOD})
@Target(ElementType.METHOD)
public @interface OnWebSocketMessage
{
/* no config */
}

View File

@ -19,22 +19,16 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.eclipse.jetty.websocket.api.Session;
/**
* Annotation for tagging methods to receive connection open events.
* <p>
* Only 1 acceptable method pattern for this annotation.<br>
* Note: {@code methodName} can be any name you want to use.
* <p>Annotation for methods to receive WebSocket connect events.</p>
* <p>Acceptable method patterns:</p>
* <ol>
* <li><code>public void methodName({@link Session} session)</code></li>
* <li>{@code public void <methodName>(Session session)}</li>
* </ol>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value =
{ElementType.METHOD})
public @interface OnWebSocketConnect
@Target(ElementType.METHOD)
public @interface OnWebSocketOpen
{
/* no config */
}

View File

@ -15,50 +15,31 @@ package org.eclipse.jetty.websocket.api.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.Session;
/**
* Tags a POJO as being a WebSocket class.
* <p>Annotation for classes to be WebSocket endpoints.</p>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value =
{ElementType.TYPE})
@Inherited
@Target(value = ElementType.TYPE)
public @interface WebSocket
{
/**
* The size of the buffer (in bytes) used to read from the network layer.
* <p>Returns whether demand for WebSocket frames is automatically performed
* upon successful return from methods annotated with {@link OnWebSocketOpen},
* {@link OnWebSocketFrame} and {@link OnWebSocketMessage}.</p>
* <p>If the demand is not automatic, then {@link Session#demand()} must be
* explicitly invoked to receive more WebSocket frames (both control and
* data frames, including CLOSE frames).</p>
*
* @return whether demand for WebSocket frames is automatic
*/
int inputBufferSize() default -1;
/**
* The maximum size of a binary message (in bytes) during parsing/generating.
* <p>
* Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
*/
int maxBinaryMessageSize() default -1;
/**
* The time in ms (milliseconds) that a websocket may be idle before closing.
*/
int idleTimeout() default -1;
/**
* The maximum size of a text message during parsing/generating.
* <p>
* Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
*/
int maxTextMessageSize() default -1;
/**
* The output frame buffering mode.
* <p>
* Default: {@link BatchMode#AUTO}
*/
BatchMode batchMode() default BatchMode.AUTO;
boolean autoDemand() default true;
}

View File

@ -1,18 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
/**
* Jetty WebSocket API : WebSocket POJO Annotations
*/
package org.eclipse.jetty.websocket.api.annotations;

View File

@ -13,8 +13,7 @@
package org.eclipse.jetty.websocket.api.exceptions;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
/**
@ -22,12 +21,10 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
* <p>
* A valid WebSocket should do one of the following:
* <ul>
* <li>Implement {@link WebSocketListener}</li>
* <li>Extend {@link WebSocketAdapter}</li>
* <li>Implement {@link Session.Listener}</li>
* <li>Declare the {@link WebSocket &#064;WebSocket} annotation on the type</li>
* </ul>
*/
@SuppressWarnings("serial")
public class InvalidWebSocketException extends WebSocketException
{
public InvalidWebSocketException(String message)

View File

@ -1,18 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
/**
* Jetty WebSocket API : Exception Types
*/
package org.eclipse.jetty.websocket.api.exceptions;

View File

@ -1,18 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
/**
* Jetty WebSocket API
*/
package org.eclipse.jetty.websocket.api;

View File

@ -35,10 +35,9 @@ import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ShutdownThread;
import org.eclipse.jetty.websocket.api.Configurable;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketContainer;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
import org.eclipse.jetty.websocket.client.internal.JettyClientUpgradeRequest;
import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler;
@ -52,7 +51,7 @@ import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebSocketClient extends ContainerLifeCycle implements WebSocketPolicy, WebSocketContainer
public class WebSocketClient extends ContainerLifeCycle implements Configurable, WebSocketContainer
{
private static final Logger LOG = LoggerFactory.getLogger(WebSocketClient.class);
private final WebSocketCoreClient coreClient;
@ -177,12 +176,6 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
dumpObjects(out, indent, getOpenSessions());
}
@Override
public WebSocketBehavior getBehavior()
{
return WebSocketBehavior.CLIENT;
}
@Override
public void addSessionListener(WebSocketSessionListener listener)
{
@ -217,42 +210,6 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
return configurationCustomizer.getIdleTimeout();
}
@Override
public int getInputBufferSize()
{
return configurationCustomizer.getInputBufferSize();
}
@Override
public int getOutputBufferSize()
{
return configurationCustomizer.getOutputBufferSize();
}
@Override
public long getMaxBinaryMessageSize()
{
return configurationCustomizer.getMaxBinaryMessageSize();
}
@Override
public long getMaxTextMessageSize()
{
return configurationCustomizer.getMaxTextMessageSize();
}
@Override
public long getMaxFrameSize()
{
return configurationCustomizer.getMaxFrameSize();
}
@Override
public boolean isAutoFragment()
{
return configurationCustomizer.isAutoFragment();
}
@Override
public void setIdleTimeout(Duration duration)
{
@ -260,42 +217,90 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
getHttpClient().setIdleTimeout(duration.toMillis());
}
@Override
public int getInputBufferSize()
{
return configurationCustomizer.getInputBufferSize();
}
@Override
public void setInputBufferSize(int size)
{
configurationCustomizer.setInputBufferSize(size);
}
@Override
public int getOutputBufferSize()
{
return configurationCustomizer.getOutputBufferSize();
}
@Override
public void setOutputBufferSize(int size)
{
configurationCustomizer.setOutputBufferSize(size);
}
@Override
public long getMaxBinaryMessageSize()
{
return configurationCustomizer.getMaxBinaryMessageSize();
}
@Override
public void setMaxBinaryMessageSize(long size)
{
configurationCustomizer.setMaxBinaryMessageSize(size);
}
@Override
public long getMaxTextMessageSize()
{
return configurationCustomizer.getMaxTextMessageSize();
}
@Override
public void setMaxTextMessageSize(long size)
{
configurationCustomizer.setMaxTextMessageSize(size);
}
@Override
public long getMaxFrameSize()
{
return configurationCustomizer.getMaxFrameSize();
}
@Override
public void setMaxFrameSize(long maxFrameSize)
{
configurationCustomizer.setMaxFrameSize(maxFrameSize);
}
@Override
public boolean isAutoFragment()
{
return configurationCustomizer.isAutoFragment();
}
@Override
public void setAutoFragment(boolean autoFragment)
{
configurationCustomizer.setAutoFragment(autoFragment);
}
@Override
public int getMaxOutgoingFrames()
{
return configurationCustomizer.getMaxOutgoingFrames();
}
@Override
public void setMaxOutgoingFrames(int maxOutgoingFrames)
{
configurationCustomizer.setMaxOutgoingFrames(maxOutgoingFrames);
}
public SocketAddress getBindAddress()
{
return getHttpClient().getBindAddress();

View File

@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.core.OpCode;
@ -37,26 +36,13 @@ import org.eclipse.jetty.websocket.core.OpCode;
*/
public class ClientDemo
{
public class TestSocket extends WebSocketAdapter
public class TestSocket extends Session.Listener.Abstract
{
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
super.onWebSocketClose(statusCode, reason);
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
if (verbose)
{
System.err.printf("%s#onWebSocketConnect %s %s\n", this.getClass().getSimpleName(), session, session.getClass().getSimpleName());
}
System.err.printf("%s#onWebSocketOpen %s %s\n", this.getClass().getSimpleName(), session, session.getClass().getSimpleName());
}
public void send(byte op, byte[] data, int maxFragmentLength)

View File

@ -16,22 +16,21 @@ package examples;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
/**
* Basic Echo Client Socket
*/
@WebSocket(maxTextMessageSize = 64 * 1024)
@WebSocket
public class SimpleEchoSocket
{
private final CountDownLatch closeLatch;
@SuppressWarnings("unused")
private Session session;
public SimpleEchoSocket()
{
@ -47,20 +46,19 @@ public class SimpleEchoSocket
public void onClose(int statusCode, String reason)
{
System.out.printf("Connection closed: %d - %s%n", statusCode, reason);
this.session = null;
this.closeLatch.countDown(); // trigger latch
}
@OnWebSocketConnect
public void onConnect(Session session)
@OnWebSocketOpen
public void onOpen(Session session)
{
System.out.printf("Got connect: %s%n", session);
this.session = session;
System.out.printf("Open: %s%n", session);
session.setMaxTextMessageSize(64 * 1024);
try
{
session.getRemote().sendString("Hello");
session.getRemote().sendString("Thanks for the conversation.");
session.close(StatusCode.NORMAL, "I'm done");
session.sendText("Hello", Callback.NOOP);
session.sendText("Thanks for the conversation.", Callback.NOOP);
session.close(StatusCode.NORMAL, "I'm done", Callback.NOOP);
}
catch (Throwable t)
{

View File

@ -19,7 +19,7 @@ import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.jupiter.TestTrackerExtension;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -40,7 +40,7 @@ public class WebSocketClientBadUriTest
{
public CountDownLatch openLatch = new CountDownLatch(1);
@OnWebSocketConnect
@OnWebSocketOpen
public void onOpen(Session session)
{
openLatch.countDown();

View File

@ -14,21 +14,20 @@
package org.eclipse.jetty.websocket.common;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketContainer;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.FrameHandler;
@ -48,71 +47,45 @@ import org.slf4j.LoggerFactory;
public class JettyWebSocketFrameHandler implements FrameHandler
{
private enum SuspendState
{
DEMANDING,
SUSPENDING,
SUSPENDED,
CLOSED
}
private final AutoLock lock = new AutoLock();
private final AtomicBoolean closeNotified = new AtomicBoolean();
private final Logger log;
private final WebSocketContainer container;
private final Object endpointInstance;
private final BatchMode batchMode;
private final AtomicBoolean closeNotified = new AtomicBoolean();
private final JettyWebSocketFrameHandlerMetadata metadata;
private MethodHandle openHandle;
private MethodHandle closeHandle;
private MethodHandle errorHandle;
private MethodHandle textHandle;
private final Class<? extends MessageSink> textSinkClass;
private MethodHandle binaryHandle;
private final Class<? extends MessageSink> textSinkClass;
private final Class<? extends MessageSink> binarySinkClass;
private MethodHandle frameHandle;
private MethodHandle pingHandle;
private MethodHandle pongHandle;
private UpgradeRequest upgradeRequest;
private UpgradeResponse upgradeResponse;
private final Configuration.Customizer customizer;
private MessageSink textSink;
private MessageSink binarySink;
private MessageSink activeMessageSink;
private WebSocketSession session;
private SuspendState state = SuspendState.DEMANDING;
private Runnable delayedOnFrame;
private CoreSession coreSession;
public JettyWebSocketFrameHandler(WebSocketContainer container,
Object endpointInstance,
MethodHandle openHandle, MethodHandle closeHandle, MethodHandle errorHandle,
MethodHandle textHandle, MethodHandle binaryHandle,
Class<? extends MessageSink> textSinkClass,
Class<? extends MessageSink> binarySinkClass,
MethodHandle frameHandle,
MethodHandle pingHandle, MethodHandle pongHandle,
BatchMode batchMode,
Configuration.Customizer customizer)
public JettyWebSocketFrameHandler(WebSocketContainer container, Object endpointInstance, JettyWebSocketFrameHandlerMetadata metadata)
{
this.log = LoggerFactory.getLogger(endpointInstance.getClass());
this.container = container;
this.endpointInstance = endpointInstance;
this.metadata = metadata;
this.openHandle = openHandle;
this.closeHandle = closeHandle;
this.errorHandle = errorHandle;
this.textHandle = textHandle;
this.binaryHandle = binaryHandle;
this.textSinkClass = textSinkClass;
this.binarySinkClass = binarySinkClass;
this.frameHandle = frameHandle;
this.pingHandle = pingHandle;
this.pongHandle = pongHandle;
this.batchMode = batchMode;
this.customizer = customizer;
this.openHandle = InvokerUtils.bindTo(metadata.getOpenHandle(), endpointInstance);
this.closeHandle = InvokerUtils.bindTo(metadata.getCloseHandle(), endpointInstance);
this.errorHandle = InvokerUtils.bindTo(metadata.getErrorHandle(), endpointInstance);
this.textHandle = InvokerUtils.bindTo(metadata.getTextHandle(), endpointInstance);
this.binaryHandle = InvokerUtils.bindTo(metadata.getBinaryHandle(), endpointInstance);
this.textSinkClass = metadata.getTextSink();
this.binarySinkClass = metadata.getBinarySink();
this.frameHandle = InvokerUtils.bindTo(metadata.getFrameHandle(), endpointInstance);
this.pingHandle = InvokerUtils.bindTo(metadata.getPingHandle(), endpointInstance);
this.pongHandle = InvokerUtils.bindTo(metadata.getPongHandle(), endpointInstance);
}
public void setUpgradeRequest(UpgradeRequest upgradeRequest)
@ -135,11 +108,6 @@ public class JettyWebSocketFrameHandler implements FrameHandler
return upgradeResponse;
}
public BatchMode getBatchMode()
{
return batchMode;
}
public WebSocketSession getSession()
{
return session;
@ -150,8 +118,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler
{
try
{
customizer.customize(coreSession);
this.coreSession = coreSession;
metadata.customize(coreSession);
session = new WebSocketSession(container, coreSession, this);
if (!session.isOpen())
throw new IllegalStateException("Session is not open");
@ -165,13 +132,11 @@ public class JettyWebSocketFrameHandler implements FrameHandler
pingHandle = InvokerUtils.bindTo(pingHandle, session);
pongHandle = InvokerUtils.bindTo(pongHandle, session);
Executor executor = container.getExecutor();
if (textHandle != null)
textSink = JettyWebSocketFrameHandlerFactory.createMessageSink(textHandle, textSinkClass, executor, session);
textSink = createMessageSink(textSinkClass, session, textHandle, isAutoDemand());
if (binaryHandle != null)
binarySink = JettyWebSocketFrameHandlerFactory.createMessageSink(binaryHandle, binarySinkClass, executor, session);
binarySink = createMessageSink(binarySinkClass, session, binaryHandle, isAutoDemand());
if (openHandle != null)
openHandle.invoke();
@ -180,74 +145,90 @@ public class JettyWebSocketFrameHandler implements FrameHandler
container.notifySessionListeners((listener) -> listener.onWebSocketSessionOpened(session));
callback.succeeded();
demand();
}
catch (Throwable cause)
{
callback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " OPEN method error: " + cause.getMessage(), cause));
}
finally
{
autoDemand();
}
}
private static MessageSink createMessageSink(Class<? extends MessageSink> sinkClass, WebSocketSession session, MethodHandle msgHandle, boolean autoDemanding)
{
if (msgHandle == null)
return null;
if (sinkClass == null)
return null;
try
{
MethodHandles.Lookup lookup = JettyWebSocketFrameHandlerFactory.getServerMethodHandleLookup();
MethodHandle ctorHandle = lookup.findConstructor(sinkClass,
MethodType.methodType(void.class, CoreSession.class, MethodHandle.class, boolean.class));
return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgHandle, autoDemanding);
}
catch (NoSuchMethodException e)
{
throw new RuntimeException("Missing expected MessageSink constructor found at: " + sinkClass.getName(), e);
}
catch (IllegalAccessException | InstantiationException | InvocationTargetException e)
{
throw new RuntimeException("Unable to create MessageSink: " + sinkClass.getName(), e);
}
catch (RuntimeException e)
{
throw e;
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
@Override
public void onFrame(Frame frame, Callback callback)
public void onFrame(Frame frame, Callback coreCallback)
{
try (AutoLock l = lock.lock())
{
switch (state)
{
case DEMANDING:
break;
case SUSPENDING:
delayedOnFrame = () -> onFrame(frame, callback);
state = SuspendState.SUSPENDED;
return;
default:
throw new IllegalStateException();
}
// If we have received a close frame, set state to closed to disallow further suspends and resumes.
if (frame.getOpCode() == OpCode.CLOSE)
state = SuspendState.CLOSED;
}
// Send to raw frame handling on user side (eg: WebSocketFrameListener)
CompletableFuture<Void> frameCallback = null;
if (frameHandle != null)
{
try
{
frameHandle.invoke(new JettyWebSocketFrame(frame));
frameCallback = new org.eclipse.jetty.websocket.api.Callback.Completable();
frameHandle.invoke(new JettyWebSocketFrame(frame), frameCallback);
}
catch (Throwable cause)
{
throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " FRAME method error: " + cause.getMessage(), cause);
coreCallback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " FRAME method error: " + cause.getMessage(), cause));
return;
}
}
Callback.Completable eventCallback = new Callback.Completable();
switch (frame.getOpCode())
{
case OpCode.CLOSE:
onCloseFrame(frame, callback);
break;
case OpCode.PING:
onPingFrame(frame, callback);
break;
case OpCode.PONG:
onPongFrame(frame, callback);
break;
case OpCode.TEXT:
onTextFrame(frame, callback);
break;
case OpCode.BINARY:
onBinaryFrame(frame, callback);
break;
case OpCode.CONTINUATION:
onContinuationFrame(frame, callback);
break;
default:
callback.failed(new IllegalStateException());
}
case OpCode.CLOSE -> onCloseFrame(frame, eventCallback);
case OpCode.PING -> onPingFrame(frame, eventCallback);
case OpCode.PONG -> onPongFrame(frame, eventCallback);
case OpCode.TEXT -> onTextFrame(frame, eventCallback);
case OpCode.BINARY -> onBinaryFrame(frame, eventCallback);
case OpCode.CONTINUATION -> onContinuationFrame(frame, eventCallback);
default -> coreCallback.failed(new IllegalStateException());
};
// Combine the callback from the frame handler and the event handler.
CompletableFuture<Void> callback = eventCallback;
if (frameCallback != null)
callback = frameCallback.thenCompose(ignored -> eventCallback);
callback.whenComplete((r, x) ->
{
if (x == null)
coreCallback.succeeded();
else
coreCallback.failed(x);
});
}
@Override
@ -257,11 +238,13 @@ public class JettyWebSocketFrameHandler implements FrameHandler
{
cause = convertCause(cause);
if (errorHandle != null)
{
errorHandle.invoke(cause);
}
else
{
if (log.isDebugEnabled())
log.warn("Unhandled Error: Endpoint {}", endpointInstance.getClass().getName(), cause);
log.debug("Unhandled Error: Endpoint {}", endpointInstance.getClass().getName(), cause);
else
log.warn("Unhandled Error: Endpoint {} : {}", endpointInstance.getClass().getName(), cause.toString());
}
@ -275,24 +258,18 @@ public class JettyWebSocketFrameHandler implements FrameHandler
}
}
private void onCloseFrame(Frame frame, Callback callback)
{
notifyOnClose(CloseStatus.getCloseStatus(frame), callback);
}
@Override
public void onClosed(CloseStatus closeStatus, Callback callback)
{
try (AutoLock l = lock.lock())
{
// We are now closed and cannot suspend or resume.
state = SuspendState.CLOSED;
}
notifyOnClose(closeStatus, callback);
container.notifySessionListeners((listener) -> listener.onWebSocketSessionClosed(session));
}
private void onCloseFrame(Frame frame, Callback callback)
{
notifyOnClose(CloseStatus.getCloseStatus(frame), callback);
}
private void notifyOnClose(CloseStatus closeStatus, Callback callback)
{
// Make sure onClose is only notified once.
@ -306,7 +283,6 @@ public class JettyWebSocketFrameHandler implements FrameHandler
{
if (closeHandle != null)
closeHandle.invoke(closeStatus.getCode(), closeStatus.getReason());
callback.succeeded();
}
catch (Throwable cause)
@ -315,40 +291,6 @@ public class JettyWebSocketFrameHandler implements FrameHandler
}
}
public String toString()
{
return String.format("%s@%x[%s]", this.getClass().getSimpleName(), this.hashCode(), endpointInstance.getClass().getName());
}
private void acceptMessage(Frame frame, Callback callback)
{
// No message sink is active
if (activeMessageSink == null)
{
callback.succeeded();
demand();
return;
}
// Accept the payload into the message sink
activeMessageSink.accept(frame, callback);
if (frame.isFin())
activeMessageSink = null;
}
private void onBinaryFrame(Frame frame, Callback callback)
{
if (activeMessageSink == null)
activeMessageSink = binarySink;
acceptMessage(frame, callback);
}
private void onContinuationFrame(Frame frame, Callback callback)
{
acceptMessage(frame, callback);
}
private void onPingFrame(Frame frame, Callback callback)
{
if (pingHandle != null)
@ -358,35 +300,34 @@ public class JettyWebSocketFrameHandler implements FrameHandler
ByteBuffer payload = frame.getPayload();
if (payload == null)
payload = BufferUtil.EMPTY_BUFFER;
else
payload = BufferUtil.copy(payload);
pingHandle.invoke(payload);
callback.succeeded();
autoDemand();
}
catch (Throwable cause)
{
throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " PING method error: " + cause.getMessage(), cause);
callback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " PING method error: " + cause.getMessage(), cause));
}
callback.succeeded();
demand();
}
else
{
// Automatically respond.
getSession().getRemote().sendPong(frame.getPayload(), new WriteCallback()
getSession().sendPong(frame.getPayload(), new org.eclipse.jetty.websocket.api.Callback()
{
@Override
public void writeSuccess()
public void succeed()
{
callback.succeeded();
demand();
autoDemand();
}
@Override
public void writeFailed(Throwable x)
public void fail(Throwable x)
{
// Ignore failures, we might be output closed and receive ping.
// Ignore failures, we might be output closed and receive a PING.
callback.succeeded();
demand();
}
});
}
@ -401,111 +342,74 @@ public class JettyWebSocketFrameHandler implements FrameHandler
ByteBuffer payload = frame.getPayload();
if (payload == null)
payload = BufferUtil.EMPTY_BUFFER;
else
payload = BufferUtil.copy(payload);
pongHandle.invoke(payload);
callback.succeeded();
autoDemand();
}
catch (Throwable cause)
{
throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " PONG method error: " + cause.getMessage(), cause);
callback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " PONG method error: " + cause.getMessage(), cause));
}
}
callback.succeeded();
demand();
else
{
autoDemand();
}
}
private void onTextFrame(Frame frame, Callback callback)
{
if (activeMessageSink == null)
activeMessageSink = textSink;
acceptMessage(frame, callback);
acceptFrame(frame, callback);
}
@Override
public boolean isAutoDemanding()
private void onBinaryFrame(Frame frame, Callback callback)
{
return false;
if (activeMessageSink == null)
activeMessageSink = binarySink;
acceptFrame(frame, callback);
}
public void suspend()
private void onContinuationFrame(Frame frame, Callback callback)
{
try (AutoLock l = lock.lock())
{
switch (state)
{
case DEMANDING:
state = SuspendState.SUSPENDING;
break;
default:
throw new IllegalStateException(state.name());
}
}
acceptFrame(frame, callback);
}
public void resume()
private void acceptFrame(Frame frame, Callback callback)
{
boolean needDemand = false;
Runnable delayedFrame = null;
try (AutoLock l = lock.lock())
// No message sink is active.
if (activeMessageSink == null)
{
switch (state)
{
case DEMANDING:
throw new IllegalStateException("Already Resumed");
case SUSPENDED:
needDemand = true;
delayedFrame = delayedOnFrame;
delayedOnFrame = null;
state = SuspendState.DEMANDING;
break;
case SUSPENDING:
if (delayedOnFrame != null)
throw new IllegalStateException();
state = SuspendState.DEMANDING;
break;
default:
throw new IllegalStateException(state.name());
}
callback.succeeded();
autoDemand();
return;
}
if (needDemand)
{
if (delayedFrame != null)
delayedFrame.run();
else
session.getCoreSession().demand(1);
}
// Accept the payload into the message sink.
activeMessageSink.accept(frame, callback);
if (frame.isFin())
activeMessageSink = null;
}
private void demand()
boolean isAutoDemand()
{
boolean demand = false;
try (AutoLock l = lock.lock())
{
switch (state)
{
case DEMANDING:
demand = true;
break;
return metadata.isAutoDemand();
}
case SUSPENDING:
state = SuspendState.SUSPENDED;
break;
default:
throw new IllegalStateException(state.name());
}
}
if (demand)
private void autoDemand()
{
if (isAutoDemand())
session.getCoreSession().demand(1);
}
public String toString()
{
return String.format("%s@%x[%s]", this.getClass().getSimpleName(), this.hashCode(), endpointInstance.getClass().getName());
}
public static Throwable convertCause(Throwable cause)
{
if (cause instanceof MessageTooLargeException)

View File

@ -19,41 +19,29 @@ import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Frame;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketConnectionListener;
import org.eclipse.jetty.websocket.api.WebSocketContainer;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPartialListener;
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.api.exceptions.InvalidWebSocketException;
import org.eclipse.jetty.websocket.common.internal.ByteBufferMessageSink;
import org.eclipse.jetty.websocket.common.internal.PartialByteBufferMessageSink;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException;
import org.eclipse.jetty.websocket.core.messages.ByteArrayMessageSink;
import org.eclipse.jetty.websocket.core.messages.ByteBufferMessageSink;
import org.eclipse.jetty.websocket.core.messages.InputStreamMessageSink;
import org.eclipse.jetty.websocket.core.messages.MessageSink;
import org.eclipse.jetty.websocket.core.messages.PartialByteBufferMessageSink;
import org.eclipse.jetty.websocket.core.messages.PartialStringMessageSink;
import org.eclipse.jetty.websocket.core.messages.ReaderMessageSink;
import org.eclipse.jetty.websocket.core.messages.StringMessageSink;
@ -68,12 +56,7 @@ import org.eclipse.jetty.websocket.core.util.ReflectUtils;
* </p>
* <ul>
* <li>Is &#64;{@link org.eclipse.jetty.websocket.api.annotations.WebSocket} annotated</li>
* <li>Extends {@link org.eclipse.jetty.websocket.api.WebSocketAdapter}</li>
* <li>Implements {@link org.eclipse.jetty.websocket.api.WebSocketListener}</li>
* <li>Implements {@link org.eclipse.jetty.websocket.api.WebSocketConnectionListener}</li>
* <li>Implements {@link org.eclipse.jetty.websocket.api.WebSocketPartialListener}</li>
* <li>Implements {@link org.eclipse.jetty.websocket.api.WebSocketPingPongListener}</li>
* <li>Implements {@link org.eclipse.jetty.websocket.api.WebSocketFrameListener}</li>
* <li>Implements {@link org.eclipse.jetty.websocket.api.Session.Listener}</li>
* </ul>
*/
public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
@ -85,14 +68,8 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
private static final InvokerUtils.Arg[] binaryBufferCallingArgs = new InvokerUtils.Arg[]{
new InvokerUtils.Arg(Session.class),
new InvokerUtils.Arg(ByteBuffer.class).required()
};
private static final InvokerUtils.Arg[] binaryArrayCallingArgs = new InvokerUtils.Arg[]{
new InvokerUtils.Arg(Session.class),
new InvokerUtils.Arg(byte[].class).required(),
new InvokerUtils.Arg(int.class), // offset
new InvokerUtils.Arg(int.class) // length
new InvokerUtils.Arg(ByteBuffer.class).required(),
new InvokerUtils.Arg(Callback.class).required()
};
private static final InvokerUtils.Arg[] inputStreamCallingArgs = new InvokerUtils.Arg[]{
@ -114,13 +91,8 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
private static final InvokerUtils.Arg[] binaryPartialBufferCallingArgs = new InvokerUtils.Arg[]{
new InvokerUtils.Arg(Session.class),
new InvokerUtils.Arg(ByteBuffer.class).required(),
new InvokerUtils.Arg(boolean.class).required()
};
private static final InvokerUtils.Arg[] binaryPartialArrayCallingArgs = new InvokerUtils.Arg[]{
new InvokerUtils.Arg(Session.class),
new InvokerUtils.Arg(byte[].class).required(),
new InvokerUtils.Arg(boolean.class).required()
new InvokerUtils.Arg(boolean.class).required(),
new InvokerUtils.Arg(Callback.class).required()
};
private final WebSocketContainer container;
@ -153,16 +125,12 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
public JettyWebSocketFrameHandlerMetadata createMetadata(Class<?> endpointClass)
{
if (WebSocketConnectionListener.class.isAssignableFrom(endpointClass))
{
if (Session.Listener.class.isAssignableFrom(endpointClass))
return createListenerMetadata(endpointClass);
}
WebSocket websocket = endpointClass.getAnnotation(WebSocket.class);
if (websocket != null)
{
return createAnnotatedMetadata(websocket, endpointClass);
}
throw new InvalidWebSocketException("Unrecognized WebSocket endpoint: " + endpointClass.getName());
}
@ -171,62 +139,10 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
{
JettyWebSocketFrameHandlerMetadata metadata = getMetadata(endpointInstance.getClass());
final MethodHandle openHandle = InvokerUtils.bindTo(metadata.getOpenHandle(), endpointInstance);
final MethodHandle closeHandle = InvokerUtils.bindTo(metadata.getCloseHandle(), endpointInstance);
final MethodHandle errorHandle = InvokerUtils.bindTo(metadata.getErrorHandle(), endpointInstance);
final MethodHandle textHandle = InvokerUtils.bindTo(metadata.getTextHandle(), endpointInstance);
final MethodHandle binaryHandle = InvokerUtils.bindTo(metadata.getBinaryHandle(), endpointInstance);
final Class<? extends MessageSink> textSinkClass = metadata.getTextSink();
final Class<? extends MessageSink> binarySinkClass = metadata.getBinarySink();
final MethodHandle frameHandle = InvokerUtils.bindTo(metadata.getFrameHandle(), endpointInstance);
final MethodHandle pingHandle = InvokerUtils.bindTo(metadata.getPingHandle(), endpointInstance);
final MethodHandle pongHandle = InvokerUtils.bindTo(metadata.getPongHandle(), endpointInstance);
BatchMode batchMode = metadata.getBatchMode();
// Decorate the endpointInstance while we are still upgrading for access to things like HttpSession.
components.getObjectFactory().decorate(endpointInstance);
return new JettyWebSocketFrameHandler(
container,
endpointInstance,
openHandle, closeHandle, errorHandle,
textHandle, binaryHandle,
textSinkClass, binarySinkClass,
frameHandle, pingHandle, pongHandle,
batchMode,
metadata);
}
public static MessageSink createMessageSink(MethodHandle msgHandle, Class<? extends MessageSink> sinkClass, Executor executor, WebSocketSession session)
{
if (msgHandle == null)
return null;
if (sinkClass == null)
return null;
try
{
MethodHandles.Lookup lookup = JettyWebSocketFrameHandlerFactory.getServerMethodHandleLookup();
MethodHandle ctorHandle = lookup.findConstructor(sinkClass,
MethodType.methodType(void.class, CoreSession.class, MethodHandle.class));
return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgHandle);
}
catch (NoSuchMethodException e)
{
throw new RuntimeException("Missing expected MessageSink constructor found at: " + sinkClass.getName(), e);
}
catch (IllegalAccessException | InstantiationException | InvocationTargetException e)
{
throw new RuntimeException("Unable to create MessageSink: " + sinkClass.getName(), e);
}
catch (RuntimeException e)
{
throw e;
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
return new JettyWebSocketFrameHandler(container, endpointInstance, metadata);
}
private MethodHandle toMethodHandle(MethodHandles.Lookup lookup, Method method)
@ -244,99 +160,121 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
private JettyWebSocketFrameHandlerMetadata createListenerMetadata(Class<?> endpointClass)
{
JettyWebSocketFrameHandlerMetadata metadata = new JettyWebSocketFrameHandlerMetadata();
metadata.setAutoDemand(Session.Listener.AutoDemanding.class.isAssignableFrom(endpointClass));
MethodHandles.Lookup lookup = JettyWebSocketFrameHandlerFactory.getServerMethodHandleLookup();
if (!WebSocketConnectionListener.class.isAssignableFrom(endpointClass))
throw new IllegalArgumentException("Class " + endpointClass + " does not implement " + WebSocketConnectionListener.class);
Method openMethod = ReflectUtils.findMethod(WebSocketConnectionListener.class, "onWebSocketConnect", Session.class);
MethodHandle open = toMethodHandle(lookup, openMethod);
metadata.setOpenHandler(open, openMethod);
Method closeMethod = ReflectUtils.findMethod(WebSocketConnectionListener.class, "onWebSocketClose", int.class, String.class);
MethodHandle close = toMethodHandle(lookup, closeMethod);
metadata.setCloseHandler(close, closeMethod);
Method errorMethod = ReflectUtils.findMethod(WebSocketConnectionListener.class, "onWebSocketError", Throwable.class);
MethodHandle error = toMethodHandle(lookup, errorMethod);
metadata.setErrorHandler(error, errorMethod);
// Simple Data Listener
if (WebSocketListener.class.isAssignableFrom(endpointClass))
Method openMethod = findMethod(endpointClass, "onWebSocketOpen", Session.class);
if (openMethod != null)
{
Method textMethod = ReflectUtils.findMethod(WebSocketListener.class, "onWebSocketText", String.class);
MethodHandle text = toMethodHandle(lookup, textMethod);
metadata.setTextHandler(StringMessageSink.class, text, textMethod);
Method binaryMethod = ReflectUtils.findMethod(WebSocketListener.class, "onWebSocketBinary", byte[].class, int.class, int.class);
MethodHandle binary = toMethodHandle(lookup, binaryMethod);
metadata.setBinaryHandle(ByteArrayMessageSink.class, binary, binaryMethod);
MethodHandle connectHandle = toMethodHandle(lookup, openMethod);
metadata.setOpenHandle(connectHandle, openMethod);
}
// Ping/Pong Listener
if (WebSocketPingPongListener.class.isAssignableFrom(endpointClass))
Method frameMethod = findMethod(endpointClass, "onWebSocketFrame", Frame.class, Callback.class);
if (frameMethod != null)
{
Method pongMethod = ReflectUtils.findMethod(WebSocketPingPongListener.class, "onWebSocketPong", ByteBuffer.class);
MethodHandle pong = toMethodHandle(lookup, pongMethod);
metadata.setPongHandle(pong, pongMethod);
Method pingMethod = ReflectUtils.findMethod(WebSocketPingPongListener.class, "onWebSocketPing", ByteBuffer.class);
MethodHandle ping = toMethodHandle(lookup, pingMethod);
metadata.setPingHandle(ping, pingMethod);
MethodHandle frameHandle = toMethodHandle(lookup, frameMethod);
metadata.setFrameHandle(frameHandle, frameMethod);
}
// Partial Data / Message Listener
if (WebSocketPartialListener.class.isAssignableFrom(endpointClass))
Method pingMethod = findMethod(endpointClass, "onWebSocketPing", ByteBuffer.class);
if (pingMethod != null)
{
Method textMethod = ReflectUtils.findMethod(WebSocketPartialListener.class, "onWebSocketPartialText", String.class, boolean.class);
MethodHandle text = toMethodHandle(lookup, textMethod);
metadata.setTextHandler(PartialStringMessageSink.class, text, textMethod);
Method binaryMethod = ReflectUtils.findMethod(WebSocketPartialListener.class, "onWebSocketPartialBinary", ByteBuffer.class, boolean.class);
MethodHandle binary = toMethodHandle(lookup, binaryMethod);
metadata.setBinaryHandle(PartialByteBufferMessageSink.class, binary, binaryMethod);
MethodHandle pingHandle = toMethodHandle(lookup, pingMethod);
metadata.setPingHandle(pingHandle, pingMethod);
}
// Frame Listener
if (WebSocketFrameListener.class.isAssignableFrom(endpointClass))
Method pongMethod = findMethod(endpointClass, "onWebSocketPong", ByteBuffer.class);
if (pongMethod != null)
{
Method frameMethod = ReflectUtils.findMethod(WebSocketFrameListener.class, "onWebSocketFrame", Frame.class);
MethodHandle frame = toMethodHandle(lookup, frameMethod);
metadata.setFrameHandler(frame, frameMethod);
MethodHandle pongHandle = toMethodHandle(lookup, pongMethod);
metadata.setPongHandle(pongHandle, pongMethod);
}
Method partialTextMethod = findMethod(endpointClass, "onWebSocketPartialText", String.class, boolean.class);
if (partialTextMethod != null)
{
MethodHandle partialTextHandle = toMethodHandle(lookup, partialTextMethod);
metadata.setTextHandle(PartialStringMessageSink.class, partialTextHandle, partialTextMethod);
}
Method partialBinaryMethod = findMethod(endpointClass, "onWebSocketPartialBinary", ByteBuffer.class, boolean.class, Callback.class);
if (partialBinaryMethod != null)
{
MethodHandle partialBinaryHandle = toMethodHandle(lookup, partialBinaryMethod);
metadata.setBinaryHandle(PartialByteBufferMessageSink.class, partialBinaryHandle, partialBinaryMethod);
}
Method textMethod = findMethod(endpointClass, "onWebSocketText", String.class);
if (textMethod != null)
{
MethodHandle textHandle = toMethodHandle(lookup, textMethod);
metadata.setTextHandle(StringMessageSink.class, textHandle, textMethod);
}
Method binaryMethod = findMethod(endpointClass, "onWebSocketBinary", ByteBuffer.class, Callback.class);
if (binaryMethod != null)
{
MethodHandle binaryHandle = toMethodHandle(lookup, binaryMethod);
metadata.setBinaryHandle(ByteBufferMessageSink.class, binaryHandle, binaryMethod);
}
Method errorMethod = findMethod(endpointClass, "onWebSocketError", Throwable.class);
if (errorMethod != null)
{
MethodHandle errorHandle = toMethodHandle(lookup, errorMethod);
metadata.setErrorHandle(errorHandle, errorMethod);
}
Method closeMethod = findMethod(endpointClass, "onWebSocketClose", int.class, String.class);
if (closeMethod != null)
{
MethodHandle closeHandle = toMethodHandle(lookup, closeMethod);
metadata.setCloseHandle(closeHandle, closeMethod);
}
return metadata;
}
private Method findMethod(Class<?> klass, String name, Class<?>... parameters)
{
// Verify if the method is overridden in the endpoint class, to avoid
// calling all methods of Session.Listener even if they are not overridden.
Method method = ReflectUtils.findMethod(klass, name, parameters);
if (method == null)
return null;
if (!isOverridden(method))
return null;
// The method is overridden, but it may be declared in a non-public
// class, for example an anonymous class, where it won't be accessible,
// therefore replace it with the accessible version from Session.Listener.
if (!Modifier.isPublic(klass.getModifiers()))
method = ReflectUtils.findMethod(Session.Listener.class, name, parameters);
return method;
}
private boolean isOverridden(Method method)
{
return method != null && method.getDeclaringClass() != Session.Listener.class;
}
private JettyWebSocketFrameHandlerMetadata createAnnotatedMetadata(WebSocket anno, Class<?> endpointClass)
{
JettyWebSocketFrameHandlerMetadata metadata = new JettyWebSocketFrameHandlerMetadata();
int max = anno.inputBufferSize();
if (max >= 0)
metadata.setInputBufferSize(max);
max = anno.maxBinaryMessageSize();
if (max >= 0)
metadata.setMaxBinaryMessageSize(max);
max = anno.maxTextMessageSize();
if (max >= 0)
metadata.setMaxTextMessageSize(max);
max = anno.idleTimeout();
if (max >= 0)
metadata.setIdleTimeout(Duration.ofMillis(max));
metadata.setBatchMode(anno.batchMode());
metadata.setAutoDemand(anno.autoDemand());
MethodHandles.Lookup lookup = getApplicationMethodHandleLookup(endpointClass);
Method onmethod;
// OnWebSocketConnect [0..1]
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnWebSocketConnect.class);
// OnWebSocketOpen [0..1]
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnWebSocketOpen.class);
if (onmethod != null)
{
assertSignatureValid(endpointClass, onmethod, OnWebSocketConnect.class);
assertSignatureValid(endpointClass, onmethod, OnWebSocketOpen.class);
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class).required();
MethodHandle methodHandle = InvokerUtils.mutatedInvoker(lookup, endpointClass, onmethod, SESSION);
metadata.setOpenHandler(methodHandle, onmethod);
metadata.setOpenHandle(methodHandle, onmethod);
}
// OnWebSocketClose [0..1]
@ -348,7 +286,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
final InvokerUtils.Arg STATUS_CODE = new InvokerUtils.Arg(int.class);
final InvokerUtils.Arg REASON = new InvokerUtils.Arg(String.class);
MethodHandle methodHandle = InvokerUtils.mutatedInvoker(lookup, endpointClass, onmethod, SESSION, STATUS_CODE, REASON);
metadata.setCloseHandler(methodHandle, onmethod);
metadata.setCloseHandle(methodHandle, onmethod);
}
// OnWebSocketError [0..1]
@ -359,7 +297,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
final InvokerUtils.Arg CAUSE = new InvokerUtils.Arg(Throwable.class).required();
MethodHandle methodHandle = InvokerUtils.mutatedInvoker(lookup, endpointClass, onmethod, SESSION, CAUSE);
metadata.setErrorHandler(methodHandle, onmethod);
metadata.setErrorHandle(methodHandle, onmethod);
}
// OnWebSocketFrame [0..1]
@ -369,13 +307,14 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
assertSignatureValid(endpointClass, onmethod, OnWebSocketFrame.class);
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
final InvokerUtils.Arg FRAME = new InvokerUtils.Arg(Frame.class).required();
MethodHandle methodHandle = InvokerUtils.mutatedInvoker(lookup, endpointClass, onmethod, SESSION, FRAME);
metadata.setFrameHandler(methodHandle, onmethod);
final InvokerUtils.Arg CALLBACK = new InvokerUtils.Arg(Callback.class).required();
MethodHandle methodHandle = InvokerUtils.mutatedInvoker(lookup, endpointClass, onmethod, SESSION, FRAME, CALLBACK);
metadata.setFrameHandle(methodHandle, onmethod);
}
// OnWebSocketMessage [0..2]
Method[] onMessages = ReflectUtils.findAnnotatedMethods(endpointClass, OnWebSocketMessage.class);
if (onMessages != null && onMessages.length > 0)
if (onMessages != null)
{
// The different kind of @OnWebSocketMessage method parameter signatures expected
for (Method onMsg : onMessages)
@ -387,7 +326,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
{
// Normal Text Message
assertSignatureValid(endpointClass, onMsg, OnWebSocketMessage.class);
metadata.setTextHandler(StringMessageSink.class, methodHandle, onMsg);
metadata.setTextHandle(StringMessageSink.class, methodHandle, onMsg);
continue;
}
@ -400,18 +339,12 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
continue;
}
methodHandle = InvokerUtils.optionalMutatedInvoker(lookup, endpointClass, onMsg, binaryArrayCallingArgs);
if (methodHandle != null)
{
// byte[] Binary Message
assertSignatureValid(endpointClass, onMsg, OnWebSocketMessage.class);
metadata.setBinaryHandle(ByteArrayMessageSink.class, methodHandle, onMsg);
continue;
}
methodHandle = InvokerUtils.optionalMutatedInvoker(lookup, endpointClass, onMsg, inputStreamCallingArgs);
if (methodHandle != null)
{
if (!metadata.isAutoDemand())
throw new InvalidWebSocketException("InputStream methods require auto-demanding WebSocket endpoints");
// InputStream Binary Message
assertSignatureValid(endpointClass, onMsg, OnWebSocketMessage.class);
metadata.setBinaryHandle(InputStreamMessageSink.class, methodHandle, onMsg);
@ -421,9 +354,12 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
methodHandle = InvokerUtils.optionalMutatedInvoker(lookup, endpointClass, onMsg, readerCallingArgs);
if (methodHandle != null)
{
if (!metadata.isAutoDemand())
throw new InvalidWebSocketException("Reader methods require auto-demanding WebSocket endpoints");
// Reader Text Message
assertSignatureValid(endpointClass, onMsg, OnWebSocketMessage.class);
metadata.setTextHandler(ReaderMessageSink.class, methodHandle, onMsg);
metadata.setTextHandle(ReaderMessageSink.class, methodHandle, onMsg);
continue;
}
@ -432,7 +368,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle
{
// Partial Text Message
assertSignatureValid(endpointClass, onMsg, OnWebSocketMessage.class);
metadata.setTextHandler(PartialStringMessageSink.class, methodHandle, onMsg);
metadata.setTextHandle(PartialStringMessageSink.class, methodHandle, onMsg);
continue;
}

View File

@ -15,38 +15,32 @@ package org.eclipse.jetty.websocket.common;
import java.lang.invoke.MethodHandle;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.exceptions.InvalidWebSocketException;
import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.messages.MessageSink;
public class JettyWebSocketFrameHandlerMetadata extends Configuration.ConfigurationCustomizer
{
private boolean autoDemand;
private MethodHandle openHandle;
private MethodHandle closeHandle;
private MethodHandle errorHandle;
private MethodHandle frameHandle;
private MethodHandle textHandle;
private Class<? extends MessageSink> textSink;
private MethodHandle binaryHandle;
private Class<? extends MessageSink> binarySink;
private MethodHandle pingHandle;
private MethodHandle pongHandle;
// Batch Configuration
private BatchMode batchMode = BatchMode.OFF;
public void setBatchMode(BatchMode batchMode)
public boolean isAutoDemand()
{
this.batchMode = batchMode;
return autoDemand;
}
public BatchMode getBatchMode()
public void setAutoDemand(boolean autoDemand)
{
return batchMode;
this.autoDemand = autoDemand;
}
public void setBinaryHandle(Class<? extends MessageSink> sinkClass, MethodHandle binary, Object origin)
@ -66,7 +60,7 @@ public class JettyWebSocketFrameHandlerMetadata extends Configuration.Configurat
return binarySink;
}
public void setCloseHandler(MethodHandle close, Object origin)
public void setCloseHandle(MethodHandle close, Object origin)
{
assertNotSet(this.closeHandle, "CLOSE Handler", origin);
this.closeHandle = close;
@ -77,7 +71,7 @@ public class JettyWebSocketFrameHandlerMetadata extends Configuration.Configurat
return closeHandle;
}
public void setErrorHandler(MethodHandle error, Object origin)
public void setErrorHandle(MethodHandle error, Object origin)
{
assertNotSet(this.errorHandle, "ERROR Handler", origin);
this.errorHandle = error;
@ -88,7 +82,7 @@ public class JettyWebSocketFrameHandlerMetadata extends Configuration.Configurat
return errorHandle;
}
public void setFrameHandler(MethodHandle frame, Object origin)
public void setFrameHandle(MethodHandle frame, Object origin)
{
assertNotSet(this.frameHandle, "FRAME Handler", origin);
this.frameHandle = frame;
@ -99,10 +93,10 @@ public class JettyWebSocketFrameHandlerMetadata extends Configuration.Configurat
return frameHandle;
}
public void setOpenHandler(MethodHandle open, Object origin)
public void setOpenHandle(MethodHandle openHandle, Object origin)
{
assertNotSet(this.openHandle, "OPEN Handler", origin);
this.openHandle = open;
this.openHandle = openHandle;
}
public MethodHandle getOpenHandle()
@ -132,7 +126,7 @@ public class JettyWebSocketFrameHandlerMetadata extends Configuration.Configurat
return pongHandle;
}
public void setTextHandler(Class<? extends MessageSink> sinkClass, MethodHandle text, Object origin)
public void setTextHandle(Class<? extends MessageSink> sinkClass, MethodHandle text, Object origin)
{
assertNotSet(this.textHandle, "TEXT Handler", origin);
this.textHandle = text;

View File

@ -1,235 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.common;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.Objects;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.core.exception.ProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.nio.charset.StandardCharsets.UTF_8;
public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket.api.RemoteEndpoint
{
private static final Logger LOG = LoggerFactory.getLogger(JettyWebSocketRemoteEndpoint.class);
private final CoreSession coreSession;
private byte messageType = -1;
private BatchMode batchMode;
public JettyWebSocketRemoteEndpoint(CoreSession coreSession, BatchMode batchMode)
{
this.coreSession = Objects.requireNonNull(coreSession);
this.batchMode = batchMode;
}
@Override
public void sendString(String text) throws IOException
{
sendBlocking(new Frame(OpCode.TEXT).setPayload(text));
}
@Override
public void sendString(String text, WriteCallback callback)
{
Callback cb = callback == null ? Callback.NOOP : Callback.from(callback::writeSuccess, callback::writeFailed);
coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(text), cb, isBatch());
}
@Override
public void sendBytes(ByteBuffer data) throws IOException
{
sendBlocking(new Frame(OpCode.BINARY).setPayload(data));
}
@Override
public void sendBytes(ByteBuffer data, WriteCallback callback)
{
coreSession.sendFrame(new Frame(OpCode.BINARY).setPayload(data),
Callback.from(callback::writeSuccess, callback::writeFailed),
isBatch());
}
@Override
public void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException
{
FutureCallback b = new FutureCallback();
sendPartialBytes(fragment, isLast, b);
b.block();
}
@Override
public void sendPartialBytes(ByteBuffer fragment, boolean isLast, WriteCallback callback)
{
sendPartialBytes(fragment, isLast, Callback.from(callback::writeSuccess, callback::writeFailed));
}
private void sendPartialBytes(ByteBuffer fragment, boolean isLast, Callback callback)
{
Frame frame;
switch (messageType)
{
case -1: // new message
frame = new Frame(OpCode.BINARY);
messageType = OpCode.BINARY;
break;
case OpCode.BINARY:
frame = new Frame(OpCode.CONTINUATION);
break;
default:
callback.failed(new ProtocolException("Attempt to send Partial Binary during active opcode " + messageType));
return;
}
frame.setPayload(fragment);
frame.setFin(isLast);
coreSession.sendFrame(frame, callback, isBatch());
if (isLast)
{
messageType = -1;
}
}
@Override
public void sendPartialString(String fragment, boolean isLast) throws IOException
{
FutureCallback b = new FutureCallback();
sendPartialText(fragment, isLast, b);
b.block();
}
@Override
public void sendPartialString(String fragment, boolean isLast, WriteCallback callback)
{
sendPartialText(fragment, isLast, Callback.from(callback::writeSuccess, callback::writeFailed));
}
@Override
public void sendPing(ByteBuffer applicationData) throws IOException
{
sendBlocking(new Frame(OpCode.PING).setPayload(applicationData));
}
@Override
public void sendPing(ByteBuffer applicationData, WriteCallback callback)
{
coreSession.sendFrame(new Frame(OpCode.PING).setPayload(applicationData),
Callback.from(callback::writeSuccess, callback::writeFailed), false);
}
@Override
public void sendPong(ByteBuffer applicationData) throws IOException
{
sendBlocking(new Frame(OpCode.PONG).setPayload(applicationData));
}
@Override
public void sendPong(ByteBuffer applicationData, WriteCallback callback)
{
coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(applicationData),
Callback.from(callback::writeSuccess, callback::writeFailed), false);
}
private void sendPartialText(String fragment, boolean isLast, Callback callback)
{
Frame frame;
switch (messageType)
{
case -1: // new message
frame = new Frame(OpCode.TEXT);
messageType = OpCode.TEXT;
break;
case OpCode.TEXT:
frame = new Frame(OpCode.CONTINUATION);
break;
default:
callback.failed(new ProtocolException("Attempt to send Partial Text during active opcode " + messageType));
return;
}
frame.setPayload(BufferUtil.toBuffer(fragment, UTF_8));
frame.setFin(isLast);
coreSession.sendFrame(frame, callback, isBatch());
if (isLast)
{
messageType = -1;
}
}
private void sendBlocking(Frame frame) throws IOException
{
FutureCallback b = new FutureCallback();
coreSession.sendFrame(frame, b, false);
b.block();
}
@Override
public BatchMode getBatchMode()
{
return batchMode;
}
@Override
public void setBatchMode(BatchMode mode)
{
batchMode = mode;
}
@Override
public int getMaxOutgoingFrames()
{
return coreSession.getMaxOutgoingFrames();
}
@Override
public void setMaxOutgoingFrames(int maxOutgoingFrames)
{
coreSession.setMaxOutgoingFrames(maxOutgoingFrames);
}
private boolean isBatch()
{
return BatchMode.ON == batchMode;
}
@Override
public SocketAddress getRemoteAddress()
{
return coreSession.getRemoteAddress();
}
@Override
public void flush() throws IOException
{
FutureCallback b = new FutureCallback();
coreSession.flush(b);
b.block();
}
}

View File

@ -24,6 +24,7 @@ import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
@ -76,7 +77,7 @@ public class SessionTracker extends AbstractLifeCycle implements WebSocketSessio
break;
// SHUTDOWN is abnormal close status so it will hard close connection after sent.
session.close(StatusCode.SHUTDOWN, "Container being shut down");
session.close(StatusCode.SHUTDOWN, "Container being shut down", Callback.NOOP);
}
});
}

View File

@ -15,29 +15,31 @@ package org.eclipse.jetty.websocket.common;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Objects;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketContainer;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.core.exception.ProtocolException;
public class WebSocketSession implements Session, SuspendToken, Dumpable
import static java.nio.charset.StandardCharsets.UTF_8;
public class WebSocketSession implements Session, Dumpable
{
private final CoreSession coreSession;
private final JettyWebSocketFrameHandler frameHandler;
private final JettyWebSocketRemoteEndpoint remoteEndpoint;
private final UpgradeRequest upgradeRequest;
private final UpgradeResponse upgradeResponse;
private byte messageType = OpCode.UNDEFINED;
public WebSocketSession(WebSocketContainer container, CoreSession coreSession, JettyWebSocketFrameHandler frameHandler)
{
@ -45,126 +47,197 @@ public class WebSocketSession implements Session, SuspendToken, Dumpable
this.coreSession = Objects.requireNonNull(coreSession);
this.upgradeRequest = frameHandler.getUpgradeRequest();
this.upgradeResponse = frameHandler.getUpgradeResponse();
this.remoteEndpoint = new JettyWebSocketRemoteEndpoint(coreSession, frameHandler.getBatchMode());
container.notifySessionListeners((listener) -> listener.onWebSocketSessionCreated(this));
}
@Override
public void close()
public void demand()
{
coreSession.close(StatusCode.NORMAL, null, Callback.NOOP);
if (frameHandler.isAutoDemand())
throw new IllegalStateException("auto-demanding endpoint cannot explicitly demand");
coreSession.demand(1);
}
@Override
public void close(CloseStatus closeStatus)
public void sendBinary(ByteBuffer buffer, Callback callback)
{
coreSession.close(closeStatus.getCode(), closeStatus.getPhrase(), Callback.NOOP);
callback = Objects.requireNonNullElse(callback, Callback.NOOP);
coreSession.sendFrame(new Frame(OpCode.BINARY).setPayload(buffer),
org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail),
false);
}
@Override
public void close(int statusCode, String reason)
public void sendPartialBinary(ByteBuffer buffer, boolean last, Callback callback)
{
coreSession.close(statusCode, reason, Callback.NOOP);
}
@Override
public void close(int statusCode, String reason, WriteCallback callback)
{
coreSession.close(statusCode, reason, Callback.from(callback::writeSuccess, callback::writeFailed));
}
@Override
public WebSocketBehavior getBehavior()
{
switch (coreSession.getBehavior())
callback = Objects.requireNonNullElse(callback, Callback.NOOP);
Frame frame = switch (messageType)
{
case CLIENT:
return WebSocketBehavior.CLIENT;
case SERVER:
return WebSocketBehavior.SERVER;
default:
return null;
case OpCode.UNDEFINED ->
{
// new message
messageType = OpCode.BINARY;
yield new Frame(OpCode.BINARY);
}
case OpCode.BINARY -> new Frame(OpCode.CONTINUATION);
default ->
{
callback.fail(new ProtocolException("Attempt to send partial BINARY during " + OpCode.name(messageType)));
yield null;
}
};
if (frame != null)
{
frame.setPayload(buffer);
frame.setFin(last);
var cb = org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail);
coreSession.sendFrame(frame, cb, false);
if (last)
messageType = OpCode.UNDEFINED;
}
}
@Override
public void sendText(String text, Callback callback)
{
callback = Objects.requireNonNullElse(callback, Callback.NOOP);
var cb = org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail);
coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(text), cb, false);
}
@Override
public void sendPartialText(String text, boolean last, Callback callback)
{
Frame frame = switch (messageType)
{
case OpCode.UNDEFINED ->
{
// new message
messageType = OpCode.TEXT;
yield new Frame(OpCode.TEXT);
}
case OpCode.TEXT -> new Frame(OpCode.CONTINUATION);
default ->
{
callback.fail(new ProtocolException("Attempt to send partial TEXT during " + OpCode.name(messageType)));
yield null;
}
};
if (frame != null)
{
frame.setPayload(BufferUtil.toBuffer(text, UTF_8));
frame.setFin(last);
var cb = org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail);
coreSession.sendFrame(frame, cb, false);
if (last)
messageType = OpCode.UNDEFINED;
}
}
@Override
public void sendPing(ByteBuffer applicationData, Callback callback)
{
coreSession.sendFrame(new Frame(OpCode.PING).setPayload(applicationData),
org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail), false);
}
@Override
public void sendPong(ByteBuffer applicationData, Callback callback)
{
coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(applicationData),
org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail), false);
}
@Override
public void close(int statusCode, String reason, Callback callback)
{
coreSession.close(statusCode, reason, org.eclipse.jetty.util.Callback.from(callback::succeed, callback::fail));
}
@Override
public Duration getIdleTimeout()
{
return coreSession.getIdleTimeout();
}
@Override
public int getInputBufferSize()
{
return coreSession.getInputBufferSize();
}
@Override
public int getOutputBufferSize()
{
return coreSession.getOutputBufferSize();
}
@Override
public long getMaxBinaryMessageSize()
{
return coreSession.getMaxBinaryMessageSize();
}
@Override
public long getMaxTextMessageSize()
{
return coreSession.getMaxTextMessageSize();
}
@Override
public long getMaxFrameSize()
{
return coreSession.getMaxFrameSize();
}
@Override
public boolean isAutoFragment()
{
return coreSession.isAutoFragment();
}
@Override
public void setIdleTimeout(Duration duration)
{
coreSession.setIdleTimeout(duration);
}
@Override
public int getInputBufferSize()
{
return coreSession.getInputBufferSize();
}
@Override
public void setInputBufferSize(int size)
{
coreSession.setInputBufferSize(size);
}
@Override
public int getOutputBufferSize()
{
return coreSession.getOutputBufferSize();
}
@Override
public void setOutputBufferSize(int size)
{
coreSession.setOutputBufferSize(size);
}
@Override
public long getMaxBinaryMessageSize()
{
return coreSession.getMaxBinaryMessageSize();
}
@Override
public void setMaxBinaryMessageSize(long size)
{
coreSession.setMaxBinaryMessageSize(size);
}
@Override
public long getMaxTextMessageSize()
{
return coreSession.getMaxTextMessageSize();
}
@Override
public void setMaxTextMessageSize(long size)
{
coreSession.setMaxTextMessageSize(size);
}
@Override
public long getMaxFrameSize()
{
return coreSession.getMaxFrameSize();
}
@Override
public void setMaxFrameSize(long maxFrameSize)
{
coreSession.setMaxFrameSize(maxFrameSize);
}
@Override
public boolean isAutoFragment()
{
return coreSession.isAutoFragment();
}
@Override
public void setAutoFragment(boolean autoFragment)
{
@ -172,15 +245,21 @@ public class WebSocketSession implements Session, SuspendToken, Dumpable
}
@Override
public String getProtocolVersion()
public int getMaxOutgoingFrames()
{
return upgradeRequest.getProtocolVersion();
return coreSession.getMaxOutgoingFrames();
}
@Override
public JettyWebSocketRemoteEndpoint getRemote()
public void setMaxOutgoingFrames(int maxOutgoingFrames)
{
return remoteEndpoint;
coreSession.setMaxOutgoingFrames(maxOutgoingFrames);
}
@Override
public String getProtocolVersion()
{
return upgradeRequest.getProtocolVersion();
}
@Override
@ -202,13 +281,13 @@ public class WebSocketSession implements Session, SuspendToken, Dumpable
}
@Override
public SocketAddress getLocalAddress()
public SocketAddress getLocalSocketAddress()
{
return coreSession.getLocalAddress();
}
@Override
public SocketAddress getRemoteAddress()
public SocketAddress getRemoteSocketAddress()
{
return coreSession.getRemoteAddress();
}
@ -225,19 +304,6 @@ public class WebSocketSession implements Session, SuspendToken, Dumpable
return this.upgradeResponse;
}
@Override
public SuspendToken suspend()
{
frameHandler.suspend();
return this;
}
@Override
public void resume()
{
frameHandler.resume();
}
public CoreSession getCoreSession()
{
return coreSession;
@ -246,21 +312,20 @@ public class WebSocketSession implements Session, SuspendToken, Dumpable
@Override
public void dump(Appendable out, String indent) throws IOException
{
Dumpable.dumpObjects(out, indent, this, upgradeRequest, coreSession, remoteEndpoint, frameHandler);
Dumpable.dumpObjects(out, indent, this, upgradeRequest, coreSession, frameHandler);
}
@Override
public String dumpSelf()
{
return String.format("%s@%x[behavior=%s,idleTimeout=%dms]",
return String.format("%s@%x[idleTimeout=%dms]",
this.getClass().getSimpleName(), hashCode(),
getPolicy().getBehavior(),
getIdleTimeout().toMillis());
}
@Override
public String toString()
{
return String.format("WebSocketSession[%s,to=%s,%s,%s]", getBehavior(), getIdleTimeout(), coreSession, frameHandler);
return String.format("WebSocketSession[to=%s,%s,%s]", getIdleTimeout(), coreSession, frameHandler);
}
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.common.internal;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.ByteBuffer;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
public class ByteBufferMessageSink extends org.eclipse.jetty.websocket.core.messages.ByteBufferMessageSink
{
public ByteBufferMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle, autoDemand, false);
MethodType onMessageType = MethodType.methodType(Void.TYPE, ByteBuffer.class, Callback.class);
if (methodHandle.type() != onMessageType)
throw InvalidSignatureException.build(onMessageType, methodHandle.type());
}
@Override
protected void invoke(MethodHandle methodHandle, ByteBuffer byteBuffer, org.eclipse.jetty.util.Callback callback) throws Throwable
{
methodHandle.invoke(byteBuffer, Callback.from(callback::succeeded, callback::failed));
}
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.common.internal;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.ByteBuffer;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
public class PartialByteBufferMessageSink extends org.eclipse.jetty.websocket.core.messages.PartialByteBufferMessageSink
{
public PartialByteBufferMessageSink(CoreSession session, MethodHandle methodHandle, boolean autoDemand)
{
super(session, methodHandle, autoDemand);
MethodType onMessageType = MethodType.methodType(Void.TYPE, ByteBuffer.class, boolean.class, Callback.class);
if (methodHandle.type() != onMessageType)
throw InvalidSignatureException.build(onMessageType, methodHandle.type());
}
@Override
protected void invoke(MethodHandle methodHandle, ByteBuffer byteBuffer, boolean fin, org.eclipse.jetty.util.Callback callback) throws Throwable
{
methodHandle.invoke(byteBuffer, fin, Callback.from(callback::succeeded, callback::failed));
}
}

View File

@ -13,24 +13,19 @@
package org.eclipse.jetty.websocket.common;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Frame;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPartialListener;
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.util.TextUtils;
@ -44,14 +39,15 @@ public class EndPoints
{
}
public static class ListenerBasicSocket implements WebSocketListener
public static class ListenerBasicSocket implements Session.Listener.AutoDemanding
{
public EventQueue events = new EventQueue();
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
public void onWebSocketBinary(ByteBuffer payload, Callback callback)
{
events.add("onWebSocketBinary([%d], %d, %d)", payload.length, offset, len);
events.add("onWebSocketBinary([%d])", payload.remaining());
callback.succeed();
}
@Override
@ -61,9 +57,9 @@ public class EndPoints
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
events.add("onWebSocketConnect(%s)", session);
events.add("onWebSocketOpen(%s)", session);
}
@Override
@ -79,7 +75,7 @@ public class EndPoints
}
}
public static class ListenerFrameSocket implements WebSocketFrameListener
public static class ListenerFrameSocket implements Session.Listener.AutoDemanding
{
public EventQueue events = new EventQueue();
@ -90,9 +86,9 @@ public class EndPoints
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
events.add("onWebSocketConnect(%s)", session);
events.add("onWebSocketOpen(%s)", session);
}
@Override
@ -102,13 +98,14 @@ public class EndPoints
}
@Override
public void onWebSocketFrame(Frame frame)
public void onWebSocketFrame(Frame frame, Callback callback)
{
events.add("onWebSocketFrame(%s)", frame.toString());
callback.succeed();
}
}
public static class ListenerPartialSocket implements WebSocketPartialListener
public static class ListenerPartialSocket implements Session.Listener.AutoDemanding
{
public EventQueue events = new EventQueue();
@ -119,9 +116,9 @@ public class EndPoints
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
events.add("onWebSocketConnect(%s)", session);
events.add("onWebSocketOpen(%s)", session);
}
@Override
@ -137,13 +134,14 @@ public class EndPoints
}
@Override
public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin)
public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin, Callback callback)
{
events.add("onWebSocketPartialBinary(%s, %b)", BufferUtil.toDetailString(payload), fin);
callback.succeed();
}
}
public static class ListenerPingPongSocket implements WebSocketPingPongListener
public static class ListenerPingPongSocket implements Session.Listener.AutoDemanding
{
public EventQueue events = new EventQueue();
@ -154,9 +152,9 @@ public class EndPoints
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
events.add("onWebSocketConnect(%s)", session);
events.add("onWebSocketOpen(%s)", session);
}
@Override
@ -184,52 +182,15 @@ public class EndPoints
@WebSocket
public static class BadDuplicateBinarySocket
{
/**
* First method
*
* @param payload the payload
* @param offset the offset
* @param len the len
*/
@OnWebSocketMessage
public void binMe(byte[] payload, int offset, int len)
public void binMe(ByteBuffer payload, Callback callback)
{
/* ignore */
callback.succeed();
}
/**
* Second method (also binary)
*
* @param stream the input stream
*/
@OnWebSocketMessage
public void streamMe(InputStream stream)
{
/* ignore */
}
}
@WebSocket
public static class AnnotatedBinaryArraySocket
{
public EventQueue events = new EventQueue();
@OnWebSocketMessage
public void onBinary(byte[] payload, int offset, int length)
{
events.add("onBinary([%d],%d,%d)", payload.length, offset, length);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason));
}
@OnWebSocketConnect
public void onConnect(Session sess)
{
events.add("onConnect(%s)", sess);
}
}
@ -251,10 +212,10 @@ public class EndPoints
events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason));
}
@OnWebSocketConnect
public void onConnect(Session sess)
@OnWebSocketOpen
public void onOpen(Session sess)
{
events.add("onConnect(%s)", sess);
events.add("onOpen(%s)", sess);
}
}
@ -269,10 +230,10 @@ public class EndPoints
events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason));
}
@OnWebSocketConnect
public void onConnect(Session sess)
@OnWebSocketOpen
public void onOpen(Session sess)
{
events.add("onConnect(%s)", sess);
events.add("onOpen(%s)", sess);
}
@OnWebSocketError
@ -299,10 +260,10 @@ public class EndPoints
events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason));
}
@OnWebSocketConnect
public void onConnect(Session sess)
@OnWebSocketOpen
public void onOpen(Session sess)
{
events.add("onConnect(%s)", sess);
events.add("onOpen(%s)", sess);
}
@OnWebSocketMessage
@ -379,18 +340,31 @@ public class EndPoints
}
}
@WebSocket(autoDemand = false)
public static class BadAutoDemandWithInputStream
{
@OnWebSocketMessage
public void onMessage(InputStream stream)
{
}
}
@WebSocket(autoDemand = false)
public static class BadAutoDemandWithReader
{
@OnWebSocketMessage
public void onMessage(Reader reader)
{
}
}
@WebSocket
public static class FrameSocket
{
/**
* A frame
*
* @param frame the frame
*/
@OnWebSocketFrame
public void frameMe(Frame frame)
public void frameMe(Frame frame, Callback callback)
{
/* ignore */
callback.succeed();
}
}
@ -401,16 +375,9 @@ public class EndPoints
public static class MyEchoBinarySocket extends MyEchoSocket
{
@OnWebSocketMessage
public void echoBin(byte[] buf, int offset, int length)
public void echoBin(ByteBuffer payload, Callback callback)
{
try
{
getRemote().sendBytes(ByteBuffer.wrap(buf, offset, length));
}
catch (IOException e)
{
e.printStackTrace();
}
getSession().sendBinary(payload, callback);
}
}
@ -423,24 +390,16 @@ public class EndPoints
public static class MyEchoSocket
{
private Session session;
private RemoteEndpoint remote;
public RemoteEndpoint getRemote()
{
return remote;
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
this.session = null;
}
@OnWebSocketConnect
public void onConnect(Session session)
@OnWebSocketOpen
public void onOpen(Session session)
{
this.session = session;
this.remote = session.getRemote();
}
public Session getSession()
{
return session;
}
@OnWebSocketMessage
@ -453,14 +412,13 @@ public class EndPoints
return;
}
try
{
remote.sendString(message);
}
catch (IOException e)
{
e.printStackTrace();
}
session.sendText(message, Callback.NOOP);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
this.session = null;
}
}
@ -478,7 +436,7 @@ public class EndPoints
@OnWebSocketMessage
public void onText(Session session, String text)
{
session.getRemote().sendString(text, null);
session.sendText(text, null);
}
}
@ -498,8 +456,8 @@ public class EndPoints
*/
public static class NotASocket
{
@OnWebSocketConnect
public void onConnect(Session session)
@OnWebSocketOpen
public void onOpen(Session session)
{
/* do nothing */
}

View File

@ -25,7 +25,6 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketConnectionListener;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.core.Behavior;
@ -91,7 +90,7 @@ public class JettyWebSocketFrameHandlerTest
return endpointFactory.newJettyFrameHandler(wsEndpoint);
}
public static class ConnectionOnly implements WebSocketConnectionListener
public static class ConnectionOnly implements Session.Listener
{
public EventQueue events = new EventQueue();
@ -102,9 +101,9 @@ public class JettyWebSocketFrameHandlerTest
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
events.add("onWebSocketConnect(%s)", session);
events.add("onWebSocketOpen(%s)", session);
}
@Override
@ -127,7 +126,7 @@ public class JettyWebSocketFrameHandlerTest
// Validate Events
socket.events.assertEvents(
"onWebSocketConnect\\([^\\)]*\\)",
"onWebSocketOpen\\([^\\)]*\\)",
"onWebSocketClose\\([^\\)]*\\)");
}
@ -222,7 +221,7 @@ public class JettyWebSocketFrameHandlerTest
// Validate Events
socket.events.assertEvents(
"onWebSocketConnect\\([^\\)]*\\)",
"onWebSocketOpen\\([^\\)]*\\)",
"onWebSocketPartialText\\(\"Hello\", false\\)",
"onWebSocketPartialText\\(\" \", false\\)",
"onWebSocketPartialText\\(\"World\", true\\)",
@ -252,9 +251,9 @@ public class JettyWebSocketFrameHandlerTest
// Validate Events
socket.events.assertEvents(
"onWebSocketConnect\\([^\\)]*\\)",
"onWebSocketOpen\\([^\\)]*\\)",
"onWebSocketText\\(\"Hello World\"\\)",
"onWebSocketBinary\\(\\[12\\], 0, 12\\)",
"onWebSocketBinary\\(\\[12\\]\\)",
"onWebSocketClose\\(NORMAL, \"Normal\"\\)"
);
}
@ -274,7 +273,7 @@ public class JettyWebSocketFrameHandlerTest
// Validate Events
socket.events.assertEvents(
"onWebSocketConnect\\([^\\)]*\\)",
"onWebSocketOpen\\([^\\)]*\\)",
"onWebSocketError\\(\\(RuntimeException\\) \"Nothing to see here\"\\)"
);
}
@ -298,7 +297,7 @@ public class JettyWebSocketFrameHandlerTest
// Validate Events
socket.events.assertEvents(
"onWebSocketConnect\\([^\\)]*\\)",
"onWebSocketOpen\\([^\\)]*\\)",
"onWebSocketFrame\\(.*TEXT@[0-9a-f]*.len=5,fin=false,.*\\)",
"onWebSocketFrame\\(.*CONTINUATION@[0-9a-f]*.len=1,fin=false,.*\\)",
"onWebSocketFrame\\(.*CONTINUATION@[0-9a-f]*.len=5,fin=true,.*\\)",
@ -330,7 +329,7 @@ public class JettyWebSocketFrameHandlerTest
// Validate Events
socket.events.assertEvents(
"onWebSocketConnect\\([^\\)]*\\)",
"onWebSocketOpen\\([^\\)]*\\)",
"onWebSocketPing\\(.*ByteBuffer.*You there.*\\)",
"onWebSocketPong\\(.*ByteBuffer.*You there.*\\)",
"onWebSocketClose\\(NORMAL, \"Normal\"\\)"

View File

@ -14,10 +14,10 @@
package org.eclipse.jetty.websocket.common;
import org.eclipse.jetty.websocket.api.exceptions.InvalidWebSocketException;
import org.eclipse.jetty.websocket.common.internal.ByteBufferMessageSink;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.exception.DuplicateAnnotationException;
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.messages.ByteArrayMessageSink;
import org.eclipse.jetty.websocket.core.messages.InputStreamMessageSink;
import org.eclipse.jetty.websocket.core.messages.ReaderMessageSink;
import org.eclipse.jetty.websocket.core.messages.StringMessageSink;
@ -104,29 +104,16 @@ public class LocalEndpointMetadataTest
assertThat(e.getMessage(), containsString("must not be static"));
}
/**
* Test Case for socket for binary array messages
*/
@Test
public void testAnnotatedBinaryArraySocket() throws Exception
public void testAnnotatedBadAutoDemandWithInputStream()
{
JettyWebSocketFrameHandlerMetadata metadata = createMetadata(EndPoints.AnnotatedBinaryArraySocket.class);
assertThrows(InvalidWebSocketException.class, () -> createMetadata(EndPoints.BadAutoDemandWithInputStream.class));
}
String classId = EndPoints.AnnotatedBinaryArraySocket.class.getSimpleName();
assertThat(classId + ".binaryHandle", metadata.getBinaryHandle(), EXISTS);
assertThat(classId + ".binarySink", metadata.getBinarySink(), equalTo(ByteArrayMessageSink.class));
assertThat(classId + ".textHandle", metadata.getTextHandle(), nullValue());
assertThat(classId + ".textSink", metadata.getTextSink(), nullValue());
assertThat(classId + ".openHandle", metadata.getOpenHandle(), EXISTS);
assertThat(classId + ".closeHandle", metadata.getCloseHandle(), EXISTS);
assertThat(classId + ".errorHandle", metadata.getErrorHandle(), nullValue());
assertThat(classId + ".frameHandle", metadata.getFrameHandle(), nullValue());
assertThat(classId + ".pingHandle", metadata.getPingHandle(), nullValue());
assertThat(classId + ".pongHandle", metadata.getPongHandle(), nullValue());
@Test
public void testAnnotatedBadAutoDemandWithReader()
{
assertThrows(InvalidWebSocketException.class, () -> createMetadata(EndPoints.BadAutoDemandWithReader.class));
}
/**
@ -165,7 +152,7 @@ public class LocalEndpointMetadataTest
String classId = EndPoints.MyEchoBinarySocket.class.getSimpleName();
assertThat(classId + ".binaryHandle", metadata.getBinaryHandle(), EXISTS);
assertThat(classId + ".binarySink", metadata.getBinarySink(), equalTo(ByteArrayMessageSink.class));
assertThat(classId + ".binarySink", metadata.getBinarySink(), equalTo(ByteBufferMessageSink.class));
assertThat(classId + ".textHandle", metadata.getTextHandle(), EXISTS);
assertThat(classId + ".textSink", metadata.getTextSink(), equalTo(StringMessageSink.class));
@ -337,7 +324,7 @@ public class LocalEndpointMetadataTest
String classId = EndPoints.ListenerBasicSocket.class.getSimpleName();
assertThat(classId + ".binaryHandle", metadata.getBinaryHandle(), EXISTS);
assertThat(classId + ".binarySink", metadata.getBinarySink(), equalTo(ByteArrayMessageSink.class));
assertThat(classId + ".binarySink", metadata.getBinarySink(), equalTo(ByteBufferMessageSink.class));
assertThat(classId + ".textHandle", metadata.getTextHandle(), EXISTS);
assertThat(classId + ".textSink", metadata.getTextSink(), equalTo(StringMessageSink.class));

View File

@ -25,6 +25,7 @@ import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.core.messages.MessageInputStream;
@ -42,7 +43,7 @@ public class MessageInputStreamTest
{
assertTimeout(Duration.ofMillis(5000), () ->
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
// Append a single message (simple, short)
Frame frame = new Frame(OpCode.TEXT);
@ -63,7 +64,7 @@ public class MessageInputStreamTest
@Test
public void testMultipleReadsIntoSingleByteArray() throws IOException
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
// Append a single message (simple, short)
Frame frame = new Frame(OpCode.TEXT);
@ -95,7 +96,7 @@ public class MessageInputStreamTest
{
assertTimeout(Duration.ofMillis(5000), () ->
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
final AtomicBoolean hadError = new AtomicBoolean(false);
final CountDownLatch startLatch = new CountDownLatch(1);
@ -140,7 +141,7 @@ public class MessageInputStreamTest
{
assertTimeout(Duration.ofMillis(5000), () ->
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
final AtomicBoolean hadError = new AtomicBoolean(false);
@ -175,7 +176,7 @@ public class MessageInputStreamTest
{
assertTimeout(Duration.ofMillis(5000), () ->
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
final AtomicBoolean hadError = new AtomicBoolean(false);
@ -221,7 +222,7 @@ public class MessageInputStreamTest
{
assertTimeout(Duration.ofMillis(5000), () ->
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
// Append parts of message
Frame msg1 = new Frame(OpCode.BINARY).setPayload("Hello ").setFin(false);
@ -248,7 +249,7 @@ public class MessageInputStreamTest
{
assertTimeout(Duration.ofMillis(5000), () ->
{
try (MessageInputStream stream = new MessageInputStream())
try (MessageInputStream stream = new MessageInputStream(new CoreSession.Empty()))
{
// Append parts of message
Frame msg1 = new Frame(OpCode.BINARY).setPayload("Hello ").setFin(false);

View File

@ -22,12 +22,13 @@ import java.util.concurrent.LinkedBlockingDeque;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.toolchain.test.Hex;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.common.internal.ByteBufferMessageSink;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.core.messages.ByteBufferMessageSink;
import org.eclipse.jetty.websocket.core.messages.MessageSink;
import org.eclipse.jetty.websocket.core.messages.StringMessageSink;
import org.slf4j.Logger;
@ -45,7 +46,6 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
private final MethodHandle wholeTextHandle;
private final MethodHandle wholeBinaryHandle;
private MessageSink messageSink;
private long maxMessageSize = 2 * 1024 * 1024;
public OutgoingMessageCapture()
{
@ -55,7 +55,7 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
MethodHandle text = lookup.findVirtual(this.getClass(), "onWholeText", MethodType.methodType(Void.TYPE, String.class));
this.wholeTextHandle = text.bindTo(this);
MethodHandle binary = lookup.findVirtual(this.getClass(), "onWholeBinary", MethodType.methodType(Void.TYPE, ByteBuffer.class));
MethodHandle binary = lookup.findVirtual(this.getClass(), "onWholeBinary", MethodType.methodType(Void.TYPE, ByteBuffer.class, Callback.class));
this.wholeBinaryHandle = binary.bindTo(this);
}
catch (NoSuchMethodException | IllegalAccessException e)
@ -65,7 +65,7 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
}
@Override
public void sendFrame(Frame frame, Callback callback, boolean batch)
public void sendFrame(Frame frame, org.eclipse.jetty.util.Callback callback, boolean batch)
{
switch (frame.getOpCode())
{
@ -96,7 +96,7 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
String event = String.format("TEXT:fin=%b:len=%d", frame.isFin(), frame.getPayloadLength());
LOG.debug(event);
events.offer(event);
messageSink = new StringMessageSink(this, wholeTextHandle);
messageSink = new StringMessageSink(this, wholeTextHandle, true);
break;
}
case OpCode.BINARY:
@ -104,7 +104,7 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
String event = String.format("BINARY:fin=%b:len=%d", frame.isFin(), frame.getPayloadLength());
LOG.debug(event);
events.offer(event);
messageSink = new ByteBufferMessageSink(this, wholeBinaryHandle);
messageSink = new ByteBufferMessageSink(this, wholeBinaryHandle, true);
break;
}
case OpCode.CONTINUATION:
@ -119,7 +119,7 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
if (OpCode.isDataFrame(frame.getOpCode()))
{
Frame copy = Frame.copy(frame);
messageSink.accept(copy, Callback.from(() -> {}, Throwable::printStackTrace));
messageSink.accept(copy, org.eclipse.jetty.util.Callback.from(() -> {}, Throwable::printStackTrace));
if (frame.isFin())
messageSink = null;
}
@ -140,16 +140,10 @@ public class OutgoingMessageCapture extends CoreSession.Empty implements CoreSes
}
@SuppressWarnings("unused")
public void onWholeBinary(ByteBuffer buf)
public void onWholeBinary(ByteBuffer buf, Callback callback)
{
ByteBuffer copy = null;
if (buf != null)
{
copy = ByteBuffer.allocate(buf.remaining());
copy.put(buf);
copy.flip();
}
this.binaryMessages.offer(copy);
this.binaryMessages.offer(BufferUtil.copy(buf));
callback.succeed();
}
private String dataHint(ByteBuffer payload)

View File

@ -13,30 +13,22 @@
package org.eclipse.jetty.websocket.common.endpoints.adapters;
import java.io.IOException;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
/**
* Example EchoSocket using Adapter.
*/
public class AdapterEchoSocket extends WebSocketAdapter
public class AdapterEchoSocket extends Session.Listener.Abstract
{
@Override
public void onWebSocketText(String message)
{
if (isConnected())
if (isOpen())
{
try
{
System.out.printf("Echoing back message [%s]%n", message);
// echo the message back
getRemote().sendString(message);
}
catch (IOException e)
{
e.printStackTrace(System.err);
}
System.out.printf("Echoing back message [%s]%n", message);
// echo the message back
getSession().sendText(message, Callback.NOOP);
}
}
}

View File

@ -15,14 +15,21 @@ package org.eclipse.jetty.websocket.common.endpoints.adapters;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
/**
* Example EchoSocket using Annotations.
*/
@WebSocket(maxTextMessageSize = 64 * 1024)
@WebSocket
public class AnnotatedEchoSocket
{
@OnWebSocketOpen
public void onOpen(Session session)
{
session.setMaxTextMessageSize(64 * 1024);
}
@OnWebSocketMessage
public void onText(Session session, String message)
{
@ -30,7 +37,7 @@ public class AnnotatedEchoSocket
{
System.out.printf("Echoing back message [%s]%n", message);
// echo the message back
session.getRemote().sendString(message, null);
session.sendText(message, null);
}
}
}

View File

@ -13,20 +13,23 @@
package org.eclipse.jetty.websocket.common.endpoints.adapters;
import java.nio.ByteBuffer;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketListener;
/**
* Example EchoSocket using Listener.
*/
public class ListenerEchoSocket implements WebSocketListener
public class ListenerEchoSocket implements Session.Listener
{
private Session outbound;
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
public void onWebSocketBinary(ByteBuffer payload, Callback callback)
{
/* only interested in text messages */
// only interested in text messages.
callback.succeed();
}
@Override
@ -36,7 +39,7 @@ public class ListenerEchoSocket implements WebSocketListener
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
this.outbound = session;
}
@ -54,7 +57,7 @@ public class ListenerEchoSocket implements WebSocketListener
{
System.out.printf("Echoing back message [%s]%n", message);
// echo the message back
outbound.getRemote().sendString(message, null);
outbound.sendText(message, null);
}
}
}

View File

@ -26,10 +26,9 @@ import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.websocket.api.Configurable;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketContainer;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.SessionTracker;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
@ -48,7 +47,7 @@ import org.slf4j.LoggerFactory;
* URI paths to WebSocket endpoints and configure WebSocket parameters such as idle timeouts,
* max WebSocket message sizes, etc.</p>
*/
public class ServerWebSocketContainer extends ContainerLifeCycle implements WebSocketContainer, WebSocketPolicy, Invocable
public class ServerWebSocketContainer extends ContainerLifeCycle implements WebSocketContainer, Configurable, Invocable
{
private static final Logger LOG = LoggerFactory.getLogger(ServerWebSocketContainer.class);
@ -113,12 +112,6 @@ public class ServerWebSocketContainer extends ContainerLifeCycle implements WebS
}
}
@Override
public WebSocketBehavior getBehavior()
{
return configuration.getBehavior();
}
@Override
public Duration getIdleTimeout()
{
@ -203,6 +196,18 @@ public class ServerWebSocketContainer extends ContainerLifeCycle implements WebS
configuration.setAutoFragment(autoFragment);
}
@Override
public int getMaxOutgoingFrames()
{
return configuration.getMaxOutgoingFrames();
}
@Override
public void setMaxOutgoingFrames(int maxOutgoingFrames)
{
configuration.setMaxOutgoingFrames(maxOutgoingFrames);
}
/**
* <p>Maps the given {@code pathSpec} to the creator of WebSocket endpoints.</p>
* <p>The {@code pathSpec} format is that supported by
@ -296,12 +301,7 @@ public class ServerWebSocketContainer extends ContainerLifeCycle implements WebS
this.invocationType = invocationType;
}
private static class Configuration extends org.eclipse.jetty.websocket.core.Configuration.ConfigurationCustomizer implements WebSocketPolicy
private static class Configuration extends org.eclipse.jetty.websocket.core.Configuration.ConfigurationCustomizer implements Configurable
{
@Override
public WebSocketBehavior getBehavior()
{
return WebSocketBehavior.SERVER;
}
}
}

View File

@ -15,17 +15,24 @@ package org.eclipse.jetty.websocket.tests;
import java.io.IOException;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@SuppressWarnings("unused")
@WebSocket(maxTextMessageSize = 100 * 1024)
@WebSocket
public class AnnoMaxMessageEndpoint
{
@OnWebSocketOpen
public void onOpen(Session session)
{
session.setMaxTextMessageSize(100 * 1024);
}
@OnWebSocketMessage
public void onMessage(Session session, String msg) throws IOException
{
session.getRemote().sendString(msg);
session.sendText(msg, Callback.NOOP);
}
}

View File

@ -24,9 +24,8 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketPartialListener;
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.api.exceptions.InvalidWebSocketException;
@ -42,26 +41,26 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class AnnotatedPartialListenerTest
{
public static class PartialEchoSocket implements WebSocketPartialListener
public static class PartialEchoSocket implements Session.Listener.AutoDemanding
{
private Session session;
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
this.session = session;
}
@Override
public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin)
public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin, Callback callback)
{
session.getRemote().sendPartialBytes(payload, fin, WriteCallback.NOOP);
session.sendPartialBinary(payload, fin, callback);
}
@Override
public void onWebSocketPartialText(String payload, boolean fin)
{
session.getRemote().sendPartialString(payload, fin, WriteCallback.NOOP);
session.sendPartialText(payload, fin, Callback.NOOP);
}
}
@ -98,12 +97,13 @@ public class AnnotatedPartialListenerTest
}
@OnWebSocketMessage
public void onMessage(ByteBuffer buffer, boolean last)
public void onMessage(ByteBuffer buffer, boolean last, Callback callback)
{
MessageSegment messageSegment = new MessageSegment();
messageSegment.buffer = BufferUtil.copy(buffer);
messageSegment.last = last;
messages.add(messageSegment);
callback.succeed();
}
}
@ -111,12 +111,12 @@ public class AnnotatedPartialListenerTest
public static class InvalidDoubleBinaryListener
{
@OnWebSocketMessage
public void onMessage(ByteBuffer bytes, boolean last)
public void onMessage(ByteBuffer bytes, boolean last, Callback callback)
{
}
@OnWebSocketMessage
public void onMessage(ByteBuffer bytes)
public void onMessage(ByteBuffer bytes, Callback callback)
{
}
}
@ -175,9 +175,10 @@ public class AnnotatedPartialListenerTest
PartialStringListener endpoint = new PartialStringListener();
try (Session session = client.connect(endpoint, serverUri).get(5, TimeUnit.SECONDS))
{
session.getRemote().sendPartialString("hell", false);
session.getRemote().sendPartialString("o w", false);
session.getRemote().sendPartialString("orld", true);
Callback.Completable.with(c -> session.sendPartialText("hell", false, c))
.compose(c -> session.sendPartialText("o w", false, c))
.compose(c -> session.sendPartialText("orld", true, c))
.get();
}
PartialStringListener.MessageSegment segment;
@ -201,9 +202,10 @@ public class AnnotatedPartialListenerTest
PartialByteBufferListener endpoint = new PartialByteBufferListener();
try (Session session = client.connect(endpoint, serverUri).get(5, TimeUnit.SECONDS))
{
session.getRemote().sendPartialBytes(BufferUtil.toBuffer("hell"), false);
session.getRemote().sendPartialBytes(BufferUtil.toBuffer("o w"), false);
session.getRemote().sendPartialBytes(BufferUtil.toBuffer("orld"), true);
Callback.Completable.with(c -> session.sendPartialBinary(BufferUtil.toBuffer("hell"), false, c))
.compose(c -> session.sendPartialBinary(BufferUtil.toBuffer("o w"), false, c))
.compose(c -> session.sendPartialBinary(BufferUtil.toBuffer("orld"), true, c))
.get();
}
PartialByteBufferListener.MessageSegment segment;

View File

@ -21,8 +21,9 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.core.WebSocketConnection;
import org.eclipse.jetty.websocket.core.WebSocketCoreSession;
@ -35,7 +36,7 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
public class CloseTrackingEndpoint extends WebSocketAdapter
public class CloseTrackingEndpoint extends Session.Listener.AbstractAutoDemanding
{
private static final Logger LOG = LoggerFactory.getLogger(CloseTrackingEndpoint.class);
@ -43,7 +44,7 @@ public class CloseTrackingEndpoint extends WebSocketAdapter
public String closeReason = null;
public CountDownLatch closeLatch = new CountDownLatch(1);
public AtomicInteger closeCount = new AtomicInteger(0);
public CountDownLatch openLatch = new CountDownLatch(1);
public CountDownLatch connectLatch = new CountDownLatch(1);
public CountDownLatch errorLatch = new CountDownLatch(1);
public LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();
@ -89,11 +90,11 @@ public class CloseTrackingEndpoint extends WebSocketAdapter
}
@Override
public void onWebSocketConnect(Session session)
public void onWebSocketOpen(Session session)
{
LOG.debug("onWebSocketConnect({})", session);
super.onWebSocketConnect(session);
openLatch.countDown();
super.onWebSocketOpen(session);
LOG.debug("onWebSocketOpen({})", session);
connectLatch.countDown();
}
@Override
@ -112,10 +113,11 @@ public class CloseTrackingEndpoint extends WebSocketAdapter
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
public void onWebSocketBinary(ByteBuffer payload, Callback callback)
{
LOG.debug("onWebSocketBinary({},{},{})", payload, offset, len);
binaryMessageQueue.offer(ByteBuffer.wrap(payload, offset, len));
LOG.debug("onWebSocketBinary({})", payload.remaining());
binaryMessageQueue.offer(BufferUtil.copy(payload));
callback.succeed();
}
public EndPoint getEndPoint()

View File

@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
@ -104,9 +105,9 @@ public class ConcurrentConnectTest
for (EventSocket l : listeners)
{
l.session.getRemote().sendString("ping");
l.session.sendText("ping", Callback.NOOP);
assertThat(l.textMessages.poll(5, TimeUnit.SECONDS), is("ping"));
l.session.close(StatusCode.NORMAL, "close from client");
l.session.close(StatusCode.NORMAL, "close from client", Callback.NOOP);
}
for (EventSocket l : listeners)

View File

@ -13,19 +13,18 @@
package org.eclipse.jetty.websocket.tests;
import java.io.IOException;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@SuppressWarnings("unused")
@WebSocket
public class ConnectMessageEndpoint
{
@OnWebSocketConnect
public void onConnect(Session session) throws IOException
@OnWebSocketOpen
public void onOpen(Session session)
{
session.getRemote().sendString("Greeting from onConnect");
session.sendText("Greeting from onOpen", Callback.NOOP);
}
}

View File

@ -22,6 +22,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.JettyUpgradeListener;
import org.eclipse.jetty.websocket.client.WebSocketClient;
@ -92,7 +93,7 @@ public class ConnectionHeaderTest
{
// Generate text frame
String msg = "this is an echo ... cho ... ho ... o";
session.getRemote().sendString(msg);
session.sendText(msg, Callback.NOOP);
// Read frame (hopefully text frame)
String response = clientEndpoint.textMessages.poll(5, TimeUnit.SECONDS);

View File

@ -0,0 +1,153 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.tests;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class DemandWithBlockingStreamsTest
{
private final Server server = new Server();
private final ServerConnector connector = new ServerConnector(server, 1, 1);
private final WebSocketClient client = new WebSocketClient();
private void start(Consumer<WebSocketUpgradeHandler> configurer) throws Exception
{
server.addConnector(connector);
ContextHandler context = new ContextHandler("/");
WebSocketUpgradeHandler wsHandler = WebSocketUpgradeHandler.from(server, context);
context.setHandler(wsHandler);
configurer.accept(wsHandler);
server.setHandler(context);
server.start();
client.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(client);
LifeCycle.stop(server);
}
@Test
public void testBinaryStreamExplicitDemandThrows() throws Exception
{
StreamEndPoint serverEndPoint = new StreamEndPoint();
start(wsHandler -> wsHandler.configure(container ->
container.addMapping("/*", (rq, rs, cb) -> serverEndPoint)));
URI uri = new URI("ws://localhost:" + connector.getLocalPort() + "/");
EventSocket clientEndPoint = new EventSocket();
client.connect(clientEndPoint, uri).get(5, TimeUnit.SECONDS);
clientEndPoint.session.sendBinary(ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), Callback.NOOP);
// The server-side tried to demand(), should get an error.
assertTrue(serverEndPoint.errorLatch.await(5, TimeUnit.SECONDS));
assertTrue(serverEndPoint.closeLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientEndPoint.closeLatch.await(5, TimeUnit.SECONDS));
assertEquals(StatusCode.SERVER_ERROR, clientEndPoint.closeCode);
}
@Test
public void testTextStreamExplicitDemandThrows() throws Exception
{
StreamEndPoint serverEndPoint = new StreamEndPoint();
start(wsHandler -> wsHandler.configure(container ->
container.addMapping("/*", (rq, rs, cb) -> serverEndPoint)));
URI uri = new URI("ws://localhost:" + connector.getLocalPort() + "/");
EventSocket clientEndPoint = new EventSocket();
client.connect(clientEndPoint, uri).get(5, TimeUnit.SECONDS);
clientEndPoint.session.sendText("hello", Callback.NOOP);
// The server-side tried to demand(), should get an error.
assertTrue(serverEndPoint.errorLatch.await(5, TimeUnit.SECONDS));
assertTrue(serverEndPoint.closeLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientEndPoint.closeLatch.await(5, TimeUnit.SECONDS));
assertEquals(StatusCode.SERVER_ERROR, clientEndPoint.closeCode);
}
@WebSocket
public static class StreamEndPoint
{
private final CountDownLatch errorLatch = new CountDownLatch(1);
private final CountDownLatch closeLatch = new CountDownLatch(1);
private Session session;
@OnWebSocketOpen
public void onOpen(Session session)
{
this.session = session;
}
@OnWebSocketMessage
public void onBinary(InputStream stream)
{
// Throws because this endpoint is auto-demanding.
session.demand();
}
@OnWebSocketMessage
public void onText(Reader reader)
{
// Throws because this endpoint is auto-demanding.
session.demand();
}
@OnWebSocketError
public void onError(Throwable cause)
{
errorLatch.countDown();
}
@OnWebSocketClose
public void onClose(int status, String reason)
{
closeLatch.countDown();
}
}
}

View File

@ -16,9 +16,9 @@ package org.eclipse.jetty.websocket.tests;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@SuppressWarnings("unused")
@WebSocket
public class EchoSocket extends EventSocket
{
@ -26,13 +26,13 @@ public class EchoSocket extends EventSocket
public void onMessage(String message) throws IOException
{
super.onMessage(message);
session.getRemote().sendString(message);
session.sendText(message, Callback.NOOP);
}
@Override
public void onMessage(byte[] buf, int offset, int len) throws IOException
public void onMessage(ByteBuffer message, Callback callback) throws IOException
{
super.onMessage(buf, offset, len);
session.getRemote().sendBytes(ByteBuffer.wrap(buf, offset, len));
super.onMessage(message, Callback.NOOP);
session.sendBinary(message, callback);
}
}

View File

@ -25,6 +25,7 @@ import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@ -152,7 +153,7 @@ public class ErrorCloseTest
serverSocket.methodsToThrow.add("onMessage");
EventSocket clientSocket = new EventSocket();
client.connect(clientSocket, serverUri).get(5, TimeUnit.SECONDS);
clientSocket.session.getRemote().sendString("trigger onMessage error");
clientSocket.session.sendText("trigger onMessage error", Callback.NOOP);
assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS));
@ -194,7 +195,7 @@ public class ErrorCloseTest
try (StacklessLogging ignored = new StacklessLogging(WebSocketSession.class))
{
clientSocket.session.getRemote().sendString("trigger onMessage error");
clientSocket.session.sendText("trigger onMessage error", Callback.NOOP);
assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS));
}

Some files were not shown because too many files have changed in this diff Show More