/* * (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); } } }