/* * The MIT License * * Copyright 2014 sorrge. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.nyan.dch.communication.transport.tcpip; import com.google.common.collect.Sets; import java.io.DataInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import org.nyan.dch.communication.Connections; import org.nyan.dch.communication.IAddress; import org.nyan.dch.communication.IConnections; import org.nyan.dch.communication.IDiscovery; import org.nyan.dch.communication.INetworkTransport; import org.nyan.dch.communication.IRemoteHost; import org.nyan.dch.misc.IStoppable; /** * * @author sorrge */ public class TCPServer implements Runnable, INetworkTransport, IStoppable { private static final Logger log = Logger.getLogger(TCPServer.class.getName()); private Selector selector; private ServerSocketChannel serverChannel; private final ReceiveWorker processor; // The buffer into which we'll read data when it's available private final ByteBuffer readBuffer = ByteBuffer.allocate(8192); private final ConcurrentLinkedQueue<IPAddress> connectionRequests = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<IPAddress> testConnectionRequests = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<SelectionKey> disconnectionRequests = new ConcurrentLinkedQueue<>(); private final Set<IPAddress> connecting = new HashSet<>(); private final Set<SelectionKey> connected = Sets.newConcurrentHashSet(); private final AtomicInteger connectionAttempts = new AtomicInteger(0); private boolean shouldStop; private final IDiscovery discovery; private final int port; public TCPServer(int port, IDiscovery discovery, Random rand) throws IOException { this.discovery = discovery; this.port = port; processor = new ReceiveWorker(rand); OpenChannel(); } public final void Restart() throws IOException { OpenChannel(); discovery.Restart(); } private void OpenChannel() throws ClosedChannelException, IOException { shouldStop = false; selector = Selector.open(); serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); InetAddress wildcard = null; serverChannel.socket().bind(new InetSocketAddress(wildcard, port)); serverChannel.register(selector, SelectionKey.OP_ACCEPT); } @Override public void run() { processor.SetServer(this); Thread procThread = new Thread(processor); procThread.start(); while (!shouldStop) { try { // Wait for an event one of the registered channels selector.select(); // Iterate over the set of keys for which events are available Iterator selectedKeys = selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { SelectionKey key = (SelectionKey)selectedKeys.next(); selectedKeys.remove(); if (!key.isValid()) { System.err.printf("Key for connection %s is not valid\n", key.attachment()); continue; } // Check what event is available and deal with it if (key.isAcceptable()) Accept(key); else if(key.isReadable()) Read(key); else if(key.isConnectable()) { if(key.attachment() == null) FinishTestConnect(key); else FinishConnect(key); } } for(SelectionKey key : selector.keys()) Write(key); while(true) { IPAddress addr = connectionRequests.poll(); if(addr == null) break; InitConnection(addr); } while(true) { IPAddress addr = testConnectionRequests.poll(); if(addr == null) break; InitTestConnection(addr); } while(true) { SelectionKey key = disconnectionRequests.poll(); if(key == null) break; DropConnection(key); } } catch (IOException e) { System.err.printf("TCP transport error: %s\n", e.getMessage()); e.printStackTrace(System.err); } catch(Exception e) { System.err.printf("Unknown server error: %s\n", e); e.printStackTrace(System.err); } } procThread.interrupt(); try { procThread.join(); } catch (InterruptedException ex) { } for(SelectionKey key : selector.keys()) try { key.channel().close(); if(connected.remove(key)) { TCPConnection conn = (TCPConnection)key.attachment(); if(conn != null) conn.ReportDisconnect(true); } } catch (IOException ex) { System.err.printf("Error closing connection: %s\n", ex.getMessage()); } try { selector.close(); } catch (IOException ex) { System.err.printf("Error closing selector: %s\n", ex.getMessage()); } connecting.clear(); connectionAttempts.set(0); connectionRequests.clear(); disconnectionRequests.clear(); } private void DropConnection(SelectionKey key) throws IOException { // System.out.printf("Server %s asked to disconnect from %s\n", processor, key.attachment()); // DumpConnections(); key.cancel(); key.channel().close(); connected.remove(key); processor.receivedItems.add(new ReceiveWorker.ConnectionChanged(key, false, true)); // DumpConnections(); } private void InitConnection(IPAddress addr) throws IOException { // System.out.printf("Server %s is requested to connect to %s\n", processor, addr); // DumpConnections(); if (connecting.contains(addr) || GetConnectedHost(addr) != null || discovery.GetMyAddress().equals(addr)) { // System.out.printf("Server %s has refused the request\n", processor); // System.out.printf("Connections %s's server has refused the connection request\n", processor.connections); connectionAttempts.decrementAndGet(); processor.receivedItems.add(new ReceiveWorker.ConnectionFailed(addr)); return; } // Create a non-blocking socket channel SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // Kick off connection establishment socketChannel.connect(addr.address); SelectionKey key = socketChannel.register(selector, SelectionKey.OP_CONNECT); key.attach(addr); connecting.add(addr); } private void FinishConnect(SelectionKey key) { IPAddress addr = (IPAddress)key.attachment(); connecting.remove(addr); connectionAttempts.decrementAndGet(); SocketChannel socketChannel = (SocketChannel)key.channel(); if(GetConnectedHost(addr) != null) { try { socketChannel.close(); } catch (IOException ex) { } key.cancel(); processor.receivedItems.add(new ReceiveWorker.ConnectionFailed(addr)); return; } // Finish the connection. If the connection operation failed // this will raise an IOException. try { socketChannel.finishConnect(); } catch (IOException e) { // Cancel the channel's registration with our selector key.cancel(); processor.receivedItems.add(new ReceiveWorker.ConnectionFailed(addr)); return; } // Register an interest in reading on this channel key.interestOps(SelectionKey.OP_READ); connected.add(key); // System.out.printf("Server %s has added a connection: %s\n", processor, key.attachment()); // DumpConnections(); processor.receivedItems.add(new ReceiveWorker.ConnectionChanged(key, true, true)); } private void Write(SelectionKey key) throws IOException { if(!(key.attachment() instanceof TCPConnection)) return; TCPConnection conn = (TCPConnection)key.attachment(); ByteBuffer data = conn.toSend.peek(); if(data == null) return; if(conn.sendBlocked && !key.isWritable()) return; SocketChannel channel = (SocketChannel)key.channel(); try { channel.write(data); } catch(IOException e) { // The remote forcibly closed the connection, cancel // the selection key and close the channel. key.cancel(); channel.close(); connected.remove(key); processor.receivedItems.add(new ReceiveWorker.ConnectionChanged(key, false, false)); return; } if(data.remaining() > 0) { conn.sendBlocked = true; if((key.interestOps() & SelectionKey.OP_WRITE) == 0) key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); return; } conn.toSend.remove(); conn.sendBlocked = false; if((key.interestOps() & SelectionKey.OP_WRITE) != 0) key.interestOps(SelectionKey.OP_READ); if(!conn.toSend.isEmpty()) selector.wakeup(); } private void Accept(SelectionKey key) throws IOException { // For an accept to be pending the channel must be a server socket channel. ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); // Accept the connection and make it non-blocking SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); // Register the new SocketChannel with our Selector, indicating // we'd like to be notified when there's data waiting to be read SelectionKey acceptedKey = socketChannel.register(this.selector, SelectionKey.OP_READ); connected.add(acceptedKey); // System.out.printf("Server %s has added a connection: %s\n", processor, socketChannel.getRemoteAddress()); // DumpConnections(); processor.receivedItems.add(new ReceiveWorker.ConnectionChanged(acceptedKey, true, false)); discovery.GetAcceptance().AcceptanceConfirmed(); } void DumpConnections() { for(SelectionKey k : connected) System.out.printf("\tServer %s is currently connected to %s\n", processor, k.attachment()); } private void Read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); // Clear out our read buffer so it's ready for new data readBuffer.clear(); // Attempt to read off the channel int numRead; try { numRead = socketChannel.read(this.readBuffer); } catch (IOException e) { // The remote forcibly closed the connection, cancel // the selection key and close the channel. key.cancel(); socketChannel.close(); connected.remove(key); processor.receivedItems.add(new ReceiveWorker.ConnectionChanged(key, false, false)); return; } if (numRead == -1) { // Remote entity shut the socket down cleanly. Do the // same from our end and close the channel. key.cancel(); socketChannel.close(); connected.remove(key); processor.receivedItems.add(new ReceiveWorker.ConnectionChanged(key, false, false)); return; } if(numRead > 0) { byte[] data = new byte[numRead]; readBuffer.rewind(); readBuffer.get(data); processor.receivedItems.add(new ReceiveWorker.DataReceived(key, data)); } else System.err.printf("Read 0 bytes from channel %s\n", socketChannel.getRemoteAddress()); } void WakeUp() { selector.wakeup(); } void Disconnect(SelectionKey key) { disconnectionRequests.add(key); selector.wakeup(); } @Override public void Close() { shouldStop = true; selector.wakeup(); discovery.Close(); } @Override public void SetConnectionsListener(IConnections listener) { processor.SetConnections(listener); } @Override public IAddress ReadAddress(DataInputStream stream) throws IOException { return new IPAddress(stream); } @Override public int GetCurrentConnectionAttempts() { return connectionAttempts.get(); } @Override public void Connect(IAddress toWhom, boolean test) { if(!(toWhom instanceof IPAddress)) throw new IllegalArgumentException("Wrong type of address supplied"); IPAddress addr = (IPAddress)toWhom; log.log(Level.INFO, "{0}onnection to {1} requested.", new Object[]{test ? "Test c" : "C", toWhom}); if(test) { if(!testConnectionRequests.contains(addr)) testConnectionRequests.add(addr); } else { connectionRequests.add((IPAddress)toWhom); connectionAttempts.incrementAndGet(); } selector.wakeup(); } @Override public IDiscovery GetDiscovery() { return discovery; } @Override public int GetConnectionsCount() { return connected.size(); } @Override public IRemoteHost GetConnectedHost(IAddress whom) { for(SelectionKey k : connected) if(k.attachment() instanceof TCPConnection) { TCPConnection conn = (TCPConnection)k.attachment(); if(whom.equals(conn.GetAddress())) return conn; } return null; } private void InitTestConnection(IPAddress addr) throws IOException { if (discovery.GetMyAddress().equals(addr)) return; // System.out.println("Testing connection to " + addr); // Create a non-blocking socket channel SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // Kick off connection establishment socketChannel.connect(addr.address); socketChannel.register(selector, SelectionKey.OP_CONNECT); } private void FinishTestConnect(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel)key.channel(); InetSocketAddress addr = (InetSocketAddress)socketChannel.getRemoteAddress(); // System.out.println("Connection test for " + socketChannel.getRemoteAddress() + " succeeded"); // Finish the connection. If the connection operation failed // this will raise an IOException. try { socketChannel.finishConnect(); } catch (IOException e) { processor.receivedItems.add(new ReceiveWorker.ConnectionFailed(new IPAddress(addr))); } socketChannel.close(); key.cancel(); } }