/* * Copyright © 2010 Martin Riedel * * This file is part of TransFile. * * TransFile is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * TransFile is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with TransFile. If not, see <http://www.gnu.org/licenses/>. */ package net.sourceforge.transfile.operations; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import net.sourceforge.transfile.operations.messages.DisconnectMessage; import net.sourceforge.transfile.operations.messages.Message; import net.sourceforge.jenerics.Tools; /** * TODO doc * * @author codistmonk (creation 2010-06-15) * */ public class SimpleSocketConnection extends AbstractConnection { private ExecutorService executor; private ObjectOutputStream output; public SimpleSocketConnection() { // Do nothing } /** * * @param localPeer * <br>Should not be null * <br>Shared parameter * @param remotePeer * <br>Should not be null * <br>Shared parameter */ public SimpleSocketConnection(final String localPeer, final String remotePeer) { super(localPeer, remotePeer); } @Override public final void connect() { if (this.getState() == State.DISCONNECTED) { this.setConnectionError(null); this.setState(State.CONNECTING); Tools.debugPrint("Connecting", this.getLocalPeer(), "to", this.getRemotePeer()); this.getExecutor().execute(this.new ConnectionTask()); } } @Override public final void disconnect() { try { if (this.output != null) { this.sendMessage(new DisconnectMessage()); } } finally { this.setExecutor(null); } } @Override public final synchronized void doSendMessage(final Message message) { try { if (this.output != null) { this.output.writeObject(message); this.output.flush(); } } catch (final IOException exception) { exception.printStackTrace(); } } /** * * @param output * <br>Should not be null * <br>Shared parameter */ final void setOutput(final ObjectOutputStream output) { synchronized (this) { if (output != this.output && this.output != null) { try { this.output.close(); } catch (final Exception exception) { this.setConnectionError(exception); } } this.output = output; } if (this.output != null) { this.setState(State.CONNECTED); } } /** * * @return * <br>A non-null value * <br>A shared value */ final synchronized ExecutorService getExecutor() { if (this.executor == null) { this.executor = Executors.newSingleThreadExecutor(); } return this.executor; } /** * * @param executor * <br>Can be null * <br>Shared parameter */ final void setExecutor(final ExecutorService executor) { if (executor != this.getExecutor() && this.getExecutor() != null) { try { synchronized (this) { this.getExecutor().shutdownNow(); } this.setState(State.DISCONNECTED); } finally { this.setOutput(null); } } this.executor = executor; } /** * TODO doc * * @author codistmonk (creation 2010-06-15) * */ private class ConnectionTask implements Runnable { /** * Package-private default constructor to suppress visibility warnings. */ ConnectionTask() { // Do nothing } @Override public final void run() { // TODO find a better fix // If a connection is canceled (using disconnect()) while the socket is trying to connect, // then the socket will still be able to connect before timing out, thus preventing a new // connection with the same port to be established, and also making the peer connection // unavailable (connected to an unused socket) // The following call to sleep() is a quick fix to this problem encountered during testing try { Thread.sleep(2 * CONNECT_INTERVAL); } catch (final InterruptedException exception1) { return; } final long maximumTime = System.currentTimeMillis() + CONNECT_TIMEOUT; final InetSocketAddress localAddress = new InetSocketAddress(getPort(SimpleSocketConnection.this.getLocalPeer())); final InetSocketAddress remoteAddress = getInetSocketAddress(SimpleSocketConnection.this.getRemotePeer()); if (this.connect(maximumTime, localAddress, remoteAddress) == null) { SimpleSocketConnection.this.setExecutor(null); } } /** * TODO doc * <br>Blocking. * * @param maximumTime * <br>Range: {@code [0L .. Long.MAX_VALUE]} * @param localAddress * <br>Not null * <br>Shared * @param remoteAddress * <br>Not null * <br>Shared * @return * <br>Not null * <br>New */ private final Socket connect(final long maximumTime, final InetSocketAddress localAddress, final InetSocketAddress remoteAddress) { do { SimpleSocketConnection.this.setConnectionError(null); try { return this.prepareToReadAndWrite(this.connect(localAddress, remoteAddress)); } catch (final Exception exception) { SimpleSocketConnection.this.setConnectionError(exception); } } while (System.currentTimeMillis() < maximumTime && !Thread.currentThread().isInterrupted()); return null; } /** * TODO doc * * @param socket * <br>Not null * @return {@code socket} * <br>Not null * @throws IOException if an I/O error occurs */ private final Socket prepareToReadAndWrite(final Socket socket) throws IOException { SimpleSocketConnection.this.setOutput(new ObjectOutputStream(socket.getOutputStream())); SimpleSocketConnection.this.getExecutor().execute(SimpleSocketConnection.this.new ReceptionTask(socket)); return socket; } /** * TODO doc * <br>Blocking. * * @param localAddress * <br>Not null * <br>Shared * @param remoteAddress * <br>Not null * <br>Shared * @return * <br>Not null * <br>New * @throws Exception if an error occurs */ private final Socket connect(final InetSocketAddress localAddress, final InetSocketAddress remoteAddress) throws Exception { final Socket result = new Socket(); result.setReuseAddress(true); result.setSoTimeout(0); result.bind(localAddress); result.connect(remoteAddress, CONNECT_INTERVAL); Tools.debugPrint(SimpleSocketConnection.this, result); return result; } } /** * TODO doc * * @author codistmonk (creation 2010-06-15) * */ private class ReceptionTask implements Runnable { private final Socket socket; private ObjectInputStream input; /** * @param socket * <br>Should not be null * <br>Shared parameter */ ReceptionTask(final Socket socket) { this.socket = socket; } @Override public final void run() { try { this.setInput(); this.receiveAndProcessObjects(); } finally { SimpleSocketConnection.this.setExecutor(null); } } /** * TODO doc * <br>Blocking. */ private void receiveAndProcessObjects() { Object object = null; do { try { object = this.readInput(); } catch (final IOException exception) { object = STOP; } catch (final ClassNotFoundException exception) { System.err.println(Tools.debug(2, exception.getMessage())); object = RETRY; } } while (object != STOP && !(object instanceof DisconnectMessage)); } /** * Creates an instance of {@link ObjectInputStream} from {@code this.socket}. * <br>Blocking. * * @throws RuntimeException if an I/O error occurs */ private final void setInput() { try { this.input = new ObjectInputStream(this.socket.getInputStream()); } catch (final IOException exception) { Tools.throwUnchecked(exception); } } /** * Waits for an object and then dispatches it if it is an instance of {@link Message}. * <br>Blocking. * * @return * <br>Maybe null * <br>Maybe shared * @throws IOException if an I/O error occurs * @throws ClassNotFoundException if the class of a serialized object cannot be found */ private final Object readInput() throws IOException, ClassNotFoundException { final Object result = this.input.readObject(); if (result instanceof Message) { SimpleSocketConnection.this.dispatchMessage((Message) result); } return result; } } static final Object STOP = null; static final Object RETRY = "retry"; /** * Time in milliseconds. */ public static final int CONNECT_INTERVAL = 100; /** * Time in milliseconds. */ public static final long CONNECT_TIMEOUT = 20000L; /** * TODO doc * * @param peer * <br>Should not be null * @return * <br>A non-null value * <br>A new value */ public static final InetSocketAddress getInetSocketAddress(final String peer) { final String[] protocolHostPort = getProtocolHostPort(peer); final String host = protocolHostPort[1]; final int port = Integer.parseInt(protocolHostPort[2]); return new InetSocketAddress(host, port); } }