mmo.client.connection.ServerConnection.java Source code

Java tutorial

Introduction

Here is the source code for mmo.client.connection.ServerConnection.java

Source

/*
 * Copyright 2015 Florian Hassanen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package mmo.client.connection;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import mmo.client.message.Message;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Client connection to mmo-server.
 * <p/>
 * Allows for sending and receiving messages, as well as querying data endpoints
 * directly.
 * <p/>
 * This class operates asynchronously. Keep this in mind when integrating
 * this code into yours. Use suitable synchronisation mechanisms (like e.g.
 * javax.swing.SwingUtilities#invokeLater when interfacing with the swing/awt
 * event loop)
 * <p/>
 * You need to open the connection by explicitly calling #open()
 */
public class ServerConnection {
    /**
     * enable this switch to print all received json messages prior to
     * decoding them
     */
    private final boolean DEBUG = false;

    private final String host;
    private final int port;
    private final String username;
    private final ConcurrentMap<MessageListener, Boolean> listeners = new ConcurrentHashMap<>();

    private final ObjectMapper mapper;
    private final ObjectWriter messageWriter;
    private final ObjectReader messageReader;

    private Channel notificationChannel;
    private Channel dataChannel;

    private NioEventLoopGroup dataGroup = new NioEventLoopGroup(1);
    private NioEventLoopGroup notificationGroup = new NioEventLoopGroup(1);
    private DataHandler dataHandler = new DataHandler();

    /**
     * Connects to mmo server with hostname and port information and login
     * anonymously.
     *
     * @param host hostname or ip of server
     * @param port port to connect to
     */
    public ServerConnection(String host, int port) {
        this(host, port, null);
    }

    /**
     * Connects to mmo server with hostname and port and login with given
     * username.
     *
     * @param host     hostname or ip of server
     * @param port     port to connect to
     * @param username login name
     */
    public ServerConnection(String host, int port, String username) {
        this.host = host;
        this.port = port;
        this.username = username;

        mapper = new ObjectMapper().setDefaultTyping(
                new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE)
                        .init(JsonTypeInfo.Id.MINIMAL_CLASS, null).inclusion(JsonTypeInfo.As.PROPERTY)
                        .typeProperty("type"))
                .setSerializationInclusion(JsonInclude.Include.NON_NULL).enable(SerializationFeature.INDENT_OUTPUT)
                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        messageWriter = mapper.writerFor(Message.class);
        messageReader = mapper.reader(Message.class);
    }

    /**
     * Opens connection to server. This method must be called explicitly.
     */
    public void open() {
        new Bootstrap().group(notificationGroup).channel(NioSocketChannel.class)
                .handler(new NotificationInitializer())
                .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(16384))
                .option(ChannelOption.TCP_NODELAY, true).connect(this.host, this.port);
        new Bootstrap().group(dataGroup).channel(NioSocketChannel.class).handler(new DataInitializer())
                .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(16384))
                .option(ChannelOption.TCP_NODELAY, true).connect(this.host, this.port);
    }

    /**
     * Closes connection to server. Per default connections will stay open
     * indefinitely. Use this method to close the connection gracefully.
     */
    public void close() {
        notificationGroup.shutdownGracefully();
        dataGroup.shutdownGracefully();
    }

    /**
     * Registers a message listener. It will be notified about every message
     * received. If a message cannot be decoded, it will call the
     * MessageListener#messageReceived with a <code>Message</code> argument of
     * <code>null</code>.
     *
     * @param listener Listener to register
     */
    public void addMessageListener(MessageListener listener) {
        listeners.putIfAbsent(listener, Boolean.TRUE);
    }

    /**
     * Removes a message listener.
     *
     * @param listener Listener to remove
     */
    public void removeMessageListener(MessageListener listener) {
        listeners.remove(listener);
    }

    /**
     * Sends a <code>Message</code> to the server.
     *
     * @param message <code>Message</code> to send.
     * @throws JsonProcessingException On encoding errors
     */
    public void sendMessage(Message message) throws JsonProcessingException {
        notificationChannel.writeAndFlush(message);
    }

    /**
     * Queries data from the server. Queries data from the given URI and decodes
     * it as value of the given class. The result is received asynchronously.
     * You may register a listener on the returned <code>Future</code> or
     * block on it to wait for the result to arrive.
     *
     * @param uri   URI to query data from
     * @param clazz Class to decode data to
     * @param <T>   <code>Class</code> type param
     * @return <code>Future</code> of value to be received and decoded
     */
    public <T> Future<T> getData(final String uri, final Class<T> clazz) {
        final Promise<T> promise = new DefaultPromise<>(dataGroup.next());
        dataGroup.submit(new Runnable() {
            @Override
            public void run() {
                dataHandler.getData(uri, clazz, promise);
            }
        });
        return promise;
    }

    private void messageReceived(Message message) {
        for (MessageListener listener : listeners.keySet()) {
            listener.messageReceived(message);
        }
    }

    private class NotificationInitializer extends ChannelInitializer<NioSocketChannel> {

        @Override
        protected void initChannel(final NioSocketChannel ch) throws Exception {
            URI uri = new URI("ws", null, host, port, "/game/" + (username == null ? "" : uriEncode(username)),
                    null, null);
            System.out.println(uri);

            notificationChannel = ch;
            ch.pipeline().addLast(
                    new HttpClientCodec(), new HttpObjectAggregator(65536), new WebSocketClientProtocolHandler(uri,
                            WebSocketVersion.V13, null, false, new DefaultHttpHeaders(), 65536, true),
                    new MessageCodec(), new NotificationHandler());
        }
    }

    private class DataInitializer extends ChannelInitializer<NioSocketChannel> {

        @Override
        protected void initChannel(final NioSocketChannel ch) throws Exception {
            dataChannel = ch;
            ch.pipeline().addLast(new HttpClientCodec()).addLast(dataHandler);

        }
    }

    private static String uriEncode(String path) {
        try {
            return new URI(null, null, path, null).getRawPath();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private class MessageCodec extends MessageToMessageCodec<TextWebSocketFrame, Message> {
        @Override
        protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
            out.add(new TextWebSocketFrame(Unpooled.wrappedBuffer(messageWriter.writeValueAsBytes(msg))));
        }

        @Override
        protected void decode(ChannelHandlerContext ctx, TextWebSocketFrame msg, List<Object> out)
                throws Exception {
            String json = msg.text();

            if (DEBUG) {
                System.out.println(json);
            }

            try {
                out.add(messageReader.readValue(json));
            } catch (IllegalArgumentException e) {
                // TODO just warn if decoded type is not implement yet
                ctx.fireExceptionCaught(e);
            }
        }
    }

    private class NotificationHandler extends SimpleChannelInboundHandler<Message> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
            messageReceived(msg);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            super.exceptionCaught(ctx, cause);
        }
    }

    private static class DataTask<T> {
        public String uri;
        public Class<T> clazz;
        public Promise<T> promise;

        public DataTask(String uri, Class<T> clazz, Promise<T> promise) {
            this.uri = uri;
            this.clazz = clazz;
            this.promise = promise;
        }
    }

    private class DataHandler extends ChannelInboundHandlerAdapter {
        private LinkedList<DataTask> queue = new LinkedList<>();
        private boolean waitingForResponse = true;

        public <T> void getData(String uri, Class<T> clazz, Promise<T> promise) {
            queue.addLast(new DataTask<>(uri, clazz, promise));
            workQueue();
        }

        private void workQueue() {
            if (queue.isEmpty()) {
                return;
            }
            if (!waitingForResponse) {
                waitingForResponse = true;

                HttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                        uriEncode(queue.getFirst().uri));

                dataChannel.writeAndFlush(req);
            }
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            waitingForResponse = false;
            workQueue();
            super.channelActive(ctx);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            try {
                if (msg instanceof LastHttpContent) {
                    LastHttpContent res = (LastHttpContent) msg;

                    DataTask task = queue.removeFirst();

                    String json = res.content().toString(CharsetUtil.UTF_8);

                    task.promise.setSuccess(mapper.readValue(json, task.clazz));

                    waitingForResponse = false;
                }

                workQueue();
            } finally {
                ReferenceCountUtil.release(msg);
            }
        }
    }
}