dorkbox.network.connection.ConnectionImpl 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 dorkbox.network.connection.bridge.ConnectionBridge;
import dorkbox.network.connection.idle.IdleBridge;
import dorkbox.network.connection.idle.IdleSender;
import dorkbox.network.connection.idle.IdleSenderFactory;
import dorkbox.network.connection.ping.PingFuture;
import dorkbox.network.connection.ping.PingMessage;
import dorkbox.network.connection.ping.PingTuple;
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
import dorkbox.network.connection.wrapper.ChannelNull;
import dorkbox.network.connection.wrapper.ChannelWrapper;
import dorkbox.network.rmi.RMI;
import dorkbox.network.rmi.RemoteObject;
import dorkbox.network.rmi.RmiBridge;
import dorkbox.network.rmi.RmiRegistration;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.udt.nio.NioUdtByteConnectorChannel;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Promise;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.slf4j.Logger;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* The "network connection" is established once the registration is validated for TCP/UDP/UDT
*/
@SuppressWarnings("unused")
@Sharable
public
class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConnection, Connection, ListenerBridge, ConnectionBridge {
private final org.slf4j.Logger logger;
private final AtomicBoolean needsLock = new AtomicBoolean(false);
private final AtomicBoolean writeSignalNeeded = new AtomicBoolean(false);
private final Object writeLock = new Object();
private final AtomicBoolean closeInProgress = new AtomicBoolean(false);
private final AtomicBoolean alreadyClosed = new AtomicBoolean(false);
private final Object closeInProgressLock = new Object();
private final Object messageInProgressLock = new Object();
private final AtomicBoolean messageInProgress = new AtomicBoolean(false);
private ISessionManager sessionManager;
private ChannelWrapper channelWrapper;
private boolean isLoopback;
private volatile PingFuture pingFuture = null;
// used to store connection local listeners (instead of global listeners). Only possible on the server.
private volatile ConnectionManager localListenerManager;
// while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error.
private boolean remoteKeyChanged;
private final EndPoint endPoint;
// when true, the connection will be closed (either as RMI or as 'normal' listener execution) when the thread execution returns control
// back to the network stack
private boolean closeAsap = false;
private volatile ObjectRegistrationLatch objectRegistrationLatch;
private final Object remoteObjectLock = new Object();
private final RmiBridge rmiBridge;
private final Map proxyIdCache = Collections.synchronizedMap(new WeakHashMap(8));
// The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter)
// The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
private final AtomicLong aes_gcm_iv = new AtomicLong(0);
/**
* All of the parameters can be null, when metaChannel wants to get the base class type
*/
@SuppressWarnings("rawtypes")
public
ConnectionImpl(final Logger logger, final EndPoint endPoint, final RmiBridge rmiBridge) {
this.logger = logger;
this.endPoint = endPoint;
this.rmiBridge = rmiBridge;
}
/**
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
*
* This happens BEFORE prep.
*/
void init(final ChannelWrapper channelWrapper, final ConnectionManager sessionManager) {
this.sessionManager = sessionManager;
this.channelWrapper = channelWrapper;
//noinspection SimplifiableIfStatement
if (this.channelWrapper instanceof ChannelNetworkWrapper) {
this.remoteKeyChanged = ((ChannelNetworkWrapper) this.channelWrapper).remoteKeyChanged();
}
else {
this.remoteKeyChanged = false;
}
isLoopback = channelWrapper.isLoopback();
}
/**
* Prepare the channel wrapper, since it doesn't have access to certain fields during it's initialization.
*
* This happens AFTER init.
*/
void prep() {
if (this.channelWrapper != null) {
this.channelWrapper.init();
}
}
/**
* @return a threadlocal AES key + IV. key=32 byte, iv=12 bytes (AES-GCM implementation). This is a threadlocal
* because multiple protocols can be performing crypto AT THE SAME TIME, and so we have to make sure that operations don't
* clobber each other
*/
public final
ParametersWithIV getCryptoParameters() {
return this.channelWrapper.cryptoParameters();
}
/**
* This is the per-message sequence number.
*
* The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter)
* The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this
* counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
*/
public final
long getNextGcmSequence() {
return aes_gcm_iv.getAndIncrement();
}
/**
* Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed.
*/
@Override
public
boolean hasRemoteKeyChanged() {
return this.remoteKeyChanged;
}
/**
* @return the remote address, as a string.
*/
@Override
public
String getRemoteHost() {
return this.channelWrapper.getRemoteHost();
}
/**
* @return true if this connection is established on the loopback interface
*/
@Override
public
boolean isLoopback() {
return isLoopback;
}
/**
* @return the endpoint associated with this connection
*/
@Override
public
EndPoint getEndPoint() {
return this.endPoint;
}
/**
* @return the connection (TCP or LOCAL) id of this connection.
*/
@Override
public
int id() {
return this.channelWrapper.id();
}
/**
* @return the connection (TCP or LOCAL) id of this connection as a HEX string.
*/
@Override
public
String idAsHex() {
return Integer.toHexString(this.channelWrapper.id());
}
/**
* Updates the ping times for this connection (called when this connection gets a REPLY ping message).
*/
public final
void updatePingResponse(PingMessage ping) {
if (this.pingFuture != null) {
this.pingFuture.setSuccess(this, ping);
}
}
/**
* Sends a "ping" packet, trying UDP, then UDT, then TCP (in that order) to measure ROUND TRIP time to the remote connection.
*
* @return Ping can have a listener attached, which will get called when the ping returns.
*/
@Override
public final
Ping ping() {
PingFuture pingFuture2 = this.pingFuture;
if (pingFuture2 != null && !pingFuture2.isSuccess()) {
pingFuture2.cancel();
}
Promise> newPromise = this.channelWrapper.getEventLoop()
.newPromise();
this.pingFuture = new PingFuture(newPromise);
PingMessage ping = new PingMessage();
ping.id = this.pingFuture.getId();
ping0(ping);
return this.pingFuture;
}
/**
* INTERNAL USE ONLY. Used to initiate a ping, and to return a ping.
* Sends a ping message attempted in the following order: UDP, UDT, TCP
*/
public final
void ping0(PingMessage ping) {
if (this.channelWrapper.udp() != null) {
UDP(ping).flush();
}
else if (this.channelWrapper.udt() != null) {
UDT(ping).flush();
}
else {
TCP(ping).flush();
}
}
/**
* Returns the last calculated TCP return trip time, or -1 if or the {@link PingMessage} response has not yet been received.
*/
public final
int getLastRoundTripTime() {
PingFuture pingFuture2 = this.pingFuture;
if (pingFuture2 != null) {
return pingFuture2.getResponse();
}
else {
return -1;
}
}
/**
* @return true if this connection is also configured to use UDP
*/
@Override
public final
boolean hasUDP() {
return this.channelWrapper.udp() != null;
}
/**
* @return true if this connection is also configured to use UDT
*/
@Override
public final
boolean hasUDT() {
return this.channelWrapper.udt() != null;
}
@Override
public
void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
super.channelWritabilityChanged(ctx);
// needed to place back-pressure when writing too much data to the connection
if (writeSignalNeeded.getAndSet(false)) {
synchronized (writeLock) {
needsLock.set(false);
writeLock.notifyAll();
}
}
}
/**
* needed to place back-pressure when writing too much data to the connection.
*
* This blocks until we are writable again
*/
private
void controlBackPressure(ConnectionPoint c) {
while (!c.isWritable()) {
needsLock.set(true);
writeSignalNeeded.set(true);
synchronized (writeLock) {
if (needsLock.get()) {
try {
// waits 1 second maximum per check. This is to guarantee that eventually (in the case of deadlocks, which i've seen)
// it will get released. The while loop makes sure it will exit when the channel is writable
writeLock.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* Expose methods to send objects to a destination.
*/
@Override
public final
ConnectionBridge send() {
return this;
}
/**
* Sends the object to other listeners INSIDE this endpoint. It does not send it to a remote address.
*/
@Override
public final
void self(Object message) {
Logger logger2 = this.logger;
if (logger2.isTraceEnabled()) {
logger2.trace("Sending LOCAL {}", message);
}
this.sessionManager.notifyOnMessage(this, message);
}
/**
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
*/
final
ConnectionPoint TCP_backpressure(Object message) {
Logger logger2 = this.logger;
if (!this.closeInProgress.get()) {
if (logger2.isTraceEnabled()) {
logger2.trace("Sending TCP {}", message);
}
ConnectionPointWriter tcp = this.channelWrapper.tcp();
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
// INSIDE the event loop
controlBackPressure(tcp);
tcp.write(message);
return tcp;
}
else {
if (logger2.isDebugEnabled()) {
logger2.debug("writing TCP while closed: {}", message);
}
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
*/
@Override
public final
ConnectionPoint TCP(Object message) {
Logger logger2 = this.logger;
if (!this.closeInProgress.get()) {
if (logger2.isTraceEnabled()) {
logger2.trace("Sending TCP {}", message);
}
ConnectionPointWriter tcp = this.channelWrapper.tcp();
tcp.write(message);
return tcp;
}
else {
if (logger2.isDebugEnabled()) {
logger2.debug("writing TCP while closed: {}", message);
}
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* Sends the object over the network using UDP (LOCAL channels do not care if its TCP or UDP)
*/
final
ConnectionPoint UDP_backpressure(Object message) {
Logger logger2 = this.logger;
if (!this.closeInProgress.get()) {
if (logger2.isTraceEnabled()) {
logger2.trace("Sending UDP {}", message);
}
ConnectionPointWriter udp = this.channelWrapper.udp();
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
// INSIDE the event loop
controlBackPressure(udp);
udp.write(message);
return udp;
}
else {
if (logger2.isDebugEnabled()) {
logger2.debug("writing UDP while closed: {}", message);
}
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* Sends the object over the network using UDP (LOCAL channels do not care if its TCP or UDP)
*/
@Override
public
ConnectionPoint UDP(Object message) {
Logger logger2 = this.logger;
if (!this.closeInProgress.get()) {
if (logger2.isTraceEnabled()) {
logger2.trace("Sending UDP {}", message);
}
ConnectionPointWriter udp = this.channelWrapper.udp();
udp.write(message);
return udp;
}
else {
if (logger2.isDebugEnabled()) {
logger2.debug("writing UDP while closed: {}", message);
}
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
*/
final
ConnectionPoint UDT_backpressure(Object message) {
Logger logger2 = this.logger;
if (!this.closeInProgress.get()) {
if (logger2.isTraceEnabled()) {
logger2.trace("Sending UDT {}", message);
}
ConnectionPointWriter udt = this.channelWrapper.udt();
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
// INSIDE the event loop
controlBackPressure(udt);
udt.write(message);
return udt;
}
else {
if (logger2.isDebugEnabled()) {
logger2.debug("writing UDT while closed: {}", message);
}
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
*/
@Override
public final
ConnectionPoint UDT(Object message) {
Logger logger2 = this.logger;
if (!this.closeInProgress.get()) {
if (logger2.isTraceEnabled()) {
logger2.trace("Sending UDT {}", message);
}
ConnectionPointWriter udt = this.channelWrapper.udt();
udt.write(message);
return udt;
}
else {
if (logger2.isDebugEnabled()) {
logger2.debug("writing UDT while closed: {}", message);
}
// we have to return something, otherwise dependent code will throw a null pointer exception
return ChannelNull.get();
}
}
/**
* Flushes the contents of the TCP/UDP/UDT/etc pipes to the actual transport.
*/
@Override
public final
void flush() {
this.channelWrapper.flush();
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final
IdleBridge sendOnIdle(@SuppressWarnings("rawtypes") IdleSender sender) {
return new IdleSenderFactory(this, sender);
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final
IdleBridge sendOnIdle(Object message) {
return new IdleSenderFactory(this, message);
}
/**
* Invoked when a {@link Channel} has been idle for a while.
*/
@Override
public
void userEventTriggered(ChannelHandlerContext context, Object event) throws Exception {
// if (e.getState() == IdleState.READER_IDLE) {
// e.getChannel().close();
// } else if (e.getState() == IdleState.WRITER_IDLE) {
// e.getChannel().write(new Object());
// } else
if (event instanceof IdleStateEvent) {
if (((IdleStateEvent) event).state() == IdleState.ALL_IDLE) {
this.sessionManager.notifyOnIdle(this);
}
}
super.userEventTriggered(context, event);
}
@Override
public
void channelRead(ChannelHandlerContext context, Object message) throws Exception {
channelRead(message);
ReferenceCountUtil.release(message);
}
public
void channelRead(Object object) throws Exception {
// prevent close from occurring SMACK in the middle of a message in progress.
// delay close until it's finished.
this.messageInProgress.set(true);
this.sessionManager.notifyOnMessage(this, object);
this.messageInProgress.set(false);
// if we are in the middle of closing, and waiting for the message, it's safe to notify it to continue.
if (this.closeInProgress.get()) {
synchronized (this.messageInProgressLock) {
this.messageInProgressLock.notifyAll();
}
}
// in some cases, we want to close the current connection -- and given the way the system is designed, we cannot always close it before
// we return. This will let us close the connection when our business logic is finished.
if (closeAsap) {
close();
}
}
@Override
public
void channelInactive(ChannelHandlerContext context) throws Exception {
// if we are in the middle of a message, hold off.
if (this.messageInProgress.get()) {
synchronized (this.messageInProgressLock) {
try {
this.messageInProgressLock.wait();
} catch (InterruptedException ignored) {
}
}
}
Channel channel = context.channel();
Class extends Channel> channelClass = channel.getClass();
boolean isTCP = channelClass == NioSocketChannel.class || channelClass == EpollSocketChannel.class;
if (this.logger.isInfoEnabled()) {
String type;
if (isTCP) {
type = "TCP";
}
else if (channelClass == NioDatagramChannel.class || channelClass == EpollDatagramChannel.class) {
type = "UDP";
}
else if (channelClass == NioUdtByteConnectorChannel.class) {
type = "UDT";
}
else if (channelClass == LocalChannel.class) {
type = "LOCAL";
}
else {
type = "UNKNOWN";
}
this.logger.info("Closed remote {} connection: {}",
type,
channel.remoteAddress()
.toString());
}
// our master channels are TCP/LOCAL (which are mutually exclusive). Only key disconnect events based on the status of them.
if (isTCP || channelClass == LocalChannel.class) {
// this is because channelInactive can ONLY happen when netty shuts down the channel.
// and connection.close() can be called by the user.
this.sessionManager.connectionDisconnected(this);
// close TCP/UDP/UDT together!
close();
}
synchronized (this.closeInProgressLock) {
this.alreadyClosed.set(true);
this.closeInProgressLock.notify();
}
}
/**
* Closes the connection
*/
@Override
public final
void close() {
// only close if we aren't already in the middle of closing.
if (this.closeInProgress.compareAndSet(false, true)) {
int idleTimeoutMs = this.endPoint.getIdleTimeout();
if (idleTimeoutMs == 0) {
// default is 2 second timeout, in milliseconds.
idleTimeoutMs = 2000;
}
// if we are in the middle of a message, hold off.
synchronized (this.messageInProgressLock) {
if (this.messageInProgress.get()) {
try {
this.messageInProgressLock.wait(idleTimeoutMs);
} catch (InterruptedException ignored) {
}
}
}
// flush any pending messages
this.channelWrapper.flush();
this.channelWrapper.close(this, this.sessionManager);
// close out the ping future
PingFuture pingFuture2 = this.pingFuture;
if (pingFuture2 != null) {
pingFuture2.cancel();
}
this.pingFuture = null;
// want to wait for the "channelInactive" method to FINISH before allowing our current thread to continue!
synchronized (this.closeInProgressLock) {
if (!this.alreadyClosed.get()) {
try {
this.closeInProgressLock.wait(idleTimeoutMs);
} catch (Exception ignored) {
}
}
}
}
}
/**
* Marks the connection to be closed as soon as possible. This is evaluated when the current
* thread execution returns to the network stack.
*/
@Override
public final
void closeAsap() {
closeAsap = true;
}
@Override
public
void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
final Channel channel = context.channel();
if (!(cause instanceof IOException)) {
// safe to ignore, since it's thrown when we try to interact with a closed socket. Race conditions cause this, and
// it is still safe to ignore.
this.logger.error("Unexpected exception while receiving data from {}", channel.remoteAddress(), cause);
// the ONLY sockets that can call this are:
// CLIENT TCP or UDP
// SERVER TCP
if (channel.isOpen()) {
channel.close();
}
} else {
// it's an IOException, just log it!
this.logger.error("Unexpected exception while communicating with {}!", channel.remoteAddress(), cause);
}
}
/**
* Expose methods to modify the connection listeners.
*/
@Override
public final
ListenerBridge listeners() {
return this;
}
/**
* Adds a listener to this connection/endpoint to be notified of
* connect/disconnect/idle/receive(object) events.
*
* If the listener already exists, it is not added again.
*
* 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(Listener listener) {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level
// meaning --
// I add one listener, and ALL connections are notified of that listener.
//
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be VERY uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager == null) {
this.localListenerManager = ((EndPointServer) this.endPoint).addListenerManager(this);
}
this.localListenerManager.add(listener);
}
}
else {
this.endPoint.listeners()
.add(listener);
}
}
/**
* 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(Listener listener) {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level
// meaning --
// I add one listener, and ALL connections are notified of that listener.
//
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager != null) {
this.localListenerManager.remove(listener);
if (!this.localListenerManager.hasListeners()) {
((EndPointServer) this.endPoint).removeListenerManager(this);
}
}
}
}
else {
this.endPoint.listeners()
.remove(listener);
}
}
/**
* 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() {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level
// meaning --
// I add one listener, and ALL connections are notified of that listener.
//
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager != null) {
this.localListenerManager.removeAll();
this.localListenerManager = null;
((EndPointServer) this.endPoint).removeListenerManager(this);
}
}
}
else {
this.endPoint.listeners()
.removeAll();
}
}
/**
* 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(Class> classType) {
if (this.endPoint instanceof EndPointServer) {
// when we are a server, NORMALLY listeners are added at the GLOBAL level
// meaning --
// I add one listener, and ALL connections are notified of that listener.
//
// HOWEVER, it is also POSSIBLE to add a local listener (via connection.addListener), meaning that ONLY
// that listener is notified on that event (ie, admin type listeners)
// synchronized because this should be uncommon, and we want to make sure that when the manager
// is empty, we can remove it from this connection.
synchronized (this) {
if (this.localListenerManager != null) {
this.localListenerManager.removeAll(classType);
if (!this.localListenerManager.hasListeners()) {
this.localListenerManager = null;
((EndPointServer) this.endPoint).removeListenerManager(this);
}
}
}
}
else {
this.endPoint.listeners()
.removeAll(classType);
}
}
@Override
public
String toString() {
return this.channelWrapper.toString();
}
@Override
public
int hashCode() {
return id();
}
@Override
public
boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ConnectionImpl other = (ConnectionImpl) obj;
if (this.channelWrapper == null) {
if (other.channelWrapper != null) {
return false;
}
}
else if (!this.channelWrapper.equals(other.channelWrapper)) {
return false;
}
return true;
}
//
//
// RMI methods
//
@SuppressWarnings({"UnnecessaryLocalVariable", "unchecked"})
@Override
public final
Iface createProxyObject(final Class remoteImplementationClass) throws IOException {
// only one register can happen at a time
synchronized (remoteObjectLock) {
objectRegistrationLatch = new ObjectRegistrationLatch();
// since this synchronous, we want to wait for the response before we continue
// this means we are creating a NEW object on the server, bound access to only this connection
TCP(new RmiRegistration(remoteImplementationClass.getName())).flush();
//noinspection Duplicates
try {
if (!objectRegistrationLatch.latch.await(2, TimeUnit.SECONDS)) {
final String errorMessage = "Timed out getting registration ID for: " + remoteImplementationClass;
logger.error(errorMessage);
throw new IOException(errorMessage);
}
} catch (InterruptedException e) {
final String errorMessage = "Error getting registration ID for: " + remoteImplementationClass;
logger.error(errorMessage, e);
throw new IOException(errorMessage, e);
}
// local var to prevent double hit on volatile field
final ObjectRegistrationLatch latch = objectRegistrationLatch;
if (latch.hasError) {
final String errorMessage = "Error getting registration ID for: " + remoteImplementationClass;
logger.error(errorMessage);
throw new IOException(errorMessage);
}
return (Iface) latch.remoteObject;
}
}
@SuppressWarnings({"UnnecessaryLocalVariable", "unchecked"})
@Override
public final
Iface getProxyObject(final int objectId) throws IOException {
// only one register can happen at a time
synchronized (remoteObjectLock) {
objectRegistrationLatch = new ObjectRegistrationLatch();
// since this synchronous, we want to wait for the response before we continue
// this means that we are ACCESSING a remote object on the server, the server checks GLOBAL, then LOCAL for this object
TCP(new RmiRegistration(objectId)).flush();
//noinspection Duplicates
try {
if (!objectRegistrationLatch.latch.await(2, TimeUnit.SECONDS)) {
final String errorMessage = "Timed out getting registration for ID: " + objectId;
logger.error(errorMessage);
throw new IOException(errorMessage);
}
} catch (InterruptedException e) {
final String errorMessage = "Error getting registration for ID: " + objectId;
logger.error(errorMessage, e);
throw new IOException(errorMessage, e);
}
// local var to prevent double hit on volatile field
final ObjectRegistrationLatch latch = objectRegistrationLatch;
if (latch.hasError) {
final String errorMessage = "Error getting registration for ID: " + objectId;
logger.error(errorMessage);
throw new IOException(errorMessage);
}
return (Iface) latch.remoteObject;
}
}
void registerInternal(final ConnectionImpl connection, final RmiRegistration remoteRegistration) {
final String implementationClassName = remoteRegistration.remoteImplementationClass;
if (implementationClassName != null) {
// THIS IS ON THE REMOTE CONNECTION (where the object will really exist)
//
// CREATE a new ID, and register the ID and new object (must create a new one) in the object maps
Class> implementationClass;
try {
implementationClass = Class.forName(implementationClassName);
} catch (Exception e) {
logger.error("Error registering RMI class " + implementationClassName, e);
connection.TCP(new RmiRegistration()).flush();
return;
}
try {
final Object remotePrimaryObject = implementationClass.newInstance();
rmiBridge.register(rmiBridge.nextObjectId(), remotePrimaryObject);
LinkedList remoteClasses = new LinkedList();
remoteClasses.add(new ClassObject(implementationClass, remotePrimaryObject));
ClassObject remoteClassObject;
while ((remoteClassObject = remoteClasses.pollFirst()) != null) {
// we have to check for any additional fields that will have proxy information
for (Field field : remoteClassObject.clazz.getDeclaredFields()) {
if (field.getAnnotation(RMI.class) != null) {
boolean prev = field.isAccessible();
field.setAccessible(true);
final Object o = field.get(remoteClassObject.object);
field.setAccessible(prev);
final Class> type = field.getType();
rmiBridge.register(rmiBridge.nextObjectId(), o);
remoteClasses.offerLast(new ClassObject(type, o));
}
}
}
connection.TCP(new RmiRegistration(remotePrimaryObject)).flush();
} catch (Exception e) {
logger.error("Error registering RMI class " + implementationClassName, e);
connection.TCP(new RmiRegistration()).flush();
}
}
else if (remoteRegistration.remoteObjectId > RmiBridge.INVALID_RMI) {
// THIS IS ON THE REMOTE CONNECTION (where the object will really exist)
//
// GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection).
Object object = getImplementationObject(remoteRegistration.remoteObjectId);
if (object != null) {
connection.TCP(new RmiRegistration(object)).flush();
} else {
connection.TCP(new RmiRegistration()).flush();
}
}
else {
// THIS IS ON THE LOCAL CONNECTION (that sent the 'create proxy object') SIDE
// the next two use a local var, so that there isn't a double hit for volatile access
final ObjectRegistrationLatch latch = this.objectRegistrationLatch;
latch.hasError = remoteRegistration.hasError;
if (!remoteRegistration.hasError) {
latch.remoteObject = remoteRegistration.remoteObject;
}
// notify the original register that it may continue. We access the volatile field directly, so that it's members are updated
objectRegistrationLatch.latch.countDown();
}
}
/**
* Used by RMI
*
* @return the registered ID for a specific object. This is used by the "local" side when setting up the to fetch an object for the
* "remote" side for RMI
*/
public
int getRegisteredId(final T object) {
// always check local before checking global, because less contention on the synchronization
RmiBridge globalRmiBridge = endPoint.globalRmiBridge;
if (globalRmiBridge == null) {
throw new NullPointerException("Unable to call 'getRegisteredId' when the globalRmiBridge is null!");
}
int object1 = globalRmiBridge.getRegisteredId(object);
if (object1 == Integer.MAX_VALUE) {
return rmiBridge.getRegisteredId(object);
} else {
return object1;
}
}
/**
* Used by RMI for the LOCAL side, to get the proxy object as an interface
*
* @param type must be the interface the proxy will bind to
*/
public
RemoteObject getProxyObject(final int objectID, final Class> type) {
// we want to have a connection specific cache of IDs, using weak references.
// because this is PER CONNECTION, this is safe.
RemoteObject remoteObject = proxyIdCache.get(objectID);
if (remoteObject == null) {
// duplicates are fine, as they represent the same object (as specified by the ID) on the remote side.
remoteObject = rmiBridge.createProxyObject(this, objectID, type);
proxyIdCache.put(objectID, remoteObject);
}
return remoteObject;
}
/**
* This is used by RMI for the REMOTE side, to get the implementation
*/
public
Object getImplementationObject(final int objectID) {
if (RmiBridge.isGlobal(objectID)) {
RmiBridge globalRmiBridge = endPoint.globalRmiBridge;
if (globalRmiBridge == null) {
throw new NullPointerException("Unable to call 'getRegisteredId' when the gloablRmiBridge is null!");
}
return globalRmiBridge.getRegisteredObject(objectID);
} else {
return rmiBridge.getRegisteredObject(objectID);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy