/* * Created by Andrey Cherkashin (acherkashin) * http://acherkashin.me * * License * Copyright (c) 2015 Andrey Cherkashin * The project released under the MIT license: http://opensource.org/licenses/MIT */ package ragefist.core.network; import com.juniform.IJUniformPacker; import com.juniform.JUniformObject; import java.io.IOException; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.UnknownHostException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; 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.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Queue; import java.util.logging.Level; import java.util.logging.Logger; import ragefist.core.network.Connection.ConnectionBuilder; /** * * @author acherkashin */ public class Server implements Runnable { private static final Logger _log = Logger.getLogger(Server.class.getName()); private final InetAddress _address; private final int _port; private final Map<Integer,Connection> _clientConnectionMap; private boolean _isRunning = false; private Selector _selector; private final ByteBuffer _readBuffer; private final ByteBuffer _writeBuffer; private final int _packetBufferSize; private final IPacketHandler _packetHandler; private final IJUniformPacker _packetPacker; private final ISocketStrategy _socketStrategy; private final ConnectionBuilder _connectionBuilder; // ---------------------------------------------------------------------- // // BUILDER // ---------------------------------------------------------------------- // public final static class ServerBuilder { public int packetBufferSize = 4096; public int readBufferSize = 1024; public int writeBufferSize = 1024; public String host = null; public int port = 0; public IPacketHandler packetHandler = null; public IJUniformPacker packetPacker = null; public ISocketStrategy socketStrategy = null; // BUILD public final Server build() throws IllegalArgumentException { if (host == null || port == 0 || packetHandler == null || socketStrategy == null ) { throw new IllegalArgumentException("One of the ServerBuilder params is not set"); } try { return new Server(this); } catch(UnknownHostException e) { throw new IllegalArgumentException("Failed to initialize InetAddress using value: " + host); } } } // ---------------------------------------------------------------------- // // PRIVATE // ---------------------------------------------------------------------- // private Server(ServerBuilder builder) throws UnknownHostException { _address = InetAddress.getByName(builder.host); _port = builder.port; _readBuffer = ByteBuffer.allocate(builder.readBufferSize); _writeBuffer = ByteBuffer.allocate(builder.writeBufferSize); _packetHandler = builder.packetHandler; _packetBufferSize = builder.packetBufferSize; _packetPacker = builder.packetPacker; _socketStrategy = builder.socketStrategy; _connectionBuilder = new ConnectionBuilder(); _connectionBuilder.packerPacker = _packetPacker; _connectionBuilder.server = this; _clientConnectionMap = new HashMap<>(); } // ---------------------------------------------------------------------- // // PUBLIC // ---------------------------------------------------------------------- // public Connection getConnectionById(int id) { return _clientConnectionMap.get(id); } public int getPacketBufferSize() { return _packetBufferSize; } public int getConnectionsCount() { return _selector.keys().size(); } /** * Opens server socket for client connections * Creates a thread pool to process sockets * @throws IOException */ public void open() throws IOException { // Creating non-blocking channel ServerSocketChannel selectable = ServerSocketChannel.open(); selectable.configureBlocking(false); // Creating socket ServerSocket server = selectable.socket(); server.bind(new InetSocketAddress(_address, _port)); // Registering to accept new connections _selector = Selector.open(); selectable.register(_selector, SelectionKey.OP_ACCEPT); _log.info("Server at host " + _address.toString() + ", port " + _port + " is opened for connections"); } public boolean isRunning() { return _isRunning; } @Override public final void run() { _isRunning = true; SelectionKey selectedKey; while(_isRunning) { try { _selector.select(); } catch (IOException ex) { _log.warning("Failed to select socket to read from"); break; } // Processing ready channels Iterator<SelectionKey> keyIterator = _selector.selectedKeys().iterator(); while(keyIterator.hasNext()) { // Take keys one by one selectedKey = keyIterator.next(); keyIterator.remove(); if (false == selectedKey.isValid()) { continue; } Connection connection = (Connection)selectedKey.attachment(); // Process a requestion switch(selectedKey.readyOps()) { case SelectionKey.OP_READ: read(selectedKey, connection); break; case SelectionKey.OP_READ | SelectionKey.OP_WRITE: write(selectedKey, connection); if (selectedKey.isValid()) { read(selectedKey, connection); } case SelectionKey.OP_WRITE: write(selectedKey, connection); break; case SelectionKey.OP_CONNECT: break; case SelectionKey.OP_ACCEPT: accept(selectedKey, connection); break; } } } } /** * Accepts new socket connections and creates a client for that * @param key SelectionKey * @param connection Existing connection or Null */ protected void accept(final SelectionKey key, Connection connection) { ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel(); SocketChannel channel; try { while((channel = serverChannel.accept()) != null) { channel.configureBlocking(false); SelectionKey connectionKey = channel.register(_selector, SelectionKey.OP_READ); // Creating connection _connectionBuilder.socket = channel.socket(); _connectionBuilder.selectionKey = connectionKey; _connectionBuilder.socketStrategy = _socketStrategy; connection = _connectionBuilder.build(); connectionKey.attach(connection); _clientConnectionMap.put(connection.getId(), connection); // Handling a connection _packetHandler.handleConnection( connection); _log.info("Accepted new connection ID = ["+connection.getId()+"]"); } } catch (IOException | IllegalArgumentException e) { _log.warning("Failed to accept new connection"); } } /** * Reads data from existing connection * @param key SelectionKey * @param connection Existing socket connection */ protected void read(final SelectionKey key, Connection connection) { if (connection == null || connection.isClosed()) { _log.warning("Tried to read from non-existent or an already closed connection"); return; } try { JUniformObject[] packets = null; try { _log.info("Reading..."); packets = connection.readPackets(_readBuffer); } catch(ClosedChannelException ex) { closeConnection(key, connection, false); } catch(ConnectException ex) { closeConnection(key, connection, true); } catch(IOException ex) { //_log.log(Level.SEVERE, "Failed to read packets from connection", ex); closeConnection(key, connection, true); } if (packets != null) { for(JUniformObject packet : packets) { _packetHandler.handlePacket(packet, connection); } } } catch (CancelledKeyException ex) { _log.log(Level.WARNING, "Failed to read from connection channel", ex); closeConnection(key, connection, true); } catch (BufferOverflowException ex) { _log.log(Level.WARNING, "Packet buffer overflow. Max packet size is " + _packetBufferSize, ex); closeConnection(key, connection, true); } } /** * Writing to a connection socket * @param key SelectionKey * @param connection Current connection */ protected void write(final SelectionKey key, Connection connection) { if (connection == null || connection.isClosed()) { _log.warning("Tried to write to non-existent or an already closed connection"); return; } byte[] packet; Queue<byte[]> queue; synchronized((queue = connection.getSendQueue())) { while(false == queue.isEmpty()) { packet = queue.poll(); _log.info("Writing from queue..."); connection.sendPacket(packet); } } key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); } /** * Closing existing connection * @param key SelectionKey * @param connection Current connection * @param isForced Was it forced to close connection or not */ protected void closeConnection(final SelectionKey key, Connection connection, boolean isForced) { connection.close(); key.attach(null); key.cancel(); _clientConnectionMap.remove(connection.getId()); _log.info("Connection ["+connection.getId()+"] is closed"); } }