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:
parent
f88f856673
commit
8dba440317
|
@ -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,7 +51,10 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint
|
|||
{
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Unexpected implementation [");
|
||||
err.append(session.getRemote().getClass().getName());
|
||||
if(session.getRemote() == null)
|
||||
err.append("<null>");
|
||||
else
|
||||
err.append(session.getRemote().getClass().getName());
|
||||
err.append("]. Expected an instanceof [");
|
||||
err.append(WebSocketRemoteEndpoint.class.getName());
|
||||
err.append("]");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -160,7 +160,7 @@ public abstract class JsrCallable extends CallableMethod
|
|||
}
|
||||
catch (DecodeException e)
|
||||
{
|
||||
session.notifyError(e);
|
||||
session.close(e);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
|
@ -46,42 +69,52 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
|
|||
* @return the buffer pool
|
||||
*/
|
||||
ByteBufferPool getBufferPool();
|
||||
|
||||
|
||||
/**
|
||||
* Get the Executor used by this connection.
|
||||
* @return the executor
|
||||
*/
|
||||
Executor getExecutor();
|
||||
|
||||
/**
|
||||
* Get Unique ID for the Connection
|
||||
* @return the unique ID for the connection
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Get the read/write idle timeout.
|
||||
*
|
||||
*
|
||||
* @return the idle timeout in milliseconds
|
||||
*/
|
||||
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>
|
||||
* Note: Non-physical connections, like during the Mux extensions, or during unit testing can result in a InetSocketAddress on port 0 and/or on localhost.
|
||||
*
|
||||
*
|
||||
* @return the local address.
|
||||
*/
|
||||
InetSocketAddress getLocalAddress();
|
||||
|
||||
|
||||
/**
|
||||
* Set the maximum number of milliseconds of idleness before the connection is closed/disconnected, (ie no frames are either sent or received)
|
||||
* @return the idle timeout in milliseconds
|
||||
*/
|
||||
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
|
||||
|
@ -92,35 +125,47 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
|
|||
* Get the remote Address in use for this connection.
|
||||
* <p>
|
||||
* Note: Non-physical connections, like during the Mux extensions, or during unit testing can result in a InetSocketAddress on port 0 and/or on localhost.
|
||||
*
|
||||
*
|
||||
* @return the remote address.
|
||||
*/
|
||||
InetSocketAddress getRemoteAddress();
|
||||
|
||||
/**
|
||||
* Test if logical connection is still open
|
||||
*
|
||||
*
|
||||
* @return true if connection is open
|
||||
*/
|
||||
boolean isOpen();
|
||||
|
||||
/**
|
||||
* Tests if the connection is actively reading.
|
||||
*
|
||||
*
|
||||
* @return true if connection is actively attempting to read.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param ms
|
||||
* the number of milliseconds of idle timeout
|
||||
* Reads from network begin here.
|
||||
* </p>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
|
|
@ -212,6 +212,8 @@ public class Parser
|
|||
|
||||
if (incomingFramesHandler == null)
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("No IncomingFrames Handler to notify");
|
||||
return;
|
||||
}
|
||||
try
|
||||
|
|
|
@ -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));
|
||||
|
@ -302,18 +301,10 @@ 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);
|
||||
}
|
||||
BatchMode batchMode = BatchMode.OFF;
|
||||
if (frame.isDataFrame())
|
||||
batchMode = getBatchMode();
|
||||
outgoing.outgoingFrame(frame, callback, batchMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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();
|
||||
|
||||
// Connect remote
|
||||
remote = remoteEndpointFactory.newRemoteEndpoint(connection,outgoingHandler,getBatchMode());
|
||||
if(LOG_OPEN.isDebugEnabled())
|
||||
LOG_OPEN.debug("[{}] {}.open() remote={}",policy.getBehavior(),this.getClass().getSimpleName(),remote);
|
||||
|
||||
// Open WebSocket
|
||||
websocket.openSession(this);
|
||||
|
||||
// Open connection
|
||||
connection.getIOState().onOpened();
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
if(connection.opening())
|
||||
{
|
||||
LOG.debug("[{}] open -> {}",getPolicy().getBehavior(),dump());
|
||||
// Connect remote
|
||||
remote = remoteEndpointFactory.newRemoteEndpoint(connection, outgoingHandler, getBatchMode());
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("[{}] {}.open() remote={}", policy.getBehavior(), this.getClass().getSimpleName(), remote);
|
||||
|
||||
// Open WebSocket - and call Application onOpen
|
||||
websocket.openSession(this);
|
||||
|
||||
// Open connection
|
||||
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());
|
||||
}
|
||||
|
||||
if (openFuture != null)
|
||||
{
|
||||
openFuture.complete(this);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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,22 +173,112 @@ 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 (close.isAbnormal())
|
||||
if (connectionState.closing())
|
||||
{
|
||||
ioState.onAbnormalClose(close);
|
||||
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 (StatusCode.isFatal(close.getStatusCode()))
|
||||
{
|
||||
fatalCloseInfo = close;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ioState.onCloseLocal(close);
|
||||
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,20 +298,35 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
|
|||
@Override
|
||||
public void close()
|
||||
{
|
||||
session.close();
|
||||
close(new CloseInfo(), Callback.NOOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} disconnect()",policy.getBehavior());
|
||||
flusher.terminate(new EOFException("Disconnected"), false);
|
||||
EndPoint endPoint = getEndPoint();
|
||||
// We need to gently close first, to allow
|
||||
// SSL close alerts to be sent by Jetty
|
||||
endPoint.shutdownOutput();
|
||||
endPoint.close();
|
||||
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"));
|
||||
EndPoint endPoint = getEndPoint();
|
||||
// We need to gently close first, to allow
|
||||
// SSL close alerts to be sent by Jetty
|
||||
endPoint.shutdownOutput();
|
||||
endPoint.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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,9 +444,13 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
|
|||
}
|
||||
|
||||
if (readMode == ReadMode.EOF)
|
||||
{
|
||||
readState.eof();
|
||||
}
|
||||
else if (!readState.suspend())
|
||||
{
|
||||
fillInterested();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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,7 +588,9 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
|
|||
public void resume()
|
||||
{
|
||||
if (readState.resume())
|
||||
{
|
||||
fillInterested();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -587,8 +598,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
|
|||
* <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,11 +663,11 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
setInitialBuffer(prefilled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
byte opCode = frame.getOpCode();
|
||||
if (opCode == OpCode.PING || opCode == OpCode.PONG)
|
||||
queue.offerFirst(entry);
|
||||
else
|
||||
queue.offerLast(entry);
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
@ -72,16 +73,22 @@ public class DelegatedContainerScope implements WebSocketContainerScope
|
|||
{
|
||||
return this.delegate.isRunning();
|
||||
}
|
||||
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -141,16 +149,22 @@ public class SimpleContainerScope extends ContainerLifeCycle implements WebSocke
|
|||
{
|
||||
this.sslContextFactory = sslContextFactory;
|
||||
}
|
||||
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
@ -72,18 +74,10 @@ public interface WebSocketContainerScope
|
|||
* @return true if container is started and running
|
||||
*/
|
||||
boolean isRunning();
|
||||
|
||||
/**
|
||||
* A Session has been opened
|
||||
*
|
||||
* @param session the session that was opened
|
||||
*/
|
||||
void onSessionOpened(WebSocketSession session);
|
||||
|
||||
/**
|
||||
* A Session has been closed
|
||||
*
|
||||
* @param session the session that was closed
|
||||
*/
|
||||
void onSessionClosed(WebSocketSession session);
|
||||
|
||||
void addSessionListener(WebSocketSessionListener listener);
|
||||
|
||||
void removeSessionListener(WebSocketSessionListener listener);
|
||||
|
||||
Collection<WebSocketSessionListener> getSessionListeners();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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,14 +171,14 @@ 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();
|
||||
callback.succeeded();
|
||||
}
|
||||
catch (WritePendingException e)
|
||||
{
|
||||
|
@ -144,8 +186,7 @@ public class FrameFlusherTest
|
|||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
if (callback != null)
|
||||
callback.failed(t);
|
||||
callback.failed(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */ }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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,18 +182,27 @@ 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
|
||||
public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
|
@ -516,31 +523,14 @@ 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
|
||||
public void register(Class<?> websocketPojo)
|
||||
{
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue