me.melchor9000.net.TCPSocket.java Source code

Java tutorial

Introduction

Here is the source code for me.melchor9000.net.TCPSocket.java

Source

/*
async-net: A basic asynchronous network library, based on netty
Copyright (C) 2016  melchor629 (melchor9000@gmail.com)
    
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 me.melchor9000.net;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.GenericFutureListener;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.net.SocketAddress;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * <p>TCP Socket for connexions to servers.</p>
 * <p>
 *     This implementation creates a TCP Socket for client only connections.
 *     To use the Socket you must connect to a remote endpoint using one of
 *     the available {@code connect} or {@code connectAsync} methods available.
 *     It is not recommended to call any {@code bind} methods because it will
 *     replace the current socket (if it is connected) with a new not connected.
 * </p>
 * <p>
 *     Receive operations will return when there is some data, that is, it will not
 *     ensure read an exact quantity of bytes. Receive operations stores all unread
 *     data into an intermediary <i>Buffer</i>. When it is full, new data gets discarded.
 *     Internal buffer, normally, has capacity for 146KB (not KiB). To know if there's bytes
 *     to read, you can call {@link #readableBytes()}.
 * </p>
 * <p>
 *     Send operations will send directly to the remote endpoint, flushing anything pending to send.
 *     This also depends on if Nagle's Algorithm is enabled or not.
 * </p>
 * <p>
 *     A TCP Connection can shutdown the output or the input or both streams. This can also be
 *     accomplished with {@link #shutdownOutput()}, {@link #shutdownOutputAsync()},
 *     {@link #shutdownInput()}, {@link #shutdownInputAsync()}, {@link #shutdown()} or
 *     {@link #shutdownAsync()}.
 * </p>
 * <p>
 *     When closed, all write and read operations will fail. {@link #isOpen()} will return null.
 *     Is possible to be notified when the socket is closed, by you or by the remote endpoint,
 *     using {@link #onClose()}, or the synchronous version {@link #waitUntilClose()}.
 * </p>
 */
public class TCPSocket extends Socket {
    protected SocketChannel socket;
    private ByteBuf readBuffer;
    private ConcurrentLinkedQueue<ReadOperation> readOperations;
    final ReadManager readManager;
    private volatile boolean isClosed = false;

    /**
     * Creates a new TCP Socket
     * @param service {@link IOService} to attach this socket
     */
    public TCPSocket(IOService service) {
        super(service);
        bootstrap.channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast("readManager", readManager);
            }
        });
        readBuffer = ByteBufAllocator.DEFAULT.directBuffer(1460, 1460 * 100).retain();
        readOperations = new ConcurrentLinkedQueue<>();
        readManager = new ReadManager();
    }

    TCPSocket(TCPAcceptor acceptor, SocketChannel socket) {
        super(acceptor.service);
        channel = this.socket = socket;
        readBuffer = ByteBufAllocator.DEFAULT.directBuffer(1460, 1460 * 100).retain();
        readOperations = new ConcurrentLinkedQueue<>();
        readManager = new ReadManager();
        socket.pipeline().addLast("readManager", readManager);
    }

    @Override
    public void bind(@NotNull SocketAddress address) {
        super.bind(address);
        channelCreated();
    }

    @Override
    public void connect(@NotNull SocketAddress address) throws InterruptedException {
        super.connect(address);
        channelCreated();
    }

    @NotNull
    @Override
    public Future<Void> connectAsync(@NotNull SocketAddress endpoint) {
        return super.connectAsync(endpoint).whenDone(new Callback<Future<Void>>() {
            @Override
            public void call(Future<Void> arg) {
                channelCreated();
            }
        });
    }

    @Override
    public long receive(ByteBuf data, int bytes) throws Throwable {
        checkSocketCreated("receive");
        return isClosed ? -1 : receiveAsync(data, bytes).getValue();
    }

    @NotNull
    @Override
    public Future<Long> receiveAsync(ByteBuf data, int bytes) {
        checkSocketCreated("receiveAsync");
        final ReadOperation op[] = new ReadOperation[1];
        final FutureImpl<Long> future = createFuture(new Procedure() {
            @Override
            public void call() {
                readOperations.remove(op[0]);
            }
        });

        if (!isClosed) {
            readOperations.add(op[0] = new ReadOperation(future, bytes, data));
            if (readManager.hasEnoughData()) {
                try {
                    readManager.checkAndSendData();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        } else {
            service.post(new Procedure() {
                @Override
                public void call() {
                    try {
                        future.postError(new IOException("End of stream"));
                    } catch (Exception e) {
                        System.out.println("Captured exception on Future.postError()");
                        e.printStackTrace();
                    }
                }
            });
        }
        return future;
    }

    /**
     * Ends the output stream of this connection, sending before any data pending
     * to send. After shutdown, any attempt to send anything will fail with an
     * {@link java.io.IOException}.
     */
    public void shutdownOutput() {
        checkSocketCreated("shutdownOutput");
        socket.shutdownOutput().syncUninterruptibly();
    }

    /**
     * Ends the output stream of this connection, sending before any data pending
     * to send. After shutdown, any attempt to send anything will fail with an
     * {@link java.io.IOException}.
     * @return {@link Future} of this task
     */
    public Future<Void> shutdownOutputAsync() {
        checkSocketCreated("shutdownOutputAsync");
        return createFuture(socket.shutdownOutput());
    }

    /**
     * Ends the input stream of this connection, discarding any pending data to be
     * acknowledged. Any attempt to receive some data will fail with an {@link IOException}
     * or returning -1.
     * {@link java.io.IOException}.
     */
    public void shutdownInput() {
        checkSocketCreated("shutdownInput");
        socket.shutdownInput().syncUninterruptibly();
    }

    /**
     * Ends the input stream of this connection, discarding any pending data to be
     * acknowledged. Any attempt to receive some data will fail with an {@link IOException}
     * or returning -1.
     * {@link java.io.IOException}.
     * @return {@link Future} of this task
     */
    public Future<Void> shutdownInputAsync() {
        checkSocketCreated("shutdownInputAsync");
        return createFuture(socket.shutdownInput());
    }

    /**
     * Ends the input and output stream of this connection, but not closes
     * the connection.
     * @see #shutdownInput() About shutdown the input stream
     * @see #shutdownOutput() About shutdown the Output stream
     */
    public void shutdown() {
        checkSocketCreated("shutdown");
        socket.shutdown().syncUninterruptibly();
    }

    /**
     * Ends the input and output stream of this connection, but not closes
     * the connection.
     * @return {@link Future} of this task
     * @see #shutdownInputAsync() About shutdown the input stream
     * @see #shutdownOutputAsync() About shutdown the Output stream
     */
    public Future<Void> shutdownAsync() {
        checkSocketCreated("shutdownAsync");
        return createFuture(socket.shutdown());
    }

    /**
     * @return the number of bytes that can be read, or -1 if EOF
     */
    public int readableBytes() {
        return isClosed ? -1 : readBuffer.readableBytes();
    }

    /**
     * Waits until this socket is closed. Any interruption won't affect
     * the wait.
     */
    public void waitUntilClose() {
        socket.closeFuture().syncUninterruptibly();
    }

    /**
     * Returns a {@link Future} that will complete when this socket is
     * closed.
     * @return a {@link Future} for the task
     */
    public Future<Void> onClose() {
        checkSocketCreated("onClose");
        return createFuture(socket.closeFuture());
    }

    @Override
    public void close() {
        super.close();
        readBuffer.release();
    }

    @NotNull
    @Override
    public Future<Void> closeAsync() {
        return super.closeAsync().whenDone(new Callback<Future<Void>>() {
            @Override
            public void call(Future<Void> arg) {
                readBuffer.release();
            }
        });
    }

    private class ReadManager extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buff = (ByteBuf) msg;
            try {
                bytesRead += buff.readableBytes();
                if (readBuffer.writerIndex() < readBuffer.maxCapacity()
                        && readBuffer.maxCapacity() - readBuffer.writerIndex() <= buff.readableBytes()) {
                    System.out
                            .println("Discarded " + (buff.readableBytes() - readBuffer.writableBytes()) + " bytes");
                    readBuffer.writeBytes(buff, 0, readBuffer.writableBytes());
                    checkAndSendData();
                } else if (readBuffer.maxCapacity() - readBuffer.writerIndex() > buff.readableBytes()) {
                    readBuffer.writeBytes(buff);
                    checkAndSendData();
                } else {
                    System.out.println("oberflou");
                }
            } finally {
                buff.release();
                fireReceivedData();
            }
        }

        /*
                @Override
                public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().flush();
        ctx.read();
                }
        */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
        }

        private void checkAndSendData() throws Exception {
            while (hasEnoughData()) {
                ReadOperation op = readOperations.poll();
                if (op.bytesToRead <= readBuffer.readableBytes()) {
                    readBuffer.readBytes(op.buffer, op.bytesToRead);
                    op.cbk.postSuccess((long) op.bytesToRead);
                } else {
                    long a = readBuffer.readableBytes();
                    readBuffer.readBytes(op.buffer, (int) a);
                    op.cbk.postSuccess(a);
                }
            }

            //Reset buffer positions
            if (readBuffer.readableBytes() == 0)
                readBuffer.readerIndex(0).writerIndex(0);
        }

        private boolean hasEnoughData() {
            return !readOperations.isEmpty() && readBuffer.readableBytes() != 0;
        }
    }

    private class ReadOperation {
        private FutureImpl<Long> cbk;
        private int bytesToRead;
        private ByteBuf buffer;

        private ReadOperation(FutureImpl<Long> cbk, int bytesToRead, ByteBuf buffer) {
            this.cbk = cbk;
            this.bytesToRead = bytesToRead;
            this.buffer = buffer;
        }
    }

    private void channelCreated() {
        socket = (SocketChannel) channel;
        if (socket != null) {
            socket.closeFuture()
                    .addListener(new GenericFutureListener<io.netty.util.concurrent.Future<? super Void>>() {
                        @Override
                        public void operationComplete(io.netty.util.concurrent.Future<? super Void> future)
                                throws Exception {
                            isClosed = true;
                        }
                    });
        }
    }
}