All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.ext.web.handler.sockjs.impl.SockJSSession Maven / Gradle / Ivy

There is a newer version: 5.0.0.CR1
Show newest version
/*
 * Copyright 2014 Red Hat, Inc.
 *
 *  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.
 */

/*
 * 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.vertx.ext.web.handler.sockjs.impl;

import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.impl.ContextInternal;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.core.net.SocketAddress;
import io.vertx.core.net.impl.ConnectionBase;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.core.shareddata.Shareable;
import io.vertx.core.streams.impl.InboundBuffer;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
import io.vertx.ext.web.handler.sockjs.SockJSSocket;

import java.util.*;

import static io.vertx.core.buffer.Buffer.buffer;

/**
 * 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 * @author Paulo Lopes */ class SockJSSession extends SockJSSocketBase implements Shareable { private static final Logger LOG = LoggerFactory.getLogger(SockJSSession.class); private final LocalMap sessions; private final Deque pendingWrites = new LinkedList<>(); private final Context context; private final InboundBuffer pendingReads; private final String id; private final long timeout; private final Handler sockHandler; private final long heartbeatID; private final List>> writeAcks = new ArrayList<>(); private TransportListener listener; private boolean closed; private boolean openWritten; private long timeoutTimerID = -1; 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 closeHandler; private Handler exceptionHandler; private boolean handleCalled; private SocketAddress localAddress; private SocketAddress remoteAddress; private String uri; private MultiMap headers; private Context transportCtx; SockJSSession(Vertx vertx, LocalMap sessions, RoutingContext rc, SockJSHandlerOptions options, Handler sockHandler) { this(vertx, sessions, rc, null, options, sockHandler); } SockJSSession(Vertx vertx, LocalMap sessions, RoutingContext rc, String id, SockJSHandlerOptions options, Handler sockHandler) { super(vertx, rc, options); this.sessions = sessions; this.id = id; this.timeout = id == null ? -1 : options.getSessionTimeout(); this.sockHandler = sockHandler; context = vertx.getOrCreateContext(); pendingReads = new InboundBuffer<>(context); // Start a heartbeat heartbeatID = vertx.setPeriodic(options.getHeartbeatInterval(), tid -> { if (listener != null) { listener.sendFrame("h", null); } }); } private void writeInternal(String msg, Promise promise) { synchronized (this) { pendingWrites.add(msg); messagesSize += msg.length(); writeAcks.add(promise); } if (listener != null) { final Context ctx = transportCtx; if (Vertx.currentContext() != ctx) { ctx.runOnContext(v -> writePendingMessages()); } else { writePendingMessages(); } } } @Override public Future write(Buffer buffer) { final ContextInternal callerCtx = (ContextInternal) vertx.getOrCreateContext(); final Promise promise = callerCtx.promise(); if (isClosed()) { final Context ctx = transportCtx; if (Vertx.currentContext() != ctx) { vertx.runOnContext(v -> promise.fail(ConnectionBase.CLOSED_EXCEPTION)); } else { promise.fail(ConnectionBase.CLOSED_EXCEPTION); } } else { final String msg = buffer.toString(); writeInternal(msg, promise); } return promise.future(); } @Override public Future write(String text) { final ContextInternal callerCtx = (ContextInternal) vertx.getOrCreateContext(); final Promise promise = callerCtx.promise(); if (isClosed()) { final Context ctx = transportCtx; if (Vertx.currentContext() != ctx) { vertx.runOnContext(v -> promise.fail(ConnectionBase.CLOSED_EXCEPTION)); } else { promise.fail(ConnectionBase.CLOSED_EXCEPTION); } } else { writeInternal(text, promise); } return promise.future(); } @Override public SockJSSession handler(Handler handler) { pendingReads.handler(handler); return this; } @Override public SockJSSession fetch(long amount) { pendingReads.fetch(amount); return this; } @Override public SockJSSession pause() { pendingReads.pause(); return this; } @Override public SockJSSession resume() { pendingReads.resume(); return this; } @Override public SockJSSession setWriteQueueMaxSize(int maxQueueSize) { if (maxQueueSize < 1) { throw new IllegalArgumentException("maxQueueSize must be >= 1"); } this.maxQueueSize = maxQueueSize; return this; } @Override public boolean writeQueueFull() { return messagesSize >= maxQueueSize; } @Override public SockJSSession drainHandler(Handler handler) { this.drainHandler = handler; return this; } @Override public SockJSSession exceptionHandler(Handler handler) { this.exceptionHandler = handler; return this; } @Override public SockJSSession endHandler(Handler endHandler) { this.endHandler = endHandler; return this; } @Override public SockJSSocket closeHandler(Handler closeHandler) { this.closeHandler = closeHandler; return this; } // 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 void close() { synchronized (this) { if (!closed) { closed = true; handleClosed(); } } doClose(); } private void doClose() { final Context ctx = transportCtx; if (ctx != Vertx.currentContext()) { ctx.runOnContext(v -> doClose()); } else { if (listener != null && handleCalled) { listener.sessionClosed(); } } } @Override public SocketAddress remoteAddress() { return remoteAddress; } @Override public SocketAddress localAddress() { return localAddress; } @Override public MultiMap headers() { return headers; } @Override public String uri() { return uri; } 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) { vertx.cancelTimer(timeoutTimerID); } } private void setTimer() { if (timeout != -1) { cancelTimer(); timeoutTimerID = vertx.setTimer(timeout, id1 -> { vertx.cancelTimer(heartbeatID); final TransportListener listener = this.listener; if (listener == null) { shutdown(); } if (listener != null) { listener.close(); } }); } } private void writePendingMessages() { final TransportListener listener = this.listener; if (listener != null) { final String json; final List>> acks; synchronized (this) { if (!pendingWrites.isEmpty()) { json = JsonCodec.encode(pendingWrites.toArray(new String[0])); pendingWrites.clear(); if (!writeAcks.isEmpty()) { acks = new ArrayList<>(writeAcks); writeAcks.clear(); } else { acks = Collections.emptyList(); } messagesSize = 0; } else { json = null; acks = Collections.emptyList(); } } if (json != null) { if (!acks.isEmpty()) { listener.sendFrame("a" + json, ar -> acks.forEach(a -> a.handle(ar))); } else { listener.sendFrame("a" + json, null); } } if (drainHandler != null) { Handler dh = drainHandler; drainHandler = null; context.runOnContext(dh); } } } Context context() { return transportCtx; } void register(HttpServerRequest req, TransportListener lst) { this.transportCtx = vertx.getOrCreateContext(); this.localAddress = req.localAddress(); this.remoteAddress = req.remoteAddress(); this.uri = req.uri(); this.headers = BaseTransport.removeCookieHeaders(req.headers()); 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. void shutdown() { super.close(); // We must call this or handlers don't get unregistered and we get a leak if (heartbeatID != -1) { vertx.cancelTimer(heartbeatID); } if (timeoutTimerID != -1) { vertx.cancelTimer(timeoutTimerID); } if (id != null) { // Can be null if websocket session sessions.remove(id); } synchronized (this) { if (!closed) { closed = true; handleClosed(); } } } private void handleClosed() { synchronized (this) { pendingReads.clear(); pendingWrites.clear(); writeAcks.forEach(handler -> context.runOnContext(v -> handler.handle(Future.failedFuture(ConnectionBase.CLOSED_EXCEPTION)))); writeAcks.clear(); } Handler handler = endHandler; if (handler != null) { context.runOnContext(handler); } handler = closeHandler; if (handler != null) { context.runOnContext(handler); } } boolean handleMessages(String messages) { List msgList = JsonCodec.decodeValues(messages); if (msgList == null) { return false; } handleMessages(msgList); return true; } private void handleMessages(List messages) { if (context == Vertx.currentContext()) { for (String msg : messages) { pendingReads.write(buffer(msg)); } } else { context.runOnContext(v -> handleMessages(messages)); } } void handleException(Throwable t) { final Handler eh = exceptionHandler; if (eh != null) { if (context == Vertx.currentContext()) { eh.handle(t); } else { context.runOnContext(v -> handleException(t)); } } else { LOG.error("Unhandled exception", t); } } void writeClosed(TransportListener lst) { writeClosed(lst, 3000, "Go away!"); } private void writeClosed(TransportListener lst, int code, String msg) { String sb = "c[" + code + ",\"" + msg + "\"]"; lst.sendFrame(sb, null); } private void writeOpen(TransportListener lst) { lst.sendFrame("o", null); openWritten = true; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy