
io.jsync.sockjs.impl.Session Maven / Gradle / Ivy
Show all versions of jsync.io Show documentation
/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* 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 io.jsync.sockjs.impl;
import io.jsync.Handler;
import io.jsync.MultiMap;
import io.jsync.buffer.Buffer;
import io.jsync.impl.AsyncInternal;
import io.jsync.json.DecodeException;
import io.jsync.logging.Logger;
import io.jsync.logging.impl.LoggerFactory;
import io.jsync.shareddata.Shareable;
import io.jsync.sockjs.SockJSSocket;
import java.net.InetSocketAddress;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
/**
* The SockJS session implementation.
*
* If multiple instances of the SockJS server are used then instances of this
* class can be accessed by different threads (not concurrently), so we store
* it in a shared data map
*
* @author Tim Fox
*/
class Session extends SockJSSocketBase implements Shareable {
private static final Logger log = LoggerFactory.getLogger(Session.class);
private final Map sessions;
private final Queue pendingWrites = new LinkedList<>();
private final Queue pendingReads = new LinkedList<>();
private final String id;
private final long timeout;
private final Handler sockHandler;
private TransportListener listener;
private Handler dataHandler;
private boolean closed;
private boolean openWritten;
private long heartbeatID = -1;
private long timeoutTimerID = -1;
private boolean paused;
private int maxQueueSize = 64 * 1024; // Message queue size is measured in *characters* (not bytes)
private int messagesSize;
private Handler drainHandler;
private Handler endHandler;
private Handler exceptionHandler;
private boolean handleCalled;
private InetSocketAddress localAddress;
private InetSocketAddress remoteAddress;
private String uri;
private MultiMap headers;
Session(AsyncInternal async, Map sessions, long heartbeatPeriod,
Handler sockHandler) {
this(async, sessions, null, -1, heartbeatPeriod, sockHandler);
}
Session(AsyncInternal async, Map sessions, String id, long timeout, long heartbeatPeriod,
Handler sockHandler) {
super(async);
this.sessions = sessions;
this.id = id;
this.timeout = timeout;
this.sockHandler = sockHandler;
// Start a heartbeat
heartbeatID = async.setPeriodic(heartbeatPeriod, new Handler() {
public void handle(Long id) {
if (listener != null) {
listener.sendFrame("h");
}
}
});
}
@Override
public synchronized SockJSSocket write(Buffer buffer) {
String msgStr = buffer.toString();
pendingWrites.add(msgStr);
this.messagesSize += msgStr.length();
if (listener != null) {
writePendingMessages();
}
return this;
}
@Override
public synchronized Session dataHandler(Handler handler) {
this.dataHandler = handler;
return this;
}
@Override
public synchronized Session pause() {
paused = true;
return this;
}
@Override
public synchronized Session resume() {
paused = false;
if (dataHandler != null) {
for (String msg : this.pendingReads) {
dataHandler.handle(new Buffer(msg));
}
}
return this;
}
@Override
public synchronized Session setWriteQueueMaxSize(int maxQueueSize) {
if (maxQueueSize < 1) {
throw new IllegalArgumentException("maxQueueSize must be >= 1");
}
this.maxQueueSize = maxQueueSize;
return this;
}
@Override
public synchronized boolean writeQueueFull() {
return messagesSize >= maxQueueSize;
}
@Override
public synchronized Session drainHandler(Handler handler) {
this.drainHandler = handler;
return this;
}
@Override
public synchronized Session exceptionHandler(Handler handler) {
this.exceptionHandler = handler;
return this;
}
@Override
public synchronized Session endHandler(Handler endHandler) {
this.endHandler = endHandler;
return this;
}
public synchronized void shutdown() {
doClose();
}
// When the user calls close() we don't actually close the session - unless it's a websocket one
// Yes, SockJS is weird, but it's hard to work out expected server behaviour when there's no spec
@Override
public synchronized void close() {
if (endHandler != null) {
endHandler.handle(null);
}
closed = true;
if (listener != null && handleCalled) {
listener.sessionClosed();
}
}
@Override
public InetSocketAddress remoteAddress() {
return remoteAddress;
}
@Override
public InetSocketAddress localAddress() {
return localAddress;
}
@Override
public MultiMap headers() {
return headers;
}
@Override
public String uri() {
return uri;
}
synchronized boolean isClosed() {
return closed;
}
synchronized void resetListener() {
listener = null;
// We set a timer that will kick in and close the session if the client doesn't come back
// We MUST ALWAYS do this or we can get a memory leak on the server
setTimer();
}
private void cancelTimer() {
if (timeoutTimerID != -1) {
async.cancelTimer(timeoutTimerID);
}
}
private void setTimer() {
if (timeout != -1) {
cancelTimer();
timeoutTimerID = async.setTimer(timeout, new Handler() {
public void handle(Long id) {
async.cancelTimer(heartbeatID);
if (listener == null) {
shutdown();
}
if (listener != null) {
listener.close();
}
}
});
}
}
synchronized void writePendingMessages() {
String json = JsonCodec.encode(pendingWrites.toArray());
listener.sendFrame("a" + json);
pendingWrites.clear();
messagesSize = 0;
if (drainHandler != null && messagesSize <= maxQueueSize / 2) {
Handler dh = drainHandler;
drainHandler = null;
dh.handle(null);
}
}
synchronized void register(final TransportListener lst) {
if (closed) {
// Closed by the application
writeClosed(lst);
// And close the listener request
lst.close();
} else if (this.listener != null) {
writeClosed(lst, 2010, "Another connection still open");
// And close the listener request
lst.close();
} else {
cancelTimer();
this.listener = lst;
if (!openWritten) {
writeOpen(lst);
sockHandler.handle(this);
handleCalled = true;
}
if (listener != null) {
if (closed) {
// Could have already been closed by the user
writeClosed(lst);
listener = null;
lst.close();
} else {
if (!pendingWrites.isEmpty()) {
writePendingMessages();
}
}
}
}
}
// Actually close the session - when the user calls close() the session actually continues to exist until timeout
// Yes, I know it's weird but that's the way SockJS likes it.
private void doClose() {
super.close(); // We must call this or handlers don't get unregistered and we get a leak
if (heartbeatID != -1) {
async.cancelTimer(heartbeatID);
}
if (timeoutTimerID != -1) {
async.cancelTimer(timeoutTimerID);
}
if (id != null) {
// Can be null if websocket session
sessions.remove(id);
}
if (endHandler != null) {
endHandler.handle(null);
}
}
private String[] parseMessageString(String msgs) {
try {
String[] parts;
if (msgs.startsWith("[")) {
//JSON array
parts = (String[]) JsonCodec.decodeValue(msgs, String[].class);
} else {
//JSON string
String str = (String) JsonCodec.decodeValue(msgs, String.class);
parts = new String[]{str};
}
return parts;
} catch (DecodeException e) {
return null;
}
}
boolean handleMessages(String messages) {
String[] msgArr = parseMessageString(messages);
if (msgArr == null) {
return false;
} else {
if (dataHandler != null) {
for (String msg : msgArr) {
if (!paused) {
try {
dataHandler.handle(new Buffer(msg));
} catch (Throwable t) {
log.error("Unhandle exception", t);
}
} else {
pendingReads.add(msg);
}
}
}
return true;
}
}
void handleException(Throwable t) {
if (exceptionHandler != null) {
exceptionHandler.handle(t);
} else {
log.error("Unhandled exception", t);
}
}
public void writeClosed(TransportListener lst) {
writeClosed(lst, 3000, "Go away!");
}
private void writeClosed(TransportListener lst, int code, String msg) {
StringBuilder sb = new StringBuilder("c[");
sb.append(String.valueOf(code)).append(",\"");
sb.append(msg).append("\"]");
lst.sendFrame(sb.toString());
}
private void writeOpen(TransportListener lst) {
StringBuilder sb = new StringBuilder("o");
lst.sendFrame(sb.toString());
openWritten = true;
}
void setInfo(InetSocketAddress localAddress, InetSocketAddress remoteAddress, String uri,
MultiMap headers) {
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
this.uri = uri;
this.headers = BaseTransport.removeCookieHeaders(headers);
}
}