/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.wsclient;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import org.helios.apmrouter.jmx.ThreadPoolFactory;
import org.helios.apmrouter.jmx.threadinfo.ExtendedThreadManager;
import org.helios.apmrouter.sender.SynchOpSupport;
import org.helios.apmrouter.util.SimpleLogger;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
import org.jboss.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import org.jboss.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import org.jboss.netty.handler.codec.http.websocketx.WebSocketVersion;
import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.json.JSONObject;
/**
* <p>Title: WebSocketClient</p>
* <p>Description: WebSocket client interface to server for browser/js emulation and non-UDP comms.</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.wsclient.WebSocketClient</code></p>
*/
public class WebSocketClient extends OneToOneDecoder implements ChannelPipelineFactory {
/** The URI of the APMRouter server to connect to */
protected final URI wsuri;
/** The client websocket handshaker */
protected final WebSocketClientHandshaker handshaker;
/** The client websocket channel handler */
protected final WebSocketClientHandler wsClientHandler;
/** The client synchronous invocation response handler */
protected final SynchInvocationHandler synchHandler = new SynchInvocationHandler();
/** The synchronous invocation handler pipeline key */
protected final String synchHandlerKey;
/** The client instance channel */
protected final Channel channel;
/** The session id assigned to a web-sock connection by the server */
protected String sessionId=null;
/** The client close future */
protected final ChannelFuture closeFuture;
/** The client bootstrap */
protected final ClientBootstrap bootstrap;
/** Indicates if this is a shared client, or an exclusive one */
protected final boolean shared;
/** The configured synch request timeout in ms. */
protected long synchRequestTimeout = DEFAULT_SYNCH_TIMEOUT;
/** The default synchronous request timeout in ms. */
public static final long DEFAULT_SYNCH_TIMEOUT = 2000;
/** The websocket client boss pool */
protected static final Executor bossPool = ThreadPoolFactory.newCachedThreadPool("org.helios.apmrouter.client.websocket", "BossPool");
/** The websocket client worker pool */
protected static final Executor workerPool = ThreadPoolFactory.newCachedThreadPool("org.helios.apmrouter.client.websocket", "WokerPool");
/** The websocket client application thread pool */
protected static final Executor applicationPool = ThreadPoolFactory.newCachedThreadPool("org.helios.apmrouter.client.websocket", "ApplicationPool");
/** The client channel factory */
protected static final NioClientSocketChannelFactory channelFactory = new NioClientSocketChannelFactory(bossPool, workerPool);
/** The client channel channel group */
protected static final ChannelGroup channelGroup = new DefaultChannelGroup("WebSocketClients");
/** The client websocket client handshaker factory */
protected static final WebSocketClientHandshakerFactory handshakerFactory = new WebSocketClientHandshakerFactory();
/** A map of WebSocketClient keyed by the URI */
protected static final Map<URI, WebSocketClient> clients = new ConcurrentHashMap<URI, WebSocketClient>();
/** Empty header map const */
public static final Map<String, String> WS_HEADER_MAP = Collections.unmodifiableMap(Collections.singletonMap("wc-client", "java-se"));
/** Shared HttpResponse decoder handler */
protected static final HttpResponseDecoder httpResponseDecoder = new HttpResponseDecoder();
/** Shared HttpRequest encoder handler */
protected static final HttpRequestEncoder httpRequestEncoder= new HttpRequestEncoder();
/** Shared json codec */
protected static final JsonCodec jsonHandler = new JsonCodec(applicationPool);
/** Shared http chunk aggregator */
protected static final HttpChunkAggregator chunkAggregator = new HttpChunkAggregator(1048576);
/** The leading string in the session id message by the server when the agent first connects */
public static final String SESSION_SIGNATURE = "{\"sessionid\":";
static {
InternalLoggerFactory.setDefaultFactory(new SimpleLoggerFactory());
if(!ExtendedThreadManager.isInstalled()) {
ExtendedThreadManager.install();
}
}
/**
* Synchronously acquires a shared WebSocketClient instance for the passed URI
* @param wsuri The URI of the APMRouter server to connect to
* @return a WebSocketClient instance
*/
public static WebSocketClient getInstance(final URI wsuri) {
if(wsuri==null) throw new IllegalArgumentException("The passed URI was null", new Throwable());
if(!"ws".equals(wsuri.getScheme().toLowerCase())) throw new IllegalArgumentException("The passed URI had an invalid scheme [" + wsuri.getScheme() + "]", new Throwable());
WebSocketClient client = clients.get(wsuri);
if(client==null) {
synchronized(clients) {
client = clients.get(wsuri);
if(client==null) {
client = new WebSocketClient(true, wsuri);
client.closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
clients.remove(wsuri);
}
});
clients.put(wsuri, client);
}
}
}
return client;
}
/**
* Synchronously acquires an exclusive WebSocketClient instance for the passed URI
* @param wsuri The URI of the APMRouter server to connect to
* @param listeners Optional listeners to register
* @return a WebSocketClient instance
*/
public static WebSocketClient getNewInstance(final URI wsuri, WebSocketEventListener...listeners) {
if(wsuri==null) throw new IllegalArgumentException("The passed URI was null", new Throwable());
if(!"ws".equals(wsuri.getScheme().toLowerCase())) throw new IllegalArgumentException("The passed URI had an invalid scheme [" + wsuri.getScheme() + "]", new Throwable());
return new WebSocketClient(false, wsuri, listeners);
}
/**
* Creates a new WebSocketClient
* @param shared true for a shared client, false for an exclusive one
* @param wsuri The URI of the APMRouter server to connect to
* @param listeners Optional listeners to register
*/
protected WebSocketClient(boolean shared, URI wsuri, WebSocketEventListener...listeners) {
this.wsuri = wsuri;
this.shared = shared;
bootstrap = new ClientBootstrap(channelFactory);
bootstrap.setPipelineFactory(this);
if(listeners!=null && listeners.length>0) {
for(WebSocketEventListener listener: listeners) {
if(listener!=null) {
addWebSocketEventListener(listener);
}
}
}
handshaker = handshakerFactory.newHandshaker(wsuri, WebSocketVersion.V13, null, true, WS_HEADER_MAP);
wsClientHandler = new WebSocketClientHandler(handshaker);
ChannelFuture connectFuture = bootstrap.connect(new InetSocketAddress(wsuri.getHost(), wsuri.getPort()));
connectFuture.syncUninterruptibly();
channel = connectFuture.getChannel();
synchHandlerKey = "Synch" + channel.getId();
closeFuture = channel.getCloseFuture();
channelGroup.add(channel);
try {
handshaker.handshake(channel).syncUninterruptibly();
} catch (Exception ex) {
throw new RuntimeException("WSClient [" + wsuri + "] failed to handshake", ex);
}
}
/** Serial factory for request ids */
protected final AtomicInteger requestSerial = new AtomicInteger();
/**
* Starts a client websocket daemon
* @param args As follows:<ul></ul>
*/
public static void main(String[] args) {
log("WebSocketClient Test");
WebSocketEventListener listener = new EmptyWebSocketResponseListener() {
@Override
public void onConnect(SocketAddress remoteAddress) {
log("Connected to [" + remoteAddress + "]");
}
@Override
public void onClose(SocketAddress remoteAddress) {
log("Disconnected from [" + remoteAddress + "]");
}
@Override
public void onError(SocketAddress remoteAddress, Throwable t) {
log("Error on channel to [" + remoteAddress + "]");
t.printStackTrace(System.err);
}
@Override
public void onMessage(SocketAddress remoteAddress, JSONObject message) {
try {
log("Message Received from [" + remoteAddress + "]-->[\n" + message.toString(2) + "\n]");
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
};
try {
//jsonHandler.addWebSocketEventListener(listener);
WebSocketClient client = new WebSocketClient(false, new URI("ws://10.230.13.15:1081/1.0/types"), listener);
JSONObject request = new JSONObject();
int reqId = client.requestSerial.incrementAndGet();
request.put("rid", reqId);
request.put("t", "req");
request.put("svc", "sub");
request.put("op", "start");
JSONObject ags = new JSONObject();
ags.put("es", "jmx");
ags.put("esn", "service:jmx:local://DefaultDomain");
ags.put("f", "org.helios.apmrouter.session:service=SharedChannelGroup");
request.put("args", ags);
//client.channel.write(request);
//Thread.currentThread().join(60000);
// function(callback, op, type, esn, filter, ex) {
//"start", "jmx", "service:jmx:local://DefaultDomain",
//"org.helios.apmrouter.session:service=SharedChannelGroup");
/*
* var req = {'t': 'req', 'svc' : 'sub', 'op' : op};
* var args = {'es' : type, 'esn': esn, 'f' : filter};
if(ex!=null) {
if($.isArray(ex)) {
args['stf'] = ex;
} else {
args['exf'] = ex;
}
}
req['args'] = args;
var cb = callback;
var rid = $.apmr.send(req, function(data){
var unsubKey = $.subscribe(topic, cb);
sub['ts'] = new Date().getTime();
sub['subId'] = data.msg;
});
$.apmr.config.subsByReqId[rid] = sub;
sub['rid'] = rid;
var topic = '/' + 'req' + '/' + rid;
sub['topic'] = topic;
*/
} catch (Exception ex) {
ex.printStackTrace(System.err);
System.exit(-1);
} finally {
log("Closing channels");
try { channelGroup.close().awaitUninterruptibly(); } catch (Exception ex) {}
log("Closing channel factory");
try { channelFactory.releaseExternalResources(); } catch (Exception ex) {}
}
}
/**
* Adds a web socket event listener
* @param listener the listener to add
*/
public void addWebSocketEventListener(WebSocketEventListener listener) {
jsonHandler.addWebSocketEventListener(listener);
}
/**
* Removes a web socket event listener
* @param listener the listener to remove
*/
public void removeWebSocketEventListener(WebSocketEventListener listener) {
jsonHandler.removeWebSocketEventListener(listener);
}
/**
* Adds the synchronous handler to the pipeline if not already installed
* @param rid The request of the pending response
* @param latch The latch to countdown on response receipt
*/
protected void addSynchHandler(long rid, CountDownLatch latch) {
if(channel.getPipeline().get(synchHandlerKey)==null) {
channel.getPipeline().addLast(synchHandlerKey, synchHandler);
synchHandler.prepSynchRequest(rid, latch);
} else {
throw new RuntimeException("Cannot wait on multiple synch requests. (rid=" + rid + ")", new Throwable());
}
}
/**
* Indicates if this client is connected
* @return true if connected, false otherwise
*/
public boolean isConnected() {
return (channel!=null && channel.isConnected());
}
/**
* Closes the client if it is exclusive
*/
public void close() {
if(!shared) {
channel.close();
}
}
/**
* Removes the synchronous handler from the pipeline
*/
protected void clearSynchHandler() {
try { channel.getPipeline().remove(synchHandlerKey); } catch (Exception ex) {}
}
/**
* Sends a JSON request to the server
* @param asynch true for asynch, false for synch
* @param request The JSONObject request
* @return The response to the send if synchronous, otherwise null
*/
public JSONObject sendRequest(final boolean asynch, final JSONObject request) {
try {
if(request==null) throw new IllegalArgumentException("The passed request was null", new Throwable());
final long rid;
final CountDownLatch latch;
try {
rid = request.getLong("rid");
} catch (Exception ex) {
throw new RuntimeException("No request id found in request", new Throwable());
}
if(!asynch) {
latch = SynchOpSupport.registerSynchOp(rid, synchRequestTimeout);
addSynchHandler(rid, latch);
} else {
latch = null;
}
ChannelFuture cf = channel.write(request);
if(asynch) {
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(!future.isSuccess()) SimpleLogger.error("Asynch request failed [" + request.toString() + "]", future.getCause());
}
});
return null;
}
return synchHandler.getSynchResponse(rid, synchRequestTimeout);
} finally {
clearSynchHandler();
}
}
/**
* Sends a string request to the server asynchronously
* @param request The string request
* @return A ChannelFuture for the completion of this send operation.
*/
public ChannelFuture sendRequest(final CharSequence request) {
if(request==null) throw new IllegalArgumentException("The passed request was null", new Throwable());
ChannelFuture cf = channel.write(request);
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(!future.isSuccess()) {
SimpleLogger.error("Failed to send request to [", wsuri, "] ", future.getCause());
}
}
});
return cf;
}
public static void log(Object msg) {
System.out.println(msg);
}
/**
* {@inheritDoc}
* @see org.jboss.netty.channel.ChannelPipelineFactory#getPipeline()
*/
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", httpResponseDecoder);
pipeline.addLast("aggregator", chunkAggregator);
pipeline.addLast("encoder", httpRequestEncoder);
pipeline.addLast("ws-handler", wsClientHandler);
pipeline.addLast("json-handler", jsonHandler);
pipeline.addLast("session-handler", this);
return pipeline;
}
/**
* {@inheritDoc}
* @see org.jboss.netty.handler.codec.oneone.OneToOneDecoder#decode(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.Channel, java.lang.Object)
*/
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception {
SimpleLogger.info("Processing SessionID handshake [", msg, "]");
ctx.getPipeline().remove(this);
sessionId = (String)msg;
return null;
}
/**
* Returns
* @return the synchRequestTimeout
*/
public long getSynchRequestTimeout() {
return synchRequestTimeout;
}
/**
* Sets
* @param synchRequestTimeout the synchRequestTimeout to set
*/
public void setSynchRequestTimeout(long synchRequestTimeout) {
this.synchRequestTimeout = synchRequestTimeout;
}
/**
* Closes this client
*/
protected void _close() {
this.channel.close().awaitUninterruptibly(500);
}
/**
* Stops the websocket client closing all connections and releasing the channel factory
*/
protected void shutdown() {
channelGroup.close().awaitUninterruptibly(500);
channelFactory.releaseExternalResources();
}
/**
* Returns the websocket client's WS URI
* @return the websocket client's WS URI
*/
public URI getWebSocketURI() {
return wsuri;
}
/**
* Returns the session id assigned to a web-sock connection by the server
* @return the session id assigned to a web-sock connection by the server
*/
public String getSessionId() {
return sessionId;
}
}