package ecologylab.oodss.distributed.client; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.util.concurrent.ConcurrentHashMap; import ecologylab.collections.Scope; import ecologylab.oodss.distributed.impl.NIODatagramCore; import ecologylab.oodss.messages.InitConnectionRequest; import ecologylab.oodss.messages.InitConnectionResponse; import ecologylab.oodss.messages.RequestMessage; import ecologylab.oodss.messages.ResponseMessage; import ecologylab.oodss.messages.ServiceMessage; import ecologylab.oodss.messages.UpdateMessage; import ecologylab.serialization.SimplTypesScope; /** * Client subclass of NIOCore. Connects with server address and only * communicates with that host. * @author bilhamil * * @param <S> application scope type parameter */ public class NIODatagramClient<S extends Scope> extends NIODatagramCore<S> { protected String sid = null; protected SelectionKey key; protected int timeoutPeriod; protected InetSocketAddress serverAddress; protected ConcurrentHashMap<Long, Thread> pendingThreadsByUID = new ConcurrentHashMap<Long, Thread>(); protected ConcurrentHashMap<Long, ResponseMessage<S>> responsesByUID = new ConcurrentHashMap<Long, ResponseMessage<S>>(); private InetSocketAddress localAddress; private int timeout; /** * Base client constructor. Initializes new datagram client and * starts the connection process. * * @param serverAddress * @param localAddress local address of the interface that you want to establish the client on * @param translationScope * @param objectRegistry application scope * @param useCompression whether or not to use compression * @param timeout timeout of messages that are not responded to */ public NIODatagramClient(InetSocketAddress serverAddress, InetSocketAddress localAddress, SimplTypesScope translationScope, S objectRegistry, boolean useCompression, int timeout) { super(translationScope, objectRegistry, useCompression); this.serverAddress = serverAddress; this.localAddress = localAddress; this.timeoutPeriod = timeout; } /** * @param serverAddress * @param localAddress * @param timeout */ public boolean connect() { DatagramChannel chan; try { chan = DatagramChannel.open(); chan.socket().bind(localAddress); chan.connect(serverAddress); chan.configureBlocking(false); key = chan.register(selector, SelectionKey.OP_READ); } catch (ClosedChannelException e) { debug("Channel isn't open but it should be!: " + e.getMessage()); e.printStackTrace(); } catch (SocketException e) { debug("Failed to open socket!: " + e.getMessage()); e.printStackTrace(); } catch (IOException e) { debug("Failed to open socket!: " + e.getMessage()); e.printStackTrace(); } this.start(); InitConnectionResponse initResponse = (InitConnectionResponse) this .sendMessage(new InitConnectionRequest()); this.sid = initResponse.getSessionId(); if (!initResponse.isOK()) { this.stop(); return false; } else { return true; } } public NIODatagramClient(InetSocketAddress serverAddress, InetSocketAddress localAddress, SimplTypesScope translationScope, S objectRegistry, int timeout) { this(serverAddress, localAddress, translationScope, objectRegistry, false, timeout); } public NIODatagramClient(InetSocketAddress serverAddress, SimplTypesScope translationScope, S objectRegistry, boolean useCompression, int timeout) { this(serverAddress, null, translationScope, objectRegistry, useCompression, timeout); } public NIODatagramClient(InetSocketAddress serverAddress, SimplTypesScope translationScope, S objectRegistry, int timeout) { this(serverAddress, translationScope, objectRegistry, false, timeout); } /** * Implements message handling. And specifies how to handle */ @Override protected void handleMessage(long uid, ServiceMessage<S> message, SelectionKey key, InetSocketAddress address) { Thread t; if (message instanceof InitConnectionRequest) { InitConnectionRequest connInit = new InitConnectionRequest(sid); InitConnectionResponse initResp = (InitConnectionResponse) this.sendMessage(connInit); if (!this.sid.equals(initResp.getSessionId())) { debug("Changing session id from: " + this.sid + " to " + initResp.getSessionId()); this.sid = initResp.getSessionId(); } return; } /* * Process message and wake up blocked thread based on uid. */ if (message instanceof UpdateMessage) { ((UpdateMessage) message).processUpdate(objectRegistry); } if (message instanceof ResponseMessage) { synchronized(pendingThreadsByUID) { t = pendingThreadsByUID.remove(uid); } ResponseMessage<S> response = (ResponseMessage<S>) message; response.processResponse(objectRegistry); if (t != null && message instanceof ResponseMessage) { synchronized (t) { responsesByUID.put(uid, response); t.notify(); } } } } @Override protected void waitForReconnect() { } /** * Send message without waiting for any kind or response. * @param message */ public void sendMessageAsync(ServiceMessage message) { this.sendMessage(message, key, getNextUID(), null); } /** * Send message and block for response. Will retransmit * transmissionCount times after which it gives up and return null. * * @param message * @param transmissionCount * @return */ public ResponseMessage<S> sendMessage(RequestMessage message, int transmissionCount) { long myUid = this.getNextUID(); Thread t = Thread.currentThread(); pendingThreadsByUID.put(myUid, t); // don't need socket address since this should be connected this.sendMessage(message, key, myUid, null); transmissionCount--; synchronized (t) { while (!responsesByUID.containsKey(myUid)) { try { t.wait(this.timeoutPeriod); if (responsesByUID.containsKey(myUid)) { pendingThreadsByUID.remove(myUid); return responsesByUID.remove(myUid); } else { if (transmissionCount <= 0) { pendingThreadsByUID.remove(myUid); return null; } else { // retransmitting this.sendMessage(message, key, myUid, null); transmissionCount--; } } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return null; } /** * Send message and block for response. Will retransmit * infinite times. * * @param message * @return */ public ResponseMessage<S> sendMessage(RequestMessage message) { long myUid = this.getNextUID(); Thread t = Thread.currentThread(); pendingThreadsByUID.put(myUid, t); // don't need socket address since this should be connected this.sendMessage(message, key, myUid, null); synchronized (t) { while (!responsesByUID.containsKey(myUid)) { try { t.wait(this.timeoutPeriod); if (responsesByUID.containsKey(myUid)) { pendingThreadsByUID.remove(myUid); return responsesByUID.remove(myUid); } if (message.isDisposable()) { pendingThreadsByUID.remove(myUid); return null; } // retransmitting this.sendMessage(message, key, myUid, null); } catch (InterruptedException e) { e.printStackTrace(); } } } return null; } protected void clearSessionId() { this.sid = null; } public boolean connected() { return key != null && key.channel().isOpen() && super.isRunning(); } public InetSocketAddress getServer() { return serverAddress; } /** * Reset's the server the client is connected to. * * @param server */ public void setServer(String server) { InetSocketAddress addr = new InetSocketAddress(server, serverAddress.getPort()); DatagramChannel chan = (DatagramChannel) key.channel(); try { chan.disconnect(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { chan.connect(addr); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } this.serverAddress = addr; } }