dorkbox.network.connection.ConnectionManager Maven / Gradle / Ivy
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection;
import com.esotericsoftware.kryo.util.IdentityMap;
import dorkbox.network.connection.bridge.ConnectionBridgeServer;
import dorkbox.network.connection.bridge.ConnectionExceptSpecifiedBridgeServer;
import dorkbox.network.connection.listenerManagement.OnConnectedManager;
import dorkbox.network.connection.listenerManagement.OnDisconnectedManager;
import dorkbox.network.connection.listenerManagement.OnIdleManager;
import dorkbox.network.connection.listenerManagement.OnMessageReceivedManager;
import dorkbox.util.ClassHelper;
import dorkbox.util.Property;
import dorkbox.util.collections.ConcurrentEntry;
import org.slf4j.Logger;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
// .equals() compares the identity on purpose,this because we cannot create two separate objects that are somehow equal to each other.
@SuppressWarnings("unchecked")
public
class ConnectionManager implements ListenerBridge, ISessionManager, ConnectionPoint, ConnectionBridgeServer,
ConnectionExceptSpecifiedBridgeServer {
/**
* Specifies the load-factor for the IdentityMap used to manage keeping track of the number of connections + listeners
*/
@Property
public static final float LOAD_FACTOR = 0.8F;
private final String loggerName;
private final OnConnectedManager onConnectedManager;
private final OnDisconnectedManager onDisconnectedManager;
private final OnIdleManager onIdleManager;
private final OnMessageReceivedManager onMessageReceivedManager;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private volatile ConcurrentEntry connectionsHead = null; // reference to the first element
// This is ONLY touched by a single thread, maintains a map of entries for FAST lookup during connection remove.
private final IdentityMap connectionEntries = new IdentityMap(32, ConnectionManager.LOAD_FACTOR);
@SuppressWarnings("unused")
private volatile IdentityMap> localManagers = new IdentityMap>(8, ConnectionManager.LOAD_FACTOR);
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
private final Object singleWriterLock2 = new Object();
private final Object singleWriterLock3 = new Object();
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater localManagersREF =
AtomicReferenceFieldUpdater.newUpdater(ConnectionManager.class,
IdentityMap.class,
"localManagers");
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater connectionsREF =
AtomicReferenceFieldUpdater.newUpdater(ConnectionManager.class,
ConcurrentEntry.class,
"connectionsHead");
/**
* Used by the listener subsystem to determine types.
*/
private final Class> baseClass;
protected final org.slf4j.Logger logger;
private final AtomicBoolean hasAddedAtLeastOnce = new AtomicBoolean(false);
final AtomicBoolean shutdown = new AtomicBoolean(false);
ConnectionManager(final String loggerName, final Class> baseClass) {
this.loggerName = loggerName;
this.logger = org.slf4j.LoggerFactory.getLogger(loggerName);
this.baseClass = baseClass;
onConnectedManager = new OnConnectedManager(logger);
onDisconnectedManager = new OnDisconnectedManager(logger);
onIdleManager = new OnIdleManager(logger);
onMessageReceivedManager = new OnMessageReceivedManager(logger);
}
/**
* Adds a listener to this connection/endpoint to be notified of connect/disconnect/idle/receive(object) events.
*
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections are
* notified of that listener.
*
* It is POSSIBLE to add a server connection ONLY (ie, not global) listener (via connection.addListener), meaning that ONLY that
* listener attached to the connection is notified on that event (ie, admin type listeners)
*/
@SuppressWarnings("rawtypes")
@Override
public final
void add(final Listener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener cannot be null.");
}
// find the class that uses Listener.class.
Class> clazz = listener.getClass();
Class>[] interfaces = clazz.getInterfaces();
// for (Class> anInterface : interfaces) {
// }
//
// while (!(clazz.getSuperclass() != Object.class)) {
// clazz = clazz.getSuperclass();
// }
// this is the connection generic parameter for the listener
Class> genericClass = ClassHelper.getGenericParameterAsClassForSuperClass(clazz, 0);
// if we are null, it means that we have no generics specified for our listener!
//noinspection IfStatementWithIdenticalBranches
if (genericClass == this.baseClass || genericClass == null) {
// we are the base class, so we are fine.
addListener0(listener);
return;
}
else if (ClassHelper.hasInterface(Connection.class, genericClass) && !ClassHelper.hasParentClass(this.baseClass, genericClass)) {
// now we must make sure that the PARENT class is NOT the base class. ONLY the base class is allowed!
addListener0(listener);
return;
}
// didn't successfully add the listener.
throw new IllegalArgumentException("Unable to add incompatible connection type as a listener! : " + this.baseClass);
}
/**
* INTERNAL USE ONLY
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private
void addListener0(final Listener listener) {
boolean found = false;
if (listener instanceof Listener.OnConnected) {
onConnectedManager.add((Listener.OnConnected) listener);
found = true;
}
if (listener instanceof Listener.OnDisconnected) {
onDisconnectedManager.add((Listener.OnDisconnected) listener);
found = true;
}
if (listener instanceof Listener.OnIdle) {
onIdleManager.add((Listener.OnIdle) listener);
found = true;
}
if (listener instanceof Listener.OnMessageReceived) {
onMessageReceivedManager.add((Listener.OnMessageReceived) listener);
found = true;
}
final Logger logger2 = this.logger;
if (!found) {
logger2.error("No matching listener types. Unable to add listener: {}",
listener.getClass()
.getName());
}
else {
hasAddedAtLeastOnce.set(true);
if (logger2.isTraceEnabled()) {
logger2.trace("listener added: {}",
listener.getClass()
.getName());
}
}
}
/**
* Removes a listener from this connection/endpoint to NO LONGER be notified of connect/disconnect/idle/receive(object) events.
*
* When called by a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections are
* notified of that listener.
*
* It is POSSIBLE to remove a server-connection 'non-global' listener (via connection.removeListener), meaning that ONLY that listener
* attached to the connection is removed
*/
@SuppressWarnings("rawtypes")
@Override
public final
void remove(final Listener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener cannot be null.");
}
boolean found = false;
if (listener instanceof Listener.OnConnected) {
found = onConnectedManager.remove((Listener.OnConnected) listener);
}
if (listener instanceof Listener.OnDisconnected) {
found |= onDisconnectedManager.remove((Listener.OnDisconnected) listener);
}
if (listener instanceof Listener.OnIdle) {
found |= onIdleManager.remove((Listener.OnIdle) listener);
}
if (listener instanceof Listener.OnMessageReceived) {
found |= onMessageReceivedManager.remove((Listener.OnMessageReceived) listener);
}
final Logger logger2 = this.logger;
if (!found) {
logger2.error("No matching listener types. Unable to remove listener: {}",
listener.getClass()
.getName());
}
else if (logger2.isTraceEnabled()) {
logger2.trace("listener removed: {}",
listener.getClass()
.getName());
}
}
/**
* Removes all registered listeners from this connection/endpoint to NO LONGER be notified of connect/disconnect/idle/receive(object)
* events.
*/
@Override
public final
void removeAll() {
onMessageReceivedManager.removeAll();
Logger logger2 = this.logger;
if (logger2.isTraceEnabled()) {
logger2.trace("ALL listeners removed !!");
}
}
/**
* Removes all registered listeners (of the object type) from this
* connection/endpoint to NO LONGER be notified of
* connect/disconnect/idle/receive(object) events.
*/
@Override
public final
void removeAll(final Class> classType) {
if (classType == null) {
throw new IllegalArgumentException("classType cannot be null.");
}
final Logger logger2 = this.logger;
if (onMessageReceivedManager.removeAll(classType)) {
if (logger2.isTraceEnabled()) {
logger2.trace("All listeners removed for type: {}",
classType.getClass()
.getName());
}
} else {
logger2.warn("No listeners found to remove for type: {}",
classType.getClass()
.getName());
}
}
/**
* Invoked when a message object was received from a remote peer.
*
* If data is sent in response to this event, the connection data is automatically flushed to the wire. If the data is sent in a separate thread,
* {@link EndPoint#send().flush()} must be called manually.
*
* {@link ISessionManager}
*/
@Override
public final
void notifyOnMessage(final C connection, final Object message) {
notifyOnMessage0(connection, message, false);
}
@SuppressWarnings("Duplicates")
private
boolean notifyOnMessage0(final C connection, final Object message, boolean foundListener) {
foundListener |= onMessageReceivedManager.notifyReceived(connection, message, shutdown);
// now have to account for additional connection listener managers (non-global).
// access a snapshot of the managers (single-writer-principle)
final IdentityMap> localManagers = localManagersREF.get(this);
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
// if we found a listener during THIS method call, we need to let the NEXT method call know,
// so it doesn't spit out error for not handling a message (since that message MIGHT have
// been found in this method).
foundListener |= localManager.notifyOnMessage0(connection, message, foundListener);
}
// only run a flush once
if (foundListener) {
connection.send()
.flush();
}
else {
Logger logger2 = this.logger;
if (logger2.isErrorEnabled()) {
this.logger.warn("----------- LISTENER NOT REGISTERED FOR TYPE: {}",
message.getClass()
.getSimpleName());
}
}
return foundListener;
}
/**
* Invoked when a Connection has been idle for a while.
*
* {@link ISessionManager}
*/
@Override
public final
void notifyOnIdle(final C connection) {
boolean foundListener = onIdleManager.notifyIdle(connection, shutdown);
if (foundListener) {
connection.send()
.flush();
}
// now have to account for additional (local) listener managers.
// access a snapshot of the managers (single-writer-principle)
final IdentityMap> localManagers = localManagersREF.get(this);
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.notifyOnIdle(connection);
}
}
/**
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
*
* {@link ISessionManager}
*/
@SuppressWarnings("Duplicates")
@Override
public
void connectionConnected(final C connection) {
addConnection(connection);
boolean foundListener = onConnectedManager.notifyConnected(connection, shutdown);
if (foundListener) {
connection.send()
.flush();
}
// now have to account for additional (local) listener managers.
// access a snapshot of the managers (single-writer-principle)
final IdentityMap> localManagers = localManagersREF.get(this);
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.connectionConnected(connection);
}
}
/**
* Invoked when a Channel was disconnected from its remote peer.
*
* {@link ISessionManager}
*/
@SuppressWarnings("Duplicates")
@Override
public
void connectionDisconnected(final C connection) {
boolean foundListener = onDisconnectedManager.notifyDisconnected(connection, shutdown);
if (foundListener) {
connection.send()
.flush();
}
// now have to account for additional (local) listener managers.
// access a snapshot of the managers (single-writer-principle)
final IdentityMap> localManagers = localManagersREF.get(this);
ConnectionManager localManager = localManagers.get(connection);
if (localManager != null) {
localManager.connectionDisconnected(connection);
// remove myself from the "global" listeners so we can have our memory cleaned up.
removeListenerManager(connection);
}
removeConnection(connection);
}
/**
* Adds a custom connection to the server.
*
* This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and
* you want *this* server instance to manage listeners + message dispatch
*
* @param connection the connection to add
*/
void addConnection(final C connection) {
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
// use-case 99% of the time)
synchronized (singleWriterLock2) {
// access a snapshot of the connections (single-writer-principle)
ConcurrentEntry head = connectionsREF.get(this);
if (!connectionEntries.containsKey(connection)) {
head = new ConcurrentEntry