diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java index 6130fe925c..9a2d06b11d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.web.socket; +import java.io.Closeable; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; @@ -32,7 +33,7 @@ import org.springframework.http.HttpHeaders; * @author Rossen Stoyanchev * @since 4.0 */ -public interface WebSocketSession { +public interface WebSocketSession extends Closeable { /** * Return a unique session identifier. @@ -51,12 +52,10 @@ public interface WebSocketSession { /** * Return the map with attributes associated with the WebSocket session. - * *
When the WebSocketSession is created, on the server side, the map can be
* through a {@link org.springframework.web.socket.server.HandshakeInterceptor}.
* On the client side, the map can be populated by passing attributes to the
- * {@link org.springframework.web.socket.client.WebSocketClient} handshake
- * methods.
+ * {@link org.springframework.web.socket.client.WebSocketClient} handshake methods.
*/
Map The Servlet API does not provide notifications when a client disconnects;
* see SERVLET_SPEC-44.
* Therefore network IO failures may occur simply because a client has gone away,
* and that can fill the logs with unnecessary stack traces.
- *
* We make a best effort to identify such network failures, on a per-server
* basis, and log them under a separate log category. A simple one-line message
* is logged at DEBUG level, while a full stack trace is shown at TRACE level.
- *
* @see #disconnectedClientLogger
*/
public static final String DISCONNECTED_CLIENT_LOG_CATEGORY =
@@ -79,6 +76,7 @@ public abstract class AbstractSockJsSession implements SockJsSession {
*/
protected static final Log disconnectedClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY);
+
private static final Set Perform cleanup and notify the {@link WebSocketHandler}.
+ * Performs cleanup and notify the {@link WebSocketHandler}.
*/
@Override
public final void close() throws IOException {
@@ -263,8 +191,7 @@ public abstract class AbstractSockJsSession implements SockJsSession {
}
/**
- * {@inheritDoc}
- * Perform cleanup and notify the {@link WebSocketHandler}.
+ * Performs cleanup and notify the {@link WebSocketHandler}.
*/
@Override
public final void close(CloseStatus status) throws IOException {
@@ -297,86 +224,28 @@ public abstract class AbstractSockJsSession implements SockJsSession {
}
}
- /**
- * Actually close the underlying WebSocket session or in the case of HTTP
- * transports complete the underlying request.
- */
- protected abstract void disconnect(CloseStatus status) throws IOException;
-
- /**
- * Close due to error arising from SockJS transport handling.
- */
- public void tryCloseWithSockJsTransportError(Throwable error, CloseStatus closeStatus) {
- if (logger.isDebugEnabled()) {
- logger.debug("Closing due to transport error for " + this);
- }
- try {
- delegateError(error);
- }
- catch (Throwable delegateException) {
- // ignore
- }
- try {
- close(closeStatus);
- }
- catch (Throwable closeException) {
- logger.debug("Failure while closing " + this, closeException);
- }
- }
-
- /**
- * For internal use within a TransportHandler and the (TransportHandler-specific)
- * session class.
- */
- protected void writeFrame(SockJsFrame frame) throws SockJsTransportFailureException {
- if (logger.isTraceEnabled()) {
- logger.trace("Preparing to write " + frame);
- }
- try {
- writeFrameInternal(frame);
- }
- catch (Throwable ex) {
- logWriteFrameFailure(ex);
- try {
- // Force disconnect (so we won't try to send close frame)
- disconnect(CloseStatus.SERVER_ERROR);
- }
- catch (Throwable disconnectFailure) {
- // Ignore
- }
- try {
- close(CloseStatus.SERVER_ERROR);
- }
- catch (Throwable t) {
- // Nothing of consequence, already forced disconnect
- }
- throw new SockJsTransportFailureException("Failed to write " + frame, this.getId(), ex);
- }
- }
-
- private void logWriteFrameFailure(Throwable failure) {
-
- @SuppressWarnings("serial")
- NestedCheckedException nestedException = new NestedCheckedException("", failure) {};
-
- if ("Broken pipe".equalsIgnoreCase(nestedException.getMostSpecificCause().getMessage()) ||
- disconnectedClientExceptions.contains(failure.getClass().getSimpleName())) {
-
- if (disconnectedClientLogger.isTraceEnabled()) {
- disconnectedClientLogger.trace("Looks like the client has gone away", failure);
- }
- else if (disconnectedClientLogger.isDebugEnabled()) {
- disconnectedClientLogger.debug("Looks like the client has gone away: " +
- nestedException.getMessage() + " (For full stack trace, set the '" +
- DISCONNECTED_CLIENT_LOG_CATEGORY + "' log category to TRACE level)");
- }
+ @Override
+ public long getTimeSinceLastActive() {
+ if (isNew()) {
+ return (System.currentTimeMillis() - this.timeCreated);
}
else {
- logger.debug("Terminating connection after failure to send message to client.", failure);
+ return (isActive() ? 0 : System.currentTimeMillis() - this.timeLastActive);
}
}
- protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException;
+ /**
+ * Should be invoked whenever the session becomes inactive.
+ */
+ protected void updateLastActiveTime() {
+ this.timeLastActive = System.currentTimeMillis();
+ }
+
+ @Override
+ public void disableHeartbeat() {
+ this.heartbeatDisabled = true;
+ cancelHeartbeat();
+ }
public void sendHeartbeat() throws SockJsTransportFailureException {
if (isActive()) {
@@ -389,11 +258,13 @@ public abstract class AbstractSockJsSession implements SockJsSession {
if (this.heartbeatDisabled) {
return;
}
- Assert.state(this.config.getTaskScheduler() != null, "Expecteded SockJS TaskScheduler.");
+
+ Assert.state(this.config.getTaskScheduler() != null, "Expected SockJS TaskScheduler");
cancelHeartbeat();
if (!isActive()) {
return;
}
+
Date time = new Date(System.currentTimeMillis() + this.config.getHeartbeatTime());
this.heartbeatTask = this.config.getTaskScheduler().schedule(new Runnable() {
public void run() {
@@ -427,12 +298,150 @@ public abstract class AbstractSockJsSession implements SockJsSession {
}
}
+ /**
+ * Polling and Streaming sessions periodically close the current HTTP request and
+ * wait for the next request to come through. During this "downtime" the session is
+ * still open but inactive and unable to send messages and therefore has to buffer
+ * them temporarily. A WebSocket session by contrast is stateful and remain active
+ * until closed.
+ */
+ public abstract boolean isActive();
+
+ /**
+ * Actually close the underlying WebSocket session or in the case of HTTP
+ * transports complete the underlying request.
+ */
+ protected abstract void disconnect(CloseStatus status) throws IOException;
+
+
+ // Frame writing
+
+ /**
+ * For internal use within a TransportHandler and the (TransportHandler-specific)
+ * session class.
+ */
+ protected void writeFrame(SockJsFrame frame) throws SockJsTransportFailureException {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Preparing to write " + frame);
+ }
+ try {
+ writeFrameInternal(frame);
+ }
+ catch (Throwable ex) {
+ logWriteFrameFailure(ex);
+ try {
+ // Force disconnect (so we won't try to send close frame)
+ disconnect(CloseStatus.SERVER_ERROR);
+ }
+ catch (Throwable disconnectFailure) {
+ // Ignore
+ }
+ try {
+ close(CloseStatus.SERVER_ERROR);
+ }
+ catch (Throwable closeFailure) {
+ // Nothing of consequence, already forced disconnect
+ }
+ throw new SockJsTransportFailureException("Failed to write " + frame, this.getId(), ex);
+ }
+ }
+
+ private void logWriteFrameFailure(Throwable failure) {
+ @SuppressWarnings("serial")
+ NestedCheckedException nestedException = new NestedCheckedException("", failure) {};
+
+ if ("Broken pipe".equalsIgnoreCase(nestedException.getMostSpecificCause().getMessage()) ||
+ disconnectedClientExceptions.contains(failure.getClass().getSimpleName())) {
+
+ if (disconnectedClientLogger.isTraceEnabled()) {
+ disconnectedClientLogger.trace("Looks like the client has gone away", failure);
+ }
+ else if (disconnectedClientLogger.isDebugEnabled()) {
+ disconnectedClientLogger.debug("Looks like the client has gone away: " +
+ nestedException.getMessage() + " (For full stack trace, set the '" +
+ DISCONNECTED_CLIENT_LOG_CATEGORY + "' log category to TRACE level)");
+ }
+ }
+ else {
+ logger.debug("Terminating connection after failure to send message to client.", failure);
+ }
+ }
+
+ protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException;
+
+
+ // Delegation methods
+
+ public void delegateConnectionEstablished() throws Exception {
+ this.state = State.OPEN;
+ this.handler.afterConnectionEstablished(this);
+ }
+
+ public void delegateMessages(String... messages) throws SockJsMessageDeliveryException {
+ List
* session.close(CloseStatus.NORMAL);
*
*/
+ @Override
void close() throws IOException;
/**
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java
index 5495bfdf41..da3adeac65 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java
@@ -53,21 +53,18 @@ import org.springframework.web.socket.sockjs.transport.SockJsSession;
*/
public abstract class AbstractSockJsSession implements SockJsSession {
- protected final Log logger = LogFactory.getLog(getClass());
+ private static enum State {NEW, OPEN, CLOSED}
/**
* Log category to use on network IO exceptions after a client has gone away.
- *
*