com.l2jfree.network.mmocore.ReadWriteThread.java Source code

Java tutorial

Introduction

Here is the source code for com.l2jfree.network.mmocore.ReadWriteThread.java

Source

/*
 * This program 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.
 * 
 * This program 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
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.l2jfree.network.mmocore;

import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayDeque;
import java.util.TreeSet;

import javolution.util.FastList;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import com.l2jfree.network.mmocore.FloodManager.ErrorMode;
import com.l2jfree.util.HexUtil;

/**
 * {@link MMOController} associated {@link WorkerThread} responsible for read-write operations, and
 * also for closing connections.
 * 
 * @author NB4L1
 * @param <T>
 * @param <RP>
 * @param <SP>
 */
final class ReadWriteThread<T extends MMOConnection<T, RP, SP>, RP extends ReceivablePacket<T, RP, SP>, SP extends SendablePacket<T, RP, SP>>
        extends AbstractSelectorThread<T, RP, SP> {
    private static final int PACKET_HEADER_SIZE = 2;

    // Implementations
    private final PacketHandler<T, RP, SP> _packetHandler;

    // Pending Close
    private final FastList<T> _pendingClose;

    // Configs
    private final int _bufferSize;
    private final int _helperBufferCount;
    private final int _maxOutgoingPacketsPerPass;
    private final int _maxIncomingPacketsPerPass;
    private final int _maxOutgoingBytesPerPass;
    private final int _maxIncomingBytesPerPass;
    private final ByteOrder _byteOrder;

    // MAIN BUFFERS
    private final ByteBuffer _directWriteBuffer;
    private final ByteBuffer _writeBuffer;
    private final ByteBuffer _readBuffer;

    // ByteBuffers General Purpose Pool
    private final ArrayDeque<ByteBuffer> _bufferPool;

    // wrapper for read and write operations
    private final MMOBuffer _mmoBuffer;
    private final DataSizeHolder _dataSizeHolder;

    public ReadWriteThread(MMOController<T, RP, SP> mmoController, MMOConfig config,
            PacketHandler<T, RP, SP> packetHandler) throws IOException {
        super(mmoController, config);

        _bufferSize = config.getBufferSize();
        _helperBufferCount = config.getHelperBufferCount();
        _maxOutgoingPacketsPerPass = config.getMaxOutgoingPacketsPerPass();
        _maxIncomingPacketsPerPass = config.getMaxIncomingPacketsPerPass();
        _maxOutgoingBytesPerPass = config.getMaxOutgoingBytesPerPass();
        _maxIncomingBytesPerPass = config.getMaxIncomingBytesPerPass();
        _byteOrder = config.getByteOrder();

        _directWriteBuffer = ByteBuffer.allocateDirect(getBufferSize()).order(getByteOrder());
        _writeBuffer = ByteBuffer.allocate(getBufferSize()).order(getByteOrder());
        _readBuffer = ByteBuffer.allocate(getBufferSize()).order(getByteOrder());

        _bufferPool = new ArrayDeque<ByteBuffer>(getHelperBufferCount());
        for (int i = 0; i < getHelperBufferCount(); i++)
            getFreeBuffers().addLast(ByteBuffer.allocate(getBufferSize()).order(getByteOrder()));
        _mmoBuffer = new MMOBuffer();
        _dataSizeHolder = new DataSizeHolder();

        _packetHandler = packetHandler;
        _pendingClose = FastList.newInstance();
    }

    final ByteBuffer getPooledBuffer() {
        final ByteBuffer buffer = getFreeBuffers().pollFirst();

        if (buffer == null)
            return ByteBuffer.allocate(getBufferSize()).order(getByteOrder());
        else
            return buffer;
    }

    final void recycleBuffer(ByteBuffer buf) {
        if (getFreeBuffers().size() < getHelperBufferCount()) {
            buf.clear();
            getFreeBuffers().addLast(buf);
        }
    }

    private ArrayDeque<ByteBuffer> getFreeBuffers() {
        return _bufferPool;
    }

    private FastList<T> getPendingClose() {
        return _pendingClose;
    }

    public PacketHandler<T, RP, SP> getPacketHandler() {
        return _packetHandler;
    }

    final void closeConnection(T con) {
        synchronized (getPendingClose()) {
            getPendingClose().addLast(con);
        }
    }

    private static String describeInterestOps(int interestOps) {
        final TreeSet<String> result = new TreeSet<String>();
        if ((interestOps & SelectionKey.OP_ACCEPT) != 0)
            result.add("ACCEPT");
        if ((interestOps & SelectionKey.OP_CONNECT) != 0)
            result.add("CONNECT");
        if ((interestOps & SelectionKey.OP_READ) != 0)
            result.add("READ");
        if ((interestOps & SelectionKey.OP_WRITE) != 0)
            result.add("WRITE");
        return StringUtils.join(result, "|");
    }

    @Override
    protected void handle(SelectionKey key) {
        System.out.println("ReadWriteThread.handle() " + describeInterestOps(key.interestOps()) + ", ready: "
                + describeInterestOps(key.readyOps()));
        switch (key.readyOps()) {
        case SelectionKey.OP_CONNECT:
            finishConnection(key);
            break;
        case SelectionKey.OP_READ:
            readPacket(key);
            break;
        case SelectionKey.OP_WRITE:
            writePacket(key);
            break;
        case SelectionKey.OP_READ | SelectionKey.OP_WRITE:
            writePacket(key);
            // key might have been invalidated on writePacket
            if (key.isValid())
                readPacket(key);
            break;
        default:
            System.err.println("Unknown readyOps: " + key.readyOps() + " for " + key.attachment());
            break;
        }
    }

    @Override
    protected void cleanup() {
        closePendingConnections();
    }

    private void finishConnection(SelectionKey key) {
        try {
            ((SocketChannel) key.channel()).finishConnect();
        } catch (IOException e) {
            @SuppressWarnings("unchecked")
            T con = (T) key.attachment();
            closeConnectionImpl(con, true);
            return;
        }

        // key might have been invalidated on finishConnect()
        if (key.isValid()) {
            key.interestOps(key.interestOps() | SelectionKey.OP_READ);
            key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT);
        }
    }

    private void readPacket(SelectionKey key) {
        @SuppressWarnings("unchecked")
        T con = (T) key.attachment();

        ByteBuffer buf = con.getReadBuffer();

        if (buf == null) {
            buf = getReadBuffer();
            buf.clear();
        }

        int readPackets = 0;
        int readBytes = 0;

        for (;;) {
            final int remainingFreeSpace = buf.remaining();
            int result = -2;

            try {
                result = con.getReadableByteChannel().read(buf);
            } catch (IOException e) {
                //error handling goes bellow
            }

            switch (result) {
            case -2: // IOException
            {
                closeConnectionImpl(con, true);
                return;
            }
            case -1: // EOS
            {
                closeConnectionImpl(con, false);
                return;
            }
            default: {
                buf.flip();
                // try to read as many packets as possible
                for (;;) {
                    final int startPos = buf.position();

                    if (readPackets >= getMaxIncomingPacketsPerPass() || readBytes >= getMaxIncomingBytesPerPass())
                        break;

                    if (!tryReadPacket2(con, buf))
                        break;

                    readPackets++;
                    readBytes += (buf.position() - startPos);
                }
                break;
            }
            }

            // stop reading, if we have reached a config limit
            if (readPackets >= getMaxIncomingPacketsPerPass() || readBytes >= getMaxIncomingBytesPerPass())
                break;

            // if the buffer wasn't filled completely, we should stop trying as the input channel is empty
            if (remainingFreeSpace > result)
                break;

            // compact the buffer for reusing the remaining bytes
            if (buf.hasRemaining())
                buf.compact();
            else
                buf.clear();
        }

        // check if there are some more bytes in buffer and allocate/compact to prevent content lose.
        if (buf.hasRemaining()) {
            if (buf == getReadBuffer()) {
                con.setReadBuffer(getPooledBuffer().put(getReadBuffer()));
            } else {
                buf.compact();
            }
        } else {
            if (buf == getReadBuffer()) {
                // no additional buffers used
            } else {
                con.setReadBuffer(null);
                recycleBuffer(buf);
            }
        }
    }

    private boolean tryReadPacket2(T con, ByteBuffer buf) {
        // check if header could be processed
        if (buf.remaining() >= 2) {
            // parse all headers and get expected packet size
            final int size = (buf.getChar() - PACKET_HEADER_SIZE);

            // do we got enough bytes for the packet?
            if (size <= buf.remaining()) {
                // avoid parsing dummy packets (packets without body)
                if (size > 0) {
                    int pos = buf.position();
                    parseClientPacket(buf, size, con);
                    buf.position(pos + size);
                } else {
                    // let's report error to trigger protection
                    getMMOController().report(ErrorMode.EMPTY_PACKET, con, null, null);
                }

                return true;
            } else {
                // we dont have enough bytes for the packet so we need to read and revert the header
                buf.position(buf.position() - PACKET_HEADER_SIZE);
                return false;
            }
        } else {
            // we dont have enough data for header so we need to read
            return false;
        }
    }

    private void parseClientPacket(ByteBuffer buf, int dataSize, T client) {
        final int pos = buf.position();
        final DataSizeHolder dsh = getDataSizeHolder().init(dataSize);

        if (client.decipher(buf, dsh) && buf.hasRemaining()) {
            // remove useless bytes
            dsh.decreaseSize(dsh.getMinPadding());
            // calculate possibly remaining useless bytes
            final int maxPossiblePadding = dsh.getMaxPadding() - dsh.getMinPadding();

            // apply limit
            final int limit = buf.limit();
            buf.limit(pos + dsh.getSize());

            final int opcode = buf.get() & 0xFF;

            if (getMMOController().canReceivePacketFrom(client, opcode)) {
                RP cp = getPacketHandler().handlePacket(buf, client, opcode);

                if (cp != null) {
                    System.out.println("READ: " + client.getState() + " " + cp.getClass().getSimpleName());

                    // remove useless bytes #2, using packet specs
                    int maxLeftoverPadding = maxPossiblePadding;
                    final int overflow = buf.remaining() - cp.getMaximumLength();
                    if (maxPossiblePadding > 0 && // there may be useless bytes
                            overflow > 0) // and we have too much
                    {
                        // avoid any damage to the packet body
                        final int removable = Math.min(overflow, maxPossiblePadding);
                        buf.limit(buf.limit() - removable);
                        maxLeftoverPadding -= removable;
                    }

                    getMmoBuffer().setByteBuffer(buf);
                    cp.setClient(client);

                    try {
                        if (getMmoBuffer().getAvailableBytes() < cp.getMinimumLength()) {
                            getMMOController().report(ErrorMode.BUFFER_UNDER_FLOW, client, cp, null);
                        } else if (getMmoBuffer().getAvailableBytes() > cp.getMaximumLength()) {
                            getMMOController().report(ErrorMode.BUFFER_OVER_FLOW, client, cp, null);
                        } else {
                            cp.read(getMmoBuffer());

                            client.executePacket(cp);

                            if (buf.hasRemaining() && // some unused data, a bad sign
                                    buf.remaining() > maxLeftoverPadding) // and definitely not padded bytes
                            {
                                // FIXME disabled until packet structures updated properly
                                //report(ErrorMode.BUFFER_OVER_FLOW, client, cp, null);

                                MMOController._log.info("Invalid packet format (buf: " + buf + ", dataSize: "
                                        + dataSize + ", pos: " + pos + ", limit: " + limit + ", opcode: 0x"
                                        + HexUtil.fillHex(opcode, 2) + ") used for reading - " + client + " - "
                                        + cp.getType() + " - " + getMMOController().getVersionInfo());
                            }
                        }
                    } catch (BufferUnderflowException e) {
                        getMMOController().report(ErrorMode.BUFFER_UNDER_FLOW, client, cp, e);
                    } catch (RuntimeException e) {
                        getMMOController().report(ErrorMode.FAILED_READING, client, cp, e);
                    }

                    getMmoBuffer().setByteBuffer(null);
                }
            }

            buf.limit(limit);
        }
    }

    private void writePacket(SelectionKey key) {
        @SuppressWarnings("unchecked")
        T con = (T) key.attachment();

        int wrotePackets = 0;
        int wroteBytes = 0;

        for (;;) {
            wrotePackets += prepareWriteBuffer2(con, wrotePackets, wroteBytes);
            wroteBytes += getDirectWriteBuffer().position();
            getDirectWriteBuffer().flip();

            int size = getDirectWriteBuffer().remaining();

            int result = -1;

            try {
                result = con.getWritableChannel().write(getDirectWriteBuffer());
            } catch (IOException e) {
                // error handling goes on the if bellow
            }

            // check if no error happened
            if (result >= 0) {
                // check if we wrote everything
                if (result == size) {
                    // complete write
                    synchronized (con) {
                        if (con.getSendQueue2().isEmpty() && !con.hasPendingWriteBuffer()) {
                            con.disableWriteInterest();
                            return;
                        } else if (wrotePackets >= getMaxOutgoingPacketsPerPass()
                                || wroteBytes >= getMaxOutgoingBytesPerPass())
                            return;
                    }
                } else
                // incomplete write
                {
                    con.createWriteBuffer(getDirectWriteBuffer());
                    return;
                }
            } else {
                closeConnectionImpl(con, true);
                return;
            }
        }
    }

    private int prepareWriteBuffer2(T con, int wrotePackets, int wroteBytes) {
        getDirectWriteBuffer().clear();

        // if theres pending content add it
        if (con.hasPendingWriteBuffer()) {
            con.movePendingWriteBufferTo(getDirectWriteBuffer());
            // ADDED PENDING TO DIRECT

            //wrotePackets += x; // not stored yet, so...
            wroteBytes += getDirectWriteBuffer().position();
        }

        // don't write additional, if there are still pending content
        if (!con.hasPendingWriteBuffer()) {
            while (getDirectWriteBuffer().remaining() >= 2) {
                final int startPos = getDirectWriteBuffer().position();

                if (wrotePackets >= getMaxOutgoingPacketsPerPass() || wroteBytes >= getMaxOutgoingBytesPerPass())
                    break;

                final SP sp;

                synchronized (con) {
                    sp = con.getSendQueue2().pollFirst();

                    if (sp == null)
                        break;
                }

                System.out.println("WRITE: " + con.getState() + " " + sp.getClass().getSimpleName());

                // put into WriteBuffer
                putPacketIntoWriteBuffer(con, sp);
                getWriteBuffer().flip();

                if (getDirectWriteBuffer().remaining() >= getWriteBuffer().limit()) {
                    // put last written packet to the direct buffer
                    getDirectWriteBuffer().put(getWriteBuffer());

                    wrotePackets++;
                    wroteBytes += (getDirectWriteBuffer().position() - startPos);
                } else {
                    // there isn't enough space in the direct buffer
                    con.createWriteBuffer(getWriteBuffer());
                    break;
                }
            }
        }

        return wrotePackets;
    }

    private void putPacketIntoWriteBuffer(T client, SP sp) {
        getWriteBuffer().clear();

        // set the write buffer
        getMmoBuffer().setByteBuffer(getWriteBuffer());

        // reserve space for the size
        getWriteBuffer().position(PACKET_HEADER_SIZE);

        // write content to buffer
        try {
            sp.write(client, getMmoBuffer());
        } catch (RuntimeException e) {
            MMOController._log.fatal("Failed writing: " + client + " - " + sp.getType() + " - "
                    + getMMOController().getVersionInfo(), e);
        }

        // calculate size and encrypt content
        int dataSize = getWriteBuffer().position() - PACKET_HEADER_SIZE;
        getWriteBuffer().position(PACKET_HEADER_SIZE);
        client.encipher(getWriteBuffer(), dataSize);

        // recalculate size after encryption
        dataSize = getWriteBuffer().position() - PACKET_HEADER_SIZE;

        // prepend header
        getWriteBuffer().position(0);
        getWriteBuffer().putChar((char) (PACKET_HEADER_SIZE + dataSize));

        getWriteBuffer().position(PACKET_HEADER_SIZE + dataSize);

        // set the write buffer
        getMmoBuffer().setByteBuffer(null);

        try {
            sp.packetWritten(client);
        } catch (RuntimeException e) {
            MMOController._log.fatal("Failed packetWritten: " + client + " - " + sp.getType() + " - "
                    + getMMOController().getVersionInfo(), e);
        }
    }

    private void closePendingConnections() {
        // process pending close
        synchronized (getPendingClose()) {
            for (FastList.Node<T> n = getPendingClose().head(), end = getPendingClose()
                    .tail(); (n = n.getNext()) != end;) {
                final T con = n.getValue();

                synchronized (con) {
                    if (con.getSendQueue2().isEmpty() && !con.hasPendingWriteBuffer() || con.closeTimeouted()) {
                        FastList.Node<T> temp = n.getPrevious();
                        getPendingClose().delete(n);
                        n = temp;
                        closeConnectionImpl(con, false);
                    }
                }
            }
        }
    }

    private void closeConnectionImpl(T con, boolean forced) {
        try {
            if (forced)
                con.onForcedDisconnection();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        try {
            // notify connection
            con.onDisconnection();
        } catch (RuntimeException e) {
            e.printStackTrace();
        } finally {
            try {
                // close socket and the SocketChannel
                IOUtils.closeQuietly(con.getSocket());
            } finally {
                con.releaseBuffers();
                // clear attachment
                con.getSelectionKey().attach(null);
                // cancel key
                con.getSelectionKey().cancel();
            }
        }
    }

    private int getBufferSize() {
        return _bufferSize;
    }

    public int getHelperBufferCount() {
        return _helperBufferCount;
    }

    private int getMaxOutgoingPacketsPerPass() {
        return _maxOutgoingPacketsPerPass;
    }

    private int getMaxIncomingPacketsPerPass() {
        return _maxIncomingPacketsPerPass;
    }

    private int getMaxOutgoingBytesPerPass() {
        return _maxOutgoingBytesPerPass;
    }

    private int getMaxIncomingBytesPerPass() {
        return _maxIncomingBytesPerPass;
    }

    private ByteOrder getByteOrder() {
        return _byteOrder;
    }

    private ByteBuffer getDirectWriteBuffer() {
        return _directWriteBuffer;
    }

    private ByteBuffer getWriteBuffer() {
        return _writeBuffer;
    }

    private ByteBuffer getReadBuffer() {
        return _readBuffer;
    }

    private MMOBuffer getMmoBuffer() {
        return _mmoBuffer;
    }

    private DataSizeHolder getDataSizeHolder() {
        return _dataSizeHolder;
    }
}