/* * This file is part of aion-emu <aion-emu.com>. * * aion-emu 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. * * aion-emu 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 aion-emu. If not, see <http://www.gnu.org/licenses/>. */ package com.aionemu.commons.network; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import org.apache.log4j.Logger; import com.aionemu.commons.options.Assertion; /** * Dispatcher that dispatch SelectionKeys set selected by Selector. * * @author -Nemesiss- * */ public abstract class Dispatcher extends Thread { /** * Logger for Dispatcher */ private static final Logger log = Logger.getLogger(Dispatcher.class); /** * Selector thats selecting ready keys. */ Selector selector; /** * ThreadPool on witch disconnection tasks will be executed. */ private final DisconnectionThreadPool dcPool; /** * Object on witch register vs selector.select are synchronized */ private final Object gate = new Object(); /** * Constructor. * * @param name * @param dcPool * @throws IOException */ public Dispatcher(String name, DisconnectionThreadPool dcPool) throws IOException { super(name); this.selector = SelectorProvider.provider().openSelector(); this.dcPool = dcPool; } /** * Add connection to pendingClose list, so this connection will be closed by this <code>Dispatcher</code> as soon as * possible. * * @param con * * @see com.aionemu.commons.network.Dispatcher#closeConnection(com.aionemu.commons.network.AConnection) */ abstract void closeConnection(AConnection con); /** * Dispatch Selected keys and process pending close. * * @throws IOException */ abstract void dispatch() throws IOException; /** * @return Selector of this Dispatcher */ public final Selector selector() { return this.selector; } /** * Dispatching Selected keys and processing pending close. * * @see java.lang.Thread#run() */ @Override public void run() { for(;;) { try { dispatch(); synchronized(gate) { } } catch(Exception e) { log.error("Dispatcher error! " + e, e); } } } /** * Register new client connected to this Dispatcher and set SelectionKey (result of registration) as this key of * given AConnection. * * @param ch * @param ops * @param att * @throws IOException */ public final void register(SelectableChannel ch, int ops, AConnection att) throws IOException { synchronized(gate) { selector.wakeup(); att.setKey(ch.register(selector, ops, att)); } } /** * Register new Acceptor this Dispatcher and return SelectionKey (result of registration). * * @param ch * @param ops * @param att * @return SelectionKey representing this registration. * @throws IOException */ public final SelectionKey register(SelectableChannel ch, int ops, Acceptor att) throws IOException { synchronized(gate) { selector.wakeup(); return ch.register(selector, ops, att); } } /** * Accept new connection. * * @param key */ final void accept(SelectionKey key) { try { ((Acceptor) key.attachment()).accept(key); } catch(Exception e) { log.error("Error while accepting connection: +" + e, e); } } /** * Read data from socketChannel represented by SelectionKey key. Parse and Process data. Prepare buffer for next * read. * * @param key */ final void read(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); AConnection con = (AConnection) key.attachment(); ByteBuffer rb = con.readBuffer; /** * Test if this build should use assertion. If NetworkAssertion == false javac will remove this code block */ if(Assertion.NetworkAssertion) { assert con.readBuffer.hasRemaining(); } /** Attempt to read off the channel */ int numRead; try { numRead = socketChannel.read(rb); } catch(IOException e) { closeConnectionImpl(con); return; } if(numRead == -1) { /** * Remote entity shut the socket down cleanly. Do the same from our end and cancel the channel. */ closeConnectionImpl(con); return; } else if(numRead == 0) { return; } rb.flip(); while(rb.remaining() > 2 && rb.remaining() >= rb.getShort(rb.position())) { /** got full message */ if(!parse(con, rb)) { closeConnectionImpl(con); return; } } if(rb.hasRemaining()) { con.readBuffer.compact(); /** * Test if this build should use assertion. If NetworkAssertion == false javac will remove this code block */ if(Assertion.NetworkAssertion) { assert con.readBuffer.hasRemaining(); } } else rb.clear(); } /** * Parse data from buffer and prepare buffer for reading just one packet - call processData(ByteBuffer b). * * @param con * Connection * @param buf * Buffer with packet data * @return True if packet was parsed. */ private boolean parse(AConnection con, ByteBuffer buf) { short sz = 0; try { sz = buf.getShort(); if(sz > 1) sz -= 2; ByteBuffer b = (ByteBuffer) buf.slice().limit(sz); b.order(ByteOrder.LITTLE_ENDIAN); /** read message fully */ buf.position(buf.position() + sz); return con.processData(b); } catch(IllegalArgumentException e) { log.warn("Error on parsing input from client - account: " + con + " packet size: " + sz + " real size:" + buf.remaining(), e); return false; } } /** * Write as much as possible data to socketChannel represented by SelectionKey key. If all data were written key * write interest will be disabled. * * @param key */ final void write(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); AConnection con = (AConnection) key.attachment(); int numWrite; ByteBuffer wb = con.writeBuffer; /** We have not writted data */ if(wb.hasRemaining()) { try { numWrite = socketChannel.write(wb); } catch(IOException e) { closeConnectionImpl(con); return; } if(numWrite == 0) { log.info("Write " + numWrite + " ip: " + con.getIP()); return; } /** Again not all data was send */ if(wb.hasRemaining()) return; } while(true) { wb.clear(); boolean writeFailed = !con.writeData(wb); if(writeFailed) { wb.limit(0); break; } /** Attempt to write to the channel */ try { numWrite = socketChannel.write(wb); } catch(IOException e) { closeConnectionImpl(con); return; } if(numWrite == 0) { log.info("Write " + numWrite + " ip: " + con.getIP()); return; } /** not all data was send */ if(wb.hasRemaining()) return; } /** * Test if this build should use assertion. If NetworkAssertion == false javac will remove this code block */ if(Assertion.NetworkAssertion) { assert !wb.hasRemaining(); } /** * We wrote away all data, so we're no longer interested in writing on this socket. */ key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); /** We wrote all data so we can close connection that is "PandingClose" */ if(con.isPendingClose()) closeConnectionImpl(con); } /** * Connection will be closed [onlyClose()] and onDisconnect() method will be executed on another thread * [DisconnectionThreadPool] after getDisconnectionDelay() time in ms. This method may only be called by current * Dispatcher Thread. * * @param con */ protected final void closeConnectionImpl(AConnection con) { /** * Test if this build should use assertion. If NetworkAssertion == false javac will remove this code block */ if(Assertion.NetworkAssertion) assert Thread.currentThread() == this; if(con.onlyClose()) dcPool.scheduleDisconnection(new DisconnectionTask(con), con.getDisconnectionDelay()); } }