org.kurento.jsonrpc.client.JsonRpcClientNettyWebSocket.java Source code

Java tutorial

Introduction

Here is the source code for org.kurento.jsonrpc.client.JsonRpcClientNettyWebSocket.java

Source

/*
 * (C) Copyright 2013 Kurento (http://kurento.org/)
 *
 * 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 org.kurento.jsonrpc.client;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import javax.net.ssl.SSLException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;

public class JsonRpcClientNettyWebSocket extends AbstractJsonRpcClientWebSocket {

    public class JsonRpcWebSocketClientHandler extends AbstractJsonRpcWebSocketClientHandler {

        private StringBuilder partialText = new StringBuilder();

        public JsonRpcWebSocketClientHandler(WebSocketClientHandshaker handshaker) {
            super(handshaker);
        }

        @Override
        public void handlerAdded(ChannelHandlerContext ctx) {
            handshakeFuture = ctx.newPromise();
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            log.debug("{} channel active", label);
            handshaker.handshake(ctx.channel());
        }

        @Override
        public void channelWritabilityChanged(ChannelHandlerContext ctx) {
            log.debug("{} channel inactive", label);
            handleReconnectDisconnection(0, "Unknown reason");
        }

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof IdleStateEvent) {
                log.debug("{} Idle state event received", label);
                handleReconnectDisconnection(0, "Idle event received");
            }
        }

        @Override
        public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
            Channel ch = ctx.channel();
            if (!handshaker.isHandshakeComplete()) {
                handshaker.finishHandshake(ch, (FullHttpResponse) msg);
                log.debug("{} WebSocket Client connected!", label);
                handshakeFuture.setSuccess();
                return;
            }

            if (msg instanceof FullHttpResponse) {
                FullHttpResponse response = (FullHttpResponse) msg;
                throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.status()
                        + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
            }

            WebSocketFrame frame = (WebSocketFrame) msg;
            if (frame instanceof TextWebSocketFrame) {
                TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
                if (textFrame.isFinalFragment()) {
                    receivedTextMessage(textFrame.text());
                } else {
                    partialText.append(textFrame.text());
                }
            } else if (frame instanceof ContinuationWebSocketFrame) {
                ContinuationWebSocketFrame continuationFrame = (ContinuationWebSocketFrame) frame;
                partialText.append(continuationFrame.text());
                if (continuationFrame.isFinalFragment()) {
                    receivedTextMessage(partialText.toString());
                    partialText.setLength(0);
                }
            } else if (frame instanceof CloseWebSocketFrame) {
                CloseWebSocketFrame closeFrame = (CloseWebSocketFrame) frame;
                log.info("{} Received close frame from server. Will close client! Reason: {}", label,
                        closeFrame.reasonText());
            } else {
                log.warn("{} Received frame of type {}. Will be ignored", label, frame.getClass().getSimpleName());
            }

        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            log.warn("{} Exception caught in Netty websocket handler", label, cause);
            if (!handshakeFuture.isDone()) {
                handshakeFuture.setFailure(cause);
            }
            try {
                close();
            } catch (IOException e) {
                log.warn("{} Exception closing Netty websocket client", label);
            }
        }

    }

    private static final Logger log = LoggerFactory.getLogger(JsonRpcClientNettyWebSocket.class);

    private volatile Channel channel;
    private volatile EventLoopGroup group;
    private volatile JsonRpcWebSocketClientHandler handler;

    public JsonRpcClientNettyWebSocket(String url) {
        this(url, null);
    }

    public JsonRpcClientNettyWebSocket(String url, JsonRpcWSConnectionListener connectionListener) {
        super(url, connectionListener);
        log.debug("{} Creating JsonRPC NETTY Websocket client", label);
    }

    @Override
    protected void sendTextMessage(String jsonMessage) throws IOException {

        if (channel == null || !channel.isWritable() || !channel.isActive()) {
            throw new IllegalStateException(
                    label + " JsonRpcClient is disconnected from WebSocket server at '" + this.uri + "'");
        }

        synchronized (channel) {
            channel.writeAndFlush(new TextWebSocketFrame(jsonMessage));
        }
    }

    @Override
    protected boolean isNativeClientConnected() {
        return channel != null && channel.isActive();
    }

    @Override
    protected void connectNativeClient() throws TimeoutException, Exception {

        if (channel == null || !channel.isActive() || group == null || group.isShuttingDown()
                || group.isShutdown()) {

            log.info("{} Connecting native client", label);

            final boolean ssl = "wss".equalsIgnoreCase(this.uri.getScheme());
            final SslContext sslCtx;
            try {
                sslCtx = ssl
                        ? SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build()
                        : null;
            } catch (SSLException e) {
                log.error("{} Could not create SSL Context", label, e);
                throw new IllegalArgumentException("Could not create SSL context. See logs for more details", e);
            }

            final String scheme = uri.getScheme() == null ? "ws" : uri.getScheme();
            final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
            final int port;
            if (uri.getPort() == -1) {
                if ("ws".equalsIgnoreCase(scheme)) {
                    port = 80;
                } else if ("wss".equalsIgnoreCase(scheme)) {
                    port = 443;
                } else {
                    port = -1;
                }
            } else {
                port = uri.getPort();
            }

            if (group == null || group.isShuttingDown() || group.isShutdown() || group.isTerminated()) {
                log.info("{} Creating new NioEventLoopGroup", label);
                group = new NioEventLoopGroup();
            }

            if (channel != null) {
                log.info("{} Closing previously existing channel when connecting native client", label);
                closeChannel();
            }

            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    log.info("{} Inititating new Netty channel. Will create new handler too!", label);
                    handler = new JsonRpcWebSocketClientHandler(WebSocketClientHandshakerFactory.newHandshaker(uri,
                            WebSocketVersion.V13, null, true, new DefaultHttpHeaders(), maxPacketSize));

                    ChannelPipeline p = ch.pipeline();
                    p.addLast("idleStateHandler", new IdleStateHandler(0, 0, idleTimeout / 1000));
                    if (sslCtx != null) {
                        p.addLast(sslCtx.newHandler(ch.alloc(), host, port));
                    }
                    p.addLast(new HttpClientCodec(), new HttpObjectAggregator(8192),
                            WebSocketClientCompressionHandler.INSTANCE, handler);
                }
            }).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectionTimeout);

            int numRetries = 0;
            final int maxRetries = 5;
            while (channel == null || !channel.isOpen()) {
                try {
                    channel = b.connect(host, port).sync().channel();
                    handler.handshakeFuture().sync();
                } catch (InterruptedException e) {
                    // This should never happen
                    log.warn("{} ERROR connecting WS Netty client, opening channel", label, e);
                } catch (Exception e) {
                    if (e.getCause() instanceof WebSocketHandshakeException && numRetries < maxRetries) {
                        log.warn(
                                "{} Upgrade exception when trying to connect to {}. Try {} of {}. Retrying in 200ms ",
                                label, uri, numRetries + 1, maxRetries);
                        Thread.sleep(200);
                        numRetries++;
                    } else {
                        throw e;
                    }
                }

            }

            channel.closeFuture().addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    log.info("{} channel closed", label);
                    handleReconnectDisconnection(1001, "Channel closed");
                }
            });

        }

    }

    @Override
    public void closeNativeClient() {
        closeChannel();

        if (group != null) {
            group.shutdownGracefully();
        } else {
            log.warn("{} Trying to close a JsonRpcClientNettyWebSocket with group == null", label);
        }
        group = null;
        handler = null;
    }

    private void closeChannel() {
        if (channel != null) {
            log.debug("{} Closing client", label);
            try {
                channel.close().sync();
            } catch (Exception e) {
                log.debug("{} Could not properly close websocket client. Reason: {}", label, e.getMessage(), e);
            }
            channel = null;
        } else {
            log.warn("{} Trying to close a JsonRpcClientNettyWebSocket with channel == null", label);
        }
    }

}