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

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 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