/*
* 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");
}
}