/* * $Id: ProtocolConnectionPool.java,v 1.5 2007-07-04 16:29:31 tigran Exp $ */ package org.dcache.net; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.HashMap; import java.util.Map; import org.dcache.util.PortRange; import static com.google.common.base.Preconditions.checkState; public class ProtocolConnectionPool implements Runnable { private static final Logger _logSocketIO = LoggerFactory.getLogger("logger.dev.org.dcache.io.socket"); private final Map<Object, SocketChannel> _acceptedSockets = new HashMap<>(); private final ChallengeReader _challengeReader; private final int _receiveBufferSize; private final PortRange _portRange; private int _port; private ServerSocketChannel _serverChannel; private long _activity; private Thread _thread; /** * Represent a "promise" to listen for incoming connections until closed. * This object is not thread-safe. */ public class Listen implements Closeable { private boolean _released; /** * Indicate that promise is no longer needed. */ @Override public void close() { if (!_released) { release(); _released = true; } } /** * Get TCP port number used by this connection pool. * @return port number */ public int getPort() { return ProtocolConnectionPool.this.getPort(); } /** * Get a {@link SocketChannel} identified by <code>challenge</code>. The * caller will block until client is connected and challenge exchange is done. * * @param challenge the identifier the client is required to present * @return {@link SocketChannel} connected to client * @throws InterruptedException if current thread was interrupted */ public SocketChannel getSocket(Object challenge) throws InterruptedException { checkState(!_released); assert _activity > 0; assert _thread != null; assert _serverChannel != null; synchronized (_acceptedSockets) { while (!_acceptedSockets.containsKey(challenge)) { _acceptedSockets.wait(); } return _acceptedSockets.remove(challenge); } } } /** * Create a new ProtocolConnectionPool on specified TCP port. If <code>listenPort</code> * is zero, then random port is used unless <i>org.dcache.net.tcp.portrange</i> * property is set. The {@link ChallengeReader} is used to associate connections * with clients. * * @param port the port on which to listen; 0 use a default range * @param bufferSize the size of the receive buffer; 0 implies a default value. * @param reader the ChallengeReader to extract challenges. */ ProtocolConnectionPool(int port, int bufferSize, ChallengeReader reader) { _challengeReader = reader; _receiveBufferSize = bufferSize; if (port != 0) { _portRange = new PortRange(port); } else { String dcachePorts = System.getProperty("org.dcache.net.tcp.portrange"); if (dcachePorts != null) { _portRange = PortRange.valueOf(dcachePorts); } else { _portRange = new PortRange(0); } } } private ServerSocketChannel open() throws IOException { ServerSocketChannel channel = ServerSocketChannel.open(); ServerSocket socket = channel.socket(); if (_receiveBufferSize > 0) { socket.setReceiveBufferSize(_receiveBufferSize); } if (_port != 0) { try { socket.bind(new InetSocketAddress((InetAddress)null, _port)); } catch (IOException e) { _logSocketIO.debug("Failed to bind to existing port: {}", e.toString()); } } if (!socket.isBound()) { _port = _portRange.bind(socket); } _logSocketIO.debug("Socket BIND local = {}:{}", socket.getInetAddress(), _port); return channel; } private void close(ServerSocketChannel channel) { _logSocketIO.debug("Socket SHUTDOWN local = {}:{}", channel.socket().getInetAddress(), channel.socket().getLocalPort()); try { channel.close(); } catch (IOException e) { _logSocketIO.warn("Failed to close socket: {}", e.toString()); } } private synchronized int getPort() { return _port; } /** * Acquire a "promise" to accept an incoming connection. This may trigger * opening a TCP port for incoming connections and starting a thread that * will handle incoming connections. */ public synchronized Listen acquire() throws IOException { if (_serverChannel == null) { _serverChannel = open(); } if (_thread == null) { _thread = new Thread(this, "ProtocolConnectionPool"); _thread.start(); } _activity++; return new Listen(); } private synchronized void release() { if (_activity == 1) { if (_thread != null) { _thread.interrupt(); _thread = null; } if (_serverChannel != null) { close(_serverChannel); _serverChannel = null; } } if (_activity > 0) { _activity--; } } @Override public void run() { ServerSocketChannel serverChannel; synchronized (this) { serverChannel = _serverChannel; } try { while (true) { SocketChannel channel = serverChannel.accept(); _logSocketIO.debug("Socket OPEN (ACCEPT) remote = {}:{} local = {}:{}", channel.socket().getInetAddress(), channel.socket().getPort(), channel.socket().getLocalAddress(), channel.socket().getLocalPort()); Object challenge = _challengeReader.getChallenge(channel); if (challenge == null) { // Unable to read challenge....skip connection _logSocketIO.debug("Socket CLOSE (no challenge) remote = {}:{} local = {}:{}", channel.socket().getInetAddress(), channel.socket().getPort(), channel.socket().getLocalAddress(), channel.socket().getLocalPort()); try { channel.close(); } catch (IOException e) { _logSocketIO.info("Failed to close client socket: {}", channel.socket()); } } else { synchronized (_acceptedSockets) { _acceptedSockets.put(challenge, channel); _acceptedSockets.notifyAll(); } } } } catch (AsynchronousCloseException e) { // Ignore thread stopped by interrupting or by closing the channel } catch (IOException e) { _logSocketIO.error("Accept loop", e); } } }