Issue #3279 - WebSocket Close Refactoring

+ FrameFlusher "close" frames are detected during
  enqueue and sets the state properly for failing
  other frames after it
+ Moving away from Blockhead(Client|Server) to using actual implementations
+ Moved tests to /jetty-websocket-tests/ to be able to use actual impl
  for both sides of testcase (client and server)
+ Corrected FrameFlusher terminate/close to not fail the close frame
  itself, but only frames that arrive AFTER the close frame.
+ Moving WebSocketCloseTest to jetty-websocket-tests to avoid
  using BlockheadClient / BlockheadServer in testing
+ Cleanup of unnecessary modifiers on interface
+ Logging error if @OnWebSocketError is undeclared
+ IOState removed
+ New ConnectionState tracks connection basics in a simpler
  method then IOState did.
+ No tracking of Remote close initiated (not needed)
+ IncomingFrames.incomingError() removed
+ Session delegates to Connection for all state changes
+ Errors can be communicated to application multiple times
+ Close is only communicated once

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2019-01-29 11:45:06 -06:00
parent f88f856673
commit 8dba440317
95 changed files with 3619 additions and 4088 deletions

View File

@ -21,7 +21,6 @@ package org.eclipse.jetty.websocket.jsr356;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.Future;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.RemoteEndpoint;
@ -52,6 +51,9 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint
{
StringBuilder err = new StringBuilder();
err.append("Unexpected implementation [");
if(session.getRemote() == null)
err.append("<null>");
else
err.append(session.getRemote().getClass().getName());
err.append("]. Expected an instanceof [");
err.append(WebSocketRemoteEndpoint.class.getName());

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.jsr356;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
@ -55,6 +56,7 @@ import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.client.io.UpgradeListener;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.scopes.DelegatedContainerScope;
import org.eclipse.jetty.websocket.common.scopes.SimpleContainerScope;
import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
@ -74,7 +76,7 @@ import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
* This should be specific to a JVM if run in a standalone mode. or specific to a WebAppContext if running on the Jetty server.
*/
@ManagedObject("JSR356 Client Container")
public class ClientContainer extends ContainerLifeCycle implements WebSocketContainer, WebSocketContainerScope
public class ClientContainer extends ContainerLifeCycle implements WebSocketContainer, WebSocketContainerScope, WebSocketSessionListener
{
private static final Logger LOG = Log.getLogger(ClientContainer.class);
@ -138,6 +140,7 @@ public class ClientContainer extends ContainerLifeCycle implements WebSocketCont
new JsrEventDriverFactory(scopeDelegate),
new JsrSessionFactory(this),
httpClient);
this.client.addSessionListener(this);
if(jsr356TrustAll != null)
{
@ -161,6 +164,7 @@ public class ClientContainer extends ContainerLifeCycle implements WebSocketCont
{
this.scopeDelegate = client;
this.client = client;
this.client.addSessionListener(this);
this.internalClient = false;
this.endpointClientMetadataCache = new ConcurrentHashMap<>();
@ -415,6 +419,24 @@ public class ClientContainer extends ContainerLifeCycle implements WebSocketCont
return scopeDelegate.getSslContextFactory();
}
@Override
public void addSessionListener(WebSocketSessionListener listener)
{
client.addSessionListener(listener);
}
@Override
public void removeSessionListener(WebSocketSessionListener listener)
{
client.removeSessionListener(listener);
}
@Override
public Collection<WebSocketSessionListener> getSessionListeners()
{
return client.getSessionListeners();
}
private EndpointInstance newClientEndpointInstance(Class<?> endpointClass, ClientEndpointConfig config)
{
try

View File

@ -160,7 +160,7 @@ public abstract class JsrCallable extends CallableMethod
}
catch (DecodeException e)
{
session.notifyError(e);
session.close(e);
}
}
else

View File

@ -18,17 +18,13 @@
package org.eclipse.jetty.websocket.jsr356.server;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.events.EventDriver;
@ -36,9 +32,9 @@ import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
import org.eclipse.jetty.websocket.common.events.EventDriverImpl;
import org.eclipse.jetty.websocket.common.frames.ContinuationFrame;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.io.LocalWebSocketConnection;
import org.eclipse.jetty.websocket.common.scopes.SimpleContainerScope;
import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
import org.eclipse.jetty.websocket.common.test.DummyConnection;
import org.eclipse.jetty.websocket.jsr356.ClientContainer;
import org.eclipse.jetty.websocket.jsr356.JsrSession;
import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
@ -46,6 +42,10 @@ import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
import org.eclipse.jetty.websocket.jsr356.server.samples.partial.PartialTrackingSocket;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
public class OnPartialTest
{
public EventDriver toEventDriver(Object websocket) throws Throwable
@ -78,14 +78,15 @@ public class OnPartialTest
// Create Local JsrSession
String id = "testSession";
URI requestURI = URI.create("ws://localhost/" + id);
DummyConnection connection = new DummyConnection();
LocalWebSocketConnection connection = new LocalWebSocketConnection(id, new MappedByteBufferPool());
ClientContainer container = new ClientContainer();
container.start();
@SuppressWarnings("resource")
JsrSession session = new JsrSession(container,id,requestURI,driver,connection);
JsrSession session = new JsrSession(container, id, requestURI, driver, connection);
session.start();
session.open();
driver.openSession(session);
return driver;
}

View File

@ -18,13 +18,10 @@
package org.eclipse.jetty.websocket.jsr356.server;
import static org.hamcrest.MatcherAssert.assertThat;
import java.net.URI;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
@ -37,15 +34,17 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.jsr356.ClientContainer;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
public class SessionTrackingTest
{
private Server server;
@ -95,16 +94,16 @@ public class SessionTrackingTest
{
CountDownLatch openedLatch = new CountDownLatch(2);
CountDownLatch closedLatch = new CountDownLatch(2);
wsServerFactory.addSessionListener(new WebSocketSession.Listener()
wsServerFactory.addSessionListener(new WebSocketSessionListener()
{
@Override
public void onOpened(WebSocketSession session)
public void onSessionOpened(WebSocketSession session)
{
openedLatch.countDown();
}
@Override
public void onClosed(WebSocketSession session)
public void onSessionClosed(WebSocketSession session)
{
closedLatch.countDown();
}

View File

@ -25,14 +25,16 @@ import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import javax.websocket.EndpointConfig;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.jsr356.server.samples.beans.DateDecoder;
import org.eclipse.jetty.websocket.jsr356.server.samples.beans.TimeEncoder;
@ -47,6 +49,7 @@ import org.eclipse.jetty.websocket.jsr356.server.samples.beans.TimeEncoder;
configurator = EchoSocketConfigurator.class)
public class ConfiguredEchoSocket
{
private static final Logger LOG = Log.getLogger(ConfiguredEchoSocket.class);
private Session session;
private EndpointConfig config;
private ServerEndpointConfig serverConfig;
@ -62,6 +65,15 @@ public class ConfiguredEchoSocket
}
}
@OnError
public void onError(Throwable cause)
{
if(LOG.isDebugEnabled())
{
LOG.debug(cause);
}
}
@OnMessage(maxMessageSize = 111222)
public String echoText(String msg)
{

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
@ -43,4 +44,10 @@ public class LargeEchoAnnotatedSocket
// reply with echo
session.getAsyncRemote().sendText(msg);
}
@SuppressWarnings("unused")
@OnError
public void onError(Throwable cause)
{
}
}

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
@ -44,4 +45,9 @@ public class LargeEchoConfiguredSocket
// reply with echo
session.getAsyncRemote().sendText(msg);
}
@OnError
public void onError(Throwable cause)
{
}
}

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-parent</artifactId>
<version>9.4.15-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-websocket-tests</artifactId>
<name>Jetty :: Websocket :: Tests</name>
<properties>
<bundle-symbolic-name>${project.groupId}.tests</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>generate-manifest</id>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=javax.servlet.ServletContainerInitializer</Provide-Capability>
<_nouses>true</_nouses>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,134 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
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.common.io.AbstractWebSocketConnection;
import org.hamcrest.Matcher;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class CloseTrackingEndpoint extends WebSocketAdapter
{
private static final Logger LOG = Log.getLogger(CloseTrackingEndpoint.class);
public int closeCode = -1;
public String closeReason = null;
public CountDownLatch closeLatch = new CountDownLatch(1);
public AtomicInteger closeCount = new AtomicInteger(0);
public CountDownLatch openLatch = new CountDownLatch(1);
public CountDownLatch errorLatch = new CountDownLatch(1);
public LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();
public AtomicReference<Throwable> error = new AtomicReference<>();
public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher<Integer> statusCodeMatcher)
throws InterruptedException
{
assertReceivedCloseEvent(clientTimeoutMs, statusCodeMatcher, null);
}
public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher<Integer> statusCodeMatcher, Matcher<String> reasonMatcher)
throws InterruptedException
{
assertThat("Client Close Event Occurred", closeLatch.await(clientTimeoutMs, TimeUnit.MILLISECONDS), is(true));
assertThat("Client Close Event Count", closeCount.get(), is(1));
assertThat("Client Close Event Status Code", closeCode, statusCodeMatcher);
if (reasonMatcher == null)
{
assertThat("Client Close Event Reason", closeReason, nullValue());
}
else
{
assertThat("Client Close Event Reason", closeReason, reasonMatcher);
}
}
public void clearQueues()
{
messageQueue.clear();
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
LOG.debug("onWebSocketClose({},{})", statusCode, reason);
super.onWebSocketClose(statusCode, reason);
closeCount.incrementAndGet();
closeCode = statusCode;
closeReason = reason;
closeLatch.countDown();
}
@Override
public void onWebSocketConnect(Session session)
{
LOG.debug("onWebSocketConnect({})", session);
super.onWebSocketConnect(session);
openLatch.countDown();
}
@Override
public void onWebSocketError(Throwable cause)
{
LOG.debug("onWebSocketError", cause);
assertThat("Unique Error Event", error.compareAndSet(null, cause), is(true));
errorLatch.countDown();
}
@Override
public void onWebSocketText(String message)
{
LOG.debug("onWebSocketText({})", message);
messageQueue.offer(message);
}
public EndPoint getEndPoint() throws Exception
{
Session session = getSession();
assertThat("Session type", session, instanceOf(WebSocketSession.class));
WebSocketSession wssession = (WebSocketSession) session;
Field fld = wssession.getClass().getDeclaredField("connection");
fld.setAccessible(true);
assertThat("Field: connection", fld, notNullValue());
Object val = fld.get(wssession);
assertThat("Connection type", val, instanceOf(AbstractWebSocketConnection.class));
@SuppressWarnings("resource")
AbstractWebSocketConnection wsconn = (AbstractWebSocketConnection) val;
return wsconn.getEndPoint();
}
}

View File

@ -0,0 +1,35 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests;
import java.io.IOException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@WebSocket
public class EchoSocket
{
@OnWebSocketMessage
public void onMessage(Session session, String msg) throws IOException
{
session.getRemote().sendString(msg);
}
}

View File

@ -0,0 +1,200 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.client;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
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.util.WSURI;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
/**
* Tests for conditions due to bad networking.
*/
public class BadNetworkTest
{
private Server server;
private WebSocketClient client;
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.getPolicy().setIdleTimeout(500);
client.start();
}
@BeforeEach
public void startServer() throws Exception
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder holder = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.getPolicy().setIdleTimeout(10000);
factory.getPolicy().setMaxTextMessageSize(1024 * 1024 * 2);
factory.register(ServerEndpoint.class);
}
});
context.addServlet(holder, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testAbruptClientClose() throws Exception
{
CloseTrackingEndpoint wsocket = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> future = client.connect(wsocket,wsUri);
// Validate that we are connected
future.get(30,TimeUnit.SECONDS);
// Have client disconnect abruptly
Session session = wsocket.getSession();
session.disconnect();
// Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection
// in the situation of a bad network connection.
wsocket.assertReceivedCloseEvent(5000, is(StatusCode.NO_CLOSE), containsString(""));
}
@Test
public void testAbruptServerClose() throws Exception
{
CloseTrackingEndpoint wsocket = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> future = client.connect(wsocket,wsUri);
// Validate that we are connected
Session session = future.get(30, TimeUnit.SECONDS);
// Have server disconnect abruptly
session.getRemote().sendString("abort");
// Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection
// in the situation of a bad network connection.
wsocket.assertReceivedCloseEvent(5000, is(StatusCode.NO_CLOSE), containsString(""));
}
public static class ServerEndpoint implements WebSocketListener
{
private static final Logger LOG = Log.getLogger(ClientCloseTest.ServerEndpoint.class);
private Session session;
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
}
@Override
public void onWebSocketText(String message)
{
try
{
if (message.equals("abort"))
{
session.disconnect();
}
else
{
// simple echo
session.getRemote().sendString(message);
}
}
catch (IOException e)
{
LOG.warn(e);
}
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
}
@Override
public void onWebSocketConnect(Session session)
{
this.session = session;
}
@Override
public void onWebSocketError(Throwable cause)
{
if (LOG.isDebugEnabled())
{
LOG.debug(cause);
}
}
}
}

View File

@ -0,0 +1,483 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.client;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.CloseException;
import org.eclipse.jetty.websocket.api.MessageTooLargeException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.util.WSURI;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.Duration.ofSeconds;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
public class ClientCloseTest
{
private Server server;
private WebSocketClient client;
private Session confirmConnection(CloseTrackingEndpoint clientSocket, Future<Session> clientFuture) throws Exception
{
// Wait for client connect on via future
Session session = clientFuture.get(30, SECONDS);
try
{
// Send message from client to server
final String echoMsg = "echo-test";
Future<Void> testFut = clientSocket.getRemote().sendStringByFuture(echoMsg);
// Wait for send future
testFut.get(5, SECONDS);
// Verify received message
String recvMsg = clientSocket.messageQueue.poll(5, SECONDS);
assertThat("Received message", recvMsg, is(echoMsg));
// Verify that there are no errors
assertThat("Error events", clientSocket.error.get(), nullValue());
}
finally
{
clientSocket.clearQueues();
}
return session;
}
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.setMaxTextMessageBufferSize(1024);
client.getPolicy().setMaxTextMessageSize(1024);
client.start();
}
@BeforeEach
public void startServer() throws Exception
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder holder = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.getPolicy().setIdleTimeout(10000);
factory.getPolicy().setMaxTextMessageSize(1024 * 1024 * 2);
factory.register(ServerEndpoint.class);
}
});
context.addServlet(holder, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testHalfClose() throws Exception
{
// Set client timeout
final int timeout = 5000;
client.setMaxIdleTimeout(timeout);
ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(1);
clientSessionTracker.addTo(client);
// Client connects
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint();
Future<Session> clientConnectFuture = client.connect(clientSocket, wsUri);
try (Session session = confirmConnection(clientSocket, clientConnectFuture))
{
// client confirms connection via echo
// client sends close frame (code 1000, normal)
final String origCloseReason = "send-more-frames";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// Verify received messages
String recvMsg = clientSocket.messageQueue.poll(5, SECONDS);
assertThat("Received message 1", recvMsg, is("Hello"));
recvMsg = clientSocket.messageQueue.poll(5, SECONDS);
assertThat("Received message 2", recvMsg, is("World"));
// Verify that there are no errors
assertThat("Error events", clientSocket.error.get(), nullValue());
// client close event on ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.NORMAL), containsString(""));
}
clientSessionTracker.assertClosedProperly(client);
}
@Test
public void testMessageTooLargeException() throws Exception
{
// Set client timeout
final int timeout = 3000;
client.setMaxIdleTimeout(timeout);
ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(1);
clientSessionTracker.addTo(client);
// Client connects
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint();
Future<Session> clientConnectFuture = client.connect(clientSocket, wsUri);
try (Session session = confirmConnection(clientSocket, clientConnectFuture))
{
// client confirms connection via echo
session.getRemote().sendString("too-large-message");
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.MESSAGE_TOO_LARGE), containsString("exceeds maximum size"));
// client should have noticed the error
assertThat("OnError Latch", clientSocket.errorLatch.await(2, SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(MessageTooLargeException.class));
}
// client triggers close event on client ws-endpoint
clientSessionTracker.assertClosedProperly(client);
}
@Test
public void testRemoteDisconnect() throws Exception
{
// Set client timeout
final int clientTimeout = 1000;
client.setMaxIdleTimeout(clientTimeout);
ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(1);
clientSessionTracker.addTo(client);
// Client connects
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint();
Future<Session> clientConnectFuture = client.connect(clientSocket, wsUri);
try (Session ignored = confirmConnection(clientSocket, clientConnectFuture))
{
// client confirms connection via echo
// client sends close frame (triggering server connection abort)
final String origCloseReason = "abort";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// client reads -1 (EOF)
// client triggers close event on client ws-endpoint
clientSocket.assertReceivedCloseEvent(clientTimeout * 2,
is(StatusCode.SHUTDOWN),
containsString("timeout"));
}
clientSessionTracker.assertClosedProperly(client);
}
@Test
public void testServerNoCloseHandshake() throws Exception
{
// Set client timeout
final int clientTimeout = 1000;
client.setMaxIdleTimeout(clientTimeout);
ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(1);
clientSessionTracker.addTo(client);
// Client connects
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint();
Future<Session> clientConnectFuture = client.connect(clientSocket, wsUri);
try (Session ignored = confirmConnection(clientSocket, clientConnectFuture))
{
// client confirms connection via echo
// client sends close frame
final String origCloseReason = "sleep|5000";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// client close should occur
clientSocket.assertReceivedCloseEvent(clientTimeout * 2,
is(StatusCode.SHUTDOWN),
containsString("timeout"));
// client idle timeout triggers close event on client ws-endpoint
assertThat("OnError Latch", clientSocket.errorLatch.await(2, SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(CloseException.class));
assertThat("OnError.cause", clientSocket.error.get().getCause(), instanceOf(TimeoutException.class));
}
clientSessionTracker.assertClosedProperly(client);
}
@Test
public void testStopLifecycle() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
int sessionCount = 3;
ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(sessionCount);
clientSessionTracker.addTo(client);
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
List<CloseTrackingEndpoint> clientSockets = new ArrayList<>();
// Open Multiple Clients
for (int i = 0; i < sessionCount; i++)
{
// Client Request Upgrade
CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint();
clientSockets.add(clientSocket);
Future<Session> clientConnectFuture = client.connect(clientSocket, wsUri);
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture);
}
assertTimeoutPreemptively(ofSeconds(5), () -> {
// client lifecycle stop (the meat of this test)
client.stop();
});
// clients disconnect
for (int i = 0; i < sessionCount; i++)
{
clientSockets.get(i).assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("Disconnected"));
}
// ensure all Sessions are gone. connections are gone. etc. (client and server)
// ensure ConnectionListener onClose is called 3 times
clientSessionTracker.assertClosedProperly(client);
}
@Test
public void testWriteException() throws Exception
{
// Set client timeout
final int timeout = 2000;
client.setMaxIdleTimeout(timeout);
ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(1);
clientSessionTracker.addTo(client);
// Client connects
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint();
Future<Session> clientConnectFuture = client.connect(clientSocket, wsUri);
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture);
// setup client endpoint for write failure (test only)
EndPoint endp = clientSocket.getEndPoint();
endp.shutdownOutput();
// TODO: race condition. Client CLOSE actions racing SERVER close actions.
// SECONDS.sleep(1); // let server detect EOF and respond
// client enqueue close frame
// should result in a client write failure
final String origCloseReason = "Normal Close from Client";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
assertThat("OnError Latch", clientSocket.errorLatch.await(2, SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(EofException.class));
// client triggers close event on client ws-endpoint
// assert - close code==1006 (abnormal)
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("Eof"));
clientSessionTracker.assertClosedProperly(client);
}
public static class ServerEndpoint implements WebSocketFrameListener, WebSocketListener
{
private static final Logger LOG = Log.getLogger(ServerEndpoint.class);
private Session session;
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
}
@Override
public void onWebSocketText(String message)
{
try
{
if (message.equals("too-large-message"))
{
// send extra large message
byte[] buf = new byte[1024 * 1024];
Arrays.fill(buf, (byte) 'x');
String bigmsg = new String(buf, UTF_8);
session.getRemote().sendString(bigmsg);
}
else
{
// simple echo
session.getRemote().sendString(message);
}
}
catch (IOException ignore)
{
LOG.debug(ignore);
}
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
}
@Override
public void onWebSocketConnect(Session session)
{
this.session = session;
}
@Override
public void onWebSocketError(Throwable cause)
{
if (LOG.isDebugEnabled())
{
LOG.debug(cause);
}
}
@Override
public void onWebSocketFrame(Frame frame)
{
if (frame.getOpCode() == OpCode.CLOSE)
{
CloseInfo closeInfo = new CloseInfo(frame);
String reason = closeInfo.getReason();
if (reason.equals("send-more-frames"))
{
try
{
session.getRemote().sendString("Hello");
session.getRemote().sendString("World");
}
catch (Throwable ignore)
{
LOG.debug("OOPS", ignore);
}
}
else if (reason.equals("abort"))
{
try
{
SECONDS.sleep(1);
LOG.info("Server aborting session abruptly");
session.disconnect();
}
catch (Throwable ignore)
{
LOG.ignore(ignore);
}
}
else if (reason.startsWith("sleep|"))
{
int idx = reason.indexOf('|');
int timeMs = Integer.parseInt(reason.substring(idx + 1));
try
{
LOG.info("Server Sleeping for {} ms", timeMs);
TimeUnit.MILLISECONDS.sleep(timeMs);
}
catch (InterruptedException ignore)
{
LOG.ignore(ignore);
}
}
}
}
}
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.client;
import java.util.concurrent.CountDownLatch;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ClientOpenSessionTracker implements Connection.Listener, WebSocketSessionListener
{
private final CountDownLatch closeSessionLatch;
private final CountDownLatch closeConnectionLatch;
public ClientOpenSessionTracker(int expectedSessions)
{
this.closeSessionLatch = new CountDownLatch(expectedSessions);
this.closeConnectionLatch = new CountDownLatch(expectedSessions);
}
public void addTo(WebSocketClient client)
{
client.addSessionListener(this);
client.addBean(this);
}
public void assertClosedProperly(WebSocketClient client) throws InterruptedException
{
assertTrue(closeConnectionLatch.await(5, SECONDS), "All Jetty Connections should have been closed");
assertTrue(closeSessionLatch.await(5, SECONDS), "All WebSocket Sessions should have been closed");
assertTrue(client.getOpenSessions().isEmpty(), "Client OpenSessions MUST be empty");
}
@Override
public void onOpened(Connection connection)
{
}
@Override
public void onClosed(Connection connection)
{
this.closeConnectionLatch.countDown();
}
@Override
public void onSessionOpened(WebSocketSession session)
{
}
@Override
public void onSessionClosed(WebSocketSession session)
{
this.closeSessionLatch.countDown();
}
}

View File

@ -0,0 +1,167 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.client;
import java.net.URI;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.util.WSURI;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.eclipse.jetty.websocket.tests.EchoSocket;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ClientSessionsTest
{
private Server server;
@BeforeEach
public void startServer() throws Exception
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder holder = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.getPolicy().setIdleTimeout(10000);
factory.getPolicy().setMaxTextMessageSize(1024 * 1024 * 2);
factory.register(EchoSocket.class);
}
});
context.addServlet(holder, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testBasicEcho_FromClient() throws Exception
{
WebSocketClient client = new WebSocketClient();
CountDownLatch onSessionCloseLatch = new CountDownLatch(1);
client.addSessionListener(new WebSocketSessionListener() {
@Override
public void onSessionOpened(WebSocketSession session)
{
}
@Override
public void onSessionClosed(WebSocketSession session)
{
onSessionCloseLatch.countDown();
}
});
client.start();
try
{
CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint();
client.getPolicy().setIdleTimeout(10000);
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("echo");
Future<Session> future = client.connect(cliSock,wsUri,request);
Session sess = null;
try
{
sess = future.get(30000, TimeUnit.MILLISECONDS);
assertThat("Session", sess, notNullValue());
assertThat("Session.open", sess.isOpen(), is(true));
assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue());
assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue());
Collection<WebSocketSession> sessions = client.getBeans(WebSocketSession.class);
assertThat("client.connectionManager.sessions.size", sessions.size(), is(1));
RemoteEndpoint remote = sess.getRemote();
remote.sendString("Hello World!");
Set<WebSocketSession> open = client.getOpenSessions();
assertThat("(Before Close) Open Sessions.size", open.size(), is(1));
String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS);
assertThat("Message", received, containsString("Hello World!"));
}
finally
{
cliSock.getSession().close();
}
cliSock.assertReceivedCloseEvent(30000, is(StatusCode.NORMAL));
assertTrue(onSessionCloseLatch.await(5, TimeUnit.SECONDS), "Saw onSessionClose events");
TimeUnit.SECONDS.sleep(1);
Set<WebSocketSession> open = client.getOpenSessions();
assertThat("(After Close) Open Sessions.size", open.size(), is(0));
}
finally
{
client.stop();
}
}
}

View File

@ -16,7 +16,7 @@
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
package org.eclipse.jetty.websocket.tests.client;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

View File

@ -0,0 +1,141 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.client;
import java.net.URI;
import java.util.concurrent.Future;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.util.WSURI;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.eclipse.jetty.websocket.tests.EchoSocket;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
/**
* This Regression Test Exists because of Client side Idle timeout, Read, and Parser bugs.
*/
public class SlowClientTest
{
private Server server;
private WebSocketClient client;
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.getPolicy().setIdleTimeout(60000);
client.start();
}
@BeforeEach
public void startServer() throws Exception
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder websocket = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.register(EchoSocket.class);
}
});
context.addServlet(websocket, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testClientSlowToSend() throws Exception
{
CloseTrackingEndpoint clientEndpoint = new CloseTrackingEndpoint();
client.getPolicy().setIdleTimeout(60000);
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> future = client.connect(clientEndpoint, wsUri);
// Confirm connected
Session session = future.get(5, SECONDS);
int messageCount = 10;
try
{
// Have client write slowly.
ClientWriteThread writer = new ClientWriteThread(clientEndpoint.getSession());
writer.setMessageCount(messageCount);
writer.setMessage("Hello");
writer.setSlowness(10);
writer.start();
writer.join();
// Close
clientEndpoint.getSession().close(StatusCode.NORMAL, "Done");
// confirm close received on server
clientEndpoint.assertReceivedCloseEvent(10000, is(StatusCode.NORMAL), containsString("Done"));
}
finally
{
if (session != null)
{
session.close();
}
}
}
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.hamcrest.Matcher;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
public abstract class AbstractCloseEndpoint extends WebSocketAdapter
{
public final Logger LOG;
public CountDownLatch closeLatch = new CountDownLatch(1);
public String closeReason = null;
public int closeStatusCode = -1;
public LinkedBlockingQueue<Throwable> errors = new LinkedBlockingQueue<>();
public AbstractCloseEndpoint()
{
this.LOG = Log.getLogger(this.getClass().getName());
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
LOG.debug("onWebSocketClose({}, {})",statusCode,reason);
this.closeStatusCode = statusCode;
this.closeReason = reason;
closeLatch.countDown();
}
@Override
public void onWebSocketError(Throwable cause)
{
errors.offer(cause);
}
public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher<Integer> statusCodeMatcher, Matcher<String> reasonMatcher)
throws InterruptedException
{
assertThat("Client Close Event Occurred", closeLatch.await(clientTimeoutMs, TimeUnit.MILLISECONDS), is(true));
assertThat("Client Close Event Status Code", closeStatusCode, statusCodeMatcher);
if (reasonMatcher == null)
{
assertThat("Client Close Event Reason", closeReason, nullValue());
}
else
{
assertThat("Client Close Event Reason", closeReason, reasonMatcher);
}
}
}

View File

@ -0,0 +1,68 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import java.util.Collection;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
/**
* On Message, return container information
*/
public class ContainerEndpoint extends AbstractCloseEndpoint
{
private final WebSocketServerFactory container;
private Session session;
public ContainerEndpoint(WebSocketServerFactory container)
{
super();
this.container = container;
}
@Override
public void onWebSocketText(String message)
{
LOG.debug("onWebSocketText({})",message);
if (message.equalsIgnoreCase("openSessions"))
{
Collection<WebSocketSession> sessions = container.getOpenSessions();
StringBuilder ret = new StringBuilder();
ret.append("openSessions.size=").append(sessions.size()).append('\n');
int idx = 0;
for (WebSocketSession sess : sessions)
{
ret.append('[').append(idx++).append("] ").append(sess.toString()).append('\n');
}
session.getRemote().sendStringByFuture(ret.toString());
}
session.close(StatusCode.NORMAL,"ContainerEndpoint");
}
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
this.session = sess;
}
}

View File

@ -0,0 +1,35 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
/**
* On Connect, close socket
*/
public class FastCloseEndpoint extends AbstractCloseEndpoint
{
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})", sess);
sess.close(StatusCode.NORMAL, "FastCloseServer");
}
}

View File

@ -0,0 +1,36 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import org.eclipse.jetty.websocket.api.Session;
/**
* On Connect, throw unhandled exception
*/
public class FastFailEndpoint extends AbstractCloseEndpoint
{
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
// Test failure due to unhandled exception
// this should trigger a fast-fail closure during open/connect
throw new RuntimeException("Intentional FastFail");
}
}

View File

@ -0,0 +1,77 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.tests.EchoSocket;
import static java.util.concurrent.TimeUnit.SECONDS;
public class ServerCloseCreator implements WebSocketCreator
{
private final WebSocketServerFactory serverFactory;
private LinkedBlockingQueue<AbstractCloseEndpoint> createdSocketQueue = new LinkedBlockingQueue<>();
public ServerCloseCreator(WebSocketServerFactory serverFactory)
{
this.serverFactory = serverFactory;
}
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
AbstractCloseEndpoint closeSocket = null;
if (req.hasSubProtocol("fastclose"))
{
closeSocket = new FastCloseEndpoint();
resp.setAcceptedSubProtocol("fastclose");
}
else if (req.hasSubProtocol("fastfail"))
{
closeSocket = new FastFailEndpoint();
resp.setAcceptedSubProtocol("fastfail");
}
else if (req.hasSubProtocol("container"))
{
closeSocket = new ContainerEndpoint(serverFactory);
resp.setAcceptedSubProtocol("container");
}
if (closeSocket != null)
{
createdSocketQueue.offer(closeSocket);
return closeSocket;
}
else
{
return new EchoSocket();
}
}
public AbstractCloseEndpoint pollLastCreated() throws InterruptedException
{
return createdSocketQueue.poll(5, SECONDS);
}
}

View File

@ -0,0 +1,277 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import java.net.URI;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.Future;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.util.WSURI;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
/**
* Tests various close scenarios
*/
public class ServerCloseTest
{
private WebSocketClient client;
private Server server;
private ServerCloseCreator serverEndpointCreator;
@BeforeEach
public void startServer() throws Exception
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder closeEndpoint = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
WebSocketServerFactory serverFactory = (WebSocketServerFactory) factory;
factory.getPolicy().setIdleTimeout(2000);
serverEndpointCreator = new ServerCloseCreator(serverFactory);
factory.setCreator(serverEndpointCreator);
}
});
context.addServlet(closeEndpoint, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.setMaxIdleTimeout(SECONDS.toMillis(2));
client.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
private void close(Session session)
{
if (session != null)
{
session.close();
}
}
/**
* Test fast close (bug #403817)
*
* @throws Exception on test failure
*/
@Test
public void fastClose() throws Exception
{
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("fastclose");
CloseTrackingEndpoint clientEndpoint = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> futSession = client.connect(clientEndpoint, wsUri, request);
Session session = null;
try
{
session = futSession.get(5, SECONDS);
// Verify that client got close
clientEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.NORMAL), containsString(""));
// Verify that server socket got close event
AbstractCloseEndpoint serverEndpoint = serverEndpointCreator.pollLastCreated();
assertThat("Fast Close Latch", serverEndpoint.closeLatch.await(5, SECONDS), is(true));
assertThat("Fast Close.statusCode", serverEndpoint.closeStatusCode, is(StatusCode.ABNORMAL));
}
finally
{
close(session);
}
}
/**
* Test fast fail (bug #410537)
*
* @throws Exception on test failure
*/
@Test
public void fastFail() throws Exception
{
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("fastfail");
CloseTrackingEndpoint clientEndpoint = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> futSession = client.connect(clientEndpoint, wsUri, request);
Session session = null;
try(StacklessLogging ignore = new StacklessLogging(FastFailEndpoint.class, WebSocketSession.class))
{
session = futSession.get(5, SECONDS);
// Verify that client got close indicating SERVER_ERROR
clientEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.SERVER_ERROR), containsString("Intentional FastFail"));
// Verify that server socket got close event
AbstractCloseEndpoint serverEndpoint = serverEndpointCreator.pollLastCreated();
serverEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.SERVER_ERROR), containsString("Intentional FastFail"));
// Validate errors (must be "java.lang.RuntimeException: Intentional Exception from onWebSocketConnect")
assertThat("socket.onErrors", serverEndpoint.errors.size(), greaterThanOrEqualTo(1));
Throwable cause = serverEndpoint.errors.poll(5, SECONDS);
assertThat("Error type", cause, instanceOf(RuntimeException.class));
// ... with optional ClosedChannelException
cause = serverEndpoint.errors.peek();
if (cause != null)
{
assertThat("Error type", cause, instanceOf(ClosedChannelException.class));
}
}
finally
{
close(session);
}
}
@Test
public void dropConnection() throws Exception
{
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("container");
CloseTrackingEndpoint clientEndpoint = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> futSession = client.connect(clientEndpoint, wsUri, request);
Session session = null;
try(StacklessLogging ignore = new StacklessLogging(WebSocketSession.class))
{
session = futSession.get(5, SECONDS);
// Cause a client endpoint failure
clientEndpoint.getEndPoint().close();
// Verify that client got close
clientEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.ABNORMAL), containsString("Disconnected"));
// Verify that server socket got close event
AbstractCloseEndpoint serverEndpoint = serverEndpointCreator.pollLastCreated();
serverEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.ABNORMAL), containsString("Disconnected"));
} finally
{
close(session);
}
}
/**
* Test session open session cleanup (bug #474936)
*
* @throws Exception on test failure
*/
@Test
public void testOpenSessionCleanup() throws Exception
{
fastFail();
fastClose();
dropConnection();
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("container");
CloseTrackingEndpoint clientEndpoint = new CloseTrackingEndpoint();
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> futSession = client.connect(clientEndpoint, wsUri, request);
Session session = null;
try(StacklessLogging ignore = new StacklessLogging(WebSocketSession.class))
{
session = futSession.get(5, SECONDS);
session.getRemote().sendString("openSessions");
String msg = clientEndpoint.messageQueue.poll(5, SECONDS);
assertThat("Should only have 1 open session", msg, containsString("openSessions.size=1\n"));
// Verify that client got close
clientEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.NORMAL), containsString("ContainerEndpoint"));
// Verify that server socket got close event
AbstractCloseEndpoint serverEndpoint = serverEndpointCreator.pollLastCreated();
assertThat("Server Open Sessions Latch", serverEndpoint.closeLatch.await(5, SECONDS), is(true));
assertThat("Server Open Sessions.statusCode", serverEndpoint.closeStatusCode, is(StatusCode.NORMAL));
assertThat("Server Open Sessions.errors", serverEndpoint.errors.size(), is(0));
}
finally
{
close(session);
}
}
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@WebSocket
public class SlowServerEndpoint
{
private static final Logger LOG = Log.getLogger(SlowServerEndpoint.class);
@OnWebSocketMessage
public void onMessage(Session session, String msg)
{
ThreadLocalRandom random = ThreadLocalRandom.current();
if (msg.startsWith("send-slow|"))
{
int idx = msg.indexOf('|');
int msgCount = Integer.parseInt(msg.substring(idx + 1));
CompletableFuture.runAsync(() ->
{
for (int i = 0; i < msgCount; i++)
{
try
{
session.getRemote().sendString("Hello/" + i + "/");
// fake some slowness
TimeUnit.MILLISECONDS.sleep(random.nextInt(2000));
}
catch (Throwable cause)
{
LOG.warn(cause);
}
}
});
}
else
{
// echo message.
try
{
session.getRemote().sendString(msg);
}
catch (IOException ignore)
{
LOG.ignore(ignore);
}
}
}
}

View File

@ -0,0 +1,140 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.tests.server;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.util.WSURI;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
/**
* This Regression Test Exists because of Server side Idle timeout, Write, and Generator bugs.
*/
public class SlowServerTest
{
private Server server;
private WebSocketClient client;
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.setMaxIdleTimeout(60000);
client.start();
}
@BeforeEach
public void startServer() throws Exception
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder websocket = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.register(SlowServerEndpoint.class);
}
});
context.addServlet(websocket, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testServerSlowToSend() throws Exception
{
CloseTrackingEndpoint clientEndpoint = new CloseTrackingEndpoint();
client.setMaxIdleTimeout(60000);
URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/ws"));
Future<Session> future = client.connect(clientEndpoint, wsUri);
Session session = null;
try
{
// Confirm connected
session = future.get(5, SECONDS);
int messageCount = 10;
session.getRemote().sendString("send-slow|" + messageCount);
// Verify receive
LinkedBlockingQueue<String> responses = clientEndpoint.messageQueue;
for (int i = 0; i < messageCount; i++)
{
String response = responses.poll(5, SECONDS);
assertThat("Server Message[" + i + "]", response, is("Hello/" + i + "/"));
}
}
finally
{
if (session != null)
{
session.close();
}
}
}
}

View File

@ -0,0 +1,21 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
org.eclipse.jetty.LEVEL=WARN
# org.eclipse.jetty.LEVEL=DEBUG
# org.eclipse.jetty.io.LEVEL=INFO
# org.eclipse.jetty.client.LEVEL=DEBUG
# org.eclipse.jetty.util.LEVEL=INFO
# org.eclipse.jetty.websocket.LEVEL=WARN
# org.eclipse.jetty.websocket.LEVEL=DEBUG
# org.eclipse.jetty.websocket.LEVEL=INFO
# org.eclipse.jetty.websocket.client.LEVEL=DEBUG
# org.eclipse.jetty.websocket.tests.client.ClientCloseTest.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.io.IOState.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.test.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.Generator.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.Parser.LEVEL=DEBUG
# org.eclipse.jetty.websocket.client.TrackingSocket.LEVEL=DEBUG
### Hide the stacktraces during testing
org.eclipse.jetty.websocket.client.internal.io.UpgradeConnection.STACKS=false

View File

@ -19,6 +19,7 @@
<module>websocket-client</module>
<module>websocket-server</module>
<module>websocket-servlet</module>
<module>jetty-websocket-tests</module>
<module>javax-websocket-client-impl</module>
<module>javax-websocket-server-impl</module>
</modules>

View File

@ -152,6 +152,26 @@ public final class StatusCode
*/
public final static int FAILED_TLS_HANDSHAKE = 1015;
/**
* Test if provided status code is a fatal failure for bad protocol behavior.
*
* @param statusCode the status code to test
* @return true if fatal status code
*/
@SuppressWarnings("Duplicates")
public static boolean isFatal(int statusCode)
{
return (statusCode == ABNORMAL) ||
(statusCode == PROTOCOL) ||
(statusCode == MESSAGE_TOO_LARGE) ||
(statusCode == BAD_DATA) ||
(statusCode == BAD_PAYLOAD) ||
(statusCode == POLICY_VIOLATION) ||
(statusCode == REQUIRED_EXTENSION) ||
(statusCode == SERVER_ERROR) ||
(statusCode == SERVICE_RESTART);
}
/**
* Test if provided status code can be sent/received on a WebSocket close.
* <p>
@ -160,6 +180,7 @@ public final class StatusCode
* @param statusCode the statusCode to test
* @return true if transmittable
*/
@SuppressWarnings("Duplicates")
public static boolean isTransmittable(int statusCode)
{
return (statusCode == NORMAL) ||
@ -173,6 +194,7 @@ public final class StatusCode
(statusCode == SERVER_ERROR) ||
(statusCode == SERVICE_RESTART) ||
(statusCode == TRY_AGAIN_LATER) ||
(statusCode == INVALID_UPSTREAM_RESPONSE);
(statusCode == INVALID_UPSTREAM_RESPONSE) ||
((statusCode >= 3000) && (statusCode <= 4999));
}
}

View File

@ -35,7 +35,7 @@ public interface WriteCallback
* @param x
* the reason for the write failure
*/
public void writeFailed(Throwable x);
void writeFailed(Throwable x);
/**
* <p>
@ -44,5 +44,5 @@ public interface WriteCallback
*
* @see #writeFailed(Throwable)
*/
public abstract void writeSuccess();
void writeSuccess();
}

View File

@ -23,8 +23,6 @@ package org.eclipse.jetty.websocket.api.extensions;
*/
public interface IncomingFrames
{
public void incomingError(Throwable t);
/**
* Process the incoming frame.
* <p>
@ -34,5 +32,5 @@ public interface IncomingFrames
*
* @param frame the frame to process
*/
public void incomingFrame(Frame frame);
void incomingFrame(Frame frame);
}

View File

@ -22,8 +22,11 @@ import java.io.IOException;
import java.net.CookieStore;
import java.net.SocketAddress;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@ -53,6 +56,7 @@ import org.eclipse.jetty.websocket.client.masks.RandomMasker;
import org.eclipse.jetty.websocket.common.SessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory;
import org.eclipse.jetty.websocket.common.scopes.SimpleContainerScope;
@ -61,7 +65,7 @@ import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
/**
* WebSocketClient provides a means of establishing connections to remote websocket endpoints.
*/
public class WebSocketClient extends ContainerLifeCycle implements WebSocketContainerScope
public class WebSocketClient extends ContainerLifeCycle implements WebSocketContainerScope, WebSocketSessionListener
{
private static final Logger LOG = Log.getLogger(WebSocketClient.class);
@ -76,6 +80,7 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
private final WebSocketExtensionFactory extensionRegistry;
private final EventDriverFactory eventDriverFactory;
private final SessionFactory sessionFactory;
private final List<WebSocketSessionListener> sessionListeners = new ArrayList<>();
// defaults to true for backwards compatibility
private boolean stopAtShutdown = true;
@ -268,6 +273,8 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
this.httpClient = httpClient;
}
this.addSessionListener(this);
// Ensure we get a Client version of the policy.
this.policy = scope.getPolicy().delegateAs(WebSocketBehavior.CLIENT);
// Support Late Binding of Object Factory (for CDI)
@ -560,7 +567,25 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
return httpClient.getSslContextFactory();
}
private synchronized void init() throws IOException
@Override
public void addSessionListener(WebSocketSessionListener listener)
{
this.sessionListeners.add(listener);
}
@Override
public void removeSessionListener(WebSocketSessionListener listener)
{
this.sessionListeners.remove(listener);
}
@Override
public Collection<WebSocketSessionListener> getSessionListeners()
{
return this.sessionListeners;
}
private synchronized void init()
{
if (isStopAtShutdown() && !ShutdownThread.isRegistered(this))
{
@ -594,7 +619,6 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
if (LOG.isDebugEnabled())
LOG.debug("Session Opened: {}",session);
addManaged(session);
LOG.debug("post-onSessionOpened() - {}", this);
}
public void setAsyncWriteTimeout(long ms)

View File

@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@ -49,6 +50,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.MultiMap;
@ -516,6 +518,7 @@ public class WebSocketUpgradeRequest extends HttpRequest implements CompleteList
// wrap in UpgradeException
handleException(new UpgradeException(requestURI,responseStatusCode,responseLine,failure));
}
return;
}
if (responseStatusCode != HttpStatus.SWITCHING_PROTOCOLS_101)
@ -527,7 +530,7 @@ public class WebSocketUpgradeRequest extends HttpRequest implements CompleteList
private void handleException(Throwable failure)
{
localEndpoint.incomingError(failure);
localEndpoint.onError(failure);
fut.completeExceptionally(failure);
}
@ -575,6 +578,13 @@ public class WebSocketUpgradeRequest extends HttpRequest implements CompleteList
WebSocketClientConnection connection = new WebSocketClientConnection(endp,wsClient.getExecutor(),wsClient.getScheduler(),localEndpoint.getPolicy(),
wsClient.getBufferPool());
Collection<Connection.Listener> connectionListeners = wsClient.getBeans(Connection.Listener.class);
if (connectionListeners != null)
{
connectionListeners.forEach((listener) -> connection.addListener(listener));
}
URI requestURI = this.getURI();
WebSocketSession session = getSessionFactory().createSession(requestURI,localEndpoint,connection);

View File

@ -1,130 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.BlockheadServer;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Tests for conditions due to bad networking.
*/
public class BadNetworkTest
{
public ByteBufferPool bufferPool = new MappedByteBufferPool();
private static BlockheadServer server;
private WebSocketClient client;
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient(bufferPool);
client.getPolicy().setIdleTimeout(250);
client.start();
}
@BeforeAll
public static void startServer() throws Exception
{
server = new BlockheadServer();
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterAll
public static void stopServer() throws Exception
{
server.stop();
}
@Test
public void testAbruptClientClose() throws Exception
{
JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
// Validate that we are connected
future.get(30,TimeUnit.SECONDS);
wsocket.waitForConnected();
// Have client disconnect abruptly
Session session = wsocket.getSession();
session.disconnect();
// Client Socket should see close
wsocket.waitForClose(10,TimeUnit.SECONDS);
// Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection
// in the situation of a bad network connection.
wsocket.assertCloseCode(StatusCode.NO_CLOSE);
}
@Test
public void testAbruptServerClose() throws Exception
{
JettyTrackingSocket wsocket = new JettyTrackingSocket();
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// Validate that we are connected
future.get(30, TimeUnit.SECONDS);
wsocket.waitForConnected();
// Have server disconnect abruptly
serverConn.abort();
// Wait for close (as response to idle timeout)
wsocket.waitForClose(10, TimeUnit.SECONDS);
// Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection
// in the situation of a bad network connection.
wsocket.assertCloseCode(StatusCode.NO_CLOSE);
}
}
}

View File

@ -1,673 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static java.time.Duration.ofSeconds;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SelectorManager;
import org.eclipse.jetty.io.SocketChannelEndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.websocket.api.ProtocolException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.Parser;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.BlockheadServer;
import org.eclipse.jetty.websocket.common.test.RawFrameBuilder;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class ClientCloseTest
{
private static final Logger LOG = Log.getLogger(ClientCloseTest.class);
private static class CloseTrackingSocket extends WebSocketAdapter
{
private static final Logger LOG = ClientCloseTest.LOG.getLogger("CloseTrackingSocket");
public int closeCode = -1;
public String closeReason = null;
public CountDownLatch closeLatch = new CountDownLatch(1);
public AtomicInteger closeCount = new AtomicInteger(0);
public CountDownLatch openLatch = new CountDownLatch(1);
public CountDownLatch errorLatch = new CountDownLatch(1);
public LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();
public AtomicReference<Throwable> error = new AtomicReference<>();
public void assertNoCloseEvent()
{
assertThat("Client Close Event",closeLatch.getCount(),is(1L));
assertThat("Client Close Event Status Code ",closeCode,is(-1));
}
public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher<Integer> statusCodeMatcher, Matcher<String> reasonMatcher)
throws InterruptedException
{
long maxTimeout = clientTimeoutMs * 4;
assertThat("Client Close Event Occurred",closeLatch.await(maxTimeout,TimeUnit.MILLISECONDS),is(true));
assertThat("Client Close Event Count",closeCount.get(),is(1));
assertThat("Client Close Event Status Code",closeCode,statusCodeMatcher);
if (reasonMatcher == null)
{
assertThat("Client Close Event Reason",closeReason,nullValue());
}
else
{
assertThat("Client Close Event Reason",closeReason,reasonMatcher);
}
}
public void clearQueues()
{
messageQueue.clear();
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
LOG.debug("onWebSocketClose({},{})",statusCode,reason);
super.onWebSocketClose(statusCode,reason);
closeCount.incrementAndGet();
closeCode = statusCode;
closeReason = reason;
closeLatch.countDown();
}
@Override
public void onWebSocketConnect(Session session)
{
LOG.debug("onWebSocketConnect({})",session);
super.onWebSocketConnect(session);
openLatch.countDown();
}
@Override
public void onWebSocketError(Throwable cause)
{
LOG.debug("onWebSocketError",cause);
assertThat("Unique Error Event", error.compareAndSet(null, cause), is(true));
errorLatch.countDown();
}
@Override
public void onWebSocketText(String message)
{
LOG.debug("onWebSocketText({})",message);
messageQueue.offer(message);
}
public EndPoint getEndPoint() throws Exception
{
Session session = getSession();
assertThat("Session type",session,instanceOf(WebSocketSession.class));
WebSocketSession wssession = (WebSocketSession)session;
Field fld = wssession.getClass().getDeclaredField("connection");
fld.setAccessible(true);
assertThat("Field: connection",fld,notNullValue());
Object val = fld.get(wssession);
assertThat("Connection type",val,instanceOf(AbstractWebSocketConnection.class));
@SuppressWarnings("resource")
AbstractWebSocketConnection wsconn = (AbstractWebSocketConnection)val;
return wsconn.getEndPoint();
}
}
private static BlockheadServer server;
private WebSocketClient client;
private void confirmConnection(CloseTrackingSocket clientSocket, Future<Session> clientFuture, BlockheadConnection serverConns) throws Exception
{
// Wait for client connect on via future
clientFuture.get(30,TimeUnit.SECONDS);
// Wait for client connect via client websocket
assertThat("Client WebSocket is Open",clientSocket.openLatch.await(30,TimeUnit.SECONDS),is(true));
try
{
// Send message from client to server
final String echoMsg = "echo-test";
Future<Void> testFut = clientSocket.getRemote().sendStringByFuture(echoMsg);
// Wait for send future
testFut.get(Timeouts.SEND, Timeouts.SEND_UNIT);
// Read Frame on server side
LinkedBlockingQueue<WebSocketFrame> serverCapture = serverConns.getFrameQueue();
WebSocketFrame frame = serverCapture.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("Server received frame",frame.getOpCode(),is(OpCode.TEXT));
assertThat("Server received frame payload",frame.getPayloadAsUTF8(),is(echoMsg));
// Server send echo reply
serverConns.write(new TextFrame().setPayload(echoMsg));
// Verify received message
String recvMsg = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("Received message",recvMsg,is(echoMsg));
// Verify that there are no errors
assertThat("Error events",clientSocket.error.get(),nullValue());
}
finally
{
clientSocket.clearQueues();
}
}
private void confirmServerReceivedCloseFrame(BlockheadConnection serverConn, int expectedCloseCode, Matcher<String> closeReasonMatcher) throws InterruptedException
{
LinkedBlockingQueue<WebSocketFrame> serverCapture = serverConn.getFrameQueue();
WebSocketFrame frame = serverCapture.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("Server close frame", frame, is(notNullValue()));
assertThat("Server received close frame",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo closeInfo = new CloseInfo(frame);
assertThat("Server received close code",closeInfo.getStatusCode(),is(expectedCloseCode));
if (closeReasonMatcher == null)
{
assertThat("Server received close reason",closeInfo.getReason(),nullValue());
}
else
{
assertThat("Server received close reason",closeInfo.getReason(),closeReasonMatcher);
}
}
public static class TestClientTransportOverHTTP extends HttpClientTransportOverHTTP
{
@Override
protected SelectorManager newSelectorManager(HttpClient client)
{
return new ClientSelectorManager(client, 1){
@Override
protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key)
{
TestEndPoint endPoint = new TestEndPoint(channel,selector,key,getScheduler());
endPoint.setIdleTimeout(client.getIdleTimeout());
return endPoint;
}
};
}
}
public static class TestEndPoint extends SocketChannelEndPoint
{
public AtomicBoolean congestedFlush = new AtomicBoolean(false);
public TestEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
{
super((SocketChannel)channel,selector,key,scheduler);
}
@Override
public boolean flush(ByteBuffer... buffers) throws IOException
{
boolean flushed = super.flush(buffers);
congestedFlush.set(!flushed);
return flushed;
}
}
@BeforeEach
public void startClient() throws Exception
{
HttpClient httpClient = new HttpClient(new TestClientTransportOverHTTP(), null);
client = new WebSocketClient(httpClient);
client.addBean(httpClient);
client.start();
}
@BeforeAll
public static void startServer() throws Exception
{
server = new BlockheadServer();
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterAll
public static void stopServer() throws Exception
{
server.stop();
}
@Test
public void testHalfClose() throws Exception
{
// Set client timeout
final int timeout = 5000;
client.setMaxIdleTimeout(timeout);
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
// client sends close frame (code 1000, normal)
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// server receives close frame
confirmServerReceivedCloseFrame(serverConn, StatusCode.NORMAL, is(origCloseReason));
// server sends 2 messages
serverConn.write(new TextFrame().setPayload("Hello"));
serverConn.write(new TextFrame().setPayload("World"));
// server sends close frame (code 1000, no reason)
CloseInfo sclose = new CloseInfo(StatusCode.NORMAL, "From Server");
serverConn.write(sclose.asFrame());
// Verify received messages
String recvMsg = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("Received message 1", recvMsg, is("Hello"));
recvMsg = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("Received message 2", recvMsg, is("World"));
// Verify that there are no errors
assertThat("Error events", clientSocket.error.get(), nullValue());
// client close event on ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.NORMAL), containsString("From Server"));
}
assertThat("Client Open Sessions", client.getOpenSessions(), empty());
}
@Disabled("Need sbordet's help here")
@Test
public void testNetworkCongestion() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
// client sends BIG frames (until it cannot write anymore)
// server must not read (for test purpose, in order to congest connection)
// when write is congested, client enqueue close frame
// client initiate write, but write never completes
EndPoint endp = clientSocket.getEndPoint();
assertThat("EndPoint is testable", endp, instanceOf(TestEndPoint.class));
TestEndPoint testendp = (TestEndPoint) endp;
char msg[] = new char[10240];
int writeCount = 0;
long writeSize = 0;
int i = 0;
while (!testendp.congestedFlush.get())
{
int z = i - ((i / 26) * 26);
char c = (char) ('a' + z);
Arrays.fill(msg, c);
clientSocket.getRemote().sendStringByFuture(String.valueOf(msg));
writeCount++;
writeSize += msg.length;
}
LOG.info("Wrote {} frames totalling {} bytes of payload before congestion kicked in", writeCount, writeSize);
// Verify timeout error
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(SocketTimeoutException.class));
}
}
@Test
public void testProtocolException() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
// client should not have received close message (yet)
clientSocket.assertNoCloseEvent();
// server sends bad close frame (too big of a reason message)
byte msg[] = new byte[400];
Arrays.fill(msg, (byte) 'x');
ByteBuffer bad = ByteBuffer.allocate(500);
RawFrameBuilder.putOpFin(bad, OpCode.CLOSE, true);
RawFrameBuilder.putLength(bad, msg.length + 2, false);
bad.putShort((short) StatusCode.NORMAL);
bad.put(msg);
BufferUtil.flipToFlush(bad, 0);
try (StacklessLogging ignore = new StacklessLogging(Parser.class))
{
serverConn.writeRaw(bad);
// client should have noticed the error
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(ProtocolException.class));
assertThat("OnError", clientSocket.error.get().getMessage(), containsString("Invalid control frame"));
// client parse invalid frame, notifies server of close (protocol error)
confirmServerReceivedCloseFrame(serverConn, StatusCode.PROTOCOL, allOf(containsString("Invalid control frame"), containsString("length")));
}
}
// client triggers close event on client ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.PROTOCOL),allOf(containsString("Invalid control frame"),containsString("length")));
assertThat("Client Open Sessions", client.getOpenSessions(), empty());
}
@Test
public void testReadEOF() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
// client sends close frame
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// server receives close frame
confirmServerReceivedCloseFrame(serverConn, StatusCode.NORMAL, is(origCloseReason));
// client should not have received close message (yet)
clientSocket.assertNoCloseEvent();
// server shuts down connection (no frame reply)
serverConn.abort();
// client reads -1 (EOF)
// client triggers close event on client ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL),
anyOf(
containsString("EOF"),
containsString("Disconnected")
));
}
assertThat("Client Open Sessions", client.getOpenSessions(), empty());
}
@Test
public void testServerNoCloseHandshake() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
// client sends close frame
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// server receives close frame
confirmServerReceivedCloseFrame(serverConn, StatusCode.NORMAL, is(origCloseReason));
// client should not have received close message (yet)
clientSocket.assertNoCloseEvent();
// server never sends close frame handshake
// server sits idle
// client idle timeout triggers close event on client ws-endpoint
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(TimeoutException.class));
// client close should occur
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL),
anyOf(
containsString("Timeout"),
containsString("Disconnected")
));
}
assertThat("Client Open Sessions", client.getOpenSessions(), empty());
}
@Test
public void testStopLifecycle() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
int clientCount = 3;
List<CloseTrackingSocket> clientSockets = new ArrayList<>();
List<CompletableFuture<BlockheadConnection>> serverConnFuts = new ArrayList<>();
List<BlockheadConnection> serverConns = new ArrayList<>();
try
{
assertTimeoutPreemptively(ofSeconds(5), ()-> {
// Open Multiple Clients
for (int i = 0; i < clientCount; i++)
{
// Client Request Upgrade
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
clientSockets.add(clientSocket);
Future<Session> clientConnectFuture = client.connect(clientSocket, server.getWsUri());
// Server accepts connection
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
serverConnFuts.add(serverConnFut);
server.addConnectFuture(serverConnFut);
BlockheadConnection serverConn = serverConnFut.get();
serverConns.add(serverConn);
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
}
// client lifecycle stop (the meat of this test)
client.stop();
// clients send close frames (code 1001, shutdown)
for (int i = 0; i < clientCount; i++)
{
// server receives close frame
confirmServerReceivedCloseFrame(serverConns.get(i), StatusCode.SHUTDOWN, containsString("Shutdown"));
}
// clients disconnect
for (int i = 0; i < clientCount; i++)
{
clientSockets.get(i).assertReceivedCloseEvent(timeout, is(StatusCode.SHUTDOWN), containsString("Shutdown"));
}
assertThat("Client Open Sessions", client.getOpenSessions(), empty());
// clients disconnect
for (int i = 0; i < clientCount; i++)
{
clientSockets.get(i).assertReceivedCloseEvent(timeout, is(StatusCode.SHUTDOWN), containsString("Shutdown"));
}
});
}
finally
{
for(BlockheadConnection serverConn: serverConns)
{
try
{
serverConn.close();
}
catch (Exception ignore)
{
}
}
}
}
@Test
public void testWriteException() throws Exception
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// client confirms connection via echo
confirmConnection(clientSocket, clientConnectFuture, serverConn);
// setup client endpoint for write failure (test only)
EndPoint endp = clientSocket.getEndPoint();
endp.shutdownOutput();
// client enqueue close frame
// client write failure
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(EofException.class));
// client triggers close event on client ws-endpoint
// assert - close code==1006 (abnormal)
// assert - close reason message contains (write failure)
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("EOF"));
}
assertThat("Client Open Sessions", client.getOpenSessions(), empty());
}
}

View File

@ -1,96 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
public class ServerWriteThread extends Thread
{
private static final Logger LOG = Log.getLogger(ServerWriteThread.class);
private final BlockheadConnection conn;
private int slowness = -1;
private int messageCount = 100;
private String message = "Hello";
public ServerWriteThread(BlockheadConnection conn)
{
this.conn = conn;
}
public String getMessage()
{
return message;
}
public int getMessageCount()
{
return messageCount;
}
public int getSlowness()
{
return slowness;
}
@Override
public void run()
{
final AtomicInteger m = new AtomicInteger();
try
{
while (m.get() < messageCount)
{
conn.write(new TextFrame().setPayload(message));
m.incrementAndGet();
if (slowness > 0)
{
TimeUnit.MILLISECONDS.sleep(slowness);
}
}
}
catch (InterruptedException e)
{
LOG.warn(e);
}
}
public void setMessage(String message)
{
this.message = message;
}
public void setMessageCount(int messageCount)
{
this.messageCount = messageCount;
}
public void setSlowness(int slowness)
{
this.slowness = slowness;
}
}

View File

@ -1,135 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import java.net.URI;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.BlockheadServer;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class SessionTest
{
private static BlockheadServer server;
@BeforeAll
public static void startServer() throws Exception
{
server = new BlockheadServer();
server.start();
}
@AfterAll
public static void stopServer() throws Exception
{
server.stop();
}
@Test
@Disabled // TODO fix frequent failure
public void testBasicEcho_FromClient() throws Exception
{
WebSocketClient client = new WebSocketClient();
client.start();
try
{
JettyTrackingSocket cliSock = new JettyTrackingSocket();
// Hook into server connection creation
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
client.getPolicy().setIdleTimeout(10000);
URI wsUri = server.getWsUri();
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("echo");
Future<Session> future = client.connect(cliSock,wsUri,request);
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// Setup echo of frames on server side
serverConn.setIncomingFrameConsumer((frame)->{
WebSocketFrame copy = WebSocketFrame.copy(frame);
serverConn.write(copy);
});
Session sess = future.get(30000, TimeUnit.MILLISECONDS);
assertThat("Session", sess, notNullValue());
assertThat("Session.open", sess.isOpen(), is(true));
assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue());
assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue());
cliSock.assertWasOpened();
cliSock.assertNotClosed();
Collection<WebSocketSession> sessions = client.getBeans(WebSocketSession.class);
assertThat("client.connectionManager.sessions.size", sessions.size(), is(1));
RemoteEndpoint remote = cliSock.getSession().getRemote();
remote.sendStringByFuture("Hello World!");
if (remote.getBatchMode() == BatchMode.ON)
{
remote.flush();
}
// wait for response from server
cliSock.waitForMessage(30000, TimeUnit.MILLISECONDS);
Set<WebSocketSession> open = client.getOpenSessions();
assertThat("(Before Close) Open Sessions.size", open.size(), is(1));
String received = cliSock.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("Message", received, containsString("Hello World!"));
cliSock.close();
}
cliSock.waitForClose(30000, TimeUnit.MILLISECONDS);
Set<WebSocketSession> open = client.getOpenSessions();
// TODO this sometimes fails!
assertThat("(After Close) Open Sessions.size", open.size(), is(0));
}
finally
{
client.stop();
}
}
}

View File

@ -1,133 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.BlockheadServer;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class SlowClientTest
{
private static BlockheadServer server;
private WebSocketClient client;
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.getPolicy().setIdleTimeout(60000);
client.start();
}
@BeforeAll
public static void startServer() throws Exception
{
server = new BlockheadServer();
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterAll
public static void stopServer() throws Exception
{
server.stop();
}
@Test
public void testClientSlowToSend() throws Exception
{
JettyTrackingSocket tsocket = new JettyTrackingSocket();
client.getPolicy().setIdleTimeout(60000);
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(tsocket, wsUri);
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
// Confirm connected
future.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT);
tsocket.waitForConnected();
int messageCount = 10;
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// Have client write slowly.
ClientWriteThread writer = new ClientWriteThread(tsocket.getSession());
writer.setMessageCount(messageCount);
writer.setMessage("Hello");
writer.setSlowness(10);
writer.start();
writer.join();
LinkedBlockingQueue<WebSocketFrame> serverFrames = serverConn.getFrameQueue();
for (int i = 0; i < messageCount; i++)
{
WebSocketFrame serverFrame = serverFrames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
String prefix = "Server frame[" + i + "]";
assertThat(prefix + ".opcode", serverFrame.getOpCode(), is(OpCode.TEXT));
assertThat(prefix + ".payload", serverFrame.getPayloadAsUTF8(), is("Hello/" + i + "/"));
}
// Close
tsocket.getSession().close(StatusCode.NORMAL, "Done");
// confirm close received on server
WebSocketFrame serverFrame = serverFrames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("close frame", serverFrame.getOpCode(), is(OpCode.CLOSE));
CloseInfo closeInfo = new CloseInfo(serverFrame);
assertThat("close info", closeInfo.getStatusCode(), is(StatusCode.NORMAL));
WebSocketFrame respClose = WebSocketFrame.copy(serverFrame);
respClose.setMask(null); // remove client mask (if present)
serverConn.write(respClose);
// Verify server response
assertTrue(tsocket.closeLatch.await(3, TimeUnit.MINUTES), "Client Socket Closed");
tsocket.assertCloseCode(StatusCode.NORMAL);
}
}
}

View File

@ -1,160 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.masks.ZeroMasker;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.BlockheadServer;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class SlowServerTest
{
private BlockheadServer server;
private WebSocketClient client;
@BeforeEach
public void startClient() throws Exception
{
client = new WebSocketClient();
client.setMaxIdleTimeout(60000);
client.start();
}
@BeforeEach
public void startServer() throws Exception
{
server = new BlockheadServer();
server.start();
}
@AfterEach
public void stopClient() throws Exception
{
client.stop();
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testServerSlowToRead() throws Exception
{
JettyTrackingSocket tsocket = new JettyTrackingSocket();
client.setMasker(new ZeroMasker());
client.setMaxIdleTimeout(60000);
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(tsocket,wsUri);
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// slow down reads
serverConn.setIncomingFrameConsumer((frame)-> {
try
{
TimeUnit.MILLISECONDS.sleep(100);
}
catch (InterruptedException ignore)
{
}
});
// Confirm connected
future.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT);
tsocket.waitForConnected();
int messageCount = 10;
// Have client write as quickly as it can.
ClientWriteThread writer = new ClientWriteThread(tsocket.getSession());
writer.setMessageCount(messageCount);
writer.setMessage("Hello");
writer.setSlowness(-1); // disable slowness
writer.start();
writer.join();
// Verify receive
LinkedBlockingQueue<WebSocketFrame> serverFrames = serverConn.getFrameQueue();
for(int i=0; i< messageCount; i++)
{
WebSocketFrame serverFrame = serverFrames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
String prefix = "Server Frame[" + i + "]";
assertThat(prefix, serverFrame, is(notNullValue()));
assertThat(prefix + ".opCode", serverFrame.getOpCode(), is(OpCode.TEXT));
assertThat(prefix + ".payload", serverFrame.getPayloadAsUTF8(), is("Hello/" + i + "/"));
}
}
}
@Test
public void testServerSlowToSend() throws Exception
{
JettyTrackingSocket clientSocket = new JettyTrackingSocket();
client.setMaxIdleTimeout(60000);
CompletableFuture<BlockheadConnection> serverConnFut = new CompletableFuture<>();
server.addConnectFuture(serverConnFut);
URI wsUri = server.getWsUri();
Future<Session> clientConnectFuture = client.connect(clientSocket,wsUri);
try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// Confirm connected
clientConnectFuture.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT);
clientSocket.waitForConnected();
// Have server write slowly.
int messageCount = 1000;
ServerWriteThread writer = new ServerWriteThread(serverConn);
writer.setMessageCount(messageCount);
writer.setMessage("Hello");
writer.setSlowness(10);
writer.start();
writer.join();
// Verify receive
assertThat("Message Receive Count", clientSocket.messageQueue.size(), is(messageCount));
}
}
}

View File

@ -7,7 +7,7 @@ org.eclipse.jetty.LEVEL=WARN
# org.eclipse.jetty.websocket.LEVEL=WARN
# org.eclipse.jetty.websocket.LEVEL=DEBUG
# org.eclipse.jetty.websocket.client.LEVEL=DEBUG
# org.eclipse.jetty.websocket.client.ClientCloseTest.LEVEL=DEBUG
# org.eclipse.jetty.websocket.tests.client.ClientCloseTest.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.io.IOState.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.test.LEVEL=DEBUG

View File

@ -1,61 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common;
import org.eclipse.jetty.websocket.common.io.IOState;
import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
/**
* Connection states as outlined in <a href="https://tools.ietf.org/html/rfc6455">RFC6455</a>.
*/
public enum ConnectionState
{
/** [RFC] Initial state of a connection, the upgrade request / response is in progress */
CONNECTING,
/**
* [Impl] Intermediate state between CONNECTING and OPEN, used to indicate that a upgrade request/response is successful, but the end-user provided socket's
* onOpen code has yet to run.
* <p>
* This state is to allow the local socket to initiate messages and frames, but to NOT start reading yet.
*/
CONNECTED,
/**
* [RFC] The websocket connection is established and open.
* <p>
* This indicates that the Upgrade has succeed, and the end-user provided socket's onOpen code has completed.
* <p>
* It is now time to start reading from the remote endpoint.
*/
OPEN,
/**
* [RFC] The websocket closing handshake is started.
* <p>
* This can be considered a half-closed state.
* <p>
* When receiving this as an event on {@link ConnectionStateListener#onConnectionStateChange(ConnectionState)} a close frame should be sent using
* the {@link CloseInfo} available from {@link IOState#getCloseInfo()}
*/
CLOSING,
/**
* [RFC] The websocket connection is closed.
* <p>
* Connection should be disconnected and no further reads or writes should occur.
*/
CLOSED;
}

View File

@ -22,19 +22,42 @@ import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
import org.eclipse.jetty.websocket.common.io.IOState;
public interface LogicalConnection extends OutgoingFrames, SuspendToken
{
/**
* Called to indicate a close frame was successfully sent to the remote.
* @param close the close details
* Test if Connection State allows for reading of frames.
*
* @return true if able to read, false otherwise.
*/
void onLocalClose(CloseInfo close);
boolean canReadWebSocketFrames();
/**
* Test if Connection State allows for writing frames.
*
* @return true if able to write, false otherwise.
*/
boolean canWriteWebSocketFrames();
/**
* Close the connection based on the cause.
*
* @param cause the cause
*/
void close(Throwable cause);
/**
* Request a local close.
*
* @param closeInfo
* @param callback
*/
void close(CloseInfo closeInfo, Callback callback);
/**
* Terminate the connection (no close frame sent)
@ -53,6 +76,12 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
*/
Executor getExecutor();
/**
* Get Unique ID for the Connection
* @return the unique ID for the connection
*/
String getId();
/**
* Get the read/write idle timeout.
*
@ -60,13 +89,6 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
*/
long getIdleTimeout();
/**
* Get the IOState of the connection.
*
* @return the IOState of the connection.
*/
IOState getIOState();
/**
* Get the local {@link InetSocketAddress} in use for this connection.
* <p>
@ -82,6 +104,17 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
*/
long getMaxIdleTimeout();
/**
* Set the maximum number of milliseconds of idleness before the connection is closed/disconnected, (ie no frames are either sent or received)
* <p>
* This idle timeout cannot be garunteed to take immediate effect for any active read/write actions.
* New read/write actions will have this new idle timeout.
*
* @param ms
* the number of milliseconds of idle timeout
*/
void setMaxIdleTimeout(long ms);
/**
* The policy that the connection is running under.
* @return the policy for the connection
@ -112,15 +145,27 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
boolean isReading();
/**
* Set the maximum number of milliseconds of idleness before the connection is closed/disconnected, (ie no frames are either sent or received)
* Set the state to opened (the application onOpen() method has been called successfully).
* <p>
* This idle timeout cannot be garunteed to take immediate effect for any active read/write actions.
* New read/write actions will have this new idle timeout.
* Reads from network begin here.
* </p>
*
* @param ms
* the number of milliseconds of idle timeout
* @return true if state is OPENED, false otherwise
*/
void setMaxIdleTimeout(long ms);
boolean opened();
/**
* Set the state to upgrade/opening handshake has completed.
*
* @return true if state is OPENING, false otherwise
*/
boolean opening();
/**
* Report that the Remote Endpoint CLOSE Frame has been received
* @param close the close frame details
*/
void remoteClose(CloseInfo close);
/**
* Set where the connection should send the incoming frames to.
@ -146,8 +191,9 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
SuspendToken suspend();
/**
* Get Unique ID for the Connection
* @return the unique ID for the connection
* Get the Connection State as a String
*
* @return the Connection State string
*/
String getId();
String toStateString();
}

View File

@ -212,6 +212,8 @@ public class Parser
if (incomingFramesHandler == null)
{
if(LOG.isDebugEnabled())
LOG.debug("No IncomingFrames Handler to notify");
return;
}
try

View File

@ -251,7 +251,6 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint
lockMsg(MsgType.BLOCKING);
try
{
connection.getIOState().assertOutputOpen();
if (LOG.isDebugEnabled())
{
LOG.debug("sendBytes with {}", BufferUtil.toDetailString(data));
@ -301,20 +300,12 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint
}
public void uncheckedSendFrame(WebSocketFrame frame, WriteCallback callback)
{
try
{
BatchMode batchMode = BatchMode.OFF;
if (frame.isDataFrame())
batchMode = getBatchMode();
connection.getIOState().assertOutputOpen();
outgoing.outgoingFrame(frame, callback, batchMode);
}
catch (IOException e)
{
callback.writeFailed(e);
}
}
@Override
public void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException

View File

@ -18,7 +18,6 @@
package org.eclipse.jetty.websocket.common;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.HashMap;
@ -30,6 +29,7 @@ import java.util.ServiceLoader;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
@ -37,13 +37,10 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ThreadClassLoaderScope;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.CloseException;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
@ -51,88 +48,20 @@ 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.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.frames.CloseFrame;
import org.eclipse.jetty.websocket.common.io.IOState;
import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
import org.eclipse.jetty.websocket.common.io.DisconnectCallback;
import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
import org.eclipse.jetty.websocket.common.scopes.WebSocketSessionScope;
@ManagedObject("A Jetty WebSocket Session")
public class WebSocketSession extends ContainerLifeCycle implements Session, RemoteEndpointFactory, WebSocketSessionScope, IncomingFrames, Connection.Listener, ConnectionStateListener
public class WebSocketSession extends ContainerLifeCycle implements Session, RemoteEndpointFactory, WebSocketSessionScope, IncomingFrames, Connection.Listener
{
public static class OnCloseLocalCallback implements WriteCallback
{
private final Callback callback;
private final LogicalConnection connection;
private final CloseInfo close;
public OnCloseLocalCallback(Callback callback, LogicalConnection connection, CloseInfo close)
{
this.callback = callback;
this.connection = connection;
this.close = close;
}
@Override
public void writeSuccess()
{
try
{
if (callback != null)
{
callback.succeeded();
}
}
finally
{
connection.onLocalClose(close);
}
}
@Override
public void writeFailed(Throwable x)
{
try
{
if (callback != null)
{
callback.failed(x);
}
}
finally
{
connection.onLocalClose(close);
}
}
}
public class DisconnectCallback implements Callback
{
@Override
public void failed(Throwable x)
{
disconnect();
}
@Override
public void succeeded()
{
disconnect();
}
}
private static final Logger LOG = Log.getLogger(WebSocketSession.class);
private static final Logger LOG_OPEN = Log.getLogger(WebSocketSession.class.getName() + "_OPEN");
private final WebSocketContainerScope containerScope;
private final URI requestURI;
private final LogicalConnection connection;
@ -151,6 +80,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
private UpgradeRequest upgradeRequest;
private UpgradeResponse upgradeResponse;
private CompletableFuture<Session> openFuture;
private AtomicBoolean onCloseCalled = new AtomicBoolean(false);
public WebSocketSession(WebSocketContainerScope containerScope, URI requestURI, EventDriver websocket, LogicalConnection connection)
{
@ -165,7 +95,6 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
this.executor = connection.getExecutor();
this.outgoingHandler = connection;
this.incomingHandler = websocket;
this.connection.getIOState().addListener(this);
this.policy = websocket.getPolicy();
this.connection.setSession(this);
@ -175,13 +104,13 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
}
/**
* Aborts the active session abruptly.
* @param statusCode the status code
* @param reason the raw reason code
* Close the active session based on the throwable
*
* @param cause the cause for closing the connection
*/
public void abort(int statusCode, String reason)
public void close(Throwable cause)
{
close(new CloseInfo(statusCode, reason), new DisconnectCallback());
connection.close(cause);
}
@Override
@ -204,12 +133,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
}
/**
* CLOSE Primary Entry Point.
*
* <ul>
* <li>atomically enqueue CLOSE frame + flip flag to reject more frames</li>
* <li>setup CLOSE frame callback: must close flusher</li>
* </ul>
* Close Primary Entry Point.
*
* @param closeInfo the close details
*/
@ -218,11 +142,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
if (LOG.isDebugEnabled())
LOG.debug("close({})", closeInfo);
if (closed.compareAndSet(false, true))
{
CloseFrame frame = closeInfo.asFrame();
connection.outgoingFrame(frame, new OnCloseLocalCallback(callback, connection, closeInfo), BatchMode.OFF);
}
connection.close(closeInfo, callback);
}
/**
@ -232,9 +152,6 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
public void disconnect()
{
connection.disconnect();
// notify of harsh disconnect
notifyClose(StatusCode.NO_CLOSE,"Harsh disconnect");
}
public void dispatch(Runnable runnable)
@ -266,14 +183,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
{
if(LOG.isDebugEnabled())
LOG.debug("stopping - {}",this);
try
{
close(StatusCode.SHUTDOWN,"Shutdown");
}
catch (Throwable t)
{
LOG.debug("During Connection Shutdown",t);
}
connection.close(new CloseInfo(StatusCode.SHUTDOWN,"Shutdown"), new DisconnectCallback(connection));
super.doStop();
}
@ -287,36 +197,6 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
getRequestURI());
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
WebSocketSession other = (WebSocketSession)obj;
if (connection == null)
{
if (other.connection != null)
{
return false;
}
}
else if (!connection.equals(other.connection))
{
return false;
}
return true;
}
public ByteBufferPool getBufferPool()
{
return this.connection.getBufferPool();
@ -385,16 +265,12 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public RemoteEndpoint getRemote()
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.getRemote()",policy.getBehavior(),this.getClass().getSimpleName());
ConnectionState state = connection.getIOState().getConnectionState();
if ((state == ConnectionState.OPEN) || (state == ConnectionState.CONNECTED))
if (LOG.isDebugEnabled())
{
return remote;
LOG.debug("[{}] {}.getRemote()", policy.getBehavior(), this.getClass().getSimpleName());
}
throw new WebSocketException("RemoteEndpoint unavailable, current state [" + state + "], expecting [OPEN or CONNECTED]");
return remote;
}
@Override
@ -427,25 +303,6 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
return this;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((connection == null)?0:connection.hashCode());
return result;
}
/**
* Incoming Errors
*/
@Override
public void incomingError(Throwable t)
{
// Forward Errors to User WebSocket Object
websocket.incomingError(t);
}
/**
* Incoming Raw Frames from Parser
*/
@ -456,11 +313,18 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
try
{
Thread.currentThread().setContextClassLoader(classLoader);
if (connection.getIOState().isInputAvailable())
if (connection.canReadWebSocketFrames())
{
// Forward Frames Through Extension List
incomingHandler.incomingFrame(frame);
}
else
{
if (LOG.isDebugEnabled())
{
LOG.debug("Attempt to process frame when in wrong connection state: " + connection.toStateString(), new RuntimeException("TRACE"));
}
}
}
finally
{
@ -491,30 +355,51 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
return "wss".equalsIgnoreCase(requestURI.getScheme());
}
public void notifyClose(int statusCode, String reason)
public void callApplicationOnClose(CloseInfo closeInfo)
{
if (LOG.isDebugEnabled())
{
LOG.debug("notifyClose({},{})",statusCode,reason);
LOG.debug("callApplicationOnClose({})", closeInfo);
}
if(onCloseCalled.compareAndSet(false,true))
{
websocket.onClose(closeInfo);
}
websocket.onClose(new CloseInfo(statusCode,reason));
}
public void notifyError(Throwable cause)
public void callApplicationOnError(Throwable cause)
{
if (LOG.isDebugEnabled())
{
LOG.debug("callApplicationOnError()", cause);
}
if (openFuture != null && !openFuture.isDone())
openFuture.completeExceptionally(cause);
incomingError(cause);
websocket.onError(cause);
}
/**
* Jetty Connection onClosed event
* Jetty Connection onSessionClosed event
*
* @param connection the connection that was closed
*/
@Override
public void onClosed(Connection connection)
{
if(LOG.isDebugEnabled())
LOG.debug("[{}] {}.onSessionClosed()",policy.getBehavior(),this.getClass().getSimpleName());
if(connection == this.connection)
{
this.connection.disconnect();
try
{
notifySessionListeners(containerScope, (listener) -> listener.onSessionClosed(this));
}
catch (Throwable cause)
{
LOG.ignore(cause);
}
}
}
/**
@ -525,49 +410,11 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public void onOpened(Connection connection)
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.onOpened()",policy.getBehavior(),this.getClass().getSimpleName());
if(LOG.isDebugEnabled())
LOG.debug("[{}] {}.onSessionOpened()",policy.getBehavior(),this.getClass().getSimpleName());
open();
}
@SuppressWarnings("incomplete-switch")
@Override
public void onConnectionStateChange(ConnectionState state)
{
switch (state)
{
case CLOSED:
IOState ioState = this.connection.getIOState();
CloseInfo close = ioState.getCloseInfo();
// confirmed close of local endpoint
notifyClose(close.getStatusCode(),close.getReason());
try
{
if (LOG.isDebugEnabled())
LOG.debug("{}.onSessionClosed()",containerScope.getClass().getSimpleName());
containerScope.onSessionClosed(this);
}
catch (Throwable t)
{
LOG.ignore(t);
}
break;
case CONNECTED:
// notify session listeners
try
{
if (LOG.isDebugEnabled())
LOG.debug("{}.onSessionOpened()",containerScope.getClass().getSimpleName());
containerScope.onSessionOpened(this);
}
catch (Throwable t)
{
LOG.ignore(t);
}
break;
}
}
@Override
public WebSocketRemoteEndpoint newRemoteEndpoint(LogicalConnection connection, OutgoingFrames outgoingFrames, BatchMode batchMode)
{
@ -579,8 +426,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
*/
public void open()
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.open()",policy.getBehavior(),this.getClass().getSimpleName());
if(LOG.isDebugEnabled())
LOG.debug("[{}] {}.open()",policy.getBehavior(),this.getClass().getSimpleName());
if (remote != null)
{
@ -591,45 +438,49 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
try(ThreadClassLoaderScope scope = new ThreadClassLoaderScope(classLoader))
{
// Upgrade success
connection.getIOState().onConnected();
if(connection.opening())
{
// Connect remote
remote = remoteEndpointFactory.newRemoteEndpoint(connection,outgoingHandler,getBatchMode());
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.open() remote={}",policy.getBehavior(),this.getClass().getSimpleName(),remote);
remote = remoteEndpointFactory.newRemoteEndpoint(connection, outgoingHandler, getBatchMode());
if (LOG.isDebugEnabled())
LOG.debug("[{}] {}.open() remote={}", policy.getBehavior(), this.getClass().getSimpleName(), remote);
// Open WebSocket
// Open WebSocket - and call Application onOpen
websocket.openSession(this);
// Open connection
connection.getIOState().onOpened();
if(connection.opened())
{
try
{
notifySessionListeners(containerScope, (listener)-> listener.onSessionOpened(this));
}
catch (Throwable t)
{
LOG.ignore(t);
}
}
else
{
// we had a failure during onOpen()
callApplicationOnClose(new CloseInfo(StatusCode.ABNORMAL, "Failed to open local endpoint"));
disconnect();
}
if (LOG.isDebugEnabled())
{
LOG.debug("[{}] open -> {}",getPolicy().getBehavior(),dump());
LOG.debug("[{}] open -> {}", getPolicy().getBehavior(), dump());
}
if(openFuture != null)
if (openFuture != null)
{
openFuture.complete(this);
}
}
catch (CloseException ce)
{
LOG.warn(ce);
close(ce.getStatusCode(),ce.getMessage());
}
catch (Throwable t)
{
LOG.warn(t);
// Exception on end-user WS-Endpoint.
// Fast-fail & close connection with reason.
int statusCode = StatusCode.SERVER_ERROR;
if(policy.getBehavior() == WebSocketBehavior.CLIENT)
{
statusCode = StatusCode.POLICY_VIOLATION;
}
close(statusCode,t.getMessage());
close(t);
}
}
@ -704,6 +555,21 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
return BatchMode.AUTO;
}
private void notifySessionListeners(WebSocketContainerScope scope, Consumer<WebSocketSessionListener> consumer)
{
for (WebSocketSessionListener listener : scope.getSessionListeners())
{
try
{
consumer.accept(listener);
}
catch (Throwable x)
{
LOG.info("Exception while invoking listener " + listener, x);
}
}
}
@Override
public String toString()
{
@ -718,11 +584,4 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
builder.append("]");
return builder.toString();
}
public static interface Listener
{
void onOpened(WebSocketSession session);
void onClosed(WebSocketSession session);
}
}

View File

@ -0,0 +1,26 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common;
public interface WebSocketSessionListener
{
void onSessionOpened(WebSocketSession session);
void onSessionClosed(WebSocketSession session);
}

View File

@ -26,9 +26,8 @@ import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BadPayloadException;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.CloseException;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
@ -85,17 +84,6 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
return session;
}
@Override
public final void incomingError(Throwable e)
{
if (LOG.isDebugEnabled())
{
LOG.debug("incomingError(" + e.getClass().getName() + ")",e);
}
onError(e);
}
@Override
public void incomingFrame(Frame frame)
{
@ -118,7 +106,7 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
CloseInfo close = new CloseInfo(closeframe,validate);
// process handshake
session.getConnection().getIOState().onCloseRemote(close);
session.getConnection().remoteClose(close);
return;
}
@ -176,15 +164,11 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
}
catch (NotUtf8Exception e)
{
terminateConnection(StatusCode.BAD_PAYLOAD,e.getMessage());
}
catch (CloseException e)
{
terminateConnection(e.getStatusCode(),e.getMessage());
session.close(new BadPayloadException(e));
}
catch (Throwable t)
{
unhandled(t);
session.close(t);
}
}
@ -202,13 +186,11 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
@Override
public void onPong(ByteBuffer buffer)
{
/* TODO: provide annotation in future */
}
@Override
public void onPing(ByteBuffer buffer)
{
/* TODO: provide annotation in future */
}
@Override
@ -230,42 +212,12 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
try
{
// Call application onOpen
this.onConnect();
}
catch (Throwable t)
{
this.session.notifyError(t);
throw t;
}
}
protected void terminateConnection(int statusCode, String rawreason)
{
if (LOG.isDebugEnabled())
LOG.debug("terminateConnection({},{})",statusCode,rawreason);
session.close(statusCode,CloseFrame.truncate(rawreason));
}
private void unhandled(Throwable t)
{
TARGET_LOG.warn("Unhandled Error (closing connection)",t);
onError(t);
if (t instanceof CloseException)
{
terminateConnection(((CloseException)t).getStatusCode(),t.getClass().getSimpleName());
return;
}
// Unhandled Error, close the connection.
switch (policy.getBehavior())
{
case SERVER:
terminateConnection(StatusCode.SERVER_ERROR,t.getClass().getSimpleName());
break;
case CLIENT:
terminateConnection(StatusCode.POLICY_VIOLATION,t.getClass().getSimpleName());
break;
this.session.close(t);
}
}
}

View File

@ -32,37 +32,37 @@ import org.eclipse.jetty.websocket.common.WebSocketSession;
public interface EventDriver extends IncomingFrames
{
public WebSocketPolicy getPolicy();
WebSocketPolicy getPolicy();
public WebSocketSession getSession();
WebSocketSession getSession();
public BatchMode getBatchMode();
BatchMode getBatchMode();
public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException;
void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException;
public void onBinaryMessage(byte[] data);
void onBinaryMessage(byte[] data);
public void onClose(CloseInfo close);
void onClose(CloseInfo close);
public void onConnect();
void onConnect();
public void onContinuationFrame(ByteBuffer buffer, boolean fin) throws IOException;
void onContinuationFrame(ByteBuffer buffer, boolean fin) throws IOException;
public void onError(Throwable t);
void onError(Throwable t);
public void onFrame(Frame frame);
void onFrame(Frame frame);
public void onInputStream(InputStream stream) throws IOException;
void onInputStream(InputStream stream) throws IOException;
public void onPing(ByteBuffer buffer);
void onPing(ByteBuffer buffer);
public void onPong(ByteBuffer buffer);
void onPong(ByteBuffer buffer);
public void onReader(Reader reader) throws IOException;
void onReader(Reader reader) throws IOException;
public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException;
void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException;
public void onTextMessage(String message);
void onTextMessage(String message);
public void openSession(WebSocketSession session);
void openSession(WebSocketSession session);
}

View File

@ -23,6 +23,8 @@ import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@ -39,6 +41,7 @@ import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
*/
public class JettyAnnotatedEventDriver extends AbstractEventDriver
{
private static final Logger LOG = Log.getLogger(JettyAnnotatedEventDriver.class);
private final JettyAnnotatedMetadata events;
private boolean hasCloseBeenCalled = false;
private BatchMode batchMode;
@ -156,6 +159,10 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
{
events.onError.call(websocket,session,cause);
}
else
{
LOG.warn("Unable to report throwable to websocket (no @OnWebSocketError handler declared): " + websocket.getClass().getName(), cause);
}
}
@Override

View File

@ -18,13 +18,10 @@
package org.eclipse.jetty.websocket.common.extensions;
import java.io.IOException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
@ -105,12 +102,6 @@ public abstract class AbstractExtension extends AbstractLifeCycle implements Ext
return policy;
}
@Override
public void incomingError(Throwable e)
{
nextIncomingError(e);
}
/**
* Used to indicate that the extension makes use of the RSV1 bit of the base websocket framing.
* <p>
@ -150,11 +141,6 @@ public abstract class AbstractExtension extends AbstractLifeCycle implements Ext
return false;
}
protected void nextIncomingError(Throwable e)
{
this.nextIncoming.incomingError(e);
}
protected void nextIncomingFrame(Frame frame)
{
log.debug("nextIncomingFrame({})",frame);

View File

@ -18,7 +18,6 @@
package org.eclipse.jetty.websocket.common.extensions;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
@ -29,7 +28,6 @@ import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -197,12 +195,6 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames
return (this.extensions != null) && (this.extensions.size() > 0);
}
@Override
public void incomingError(Throwable e)
{
nextIncoming.incomingError(e);
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -42,13 +42,6 @@ public class IdentityExtension extends AbstractExtension
return "identity";
}
@Override
public void incomingError(Throwable e)
{
// pass through
nextIncomingError(e);
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -33,6 +33,8 @@ import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -46,39 +48,54 @@ import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.ConnectionState;
import org.eclipse.jetty.websocket.common.Generator;
import org.eclipse.jetty.websocket.common.LogicalConnection;
import org.eclipse.jetty.websocket.common.Parser;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
import org.eclipse.jetty.websocket.common.frames.CloseFrame;
import static org.eclipse.jetty.websocket.api.WebSocketBehavior.SERVER;
/**
* Provides the implementation of {@link LogicalConnection} within the framework of the new {@link org.eclipse.jetty.io.Connection} framework of {@code jetty-io}.
*/
public abstract class AbstractWebSocketConnection extends AbstractConnection implements LogicalConnection, Connection.UpgradeTo, ConnectionStateListener, Dumpable
public abstract class AbstractWebSocketConnection extends AbstractConnection implements LogicalConnection, Connection.UpgradeTo, Dumpable
{
private static class CallbackBridge implements WriteCallback
{
private final Callback callback;
public CallbackBridge(Callback callback)
{
this.callback = callback != null ? callback : Callback.NOOP;
}
@Override
public void writeFailed(Throwable x)
{
callback.failed(x);
}
@Override
public void writeSuccess()
{
callback.succeeded();
}
}
private class Flusher extends FrameFlusher
{
private Flusher(ByteBufferPool bufferPool, Generator generator, EndPoint endpoint)
{
super(bufferPool,generator,endpoint,getPolicy().getMaxBinaryMessageBufferSize(),8);
super(bufferPool, generator, endpoint, getPolicy().getMaxBinaryMessageBufferSize(), 8);
}
@Override
public void onCompleteFailure(Throwable failure)
{
super.onCompleteFailure(failure);
notifyError(failure);
if (ioState.wasAbnormalClose())
{
LOG.ignore(failure);
return;
}
if (LOG.isDebugEnabled())
LOG.debug("Write flush failure", failure);
ioState.onWriteFailure(failure);
disconnect();
AbstractWebSocketConnection.this.close(failure);
}
}
@ -124,31 +141,28 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
private final Generator generator;
private final Parser parser;
private final WebSocketPolicy policy;
private final ReadState readState;
private final ReadState readState = new ReadState();
private final ConnectionState connectionState = new ConnectionState();
private final FrameFlusher flusher;
private final String id;
private WebSocketSession session;
private List<ExtensionConfig> extensions;
private List<ExtensionConfig> extensions = new ArrayList<>();
private ByteBuffer prefillBuffer;
private ReadMode readMode = ReadMode.PARSE;
private IOState ioState;
private Stats stats = new Stats();
private CloseInfo fatalCloseInfo;
public AbstractWebSocketConnection(EndPoint endp, Executor executor, Scheduler scheduler, WebSocketPolicy policy, ByteBufferPool bufferPool)
{
super(endp,executor);
super(endp, executor);
this.id = Long.toString(ID_GEN.incrementAndGet());
this.policy = policy;
this.bufferPool = bufferPool;
this.generator = new Generator(policy,bufferPool);
this.parser = new Parser(policy,bufferPool);
this.generator = new Generator(policy, bufferPool);
this.parser = new Parser(policy, bufferPool);
this.scheduler = scheduler;
this.extensions = new ArrayList<>();
this.readState = new ReadState();
this.ioState = new IOState();
this.ioState.addListener(this);
this.flusher = new Flusher(bufferPool,generator,endp);
this.flusher = new Flusher(bufferPool, generator, endp);
this.setInputBufferSize(policy.getInputBufferSize());
this.setMaxIdleTimeout(policy.getIdleTimeout());
}
@ -159,21 +173,111 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
return super.getExecutor();
}
@Override
public void onLocalClose(CloseInfo close)
public void close(CloseInfo close, final Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("Local Close Confirmed {}",close);
if (connectionState.closing())
{
boolean transmit = close.getStatusCode() == StatusCode.NO_CODE || StatusCode.isTransmittable(close.getStatusCode());
if (transmit)
{
CloseFrame frame = close.asFrame();
outgoingFrame(frame, new CallbackBridge(callback), BatchMode.OFF);
if (close.isAbnormal())
if (StatusCode.isFatal(close.getStatusCode()))
{
ioState.onAbnormalClose(close);
fatalCloseInfo = close;
}
}
else
{
ioState.onCloseLocal(close);
disconnect();
}
}
else
{
if (callback != null)
{
callback.failed(new IllegalStateException("Local Close already called"));
}
}
}
/**
* Close the connection based on the throwable
*
* @param cause the cause
*/
public void close(Throwable cause)
{
session.callApplicationOnError(cause);
int statusCode = policy.getBehavior() == SERVER ? StatusCode.SERVER_ERROR : StatusCode.ABNORMAL;
if (cause instanceof CloseException)
{
statusCode = ((CloseException) cause).getStatusCode();
}
String reason = cause.getMessage();
if (StringUtil.isBlank(reason))
{
// an exception without a message.
reason = cause.getClass().getSimpleName();
}
CloseInfo closeInfo = new CloseInfo(statusCode, reason);
session.callApplicationOnClose(closeInfo);
close(closeInfo, new DisconnectCallback(this));
}
@Override
public boolean canWriteWebSocketFrames()
{
return connectionState.canWriteWebSocketFrames();
}
@Override
public boolean canReadWebSocketFrames()
{
return connectionState.canReadWebSocketFrames();
}
@Override
public String toStateString()
{
return connectionState.toString();
}
@Override
public boolean opening()
{
return connectionState.opening();
}
@Override
public boolean opened()
{
if (connectionState.opened())
{
if (BufferUtil.hasContent(prefillBuffer))
{
if (LOG.isDebugEnabled())
{
LOG.debug("Parsing Upgrade prefill buffer ({} remaining)", prefillBuffer.remaining());
}
parser.parse(prefillBuffer);
}
fillInterested();
return true;
}
return false;
}
@Override
public void remoteClose(CloseInfo close)
{
session.callApplicationOnClose(close);
close(close, new DisconnectCallback(this));
}
@Override
public void setSession(WebSocketSession session)
@ -194,21 +298,36 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
@Override
public void close()
{
session.close();
close(new CloseInfo(), Callback.NOOP);
}
@Override
public void disconnect()
{
if (connectionState.disconnected())
{
/* Use prior Fatal Close Info if present, otherwise
* because if could be from a failed close handshake where
* the local initiated, but the remote never responded.
*/
CloseInfo closeInfo = fatalCloseInfo;
if(closeInfo == null)
{
closeInfo = new CloseInfo(StatusCode.ABNORMAL, "Disconnected");
}
session.callApplicationOnClose(closeInfo);
if (LOG.isDebugEnabled())
LOG.debug("{} disconnect()",policy.getBehavior());
flusher.terminate(new EOFException("Disconnected"), false);
{
LOG.debug("{} disconnect()", policy.getBehavior());
}
flusher.terminate(new EOFException("Disconnected"));
EndPoint endPoint = getEndPoint();
// We need to gently close first, to allow
// SSL close alerts to be sent by Jetty
endPoint.shutdownOutput();
endPoint.close();
}
}
@Override
public void fillInterested()
@ -252,12 +371,6 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
return getEndPoint().getIdleTimeout();
}
@Override
public IOState getIOState()
{
return ioState;
}
@Override
public long getMaxIdleTimeout()
{
@ -303,86 +416,20 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
return readState.isReading();
}
/**
* Physical connection disconnect.
* <p>
* Not related to WebSocket close handshake.
*/
@Override
public void onClose()
{
if (LOG.isDebugEnabled())
LOG.debug("{} onClose()",policy.getBehavior());
super.onClose();
ioState.onDisconnected();
}
@Override
public void onConnectionStateChange(ConnectionState state)
{
if (LOG.isDebugEnabled())
LOG.debug("{} Connection State Change: {}",policy.getBehavior(),state);
switch (state)
{
case OPEN:
if (BufferUtil.hasContent(prefillBuffer))
{
if (LOG.isDebugEnabled())
{
LOG.debug("Parsing Upgrade prefill buffer ({} remaining)",prefillBuffer.remaining());
}
parser.parse(prefillBuffer);
}
if (LOG.isDebugEnabled())
{
LOG.debug("OPEN: normal fillInterested");
}
// TODO: investigate what happens if a failure occurs during prefill, and an attempt to write close fails,
// should a fill interested occur? or just a quick disconnect?
fillInterested();
break;
case CLOSED:
if (LOG.isDebugEnabled())
LOG.debug("CLOSED - wasAbnormalClose: {}", ioState.wasAbnormalClose());
if (ioState.wasAbnormalClose())
{
// Fire out a close frame, indicating abnormal shutdown, then disconnect
session.close(StatusCode.SHUTDOWN,"Abnormal Close - " + ioState.getCloseInfo().getReason());
}
else
{
// Just disconnect
this.disconnect();
}
break;
case CLOSING:
if (LOG.isDebugEnabled())
LOG.debug("CLOSING - wasRemoteCloseInitiated: {}", ioState.wasRemoteCloseInitiated());
// First occurrence of .onCloseLocal or .onCloseRemote use
if (ioState.wasRemoteCloseInitiated())
{
CloseInfo close = ioState.getCloseInfo();
session.close(close.getStatusCode(), close.getReason());
}
default:
break;
}
}
@Override
public void onFillable()
{
if (LOG.isDebugEnabled())
LOG.debug("{} onFillable()",policy.getBehavior());
{
LOG.debug("{} onFillable()", policy.getBehavior());
}
stats.countOnFillableEvents.incrementAndGet();
ByteBuffer buffer = bufferPool.acquire(getInputBufferSize(),true);
ByteBuffer buffer = bufferPool.acquire(getInputBufferSize(), true);
try
{
if(readMode == ReadMode.PARSE)
if (readMode == ReadMode.PARSE)
{
readMode = readParse(buffer);
}
@ -397,10 +444,14 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
}
if (readMode == ReadMode.EOF)
{
readState.eof();
}
else if (!readState.suspend())
{
fillInterested();
}
}
@Override
protected void onFillInterestedFailed(Throwable cause)
@ -414,63 +465,28 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
* Extra bytes from the initial HTTP upgrade that need to
* be processed by the websocket parser before starting
* to read bytes from the connection
*
* @param prefilled the bytes of prefilled content encountered during upgrade
*/
protected void setInitialBuffer(ByteBuffer prefilled)
{
if (LOG.isDebugEnabled())
{
LOG.debug("set Initial Buffer - {}",BufferUtil.toDetailString(prefilled));
LOG.debug("set Initial Buffer - {}", BufferUtil.toDetailString(prefilled));
}
prefillBuffer = prefilled;
}
private void notifyError(Throwable t)
{
getParser().getIncomingFramesHandler().incomingError(t);
}
@Override
public void onOpen()
{
if(LOG.isDebugEnabled())
LOG.debug("[{}] {}.onOpened()",policy.getBehavior(),this.getClass().getSimpleName());
super.onOpen();
this.ioState.onOpened();
}
/**
* Event for no activity on connection (read or write)
*
* @return true to signal that the endpoint must be closed, false to keep the endpoint open
*/
@Override
protected boolean onReadTimeout(Throwable timeout)
{
IOState state = getIOState();
ConnectionState cstate = state.getConnectionState();
if (LOG.isDebugEnabled())
LOG.debug("{} Read Timeout - {}",policy.getBehavior(),cstate);
if (cstate == ConnectionState.CLOSED)
{
if (LOG.isDebugEnabled())
LOG.debug("onReadTimeout - Connection Already CLOSED");
// close already completed, extra timeouts not relevant
// allow underlying connection and endpoint to disconnect on its own
return true;
}
try
{
notifyError(timeout);
}
finally
{
// This is an Abnormal Close condition
session.close(StatusCode.SHUTDOWN,"Idle Timeout");
}
return false;
close(new CloseException(StatusCode.SHUTDOWN, timeout));
return false; // let websocket perform close handshake
}
/**
@ -481,10 +497,13 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
{
if (LOG.isDebugEnabled())
{
LOG.debug("outgoingFrame({}, {})",frame,callback);
LOG.debug("outgoingFrame({}, {})", frame, callback);
}
flusher.enqueue(frame,callback,batchMode);
if (flusher.enqueue(frame, callback, batchMode))
{
flusher.iterate();
}
}
private ReadMode readDiscard(ByteBuffer buffer)
@ -502,13 +521,17 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
else if (filled < 0)
{
if (LOG.isDebugEnabled())
LOG.debug("read - EOF Reached (remote: {})",getRemoteAddress());
{
LOG.debug("read - EOF Reached (remote: {})", getRemoteAddress());
}
return ReadMode.EOF;
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Discarded {} bytes - {}",filled,BufferUtil.toDetailString(buffer));
{
LOG.debug("Discarded {} bytes - {}", filled, BufferUtil.toDetailString(buffer));
}
}
}
}
@ -530,13 +553,15 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
try
{
// Process the content from the Endpoint next
while(true) // TODO: should this honor the LogicalConnection.suspend() ?
while (true) // TODO: should this honor the LogicalConnection.suspend() ?
{
int filled = endPoint.fill(buffer);
if (filled < 0)
{
LOG.debug("read - EOF Reached (remote: {})",getRemoteAddress());
ioState.onReadFailure(new EOFException("Remote Read EOF"));
if (LOG.isDebugEnabled())
{
LOG.debug("read - EOF Reached (remote: {})", getRemoteAddress());
}
return ReadMode.EOF;
}
else if (filled == 0)
@ -547,30 +572,14 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
if (LOG.isDebugEnabled())
{
LOG.debug("Filled {} bytes - {}",filled,BufferUtil.toDetailString(buffer));
LOG.debug("Filled {} bytes - {}", filled, BufferUtil.toDetailString(buffer));
}
parser.parse(buffer);
}
}
catch (IOException e)
{
LOG.warn(e);
session.notifyError(e);
session.abort(StatusCode.PROTOCOL,e.getMessage());
return ReadMode.DISCARD;
}
catch (CloseException e)
{
LOG.debug(e);
session.notifyError(e);
session.close(e.getStatusCode(),e.getMessage());
return ReadMode.DISCARD;
}
catch (Throwable t)
{
LOG.warn(t);
session.abort(StatusCode.ABNORMAL,t.getMessage());
// TODO: should probably only switch to discard if a non-ws-endpoint error
close(t);
return ReadMode.DISCARD;
}
}
@ -579,16 +588,17 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
public void resume()
{
if (readState.resume())
{
fillInterested();
}
}
/**
* Get the list of extensions in use.
* <p>
* This list is negotiated during the WebSocket Upgrade Request/Response handshake.
*
* @param extensions
* the list of negotiated extensions in use.
* @param extensions the list of negotiated extensions in use.
*/
public void setExtensions(List<ExtensionConfig> extensions)
{
@ -619,25 +629,30 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
}
@Override
public String dumpSelf() {
public String dumpSelf()
{
return String.format("%s@%x", this.getClass().getSimpleName(), hashCode());
}
public void dump(Appendable out, String indent) throws IOException {
public void dump(Appendable out, String indent) throws IOException
{
EndPoint endp = getEndPoint();
Object endpRef = endp.toString();
if(endp instanceof AbstractEndPoint)
if (endp instanceof AbstractEndPoint)
{
endpRef = ((AbstractEndPoint) endp).toEndPointString();
Dumpable.dumpObjects(out, indent, this, endpRef, ioState, flusher, generator, parser);
}
Dumpable.dumpObjects(out, indent, this, endpRef, flusher, generator, parser);
}
@Override
public String toConnectionString()
{
return String.format("%s@%x[ios=%s,f=%s,g=%s,p=%s]",
return String.format("%s@%x[s=%s,f=%s,g=%s,p=%s]",
getClass().getSimpleName(),
hashCode(),
ioState,flusher,generator,parser);
connectionState,
flusher, generator, parser);
}
/**
@ -648,7 +663,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
@Override
public void onUpgradeTo(ByteBuffer prefilled)
{
if(LOG.isDebugEnabled())
if (LOG.isDebugEnabled())
{
LOG.debug("onUpgradeTo({})", BufferUtil.toDetailString(prefilled));
}

View File

@ -0,0 +1,224 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import java.util.concurrent.atomic.AtomicReference;
/**
* WebSocket Connection State.
* <p>
* {@code State} can only go in one direction from {@code HANDSHAKING} to {@code DISCONNECTED}.
* </p>
*/
public class ConnectionState
{
private final AtomicReference<State> state = new AtomicReference<>(State.HANDSHAKING);
/**
* Test to see if state allows writing of WebSocket frames
*
* @return true if state allows for writing of websocket frames
*/
public boolean canWriteWebSocketFrames()
{
State current = state.get();
return current == State.OPENING || current == State.OPENED;
}
/**
* Tests to see if state allows for reading of WebSocket frames
*
* @return true if state allows for reading of websocket frames
*/
public boolean canReadWebSocketFrames()
{
State current = state.get();
return current == State.OPENED || current == State.CLOSING;
}
/**
* Requests that the connection migrate to OPENING state
*
* @return true if OPENING state attained
*/
public boolean opening()
{
while (true)
{
State current = state.get();
switch (current)
{
case HANDSHAKING:
if (state.compareAndSet(current, State.OPENING))
{
return true;
}
break;
case DISCONNECTED:
return false;
default:
throw new IllegalStateException(toString(current));
}
}
}
/**
* Requests that the connection migrate to OPENED state
*
* @return true if OPENED state attained
*/
public boolean opened()
{
while (true)
{
State current = state.get();
switch (current)
{
case OPENING:
if (state.compareAndSet(current, State.OPENED))
{
return true;
}
break;
case CLOSING: // connection went from OPENING to CLOSING (likely to to failure to onOpen)
return false;
case DISCONNECTED:
return false;
default:
throw new IllegalStateException(toString(current));
}
}
}
/**
* The Local Endpoint wants to close.
*
* @return true if this is the first local close
*/
public boolean closing()
{
while (true)
{
State current = state.get();
switch (current)
{
case OPENING:
if (state.compareAndSet(current, State.CLOSING))
{
return true;
}
break;
case OPENED:
if (state.compareAndSet(current, State.CLOSING))
{
return true;
}
break;
case CLOSING: // already closing
return false;
case DISCONNECTED:
return false;
default:
throw new IllegalStateException(toString(current));
}
}
}
/**
* Final Terminal state indicating the connection is disconnected
*
* @return true if disconnected reached for the first time
*/
public boolean disconnected()
{
while (true)
{
State current = state.get();
switch (current)
{
case DISCONNECTED:
return false;
default:
if (state.compareAndSet(current, State.DISCONNECTED))
{
return true;
}
break;
}
}
}
private String toString(State state)
{
return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), state);
}
@Override
public String toString()
{
return toString(state.get());
}
private enum State
{
/**
* Initial state of a connection, the upgrade request / response handshake is in progress
*/
HANDSHAKING,
/**
* Intermediate state between HANDSHAKING and OPENED, used to indicate that a upgrade
* request/response handshake is successful, but the Application provided socket's
* onOpen code has yet completed.
* <p>
* This state is to allow the local socket endpoint to initiate the sending of messages and
* frames, but to NOT start reading yet.
*/
OPENING,
/**
* The websocket connection is established and open.
* <p>
* This indicates that the Upgrade has succeed, and the Application provided
* socket's onOpen code has returned.
* <p>
* It is now time to start reading from the remote endpoint.
*/
OPENED,
/**
* The WebSocket is closing
* <p>
* There are several conditions that would start this state.
* <p>
* - A CLOSE Frame has been received (and parsed) from the remote endpoint
* - A CLOSE Frame has been enqueued by the local endpoint
*/
CLOSING,
/**
* The WebSocket connection is disconnected.
* <p>
* Connection is complete and no longer valid.
*/
DISCONNECTED
}
}

View File

@ -0,0 +1,44 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.common.LogicalConnection;
public class DisconnectCallback implements Callback
{
private final LogicalConnection connection;
public DisconnectCallback(LogicalConnection connection)
{
this.connection = connection;
}
@Override
public void failed(Throwable x)
{
connection.disconnect();
}
@Override
public void succeeded()
{
connection.disconnect();
}
}

View File

@ -53,6 +53,7 @@ public class FrameFlusher extends IteratingCallback
private final List<FrameEntry> entries;
private final List<ByteBuffer> buffers;
private boolean closed;
private boolean canEnqueue = true;
private Throwable terminated;
private ByteBuffer aggregate;
private BatchMode batchMode;
@ -68,28 +69,52 @@ public class FrameFlusher extends IteratingCallback
this.buffers = new ArrayList<>((maxGather * 2) + 1);
}
public void enqueue(Frame frame, WriteCallback callback, BatchMode batchMode)
public boolean enqueue(Frame frame, WriteCallback callback, BatchMode batchMode)
{
FrameEntry entry = new FrameEntry(frame, callback, batchMode);
Throwable closed;
Throwable dead;
synchronized (this)
{
closed = terminated;
if (closed == null)
if (canEnqueue)
{
dead = terminated;
if (dead == null)
{
byte opCode = frame.getOpCode();
if (opCode == OpCode.PING || opCode == OpCode.PONG)
{
queue.offerFirst(entry);
}
else
{
queue.offerLast(entry);
}
if (opCode == OpCode.CLOSE)
{
this.canEnqueue = false;
}
}
}
else
{
dead = new ClosedChannelException();
}
}
if (closed == null)
iterate();
else
notifyCallbackFailure(callback, closed);
if (dead == null)
{
if (LOG.isDebugEnabled())
{
LOG.debug("Enqueued {} to {}", entry, this);
}
return true;
}
notifyCallbackFailure(callback, dead);
return false;
}
@Override
@ -103,12 +128,16 @@ public class FrameFlusher extends IteratingCallback
synchronized (this)
{
if (closed)
{
return Action.SUCCEEDED;
}
if (terminated != null)
{
throw terminated;
}
while (!queue.isEmpty() && entries.size() <= maxGather)
while (!queue.isEmpty() && entries.size() < maxGather)
{
FrameEntry entry = queue.poll();
currentBatchMode = BatchMode.max(currentBatchMode, entry.batchMode);
@ -243,7 +272,12 @@ public class FrameFlusher extends IteratingCallback
entry.release();
if (entry.frame.getOpCode() == OpCode.CLOSE)
{
terminate(new ClosedChannelException(), true);
synchronized (this)
{
// we know that enqueue protects us.
// and the processing will not contain extra frame entries.
closed = true;
}
endPoint.shutdownOutput();
}
}
@ -255,12 +289,13 @@ public class FrameFlusher extends IteratingCallback
{
releaseAggregate();
Throwable closed;
synchronized (this)
{
closed = terminated;
if (closed == null)
if (terminated == null) {
terminated = failure;
if (LOG.isDebugEnabled())
LOG.debug("Write flush failure", failure);
}
entries.addAll(queue);
queue.clear();
}
@ -282,19 +317,18 @@ public class FrameFlusher extends IteratingCallback
}
}
void terminate(Throwable cause, boolean close)
void terminate(Throwable cause)
{
Throwable reason;
synchronized (this)
{
closed = close;
reason = terminated;
if (reason == null)
terminated = cause;
}
if (LOG.isDebugEnabled())
LOG.debug("{} {}", reason == null ? "Terminating" : "Terminated", this);
if (reason == null && !close)
if (reason == null)
iterate();
}

View File

@ -35,12 +35,6 @@ public class FramePipes
this.outgoing = outgoing;
}
@Override
public void incomingError(Throwable t)
{
/* cannot send exception on */
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -1,631 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import java.io.EOFException;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.ConnectionState;
/**
* Simple state tracker for Input / Output and {@link ConnectionState}.
* <p>
* Use the various known .on*() methods to trigger a state change.
* <ul>
* <li>{@link #onOpened()} - connection has been opened</li>
* </ul>
*/
public class IOState
{
/**
* The source of a close handshake. (ie: who initiated it).
*/
private enum CloseHandshakeSource
{
/** No close handshake initiated (yet) */
NONE,
/** Local side initiated the close handshake */
LOCAL,
/** Remote side initiated the close handshake */
REMOTE,
/** An abnormal close situation (disconnect, timeout, etc...) */
ABNORMAL
}
public static interface ConnectionStateListener
{
public void onConnectionStateChange(ConnectionState state);
}
private static final Logger LOG = Log.getLogger(IOState.class);
private ConnectionState state;
private final List<ConnectionStateListener> listeners = new CopyOnWriteArrayList<>();
/**
* Is input on websocket available (for reading frames).
* Used to determine close handshake completion, and track half-close states
*/
private boolean inputAvailable;
/**
* Is output on websocket available (for writing frames).
* Used to determine close handshake completion, and track half-closed states.
*/
private boolean outputAvailable;
/**
* Initiator of the close handshake.
* Used to determine who initiated a close handshake for reply reasons.
*/
private CloseHandshakeSource closeHandshakeSource;
/**
* The close info for the initiator of the close handshake.
* It is possible in abnormal close scenarios to have a different
* final close info that is used to notify the WS-Endpoint's onClose()
* events with.
*/
private CloseInfo closeInfo;
/**
* Atomic reference to the final close info.
* This can only be set once, and is used for the WS-Endpoint's onClose()
* event.
*/
private AtomicReference<CloseInfo> finalClose = new AtomicReference<>();
/**
* Tracker for if the close handshake was completed successfully by
* both sides. False if close was sudden or abnormal.
*/
private boolean cleanClose;
/**
* Create a new IOState, initialized to {@link ConnectionState#CONNECTING}
*/
public IOState()
{
this.state = ConnectionState.CONNECTING;
this.inputAvailable = false;
this.outputAvailable = false;
this.closeHandshakeSource = CloseHandshakeSource.NONE;
this.closeInfo = null;
this.cleanClose = false;
}
public void addListener(ConnectionStateListener listener)
{
listeners.add(listener);
}
public void assertInputOpen() throws IOException
{
if (!isInputAvailable())
{
throw new IOException("Connection input is closed");
}
}
public void assertOutputOpen() throws IOException
{
if (!isOutputAvailable())
{
throw new IOException("Connection output is closed");
}
}
public CloseInfo getCloseInfo()
{
CloseInfo ci = finalClose.get();
if (ci != null)
{
return ci;
}
return closeInfo;
}
public ConnectionState getConnectionState()
{
return state;
}
public boolean isClosed()
{
synchronized (this)
{
return (state == ConnectionState.CLOSED);
}
}
public boolean isInputAvailable()
{
return inputAvailable;
}
public boolean isOpen()
{
return !isClosed();
}
public boolean isOutputAvailable()
{
return outputAvailable;
}
private void notifyStateListeners(ConnectionState state)
{
if (LOG.isDebugEnabled())
LOG.debug("Notify State Listeners: {}",state);
for (ConnectionStateListener listener : listeners)
{
if (LOG.isDebugEnabled())
{
LOG.debug("{}.onConnectionStateChange({})",listener.getClass().getSimpleName(),state.name());
}
listener.onConnectionStateChange(state);
}
}
/**
* A websocket connection has been disconnected for abnormal close reasons.
* <p>
* This is the low level disconnect of the socket. It could be the result of a normal close operation, from an IO error, or even from a timeout.
* @param close the close information
*/
public void onAbnormalClose(CloseInfo close)
{
if (LOG.isDebugEnabled())
LOG.debug("onAbnormalClose({})",close);
ConnectionState event = null;
synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
// already closed
return;
}
if (this.state == ConnectionState.OPEN)
{
this.cleanClose = false;
}
this.state = ConnectionState.CLOSED;
finalClose.compareAndSet(null,close);
this.inputAvailable = false;
this.outputAvailable = false;
this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
}
/**
* A close handshake has been issued from the local endpoint
* @param closeInfo the close information
*/
public void onCloseLocal(CloseInfo closeInfo)
{
boolean open = false;
synchronized (this)
{
ConnectionState initialState = this.state;
if (LOG.isDebugEnabled())
LOG.debug("onCloseLocal({}) : {}", closeInfo, initialState);
if (initialState == ConnectionState.CLOSED)
{
// already closed
if (LOG.isDebugEnabled())
LOG.debug("already closed");
return;
}
if (initialState == ConnectionState.CONNECTED)
{
// fast close. a local close request from end-user onConnect/onOpen method
if (LOG.isDebugEnabled())
LOG.debug("FastClose in CONNECTED detected");
open = true;
}
}
if (open)
openAndCloseLocal(closeInfo);
else
closeLocal(closeInfo);
}
private void openAndCloseLocal(CloseInfo closeInfo)
{
// Force the state open (to allow read/write to endpoint)
onOpened();
if (LOG.isDebugEnabled())
LOG.debug("FastClose continuing with Closure");
closeLocal(closeInfo);
}
private void closeLocal(CloseInfo closeInfo)
{
ConnectionState event = null;
ConnectionState abnormalEvent = null;
synchronized (this)
{
if (LOG.isDebugEnabled())
LOG.debug("onCloseLocal(), input={}, output={}", inputAvailable, outputAvailable);
this.closeInfo = closeInfo;
// Turn off further output.
outputAvailable = false;
if (closeHandshakeSource == CloseHandshakeSource.NONE)
{
closeHandshakeSource = CloseHandshakeSource.LOCAL;
}
if (!inputAvailable)
{
if (LOG.isDebugEnabled())
LOG.debug("Close Handshake satisfied, disconnecting");
cleanClose = true;
this.state = ConnectionState.CLOSED;
finalClose.compareAndSet(null,closeInfo);
event = this.state;
}
else if (this.state == ConnectionState.OPEN)
{
// We are now entering CLOSING (or half-closed).
this.state = ConnectionState.CLOSING;
event = this.state;
// If abnormal, we don't expect an answer.
if (closeInfo.isAbnormal())
{
abnormalEvent = ConnectionState.CLOSED;
finalClose.compareAndSet(null,closeInfo);
cleanClose = false;
outputAvailable = false;
inputAvailable = false;
closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
}
}
}
// Only notify on state change events
if (event != null)
{
notifyStateListeners(event);
if (abnormalEvent != null)
{
notifyStateListeners(abnormalEvent);
}
}
}
/**
* A close handshake has been received from the remote endpoint
* @param closeInfo the close information
*/
public void onCloseRemote(CloseInfo closeInfo)
{
if (LOG.isDebugEnabled())
LOG.debug("onCloseRemote({})", closeInfo);
ConnectionState event = null;
synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
// already closed
return;
}
if (LOG.isDebugEnabled())
LOG.debug("onCloseRemote(), input={}, output={}", inputAvailable, outputAvailable);
this.closeInfo = closeInfo;
// turn off further input
inputAvailable = false;
if (closeHandshakeSource == CloseHandshakeSource.NONE)
{
closeHandshakeSource = CloseHandshakeSource.REMOTE;
}
if (!outputAvailable)
{
LOG.debug("Close Handshake satisfied, disconnecting");
cleanClose = true;
state = ConnectionState.CLOSED;
finalClose.compareAndSet(null,closeInfo);
event = this.state;
}
else if (this.state == ConnectionState.OPEN)
{
// We are now entering CLOSING (or half-closed)
this.state = ConnectionState.CLOSING;
event = this.state;
}
}
// Only notify on state change events
if (event != null)
{
notifyStateListeners(event);
}
}
/**
* WebSocket has successfully upgraded, but the end-user onOpen call hasn't run yet.
* <p>
* This is an intermediate state between the RFC's {@link ConnectionState#CONNECTING} and {@link ConnectionState#OPEN}
*/
public void onConnected()
{
ConnectionState event = null;
synchronized (this)
{
if (this.state != ConnectionState.CONNECTING)
{
LOG.debug("Unable to set to connected, not in CONNECTING state: {}",this.state);
return;
}
this.state = ConnectionState.CONNECTED;
inputAvailable = false; // cannot read (yet)
outputAvailable = true; // write allowed
event = this.state;
}
notifyStateListeners(event);
}
/**
* A websocket connection has failed its upgrade handshake, and is now closed.
*/
public void onFailedUpgrade()
{
assert (this.state == ConnectionState.CONNECTING);
ConnectionState event = null;
synchronized (this)
{
this.state = ConnectionState.CLOSED;
cleanClose = false;
inputAvailable = false;
outputAvailable = false;
event = this.state;
}
notifyStateListeners(event);
}
/**
* A websocket connection has finished its upgrade handshake, and is now open.
*/
public void onOpened()
{
if(LOG.isDebugEnabled())
LOG.debug("onOpened()");
ConnectionState event = null;
synchronized (this)
{
if (this.state == ConnectionState.OPEN)
{
// already opened
return;
}
if (this.state != ConnectionState.CONNECTED)
{
LOG.debug("Unable to open, not in CONNECTED state: {}",this.state);
return;
}
this.state = ConnectionState.OPEN;
this.inputAvailable = true;
this.outputAvailable = true;
event = this.state;
}
notifyStateListeners(event);
}
/**
* The local endpoint has reached a read failure.
* <p>
* This could be a normal result after a proper close handshake, or even a premature close due to a connection disconnect.
* @param t the read failure
*/
public void onReadFailure(Throwable t)
{
ConnectionState event = null;
synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
// already closed
return;
}
// Build out Close Reason
String reason = "WebSocket Read Failure";
if (t instanceof EOFException)
{
reason = "WebSocket Read EOF";
Throwable cause = t.getCause();
if ((cause != null) && (StringUtil.isNotBlank(cause.getMessage())))
{
reason = "EOF: " + cause.getMessage();
}
}
else
{
if (StringUtil.isNotBlank(t.getMessage()))
{
reason = t.getMessage();
}
}
CloseInfo close = new CloseInfo(StatusCode.ABNORMAL,reason);
finalClose.compareAndSet(null,close);
this.cleanClose = false;
this.state = ConnectionState.CLOSED;
this.closeInfo = close;
this.inputAvailable = false;
this.outputAvailable = false;
this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
}
/**
* The local endpoint has reached a write failure.
* <p>
* A low level I/O failure, or even a jetty side EndPoint close (from idle timeout) are a few scenarios
* @param t the throwable that caused the write failure
*/
public void onWriteFailure(Throwable t)
{
ConnectionState event = null;
synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
// already closed
return;
}
// Build out Close Reason
String reason = "WebSocket Write Failure";
if (t instanceof EOFException)
{
reason = "WebSocket Write EOF";
Throwable cause = t.getCause();
if ((cause != null) && (StringUtil.isNotBlank(cause.getMessage())))
{
reason = "EOF: " + cause.getMessage();
}
}
else
{
if (StringUtil.isNotBlank(t.getMessage()))
{
reason = t.getMessage();
}
}
CloseInfo close = new CloseInfo(StatusCode.ABNORMAL,reason);
finalClose.compareAndSet(null,close);
this.cleanClose = false;
this.state = ConnectionState.CLOSED;
this.inputAvailable = false;
this.outputAvailable = false;
this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
}
public void onDisconnected()
{
ConnectionState event = null;
synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
// already closed
return;
}
CloseInfo close = new CloseInfo(StatusCode.ABNORMAL,"Disconnected");
this.cleanClose = false;
this.state = ConnectionState.CLOSED;
this.closeInfo = close;
this.inputAvailable = false;
this.outputAvailable = false;
this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
}
@Override
public String toString()
{
StringBuilder str = new StringBuilder();
str.append(this.getClass().getSimpleName());
str.append("@").append(Integer.toHexString(hashCode()));
str.append("[").append(state);
str.append(',');
if (!inputAvailable)
{
str.append('!');
}
str.append("in,");
if (!outputAvailable)
{
str.append('!');
}
str.append("out");
if ((state == ConnectionState.CLOSED) || (state == ConnectionState.CLOSING))
{
CloseInfo ci = finalClose.get();
if (ci != null)
{
str.append(",finalClose=").append(ci);
}
else
{
str.append(",close=").append(closeInfo);
}
str.append(",clean=").append(cleanClose);
str.append(",closeSource=").append(closeHandshakeSource);
}
str.append(']');
return str.toString();
}
public boolean wasAbnormalClose()
{
return closeHandshakeSource == CloseHandshakeSource.ABNORMAL;
}
public boolean wasCleanClose()
{
return cleanClose;
}
public boolean wasLocalCloseInitiated()
{
return closeHandshakeSource == CloseHandshakeSource.LOCAL;
}
public boolean wasRemoteCloseInitiated()
{
return closeHandshakeSource == CloseHandshakeSource.REMOTE;
}
}

View File

@ -18,13 +18,14 @@
package org.eclipse.jetty.websocket.common.scopes;
import java.util.Collection;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
public class DelegatedContainerScope implements WebSocketContainerScope
{
@ -74,14 +75,20 @@ public class DelegatedContainerScope implements WebSocketContainerScope
}
@Override
public void onSessionOpened(WebSocketSession session)
public void addSessionListener(WebSocketSessionListener listener)
{
this.delegate.onSessionOpened(session);
this.delegate.addSessionListener(listener);
}
@Override
public void onSessionClosed(WebSocketSession session)
public void removeSessionListener(WebSocketSessionListener listener)
{
this.delegate.onSessionClosed(session);
this.delegate.removeSessionListener(listener);
}
@Override
public Collection<WebSocketSessionListener> getSessionListeners()
{
return this.delegate.getSessionListeners();
}
}

View File

@ -18,6 +18,9 @@
package org.eclipse.jetty.websocket.common.scopes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
@ -25,11 +28,13 @@ import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.DeprecationWarning;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
public class SimpleContainerScope extends ContainerLifeCycle implements WebSocketContainerScope
{
@ -37,7 +42,9 @@ public class SimpleContainerScope extends ContainerLifeCycle implements WebSocke
private final DecoratedObjectFactory objectFactory;
private final WebSocketPolicy policy;
private final Executor executor;
private final Logger logger;
private SslContextFactory sslContextFactory;
private List<WebSocketSessionListener> sessionListeners = new ArrayList<>();
public SimpleContainerScope(WebSocketPolicy policy)
{
@ -62,6 +69,7 @@ public class SimpleContainerScope extends ContainerLifeCycle implements WebSocke
public SimpleContainerScope(WebSocketPolicy policy, ByteBufferPool bufferPool, Executor executor, SslContextFactory ssl, DecoratedObjectFactory objectFactory)
{
this.logger = Log.getLogger(this.getClass());
this.policy = policy;
this.bufferPool = bufferPool;
@ -143,14 +151,20 @@ public class SimpleContainerScope extends ContainerLifeCycle implements WebSocke
}
@Override
public void onSessionOpened(WebSocketSession session)
public void addSessionListener(WebSocketSessionListener listener)
{
/* do nothing */
this.sessionListeners.add(listener);
}
@Override
public void onSessionClosed(WebSocketSession session)
public void removeSessionListener(WebSocketSessionListener listener)
{
/* do nothing */
this.sessionListeners.remove(listener);
}
@Override
public Collection<WebSocketSessionListener> getSessionListeners()
{
return sessionListeners;
}
}

View File

@ -18,13 +18,14 @@
package org.eclipse.jetty.websocket.common.scopes;
import java.util.Collection;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
/**
* Defined Scope for a WebSocketContainer.
@ -59,6 +60,7 @@ public interface WebSocketContainerScope
*/
WebSocketPolicy getPolicy();
/**
* The SslContextFactory in use by the container.
*
@ -73,17 +75,9 @@ public interface WebSocketContainerScope
*/
boolean isRunning();
/**
* A Session has been opened
*
* @param session the session that was opened
*/
void onSessionOpened(WebSocketSession session);
void addSessionListener(WebSocketSessionListener listener);
/**
* A Session has been closed
*
* @param session the session that was closed
*/
void onSessionClosed(WebSocketSession session);
void removeSessionListener(WebSocketSessionListener listener);
Collection<WebSocketSessionListener> getSessionListeners();
}

View File

@ -18,9 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@ -30,9 +27,11 @@ import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.eclipse.jetty.websocket.common.util.MaskedByteBuffer;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class ClosePayloadParserTest
{
@Test
@ -59,7 +58,6 @@ public class ClosePayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.CLOSE,1);
CloseInfo close = new CloseInfo(capture.getFrames().poll());
assertThat("CloseFrame.statusCode",close.getStatusCode(),is(StatusCode.NORMAL));

View File

@ -18,10 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -31,9 +27,12 @@ import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class GeneratorParserRoundtripTest
{
public ByteBufferPool bufferPool = new MappedByteBufferPool();
@ -70,7 +69,6 @@ public class GeneratorParserRoundtripTest
}
// Validate
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
TextFrame txt = (TextFrame)capture.getFrames().poll();
@ -115,7 +113,6 @@ public class GeneratorParserRoundtripTest
}
// Validate
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
TextFrame txt = (TextFrame)capture.getFrames().poll();

View File

@ -18,11 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@ -42,9 +37,13 @@ import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitGenerator;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.eclipse.jetty.websocket.common.util.Hex;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ParserTest
{
/**
@ -111,7 +110,6 @@ public class ParserTest
parser.setIncomingFramesHandler(capture);
parser.parseQuietly(completeBuf);
capture.assertErrorCount(0);
capture.assertHasFrame(OpCode.TEXT,1);
capture.assertHasFrame(OpCode.CONTINUATION,4);
capture.assertHasFrame(OpCode.CLOSE,1);
@ -135,7 +133,6 @@ public class ParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(completeBuf);
capture.assertErrorCount(0);
capture.assertHasFrame(OpCode.TEXT,1);
capture.assertHasFrame(OpCode.CLOSE,1);
capture.assertHasFrame(OpCode.PONG,1);
@ -185,7 +182,6 @@ public class ParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(completeBuf);
capture.assertErrorCount(0);
capture.assertHasFrame(OpCode.TEXT,textCount);
capture.assertHasFrame(OpCode.CONTINUATION,continuationCount);
capture.assertHasFrame(OpCode.CLOSE,1);
@ -204,7 +200,6 @@ public class ParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
assertThat("Frame Count",capture.getFrames().size(),is(0));
}
@ -240,7 +235,6 @@ public class ParserTest
networkBytes.position(networkBytes.position() + windowSize);
}
capture.assertNoErrors();
assertThat("Frame Count",capture.getFrames().size(),is(2));
WebSocketFrame frame = capture.getFrames().poll();
assertThat("Frame[0].opcode",frame.getOpCode(),is(OpCode.TEXT));

View File

@ -18,9 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
@ -29,9 +26,11 @@ import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.frames.PingFrame;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PingPayloadParserTest
{
@Test
@ -49,7 +48,6 @@ public class PingPayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PING,1);
PingFrame ping = (PingFrame)capture.getFrames().poll();

View File

@ -18,9 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
@ -29,9 +26,11 @@ import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
/**
* Collection of Example packets as found in <a href="https://tools.ietf.org/html/rfc6455#section-5.7">RFC 6455 Examples section</a>
*/
@ -66,7 +65,6 @@ public class RFC6455ExamplesParserTest
BufferUtil.flipToFlush(buf,0);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
capture.assertHasFrame(OpCode.CONTINUATION,1);
@ -94,7 +92,6 @@ public class RFC6455ExamplesParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PONG,1);
WebSocketFrame pong = capture.getFrames().poll();
@ -118,7 +115,6 @@ public class RFC6455ExamplesParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
WebSocketFrame txt = capture.getFrames().poll();
@ -149,7 +145,6 @@ public class RFC6455ExamplesParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame bin = capture.getFrames().poll();
@ -188,7 +183,6 @@ public class RFC6455ExamplesParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame bin = capture.getFrames().poll();
@ -219,7 +213,6 @@ public class RFC6455ExamplesParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PING,1);
WebSocketFrame ping = capture.getFrames().poll();
@ -243,7 +236,6 @@ public class RFC6455ExamplesParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
WebSocketFrame txt = capture.getFrames().poll();

View File

@ -18,13 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@ -35,9 +28,15 @@ import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.eclipse.jetty.websocket.common.util.MaskedByteBuffer;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class TextPayloadParserTest
{
@Test
@ -98,7 +97,6 @@ public class TextPayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
WebSocketFrame txt = capture.getFrames().poll();
assertThat("TextFrame.data",txt.getPayloadAsUTF8(),is(expectedText));
@ -133,7 +131,6 @@ public class TextPayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
WebSocketFrame txt = capture.getFrames().poll();
assertThat("TextFrame.data",txt.getPayloadAsUTF8(),is(expectedText));
@ -170,7 +167,6 @@ public class TextPayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
capture.assertHasFrame(OpCode.CONTINUATION,1);
WebSocketFrame txt = capture.getFrames().poll();
@ -198,7 +194,6 @@ public class TextPayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
WebSocketFrame txt = capture.getFrames().poll();
assertThat("TextFrame.data",txt.getPayloadAsUTF8(),is(expectedText));
@ -224,7 +219,6 @@ public class TextPayloadParserTest
parser.setIncomingFramesHandler(capture);
parser.parse(buf);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
WebSocketFrame txt = capture.getFrames().poll();
assertThat("TextFrame.data",txt.getPayloadAsUTF8(),is(expectedText));

View File

@ -18,11 +18,6 @@
package org.eclipse.jetty.websocket.common;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -37,6 +32,11 @@ import org.eclipse.jetty.websocket.common.test.OutgoingFramesCapture;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class WebSocketRemoteEndpointTest
{
public ByteBufferPool bufferPool = new MappedByteBufferPool();
@ -48,8 +48,8 @@ public class WebSocketRemoteEndpointTest
LocalWebSocketConnection conn = new LocalWebSocketConnection(id,bufferPool);
OutgoingFramesCapture outgoing = new OutgoingFramesCapture();
WebSocketRemoteEndpoint remote = new WebSocketRemoteEndpoint(conn,outgoing);
conn.connect();
conn.open();
conn.opening();
conn.opened();
// Start text message
remote.sendPartialString("Hello ",false);
@ -73,8 +73,8 @@ public class WebSocketRemoteEndpointTest
LocalWebSocketConnection conn = new LocalWebSocketConnection(id,bufferPool);
OutgoingFramesCapture outgoing = new OutgoingFramesCapture();
WebSocketRemoteEndpoint remote = new WebSocketRemoteEndpoint(conn,outgoing);
conn.connect();
conn.open();
conn.opening();
conn.opened();
// Start text message
remote.sendPartialString("Hello ",false);
@ -98,8 +98,8 @@ public class WebSocketRemoteEndpointTest
LocalWebSocketConnection conn = new LocalWebSocketConnection(testInfo.getDisplayName(), bufferPool);
OutgoingFrames orderingAssert = new SaneFrameOrderingAssertion();
WebSocketRemoteEndpoint remote = new WebSocketRemoteEndpoint(conn, orderingAssert);
conn.connect();
conn.open();
conn.opening();
conn.opened();
int largeMessageSize = 60000;
byte buf[] = new byte[largeMessageSize];

View File

@ -18,9 +18,6 @@
package org.eclipse.jetty.websocket.common.ab;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@ -37,9 +34,11 @@ import org.eclipse.jetty.websocket.common.test.ByteBufferAssert;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitGenerator;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
/**
* Text Message Spec testing the {@link Generator} and {@link Parser}
*/
@ -311,7 +310,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();
@ -345,7 +343,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();
@ -379,7 +376,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();
@ -413,7 +409,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();
@ -449,7 +444,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();
@ -486,7 +480,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();
@ -497,7 +490,6 @@ public class TestABCase1_1
@Test
public void testParseEmptyTextCase1_1_1()
{
ByteBuffer expected = ByteBuffer.allocate(5);
expected.put(new byte[]
@ -510,7 +502,6 @@ public class TestABCase1_1
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.TEXT,1);
Frame pActual = capture.getFrames().poll();

View File

@ -18,9 +18,6 @@
package org.eclipse.jetty.websocket.common.ab;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
@ -36,9 +33,11 @@ import org.eclipse.jetty.websocket.common.test.ByteBufferAssert;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitGenerator;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
/**
* Binary Message Spec testing the {@link Generator} and {@link Parser}
*/
@ -330,7 +329,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();
@ -364,7 +362,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();
@ -398,7 +395,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();
@ -432,7 +428,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();
@ -467,7 +462,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();
@ -504,7 +498,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();
@ -528,7 +521,6 @@ public class TestABCase1_2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.BINARY,1);
Frame pActual = capture.getFrames().poll();

View File

@ -18,12 +18,6 @@
package org.eclipse.jetty.websocket.common.ab;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -42,9 +36,14 @@ import org.eclipse.jetty.websocket.common.test.ByteBufferAssert;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitGenerator;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class TestABCase2
{
private WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
@ -192,7 +191,6 @@ public class TestABCase2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PING,1);
Frame pActual = capture.getFrames().poll();
@ -222,7 +220,6 @@ public class TestABCase2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PING,1);
Frame pActual = capture.getFrames().poll();
@ -245,7 +242,6 @@ public class TestABCase2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PING,1);
Frame pActual = capture.getFrames().poll();
@ -276,7 +272,6 @@ public class TestABCase2
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.PING,1);
Frame pActual = capture.getFrames().poll();

View File

@ -18,10 +18,6 @@
package org.eclipse.jetty.websocket.common.ab;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@ -40,9 +36,12 @@ import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.UnitGenerator;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.eclipse.jetty.websocket.common.util.Hex;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class TestABCase7_3
{
private WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
@ -79,7 +78,6 @@ public class TestABCase7_3
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.CLOSE,1);
Frame pActual = capture.getFrames().poll();
@ -139,7 +137,6 @@ public class TestABCase7_3
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.CLOSE,1);
Frame pActual = capture.getFrames().poll();
@ -196,7 +193,6 @@ public class TestABCase7_3
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.CLOSE,1);
Frame pActual = capture.getFrames().poll();
@ -265,7 +261,6 @@ public class TestABCase7_3
parser.setIncomingFramesHandler(capture);
parser.parse(expected);
capture.assertNoErrors();
capture.assertHasFrame(OpCode.CLOSE,1);
Frame pActual = capture.getFrames().poll();

View File

@ -18,11 +18,13 @@
package org.eclipse.jetty.websocket.common.events;
import static org.eclipse.jetty.websocket.common.test.MoreMatchers.regex;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import examples.AdapterConnectCloseSocket;
import examples.AnnotatedBinaryArraySocket;
import examples.AnnotatedBinaryStreamSocket;
import examples.AnnotatedFramesSocket;
import examples.AnnotatedTextSocket;
import examples.ListenerBasicSocket;
import examples.ListenerPingPongSocket;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.websocket.api.StatusCode;
@ -42,13 +44,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import examples.AdapterConnectCloseSocket;
import examples.AnnotatedBinaryArraySocket;
import examples.AnnotatedBinaryStreamSocket;
import examples.AnnotatedFramesSocket;
import examples.AnnotatedTextSocket;
import examples.ListenerBasicSocket;
import examples.ListenerPingPongSocket;
import static org.eclipse.jetty.websocket.common.test.MoreMatchers.regex;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
public class EventDriverTest
{
@ -110,7 +109,7 @@ public class EventDriverTest
try (LocalWebSocketSession conn = new CloseableLocalWebSocketSession(container,testInfo.getDisplayName(),driver))
{
conn.open();
driver.incomingError(new WebSocketException("oof"));
driver.onError(new WebSocketException("oof"));
driver.incomingFrame(new CloseInfo(StatusCode.NORMAL).asFrame());
assertThat(socket.capture.safePoll(), startsWith("onConnect"));

View File

@ -36,12 +36,6 @@ public class DummyIncomingFrames implements IncomingFrames
this.id = id;
}
@Override
public void incomingError(Throwable e)
{
LOG.debug("incomingError()",e);
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -18,12 +18,6 @@
package org.eclipse.jetty.websocket.common.extensions.compress;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -61,9 +55,14 @@ import org.eclipse.jetty.websocket.common.test.ByteBufferAssert;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.OutgoingNetworkBytesCapture;
import org.eclipse.jetty.websocket.common.test.UnitParser;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class DeflateFrameExtensionTest extends AbstractExtensionTest
{
private static final Logger LOG = Log.getLogger(DeflateFrameExtensionTest.class);
@ -436,11 +435,6 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest
throw new RuntimeIOException(x);
}
}
@Override
public void incomingError(Throwable t)
{
}
});
BinaryFrame frame = new BinaryFrame();

View File

@ -0,0 +1,89 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ConnectionStateTest
{
@Test
public void testHandshakeToOpened()
{
ConnectionState state = new ConnectionState();
assertFalse(state.canWriteWebSocketFrames(), "Handshaking canWriteWebSocketFrames");
assertFalse(state.canReadWebSocketFrames(), "Handshaking canReadWebSocketFrames");
assertTrue(state.opening(), "Opening");
assertTrue(state.canWriteWebSocketFrames(), "Opening canWriteWebSocketFrames");
assertFalse(state.canReadWebSocketFrames(), "Opening canReadWebSocketFrames");
assertTrue(state.opened(), "Opened");
assertTrue(state.canWriteWebSocketFrames(), "Opened canWriteWebSocketFrames");
assertTrue(state.canReadWebSocketFrames(), "Opened canReadWebSocketFrames");
}
@Test
public void testOpened_Closing()
{
ConnectionState state = new ConnectionState();
assertTrue(state.opening(), "Opening");
assertTrue(state.opened(), "Opened");
assertTrue(state.closing(), "Closing (initial)");
// A closing state allows for read, but not write
assertFalse(state.canWriteWebSocketFrames(), "Closing canWriteWebSocketFrames");
assertTrue(state.canReadWebSocketFrames(), "Closing canReadWebSocketFrames");
// Closing again shouldn't allow for another close frame to be sent
assertFalse(state.closing(), "Closing (extra)");
}
@Test
public void testOpened_Closing_Disconnected()
{
ConnectionState state = new ConnectionState();
assertTrue(state.opening(), "Opening");
assertTrue(state.opened(), "Opened");
assertTrue(state.closing(), "Closing");
assertTrue(state.disconnected(), "Disconnected");
assertFalse(state.canWriteWebSocketFrames(), "Disconnected canWriteWebSocketFrames");
assertFalse(state.canReadWebSocketFrames(), "Disconnected canReadWebSocketFrames");
}
@Test
public void testOpened_Harsh_Disconnected()
{
ConnectionState state = new ConnectionState();
assertTrue(state.opening(), "Opening");
assertTrue(state.opened(), "Opened");
// INTENTIONALLY HAD NO CLOSING - assertTrue(state.closing(), "Closing");
assertTrue(state.disconnected(), "Disconnected");
assertFalse(state.canWriteWebSocketFrames(), "Disconnected canWriteWebSocketFrames");
assertFalse(state.canReadWebSocketFrames(), "Disconnected canReadWebSocketFrames");
}
}

View File

@ -18,31 +18,75 @@
package org.eclipse.jetty.websocket.common.io;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.WritePendingException;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.Generator;
import org.eclipse.jetty.websocket.common.Parser;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FrameFlusherTest
{
public ByteBufferPool bufferPool = new MappedByteBufferPool();
/**
* Ensure post-close frames have their associated callbacks properly notified.
*/
@Test
public void testPostCloseFrameCallbacks() throws ExecutionException, InterruptedException, TimeoutException
{
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
Generator generator = new Generator(policy, bufferPool);
CapturingEndPoint endPoint = new CapturingEndPoint(WebSocketPolicy.newClientPolicy(), bufferPool);
int bufferSize = policy.getMaxBinaryMessageBufferSize();
int maxGather = 1;
FrameFlusher frameFlusher = new FrameFlusher(bufferPool, generator, endPoint, bufferSize, maxGather);
BatchMode batchMode = BatchMode.OFF;
Frame closeFrame = new CloseInfo(StatusCode.MESSAGE_TOO_LARGE, "Message be to big").asFrame();
Frame textFrame = new TextFrame().setPayload("Hello").setFin(true);
FutureWriteCallback closeCallback = new FutureWriteCallback();
FutureWriteCallback textFrameCallback = new FutureWriteCallback();
assertTrue(frameFlusher.enqueue(closeFrame, closeCallback, batchMode));
assertFalse(frameFlusher.enqueue(textFrame, textFrameCallback, batchMode));
frameFlusher.iterate();
closeCallback.get(5, TimeUnit.SECONDS);
// If this throws a TimeoutException then the callback wasn't called.
ExecutionException x = assertThrows(ExecutionException.class,
()-> textFrameCallback.get(5, TimeUnit.SECONDS));
assertThat(x.getCause(), instanceOf(ClosedChannelException.class));
}
/**
* Ensure that FrameFlusher honors the correct order of websocket frames.
*
@ -53,13 +97,13 @@ public class FrameFlusherTest
{
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
Generator generator = new Generator(policy, bufferPool);
SaneFrameOrderingEndPoint endPoint = new SaneFrameOrderingEndPoint(WebSocketPolicy.newClientPolicy(), bufferPool);
CapturingEndPoint endPoint = new CapturingEndPoint(WebSocketPolicy.newClientPolicy(), bufferPool);
int bufferSize = policy.getMaxBinaryMessageBufferSize();
int maxGather = 8;
FrameFlusher frameFlusher = new FrameFlusher(bufferPool, generator, endPoint, bufferSize, maxGather);
int largeMessageSize = 60000;
byte buf[] = new byte[largeMessageSize];
byte[] buf = new byte[largeMessageSize];
Arrays.fill(buf, (byte) 'x');
String largeMessage = new String(buf, UTF_8);
@ -78,10 +122,15 @@ public class FrameFlusherTest
WebSocketFrame frame;
if (i % 2 == 0)
{
frame = new TextFrame().setPayload(largeMessage);
}
else
{
frame = new TextFrame().setPayload("Short Message: " + i);
}
frameFlusher.enqueue(frame, callback, batchMode);
frameFlusher.iterate();
callback.get();
}
}
@ -93,31 +142,24 @@ public class FrameFlusherTest
});
serverTask.get();
System.out.printf("Received: %,d frames / %,d errors%n", endPoint.incomingFrames, endPoint.incomingErrors);
System.out.printf("Received: %,d frames%n", endPoint.incomingFrames.size());
}
public static class SaneFrameOrderingEndPoint extends MockEndPoint implements IncomingFrames
public static class CapturingEndPoint extends MockEndPoint implements IncomingFrames
{
public Parser parser;
public int incomingFrames;
public int incomingErrors;
public LinkedBlockingQueue<Frame> incomingFrames = new LinkedBlockingQueue<>();
public SaneFrameOrderingEndPoint(WebSocketPolicy policy, ByteBufferPool bufferPool)
public CapturingEndPoint(WebSocketPolicy policy, ByteBufferPool bufferPool)
{
parser = new Parser(policy, bufferPool);
parser.setIncomingFramesHandler(this);
}
@Override
public void incomingError(Throwable t)
{
incomingErrors++;
}
@Override
public void incomingFrame(Frame frame)
{
incomingFrames++;
incomingFrames.offer(frame);
}
@Override
@ -129,13 +171,13 @@ public class FrameFlusherTest
@Override
public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException
{
Objects.requireNonNull(callback);
try
{
for (ByteBuffer buffer : buffers)
{
parser.parse(buffer);
}
if (callback != null)
callback.succeeded();
}
catch (WritePendingException e)
@ -144,7 +186,6 @@ public class FrameFlusherTest
}
catch (Throwable t)
{
if (callback != null)
callback.failed(t);
}
}

View File

@ -1,245 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.LinkedList;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.ConnectionState;
import org.junit.jupiter.api.Test;
public class IOStateTest
{
public static class StateTracker implements IOState.ConnectionStateListener
{
private LinkedList<ConnectionState> transitions = new LinkedList<>();
public void assertTransitions(ConnectionState ...states)
{
assertThat("Transitions.count",transitions.size(),is(states.length));
if (states.length > 0)
{
int len = states.length;
for (int i = 0; i < len; i++)
{
assertThat("Transitions[" + i + "]",transitions.get(i),is(states[i]));
}
}
}
public LinkedList<ConnectionState> getTransitions()
{
return transitions;
}
@Override
public void onConnectionStateChange(ConnectionState state)
{
transitions.add(state);
}
}
private void assertCleanClose(IOState state, boolean expected)
{
assertThat("State.cleanClose",state.wasCleanClose(),is(expected));
}
private void assertInputAvailable(IOState state, boolean available)
{
assertThat("State.inputAvailable",state.isInputAvailable(),is(available));
}
private void assertLocalInitiated(IOState state, boolean expected)
{
assertThat("State.localCloseInitiated",state.wasLocalCloseInitiated(),is(expected));
}
private void assertOutputAvailable(IOState state, boolean available)
{
assertThat("State.outputAvailable",state.isOutputAvailable(),is(available));
}
private void assertRemoteInitiated(IOState state, boolean expected)
{
assertThat("State.remoteCloseInitiated",state.wasRemoteCloseInitiated(),is(expected));
}
private void assertState(IOState state, ConnectionState expectedState)
{
assertThat("State",state.getConnectionState(),is(expectedState));
}
@Test
public void testConnectAbnormalClose()
{
IOState state = new IOState();
StateTracker tracker = new StateTracker();
state.addListener(tracker);
assertState(state,ConnectionState.CONNECTING);
// connect
state.onConnected();
assertInputAvailable(state,false);
assertOutputAvailable(state,true);
// open
state.onOpened();
assertInputAvailable(state,true);
assertOutputAvailable(state,true);
// disconnect
state.onAbnormalClose(new CloseInfo(StatusCode.NO_CLOSE,"Oops"));
assertInputAvailable(state,false);
assertOutputAvailable(state,false);
tracker.assertTransitions(ConnectionState.CONNECTED,ConnectionState.OPEN,ConnectionState.CLOSED);
assertState(state,ConnectionState.CLOSED);
// not clean
assertCleanClose(state,false);
assertLocalInitiated(state,false);
assertRemoteInitiated(state,false);
}
@Test
public void testConnectCloseLocalInitiated()
{
IOState state = new IOState();
StateTracker tracker = new StateTracker();
state.addListener(tracker);
assertState(state,ConnectionState.CONNECTING);
// connect
state.onConnected();
assertInputAvailable(state,false);
assertOutputAvailable(state,true);
// open
state.onOpened();
assertInputAvailable(state,true);
assertOutputAvailable(state,true);
// close (local initiated)
state.onCloseLocal(new CloseInfo(StatusCode.NORMAL,"Hi"));
assertInputAvailable(state,true);
assertOutputAvailable(state,false);
assertState(state,ConnectionState.CLOSING);
// close (remote response)
state.onCloseRemote(new CloseInfo(StatusCode.NORMAL,"Hi"));
assertInputAvailable(state,false);
assertOutputAvailable(state,false);
tracker.assertTransitions(ConnectionState.CONNECTED,ConnectionState.OPEN,ConnectionState.CLOSING,ConnectionState.CLOSED);
assertState(state,ConnectionState.CLOSED);
// not clean
assertCleanClose(state,true);
assertLocalInitiated(state,true);
assertRemoteInitiated(state,false);
}
@Test
public void testConnectCloseRemoteInitiated()
{
IOState state = new IOState();
StateTracker tracker = new StateTracker();
state.addListener(tracker);
assertState(state,ConnectionState.CONNECTING);
// connect
state.onConnected();
assertInputAvailable(state,false);
assertOutputAvailable(state,true);
// open
state.onOpened();
assertInputAvailable(state,true);
assertOutputAvailable(state,true);
// close (remote initiated)
state.onCloseRemote(new CloseInfo(StatusCode.NORMAL,"Hi"));
assertInputAvailable(state,false);
assertOutputAvailable(state,true);
assertState(state,ConnectionState.CLOSING);
// close (local response)
state.onCloseLocal(new CloseInfo(StatusCode.NORMAL,"Hi"));
assertInputAvailable(state,false);
assertOutputAvailable(state,false);
tracker.assertTransitions(ConnectionState.CONNECTED,ConnectionState.OPEN,ConnectionState.CLOSING,ConnectionState.CLOSED);
assertState(state,ConnectionState.CLOSED);
// not clean
assertCleanClose(state,true);
assertLocalInitiated(state,false);
assertRemoteInitiated(state,true);
}
@Test
public void testConnectFailure()
{
IOState state = new IOState();
StateTracker tracker = new StateTracker();
state.addListener(tracker);
assertState(state,ConnectionState.CONNECTING);
// fail upgrade
state.onFailedUpgrade();
tracker.assertTransitions(ConnectionState.CLOSED);
assertState(state,ConnectionState.CLOSED);
assertInputAvailable(state,false);
assertOutputAvailable(state,false);
// not clean
assertCleanClose(state,false);
assertLocalInitiated(state,false);
assertRemoteInitiated(state,false);
}
@Test
public void testInit()
{
IOState state = new IOState();
StateTracker tracker = new StateTracker();
state.addListener(tracker);
assertState(state,ConnectionState.CONNECTING);
// do nothing
tracker.assertTransitions();
assertState(state,ConnectionState.CONNECTING);
// not connected yet
assertInputAvailable(state,false);
assertOutputAvailable(state,false);
// no close yet
assertCleanClose(state,false);
assertLocalInitiated(state,false);
assertRemoteInitiated(state,false);
}
}

View File

@ -22,34 +22,36 @@ import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.CloseException;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.ConnectionState;
import org.eclipse.jetty.websocket.common.LogicalConnection;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
public class LocalWebSocketConnection implements LogicalConnection, IncomingFrames, ConnectionStateListener
public class LocalWebSocketConnection implements LogicalConnection, IncomingFrames
{
private static final Logger LOG = Log.getLogger(LocalWebSocketConnection.class);
private final String id;
private final ByteBufferPool bufferPool;
private final Executor executor;
private final ConnectionState connectionState = new ConnectionState();
private WebSocketSession session;
private WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
private IncomingFrames incoming;
private IOState ioState = new IOState();
public LocalWebSocketConnection(ByteBufferPool bufferPool)
{
this("anon",bufferPool);
this("anon", bufferPool);
}
public LocalWebSocketConnection(String id, ByteBufferPool bufferPool)
@ -57,38 +59,53 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
this.id = id;
this.bufferPool = bufferPool;
this.executor = new ExecutorThreadPool();
this.ioState.addListener(this);
}
@Override
public Executor getExecutor()
public boolean canReadWebSocketFrames()
{
return executor;
return connectionState.canReadWebSocketFrames();
}
@Override
public void setSession(WebSocketSession session)
public boolean canWriteWebSocketFrames()
{
return connectionState.canWriteWebSocketFrames();
}
@Override
public void onLocalClose(CloseInfo closeInfo)
public void close(Throwable cause)
{
ioState.onCloseLocal(closeInfo);
Callback callback = Callback.NOOP;
if (cause instanceof CloseException)
{
callback = new DisconnectCallback();
}
close(cause, callback);
}
public void connect()
@Override
public void close(CloseInfo close, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("connect()");
ioState.onConnected();
if (connectionState.closing())
{
// pretend we sent the close frame and the remote responded
session.callApplicationOnClose(close);
disconnect();
}
else
{
if (callback != null)
{
callback.failed(new IllegalStateException("Local Close already called"));
}
}
}
@Override
public void disconnect()
{
if (LOG.isDebugEnabled())
LOG.debug("disconnect()");
connectionState.disconnected();
}
@Override
@ -97,6 +114,12 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
return this.bufferPool;
}
@Override
public Executor getExecutor()
{
return executor;
}
@Override
public String getId()
{
@ -114,12 +137,6 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
return incoming;
}
@Override
public IOState getIOState()
{
return ioState;
}
@Override
public InetSocketAddress getLocalAddress()
{
@ -132,24 +149,28 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
return 0;
}
@Override
public void setMaxIdleTimeout(long ms)
{
}
@Override
public WebSocketPolicy getPolicy()
{
return policy;
}
public void setPolicy(WebSocketPolicy policy)
{
this.policy = policy;
}
@Override
public InetSocketAddress getRemoteAddress()
{
return null;
}
@Override
public void incomingError(Throwable e)
{
incoming.incomingError(e);
}
@Override
public void incomingFrame(Frame frame)
{
@ -159,7 +180,7 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
@Override
public boolean isOpen()
{
return getIOState().isOpen();
return true;
}
@Override
@ -169,33 +190,15 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
}
@Override
public void onConnectionStateChange(ConnectionState state)
public boolean opened()
{
if (LOG.isDebugEnabled())
LOG.debug("Connection State Change: {}",state);
switch (state)
{
case CLOSED:
this.disconnect();
break;
case CLOSING:
if (ioState.wasRemoteCloseInitiated())
{
// send response close frame
CloseInfo close = ioState.getCloseInfo();
LOG.debug("write close frame: {}",close);
ioState.onCloseLocal(close);
}
default:
break;
}
return connectionState.opened();
}
public void open()
@Override
public boolean opening()
{
if (LOG.isDebugEnabled())
LOG.debug("open()");
ioState.onOpened();
return connectionState.opening();
}
@Override
@ -204,12 +207,13 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
}
@Override
public void resume()
public void remoteClose(CloseInfo close)
{
close(close, Callback.NOOP);
}
@Override
public void setMaxIdleTimeout(long ms)
public void resume()
{
}
@ -219,9 +223,10 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
this.incoming = incoming;
}
public void setPolicy(WebSocketPolicy policy)
@Override
public void setSession(WebSocketSession session)
{
this.policy = policy;
this.session = session;
}
@Override
@ -230,9 +235,58 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram
return null;
}
@Override
public String toStateString()
{
return connectionState.toString();
}
@Override
public String toString()
{
return String.format("%s[%s]",LocalWebSocketConnection.class.getSimpleName(),id);
return String.format("%s[%s]", LocalWebSocketConnection.class.getSimpleName(), id);
}
private void close(Throwable cause, Callback callback)
{
session.callApplicationOnError(cause);
close(new CloseInfo(StatusCode.SERVER_ERROR, cause.getMessage()), callback);
}
private class DisconnectCallback implements Callback
{
@Override
public void failed(Throwable x)
{
disconnect();
}
@Override
public void succeeded()
{
disconnect();
}
}
private class CallbackBridge implements WriteCallback
{
final Callback callback;
public CallbackBridge(Callback callback)
{
this.callback = callback;
}
@Override
public void writeFailed(Throwable x)
{
callback.failed(x);
}
@Override
public void writeSuccess()
{
callback.succeeded();
}
}
}

View File

@ -19,6 +19,9 @@
package org.eclipse.jetty.websocket.common.test;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.io.ByteBufferPool;
@ -27,25 +30,23 @@ import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory;
import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
public class BlockheadClient extends HttpClient implements WebSocketContainerScope
{
private WebSocketPolicy policy;
private ByteBufferPool bufferPool;
private WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
private ByteBufferPool bufferPool = new MappedByteBufferPool();
private ExtensionFactory extensionFactory;
private DecoratedObjectFactory objectFactory;
private DecoratedObjectFactory objectFactory = new DecoratedObjectFactory();
private List<WebSocketSessionListener> listeners = new ArrayList<>();
public BlockheadClient()
{
super(null);
setName("Blockhead-CLIENT");
this.policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
this.bufferPool = new MappedByteBufferPool();
this.extensionFactory = new WebSocketExtensionFactory(this);
this.objectFactory = new DecoratedObjectFactory();
}
public ByteBufferPool getBufferPool()
@ -69,16 +70,26 @@ public class BlockheadClient extends HttpClient implements WebSocketContainerSco
return policy;
}
@Override
public void addSessionListener(WebSocketSessionListener listener)
{
listeners.add(listener);
}
@Override
public void removeSessionListener(WebSocketSessionListener listener)
{
listeners.remove(listener);
}
@Override
public Collection<WebSocketSessionListener> getSessionListeners()
{
return listeners;
}
public BlockheadClientRequest newWsRequest(URI destURI)
{
return new BlockheadClientRequest(this, destURI);
}
@Override
public void onSessionOpened(WebSocketSession session)
{ /* ignored */ }
@Override
public void onSessionClosed(WebSocketSession session)
{ /* ignored */ }
}

View File

@ -347,15 +347,8 @@ public class BlockheadConnection extends AbstractConnection implements Connectio
public class IncomingCapture implements IncomingFrames
{
public final LinkedBlockingQueue<WebSocketFrame> incomingFrames = new LinkedBlockingQueue<>();
public final LinkedBlockingQueue<Throwable> incomingErrors = new LinkedBlockingQueue<>();
public Consumer<Frame> frameConsumer;
@Override
public void incomingError(Throwable cause)
{
incomingErrors.offer(cause);
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -22,6 +22,7 @@ import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
@ -33,25 +34,35 @@ import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.LogicalConnection;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.io.IOState;
public class DummyConnection implements LogicalConnection
{
private static final Logger LOG = Log.getLogger(DummyConnection.class);
private IOState iostate;
public DummyConnection()
{
this.iostate = new IOState();
}
@Override
public void setSession(WebSocketSession session)
public boolean canReadWebSocketFrames()
{
return true;
}
@Override
public void onLocalClose(CloseInfo close)
public boolean canWriteWebSocketFrames()
{
return true;
}
@Override
public void close(Throwable cause)
{
LOG.warn(cause);
}
@Override
public void close(CloseInfo closeInfo, Callback callback)
{
}
@ -84,12 +95,6 @@ public class DummyConnection implements LogicalConnection
return 0;
}
@Override
public IOState getIOState()
{
return this.iostate;
}
@Override
public InetSocketAddress getLocalAddress()
{
@ -102,6 +107,11 @@ public class DummyConnection implements LogicalConnection
return 0;
}
@Override
public void setMaxIdleTimeout(long ms)
{
}
@Override
public WebSocketPolicy getPolicy()
{
@ -126,6 +136,18 @@ public class DummyConnection implements LogicalConnection
return false;
}
@Override
public boolean opened()
{
return false;
}
@Override
public boolean opening()
{
return false;
}
@Override
public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode)
{
@ -133,12 +155,13 @@ public class DummyConnection implements LogicalConnection
}
@Override
public void resume()
public void remoteClose(CloseInfo close)
{
}
@Override
public void setMaxIdleTimeout(long ms)
public void resume()
{
}
@ -149,9 +172,20 @@ public class DummyConnection implements LogicalConnection
LOG.debug("setNextIncomingFrames({})",incoming);
}
@Override
public void setSession(WebSocketSession session)
{
}
@Override
public SuspendToken suspend()
{
return null;
}
@Override
public String toStateString()
{
return "no-state-in-dummy";
}
}

View File

@ -18,33 +18,23 @@
package org.eclipse.jetty.websocket.common.test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
public class IncomingFramesCapture implements IncomingFrames
{
private static final Logger LOG = Log.getLogger(IncomingFramesCapture.class);
private LinkedBlockingQueue<WebSocketFrame> frames = new LinkedBlockingQueue<>();
private LinkedBlockingQueue<Throwable> errors = new LinkedBlockingQueue<>();
public void assertErrorCount(int expectedCount)
{
assertThat("Captured error count",errors.size(),is(expectedCount));
}
public void assertFrameCount(int expectedCount)
{
@ -64,11 +54,6 @@ public class IncomingFramesCapture implements IncomingFrames
assertThat("Captured frame count",frames.size(),is(expectedCount));
}
public void assertHasErrors(Class<? extends WebSocketException> errorType, int expectedCount)
{
assertThat(errorType.getSimpleName(),getErrorCount(errorType),is(expectedCount));
}
public void assertHasFrame(byte op)
{
assertThat(OpCode.name(op),getFrameCount(op),greaterThanOrEqualTo(1));
@ -85,11 +70,6 @@ public class IncomingFramesCapture implements IncomingFrames
assertThat("Frame count",frames.size(),is(0));
}
public void assertNoErrors()
{
assertThat("Error count",errors.size(),is(0));
}
public void clear()
{
frames.clear();
@ -106,24 +86,6 @@ public class IncomingFramesCapture implements IncomingFrames
}
}
public int getErrorCount(Class<? extends Throwable> errorType)
{
int count = 0;
for (Throwable error : errors)
{
if (errorType.isInstance(error))
{
count++;
}
}
return count;
}
public Queue<Throwable> getErrors()
{
return errors;
}
public int getFrameCount(byte op)
{
int count = 0;
@ -142,13 +104,6 @@ public class IncomingFramesCapture implements IncomingFrames
return frames;
}
@Override
public void incomingError(Throwable e)
{
LOG.debug(e);
errors.add(e);
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -29,10 +29,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -67,6 +64,7 @@ import org.eclipse.jetty.websocket.common.LogicalConnection;
import org.eclipse.jetty.websocket.common.SessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.WebSocketSessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
@ -81,7 +79,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
/**
* Factory to create WebSocket connections
*/
public class WebSocketServerFactory extends ContainerLifeCycle implements WebSocketCreator, WebSocketContainerScope, WebSocketServletFactory
public class WebSocketServerFactory extends ContainerLifeCycle implements WebSocketCreator, WebSocketContainerScope, WebSocketServletFactory, WebSocketSessionListener
{
private static final Logger LOG = Log.getLogger(WebSocketServerFactory.class);
@ -89,7 +87,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
private final Map<Integer, WebSocketHandshake> handshakes = new HashMap<>();
// TODO: obtain shared (per server scheduler, somehow)
private final Scheduler scheduler = new ScheduledExecutorScheduler();
private final List<WebSocketSession.Listener> listeners = new CopyOnWriteArrayList<>();
private final List<WebSocketSessionListener> listeners = new ArrayList<>();
private final String supportedVersions;
private final WebSocketPolicy defaultPolicy;
private final EventDriverFactory eventDriverFactory;
@ -184,16 +182,25 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
addBean(scheduler);
addBean(bufferPool);
listeners.add(this);
}
public void addSessionListener(WebSocketSession.Listener listener)
@Override
public void addSessionListener(WebSocketSessionListener listener)
{
listeners.add(listener);
this.listeners.add(listener);
}
public void removeSessionListener(WebSocketSession.Listener listener)
@Override
public void removeSessionListener(WebSocketSessionListener listener)
{
listeners.remove(listener);
this.listeners.remove(listener);
}
@Override
public Collection<WebSocketSessionListener> getSessionListeners()
{
return this.listeners;
}
@Override
@ -516,29 +523,12 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
public void onSessionOpened(WebSocketSession session)
{
addManaged(session);
notifySessionListeners(listener -> listener.onOpened(session));
}
@Override
public void onSessionClosed(WebSocketSession session)
{
removeBean(session);
notifySessionListeners(listener -> listener.onClosed(session));
}
private void notifySessionListeners(Consumer<WebSocketSession.Listener> consumer)
{
for (WebSocketSession.Listener listener : listeners)
{
try
{
consumer.accept(listener);
}
catch (Throwable x)
{
LOG.info("Exception while invoking listener " + listener, x);
}
}
}
@Override

View File

@ -18,10 +18,6 @@
package org.eclipse.jetty.websocket.server;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@ -43,6 +39,10 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
public class IdleTimeoutTest
{
@ -123,7 +123,7 @@ public class IdleTimeoutTest
assertThat("frame opcode",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo close = new CloseInfo(frame);
assertThat("close code",close.getStatusCode(),is(StatusCode.SHUTDOWN));
assertThat("close reason",close.getReason(),containsString("Timeout"));
assertThat("close reason",close.getReason(),containsString("timeout"));
}
}
}

View File

@ -1,376 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.test.BlockheadClient;
import org.eclipse.jetty.websocket.common.test.BlockheadClientRequest;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.eclipse.jetty.websocket.server.helper.RFCSocket;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/**
* Tests various close scenarios that should result in Open Session cleanup
*/
public class ManyConnectionsCleanupTest
{
static class AbstractCloseSocket extends WebSocketAdapter
{
public CountDownLatch closeLatch = new CountDownLatch(1);
public String closeReason = null;
public int closeStatusCode = -1;
public List<Throwable> errors = new ArrayList<>();
@Override
public void onWebSocketClose(int statusCode, String reason)
{
LOG.debug("onWebSocketClose({}, {})",statusCode,reason);
this.closeStatusCode = statusCode;
this.closeReason = reason;
closeLatch.countDown();
}
@Override
public void onWebSocketError(Throwable cause)
{
errors.add(cause);
}
}
@SuppressWarnings("serial")
public static class CloseServlet extends WebSocketServlet implements WebSocketCreator
{
private WebSocketServerFactory serverFactory;
private AtomicInteger calls = new AtomicInteger(0);
@Override
public void configure(WebSocketServletFactory factory)
{
factory.setCreator(this);
if (factory instanceof WebSocketServerFactory)
{
this.serverFactory = (WebSocketServerFactory)factory;
}
}
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
if (req.hasSubProtocol("fastclose"))
{
closeSocket = new FastCloseSocket(calls);
return closeSocket;
}
if (req.hasSubProtocol("fastfail"))
{
closeSocket = new FastFailSocket(calls);
return closeSocket;
}
if (req.hasSubProtocol("container"))
{
closeSocket = new ContainerSocket(serverFactory,calls);
return closeSocket;
}
return new RFCSocket();
}
}
/**
* On Message, return container information
*/
public static class ContainerSocket extends AbstractCloseSocket
{
private static final Logger LOG = Log.getLogger(ManyConnectionsCleanupTest.ContainerSocket.class);
private final WebSocketServerFactory container;
private final AtomicInteger calls;
private Session session;
public ContainerSocket(WebSocketServerFactory container, AtomicInteger calls)
{
this.container = container;
this.calls = calls;
}
@Override
public void onWebSocketText(String message)
{
LOG.debug("onWebSocketText({})",message);
calls.incrementAndGet();
if (message.equalsIgnoreCase("openSessions"))
{
Collection<WebSocketSession> sessions = container.getOpenSessions();
StringBuilder ret = new StringBuilder();
ret.append("openSessions.size=").append(sessions.size()).append('\n');
int idx = 0;
for (WebSocketSession sess : sessions)
{
ret.append('[').append(idx++).append("] ").append(sess.toString()).append('\n');
}
session.getRemote().sendStringByFuture(ret.toString());
session.close(StatusCode.NORMAL,"ContainerSocket");
} else if(message.equalsIgnoreCase("calls"))
{
session.getRemote().sendStringByFuture(String.format("calls=%,d",calls.get()));
}
}
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
this.session = sess;
}
}
/**
* On Connect, close socket
*/
public static class FastCloseSocket extends AbstractCloseSocket
{
private static final Logger LOG = Log.getLogger(ManyConnectionsCleanupTest.FastCloseSocket.class);
private final AtomicInteger calls;
public FastCloseSocket(AtomicInteger calls)
{
this.calls = calls;
}
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
calls.incrementAndGet();
sess.close(StatusCode.NORMAL,"FastCloseServer");
}
}
/**
* On Connect, throw unhandled exception
*/
public static class FastFailSocket extends AbstractCloseSocket
{
private static final Logger LOG = Log.getLogger(ManyConnectionsCleanupTest.FastFailSocket.class);
private final AtomicInteger calls;
public FastFailSocket(AtomicInteger calls)
{
this.calls = calls;
}
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
calls.incrementAndGet();
// Test failure due to unhandled exception
// this should trigger a fast-fail closure during open/connect
throw new RuntimeException("Intentional FastFail");
}
}
private static final Logger LOG = Log.getLogger(ManyConnectionsCleanupTest.class);
private static BlockheadClient client;
private static SimpleServletServer server;
private static AbstractCloseSocket closeSocket;
@BeforeAll
public static void startServer() throws Exception
{
server = new SimpleServletServer(new CloseServlet());
server.start();
}
@AfterAll
public static void stopServer()
{
server.stop();
}
@BeforeAll
public static void startClient() throws Exception
{
client = new BlockheadClient();
client.setIdleTimeout(TimeUnit.SECONDS.toMillis(2));
client.start();
}
@AfterAll
public static void stopClient() throws Exception
{
client.stop();
}
/**
* Test session open session cleanup (bug #474936)
*
* @throws Exception
* on test failure
*/
@Test
public void testOpenSessionCleanup() throws Exception
{
int iterationCount = 20;
// TODO: consider a SilentLogging alternative class
try(StacklessLogging ignore = new StacklessLogging(FastFailSocket.class, WebSocketSession.class))
{
for (int requests = 0; requests < iterationCount; requests++)
{
fastFail();
fastClose();
dropConnection();
}
}
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "container");
request.idleTimeout(1, TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
clientConn.write(new TextFrame().setPayload("calls"));
clientConn.write(new TextFrame().setPayload("openSessions"));
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
WebSocketFrame frame;
String resp;
frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[0].opcode",frame.getOpCode(),is(OpCode.TEXT));
resp = frame.getPayloadAsUTF8();
assertThat("Should only have 1 open session",resp,containsString("calls=" + ((iterationCount * 2) + 1)));
frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[1].opcode",frame.getOpCode(),is(OpCode.TEXT));
resp = frame.getPayloadAsUTF8();
assertThat("Should only have 1 open session",resp,containsString("openSessions.size=1\n"));
frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[2].opcode",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo close = new CloseInfo(frame);
assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.NORMAL));
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Open Sessions Latch",closeSocket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("Open Sessions.statusCode",closeSocket.closeStatusCode,is(StatusCode.NORMAL));
assertThat("Open Sessions.errors",closeSocket.errors.size(),is(0));
}
}
private void fastClose() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "fastclose");
request.idleTimeout(1, TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT);
StacklessLogging ignore = new StacklessLogging(WebSocketSession.class))
{
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
CloseInfo close = new CloseInfo(StatusCode.NORMAL,"Normal");
assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.NORMAL));
// Notify server of close handshake
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Fast Close Latch",closeSocket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("Fast Close.statusCode",closeSocket.closeStatusCode,is(StatusCode.NORMAL));
}
}
private void fastFail() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "fastfail");
request.idleTimeout(1, TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT);
StacklessLogging ignore = new StacklessLogging(WebSocketSession.class))
{
CloseInfo close = new CloseInfo(StatusCode.NORMAL,"Normal");
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Fast Fail Latch",closeSocket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("Fast Fail.statusCode",closeSocket.closeStatusCode,is(StatusCode.SERVER_ERROR));
assertThat("Fast Fail.errors",closeSocket.errors.size(),is(1));
}
}
private void dropConnection() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "container");
request.idleTimeout(1, TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT);
StacklessLogging ignore = new StacklessLogging(WebSocketSession.class))
{
clientConn.abort();
}
}
}

View File

@ -19,7 +19,6 @@
package org.eclipse.jetty.websocket.server;
import java.net.URI;
import javax.servlet.http.HttpServlet;
import org.eclipse.jetty.http.HttpVersion;
@ -92,9 +91,6 @@ public class SimpleServletServer
sslContextFactory.setKeyStorePath(MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath());
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setKeyManagerPassword("keypwd");
sslContextFactory.setExcludeCipherSuites("SSL_RSA_WITH_DES_CBC_SHA","SSL_DHE_RSA_WITH_DES_CBC_SHA","SSL_DHE_DSS_WITH_DES_CBC_SHA",
"SSL_RSA_EXPORT_WITH_RC4_40_MD5","SSL_RSA_EXPORT_WITH_DES40_CBC_SHA","SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA");
sslContextFactory.setEndpointIdentificationAlgorithm(null);
// SSL HTTP Configuration

View File

@ -1,412 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.test.BlockheadClient;
import org.eclipse.jetty.websocket.common.test.BlockheadClientRequest;
import org.eclipse.jetty.websocket.common.test.BlockheadConnection;
import org.eclipse.jetty.websocket.common.test.Timeouts;
import org.eclipse.jetty.websocket.server.helper.RFCSocket;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* Tests various close scenarios
*/
public class WebSocketCloseTest
{
static class AbstractCloseSocket extends WebSocketAdapter
{
public CountDownLatch closeLatch = new CountDownLatch(1);
public String closeReason = null;
public int closeStatusCode = -1;
public List<Throwable> errors = new ArrayList<>();
@Override
public void onWebSocketClose(int statusCode, String reason)
{
LOG.debug("onWebSocketClose({}, {})",statusCode,reason);
this.closeStatusCode = statusCode;
this.closeReason = reason;
closeLatch.countDown();
}
@Override
public void onWebSocketError(Throwable cause)
{
errors.add(cause);
}
}
@SuppressWarnings("serial")
public static class CloseServlet extends WebSocketServlet implements WebSocketCreator
{
private WebSocketServerFactory serverFactory;
@Override
public void configure(WebSocketServletFactory factory)
{
factory.setCreator(this);
if (factory instanceof WebSocketServerFactory)
{
this.serverFactory = (WebSocketServerFactory)factory;
}
}
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
if (req.hasSubProtocol("fastclose"))
{
closeSocket = new FastCloseSocket();
return closeSocket;
}
if (req.hasSubProtocol("fastfail"))
{
closeSocket = new FastFailSocket();
return closeSocket;
}
if (req.hasSubProtocol("container"))
{
closeSocket = new ContainerSocket(serverFactory);
return closeSocket;
}
return new RFCSocket();
}
}
/**
* On Message, return container information
*/
public static class ContainerSocket extends AbstractCloseSocket
{
private static final Logger LOG = Log.getLogger(WebSocketCloseTest.ContainerSocket.class);
private final WebSocketServerFactory container;
private Session session;
public ContainerSocket(WebSocketServerFactory container)
{
this.container = container;
}
@Override
public void onWebSocketText(String message)
{
LOG.debug("onWebSocketText({})",message);
if (message.equalsIgnoreCase("openSessions"))
{
Collection<WebSocketSession> sessions = container.getOpenSessions();
StringBuilder ret = new StringBuilder();
ret.append("openSessions.size=").append(sessions.size()).append('\n');
int idx = 0;
for (WebSocketSession sess : sessions)
{
ret.append('[').append(idx++).append("] ").append(sess.toString()).append('\n');
}
session.getRemote().sendStringByFuture(ret.toString());
}
session.close(StatusCode.NORMAL,"ContainerSocket");
}
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
this.session = sess;
}
}
/**
* On Connect, close socket
*/
public static class FastCloseSocket extends AbstractCloseSocket
{
private static final Logger LOG = Log.getLogger(WebSocketCloseTest.FastCloseSocket.class);
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
sess.close(StatusCode.NORMAL,"FastCloseServer");
}
}
/**
* On Connect, throw unhandled exception
*/
public static class FastFailSocket extends AbstractCloseSocket
{
private static final Logger LOG = Log.getLogger(WebSocketCloseTest.FastFailSocket.class);
@Override
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
// Test failure due to unhandled exception
// this should trigger a fast-fail closure during open/connect
throw new RuntimeException("Intentional FastFail");
}
}
private static final Logger LOG = Log.getLogger(WebSocketCloseTest.class);
private static BlockheadClient client;
private static SimpleServletServer server;
private static AbstractCloseSocket closeSocket;
@BeforeAll
public static void startServer() throws Exception
{
server = new SimpleServletServer(new CloseServlet());
server.start();
}
@AfterAll
public static void stopServer()
{
server.stop();
}
@BeforeAll
public static void startClient() throws Exception
{
client = new BlockheadClient();
client.setIdleTimeout(TimeUnit.SECONDS.toMillis(2));
client.start();
}
@AfterAll
public static void stopClient() throws Exception
{
client.stop();
}
/**
* Test fast close (bug #403817)
*
* @throws Exception
* on test failure
*/
@Test
public void testFastClose() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "fastclose");
request.idleTimeout(5,TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
// Verify that client got close frame
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
WebSocketFrame frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[0].opcode",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo close = new CloseInfo(frame);
assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.NORMAL));
// Notify server of close handshake
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Fast Close Latch",closeSocket.closeLatch.await(5,TimeUnit.SECONDS),is(true));
assertThat("Fast Close.statusCode",closeSocket.closeStatusCode,is(StatusCode.NORMAL));
}
}
/**
* Test fast fail (bug #410537)
*
* @throws Exception
* on test failure
*/
@Test
public void testFastFail() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "fastfail");
request.idleTimeout(5,TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (StacklessLogging ignore = new StacklessLogging(FastFailSocket.class, WebSocketSession.class);
BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
WebSocketFrame frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[0].opcode",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo close = new CloseInfo(frame);
assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.SERVER_ERROR));
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Fast Fail Latch",closeSocket.closeLatch.await(5,TimeUnit.SECONDS),is(true));
assertThat("Fast Fail.statusCode",closeSocket.closeStatusCode,is(StatusCode.SERVER_ERROR));
assertThat("Fast Fail.errors",closeSocket.errors.size(),is(1));
}
}
/**
* Test session open session cleanup (bug #474936)
*
* @throws Exception
* on test failure
*/
@Test
@Disabled("Flappy test, needs work")
public void testOpenSessionCleanup() throws Exception
{
fastFail();
fastClose();
dropConnection();
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "container");
request.idleTimeout(1,TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
TextFrame text = new TextFrame();
text.setPayload("openSessions");
clientConn.write(text);
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
WebSocketFrame frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[0].opcode",frame.getOpCode(),is(OpCode.TEXT));
String resp = frame.getPayloadAsUTF8();
assertThat("Should only have 1 open session",resp,containsString("openSessions.size=1\n"));
frame = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
assertThat("frames[1].opcode",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo close = new CloseInfo(frame);
assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.NORMAL));
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Open Sessions Latch",closeSocket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("Open Sessions.statusCode",closeSocket.closeStatusCode,is(StatusCode.NORMAL));
assertThat("Open Sessions.errors",closeSocket.errors.size(),is(0));
}
}
@SuppressWarnings("Duplicates")
private void fastClose() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "fastclose");
request.idleTimeout(1,TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (StacklessLogging ignore = new StacklessLogging(WebSocketSession.class);
BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
WebSocketFrame received = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
CloseInfo close = new CloseInfo(StatusCode.NORMAL,"Normal");
assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.NORMAL));
// Notify server of close handshake
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Fast Close Latch",closeSocket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("Fast Close.statusCode",closeSocket.closeStatusCode,is(StatusCode.NORMAL));
}
}
private void fastFail() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "fastfail");
request.idleTimeout(1,TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (StacklessLogging ignore = new StacklessLogging(WebSocketSession.class);
BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
WebSocketFrame received = frames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT);
CloseInfo close = new CloseInfo(StatusCode.NORMAL,"Normal");
clientConn.write(close.asFrame()); // respond with close
// ensure server socket got close event
assertThat("Fast Fail Latch",closeSocket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("Fast Fail.statusCode",closeSocket.closeStatusCode,is(StatusCode.SERVER_ERROR));
assertThat("Fast Fail.errors",closeSocket.errors.size(),is(1));
}
}
@SuppressWarnings("Duplicates")
private void dropConnection() throws Exception
{
BlockheadClientRequest request = client.newWsRequest(server.getServerUri());
request.header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, "container");
request.idleTimeout(1,TimeUnit.SECONDS);
Future<BlockheadConnection> connFut = request.sendAsync();
try (StacklessLogging ignore = new StacklessLogging(WebSocketSession.class);
BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
clientConn.abort();
}
}
}

View File

@ -18,33 +18,71 @@
package org.eclipse.jetty.websocket.server;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.websocket.server.helper.EchoSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.websocket.server.browser.BrowserDebugTool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class WebSocketProtocolTest
{
private BrowserDebugTool server;
private Server server;
@BeforeEach
public void startServer() throws Exception
{
server = new BrowserDebugTool();
server.prepare(0);
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
ServletHolder holder = new ServletHolder(new WebSocketServlet()
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.getPolicy().setIdleTimeout(10000);
factory.getPolicy().setMaxTextMessageSize(1024 * 1024 * 2);
factory.setCreator((req, resp) -> {
if(req.hasSubProtocol("echo"))
{
resp.setAcceptedSubProtocol("echo");
}
return new EchoSocket();
});
}
});
context.addServlet(holder, "/ws");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
server.start();
}
@ -57,11 +95,15 @@ public class WebSocketProtocolTest
@Test
public void testWebSocketProtocolResponse() throws Exception
{
try (Socket client = new Socket("localhost", server.getPort()))
URI uri = server.getURI();
String host = uri.getHost();
int port = uri.getPort();
try (Socket client = new Socket(host, port))
{
byte[] key = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
StringBuilder request = new StringBuilder();
request.append("GET / HTTP/1.1\r\n")
request.append("GET /ws HTTP/1.1\r\n")
.append("Host: localhost\r\n")
.append("Connection: Upgrade\r\n")
.append("Upgrade: websocket\r\n")

View File

@ -25,6 +25,7 @@ import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
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.WebSocket;
import org.eclipse.jetty.websocket.common.util.TextUtil;
@ -80,4 +81,13 @@ public class ABSocket
LOG.warn("Unable to echo TEXT message",e);
}
}
@OnWebSocketError
public void onError(Throwable cause)
{
if (LOG.isDebugEnabled())
{
LOG.debug("onError", cause);
}
}
}

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@ -64,4 +65,13 @@ public class BigEchoSocket
if (remote.getBatchMode() == BatchMode.ON)
remote.flush();
}
@OnWebSocketError
public void onError(Throwable cause)
{
if(LOG.isDebugEnabled())
{
LOG.debug("onError()", cause);
}
}
}

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
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.WebSocket;
@ -73,4 +74,10 @@ public class RFCSocket
if (remote.getBatchMode() == BatchMode.ON)
remote.flush();
}
@OnWebSocketError
public void onError(Throwable cause)
{
LOG.warn(cause);
}
}

View File

@ -28,6 +28,7 @@ import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
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.WebSocket;
@ -115,6 +116,15 @@ public class SessionSocket
}
}
@OnWebSocketError
public void onError(Throwable cause)
{
if(LOG.isDebugEnabled())
{
LOG.debug("onError()", cause);
}
}
protected void sendString(String text) throws IOException
{
RemoteEndpoint remote = session.getRemote();

View File

@ -18,10 +18,7 @@
package org.eclipse.jetty.websocket.server.misbehaving;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@ -42,6 +39,11 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
/**
* Testing badly behaving Socket class implementations to get the best
* error messages and state out of the websocket implementation.
@ -107,10 +109,14 @@ public class MisbehavingClassTest
assertThat("Close Latch",socket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("closeStatusCode",socket.closeStatusCode,is(StatusCode.SERVER_ERROR));
// Validate errors
assertThat("socket.onErrors",socket.errors.size(),is(1));
// Validate errors (must be "java.lang.RuntimeException: Intentional Exception from onWebSocketConnect")
assertThat("socket.onErrors",socket.errors.size(),greaterThanOrEqualTo(1));
Throwable cause = socket.errors.pop();
assertThat("Error type",cause,instanceOf(RuntimeException.class));
// ... with optional ClosedChannelException
cause = socket.errors.peek();
if(cause != null)
assertThat("Error type",cause,instanceOf(ClosedChannelException.class));
}
}
@ -126,7 +132,7 @@ public class MisbehavingClassTest
Future<BlockheadConnection> connFut = request.sendAsync();
try (StacklessLogging ignore = new StacklessLogging(AnnotatedRuntimeOnConnectSocket.class, WebSocketSession.class);
try (StacklessLogging ignore = new StacklessLogging(AnnotatedRuntimeOnConnectSocket.class /*, WebSocketSession.class*/);
BlockheadConnection clientConn = connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT))
{
LinkedBlockingQueue<WebSocketFrame> frames = clientConn.getFrameQueue();
@ -141,10 +147,14 @@ public class MisbehavingClassTest
assertThat("Close Latch",socket.closeLatch.await(1,TimeUnit.SECONDS),is(true));
assertThat("closeStatusCode",socket.closeStatusCode,is(StatusCode.SERVER_ERROR));
// Validate errors
assertThat("socket.onErrors",socket.errors.size(),is(1));
// Validate errors (must be "java.lang.RuntimeException: Intentional Exception from onWebSocketConnect")
assertThat("socket.onErrors",socket.errors.size(),greaterThanOrEqualTo(1));
Throwable cause = socket.errors.pop();
assertThat("Error type",cause,instanceOf(RuntimeException.class));
// ... with optional ClosedChannelException
cause = socket.errors.peek();
if(cause != null)
assertThat("Error type",cause,instanceOf(ClosedChannelException.class));
}
}
}

View File

@ -4,6 +4,7 @@ org.eclipse.jetty.LEVEL=WARN
# org.eclipse.jetty.io.WriteFlusher.LEVEL=DEBUG
# org.eclipse.jetty.websocket.LEVEL=DEBUG
# org.eclipse.jetty.websocket.LEVEL=INFO
# org.eclipse.jetty.websocket.common.message.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.io.LEVEL=DEBUG
# org.eclipse.jetty.websocket.server.ab.LEVEL=DEBUG
# org.eclipse.jetty.websocket.common.Parser.LEVEL=DEBUG

View File

@ -1,2 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.websocket.LEVEL=DEBUG