/** * The MIT License * Copyright (c) 2010 Tad Glines * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.glines.socketio.server; import java.security.SecureRandom; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.eclipse.jetty.util.log.Log; import com.glines.socketio.common.ConnectionState; import com.glines.socketio.common.DisconnectReason; import com.glines.socketio.common.SocketIOException; class SocketIOSessionManager implements SocketIOSession.Factory { private static final char[] BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" .toCharArray(); private static final int SESSION_ID_LENGTH = 20; private static Random random = new SecureRandom(); private ConcurrentMap<String, SocketIOSession> socketIOSessions = new ConcurrentHashMap<String, SocketIOSession>(); private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); private static String generateRandomString(int length) { StringBuilder result = new StringBuilder(length); byte[] bytes = new byte[length]; random.nextBytes(bytes); for (int i = 0; i < bytes.length; i++) { result.append(BASE64_ALPHABET[bytes[i] & 0x3F]); } return result.toString(); } private class SessionImpl implements SocketIOSession { private final String sessionId; private SocketIOInbound inbound; private SessionTransportHandler handler = null; private ConnectionState state = ConnectionState.CONNECTING; private long hbDelay = 0; private SessionTask hbDelayTask = null; private long timeout = 0; private SessionTask timeoutTask = null; private boolean timedout = false; private AtomicLong messageId = new AtomicLong(0); private String closeId = null; SessionImpl(String sessionId, SocketIOInbound inbound) { this.sessionId = sessionId; this.inbound = inbound; } @Override public String generateRandomString(int length) { return SocketIOSessionManager.generateRandomString(length); } @Override public String getSessionId() { return sessionId; } @Override public ConnectionState getConnectionState() { return state; } @Override public SocketIOInbound getInbound() { return inbound; } @Override public SessionTransportHandler getTransportHandler() { return handler; } private void onTimeout() { Log.debug("Session["+sessionId+"]: onTimeout"); if (!timedout) { timedout = true; state = ConnectionState.CLOSED; onDisconnect(DisconnectReason.TIMEOUT); handler.abort(); } } @Override public void startTimeoutTimer() { clearTimeoutTimer(); if (!timedout && timeout > 0) { timeoutTask = scheduleTask(new Runnable() { @Override public void run() { SessionImpl.this.onTimeout(); } }, timeout); } } @Override public void clearTimeoutTimer() { if (timeoutTask != null) { timeoutTask.cancel(); timeoutTask = null; } } private void sendPing() { String data = "" + messageId.incrementAndGet(); Log.debug("Session["+sessionId+"]: sendPing " + data); try { handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.PING, 0, data)); } catch (SocketIOException e) { Log.debug("handler.sendMessage failed: ", e); handler.abort(); } startTimeoutTimer(); } @Override public void startHeartbeatTimer() { clearHeartbeatTimer(); if (!timedout && hbDelay > 0) { hbDelayTask = scheduleTask(new Runnable() { @Override public void run() { sendPing(); } }, hbDelay); } } @Override public void clearHeartbeatTimer() { if (hbDelayTask != null) { hbDelayTask.cancel(); hbDelayTask = null; } } @Override public void setHeartbeat(long delay) { hbDelay = delay; } @Override public long getHeartbeat() { return hbDelay; } @Override public void setTimeout(long timeout) { this.timeout = timeout; } @Override public long getTimeout() { return timeout; } @Override public void startClose() { state = ConnectionState.CLOSING; closeId = "server"; try { handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, closeId)); } catch (SocketIOException e) { Log.debug("handler.sendMessage failed: ", e); handler.abort(); } } @Override public void onMessage(SocketIOFrame message) { switch (message.getFrameType()) { case SESSION_ID: case HEARTBEAT_INTERVAL: // Ignore these two messages types as they are only intended to be from server to client. break; case CLOSE: Log.debug("Session["+sessionId+"]: onClose: " + message.getData()); onClose(message.getData()); break; case PING: Log.debug("Session["+sessionId+"]: onPing: " + message.getData()); onPing(message.getData()); break; case PONG: Log.debug("Session["+sessionId+"]: onPong: " + message.getData()); onPong(message.getData()); break; case DATA: Log.debug("Session["+sessionId+"]: onMessage: " + message.getData()); onMessage(message.getData()); break; default: // Ignore unknown message types break; } } @Override public void onPing(String data) { try { handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.PONG, 0, data)); } catch (SocketIOException e) { Log.debug("handler.sendMessage failed: ", e); handler.abort(); } } @Override public void onPong(String data) { clearTimeoutTimer(); } @Override public void onClose(String data) { if (state == ConnectionState.CLOSING) { if (closeId != null && closeId.equals(data)) { state = ConnectionState.CLOSED; onDisconnect(DisconnectReason.CLOSED); handler.abort(); } else { try { handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data)); } catch (SocketIOException e) { Log.debug("handler.sendMessage failed: ", e); handler.abort(); } } } else { state = ConnectionState.CLOSING; try { handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data)); handler.disconnectWhenEmpty(); } catch (SocketIOException e) { Log.debug("handler.sendMessage failed: ", e); handler.abort(); } } } @Override public SessionTask scheduleTask(Runnable task, long delay) { final Future<?> future = executor.schedule(task, delay, TimeUnit.MILLISECONDS); return new SessionTask() { @Override public boolean cancel() { return future.cancel(false); } }; } @Override public void onConnect(SessionTransportHandler handler) { if (handler == null) { state = ConnectionState.CLOSED; inbound = null; socketIOSessions.remove(sessionId); } else if (this.handler == null) { this.handler = handler; try { state = ConnectionState.CONNECTED; inbound.onConnect(handler); } catch (Throwable e) { Log.warn("Session["+sessionId+"]: Exception thrown by SocketIOInbound.onConnect()", e); state = ConnectionState.CLOSED; handler.abort(); } } else { handler.abort(); } } @Override public void onMessage(String message) { if (inbound != null) { try { inbound.onMessage(SocketIOFrame.TEXT_MESSAGE_TYPE, message); } catch (Throwable e) { Log.warn("Session["+sessionId+"]: Exception thrown by SocketIOInbound.onMessage()", e); } } } @Override public void onDisconnect(DisconnectReason reason) { Log.debug("Session["+sessionId+"]: onDisconnect: " + reason); clearTimeoutTimer(); clearHeartbeatTimer(); if (inbound != null) { state = ConnectionState.CLOSED; try { inbound.onDisconnect(reason, null); } catch (Throwable e) { Log.warn("Session["+sessionId+"]: Exception thrown by SocketIOInbound.onDisconnect()", e); } inbound = null; } } @Override public void onShutdown() { Log.debug("Session["+sessionId+"]: onShutdown"); if (inbound != null) { if (state == ConnectionState.CLOSING) { if (closeId != null) { onDisconnect(DisconnectReason.CLOSE_FAILED); } else { onDisconnect(DisconnectReason.CLOSED_REMOTELY); } } else { onDisconnect(DisconnectReason.ERROR); } } socketIOSessions.remove(sessionId); } } private String generateSessionId() { return generateRandomString(SESSION_ID_LENGTH); } @Override public SocketIOSession createSession(SocketIOInbound inbound) { SessionImpl impl = new SessionImpl(generateSessionId(), inbound); socketIOSessions.put(impl.getSessionId(), impl); return impl; } @Override public SocketIOSession getSession(String sessionId) { return socketIOSessions.get(sessionId); } }