| /* | |
| * Copyright 2013 gitblit.com. | |
| * | |
| * 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 com.gitblit.fanout; | |
| import java.io.IOException; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.nio.ByteBuffer; | |
| import java.nio.channels.SelectionKey; | |
| import java.nio.channels.Selector; | |
| import java.nio.channels.SocketChannel; | |
| import java.nio.charset.Charset; | |
| import java.nio.charset.CharsetDecoder; | |
| import java.text.MessageFormat; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.Date; | |
| import java.util.Iterator; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Set; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| /** | |
| * Fanout client class. | |
| * | |
| * @author James Moger | |
| * | |
| */ | |
| public class FanoutClient implements Runnable { | |
| private final static Logger logger = LoggerFactory.getLogger(FanoutClient.class); | |
| private final int clientTimeout = 500; | |
| private final int reconnectTimeout = 2000; | |
| private final String host; | |
| private final int port; | |
| private final List<FanoutListener> listeners; | |
| private String id; | |
| private volatile Selector selector; | |
| private volatile SocketChannel socketCh; | |
| private Thread clientThread; | |
| private final AtomicBoolean isConnected; | |
| private final AtomicBoolean isRunning; | |
| private final AtomicBoolean isAutomaticReconnect; | |
| private final ByteBuffer writeBuffer; | |
| private final ByteBuffer readBuffer; | |
| private final CharsetDecoder decoder; | |
| private final Set<String> subscriptions; | |
| private boolean resubscribe; | |
| public interface FanoutListener { | |
| public void pong(Date timestamp); | |
| public void announcement(String channel, String message); | |
| } | |
| public static class FanoutAdapter implements FanoutListener { | |
| @Override | |
| public void pong(Date timestamp) { } | |
| @Override | |
| public void announcement(String channel, String message) { } | |
| } | |
| public static void main(String args[]) throws Exception { | |
| FanoutClient client = new FanoutClient("localhost", 2000); | |
| client.addListener(new FanoutAdapter() { | |
| @Override | |
| public void pong(Date timestamp) { | |
| System.out.println("Pong. " + timestamp); | |
| } | |
| @Override | |
| public void announcement(String channel, String message) { | |
| System.out.println(MessageFormat.format("Here ye, Here ye. {0} says {1}", channel, message)); | |
| } | |
| }); | |
| client.start(); | |
| Thread.sleep(5000); | |
| client.ping(); | |
| client.subscribe("james"); | |
| client.announce("james", "12345"); | |
| client.subscribe("c52f99d16eb5627877ae957df7ce1be102783bd5"); | |
| while (true) { | |
| Thread.sleep(10000); | |
| client.ping(); | |
| } | |
| } | |
| public FanoutClient(String host, int port) { | |
| this.host = host; | |
| this.port = port; | |
| readBuffer = ByteBuffer.allocateDirect(FanoutConstants.BUFFER_LENGTH); | |
| writeBuffer = ByteBuffer.allocateDirect(FanoutConstants.BUFFER_LENGTH); | |
| decoder = Charset.forName(FanoutConstants.CHARSET).newDecoder(); | |
| listeners = Collections.synchronizedList(new ArrayList<FanoutListener>()); | |
| subscriptions = new LinkedHashSet<String>(); | |
| isRunning = new AtomicBoolean(false); | |
| isConnected = new AtomicBoolean(false); | |
| isAutomaticReconnect = new AtomicBoolean(true); | |
| } | |
| public void addListener(FanoutListener listener) { | |
| listeners.add(listener); | |
| } | |
| public void removeListener(FanoutListener listener) { | |
| listeners.remove(listener); | |
| } | |
| public boolean isAutomaticReconnect() { | |
| return isAutomaticReconnect.get(); | |
| } | |
| public void setAutomaticReconnect(boolean value) { | |
| isAutomaticReconnect.set(value); | |
| } | |
| public void ping() { | |
| confirmConnection(); | |
| write("ping"); | |
| } | |
| public void status() { | |
| confirmConnection(); | |
| write("status"); | |
| } | |
| public void subscribe(String channel) { | |
| confirmConnection(); | |
| if (subscriptions.add(channel)) { | |
| write("subscribe " + channel); | |
| } | |
| } | |
| public void unsubscribe(String channel) { | |
| confirmConnection(); | |
| if (subscriptions.remove(channel)) { | |
| write("unsubscribe " + channel); | |
| } | |
| } | |
| public void announce(String channel, String message) { | |
| confirmConnection(); | |
| write("announce " + channel + " " + message); | |
| } | |
| private void confirmConnection() { | |
| if (!isConnected()) { | |
| throw new RuntimeException("Fanout client is disconnected!"); | |
| } | |
| } | |
| public boolean isConnected() { | |
| return isRunning.get() && socketCh != null && isConnected.get(); | |
| } | |
| /** | |
| * Start client connection and return immediately. | |
| */ | |
| public void start() { | |
| if (isRunning.get()) { | |
| logger.warn("Fanout client is already running"); | |
| return; | |
| } | |
| clientThread = new Thread(this, "Fanout client"); | |
| clientThread.start(); | |
| } | |
| /** | |
| * Start client connection and wait until it has connected. | |
| */ | |
| public void startSynchronously() { | |
| start(); | |
| while (!isConnected()) { | |
| try { | |
| Thread.sleep(100); | |
| } catch (Exception e) { | |
| } | |
| } | |
| } | |
| /** | |
| * Stops client connection. This method returns when the connection has | |
| * been completely shutdown. | |
| */ | |
| public void stop() { | |
| if (!isRunning.get()) { | |
| logger.warn("Fanout client is not running"); | |
| return; | |
| } | |
| isRunning.set(false); | |
| try { | |
| if (clientThread != null) { | |
| clientThread.join(); | |
| clientThread = null; | |
| } | |
| } catch (InterruptedException e1) { | |
| } | |
| } | |
| @Override | |
| public void run() { | |
| resetState(); | |
| isRunning.set(true); | |
| while (isRunning.get()) { | |
| // (re)connect | |
| if (socketCh == null) { | |
| try { | |
| InetAddress addr = InetAddress.getByName(host); | |
| socketCh = SocketChannel.open(new InetSocketAddress(addr, port)); | |
| socketCh.configureBlocking(false); | |
| selector = Selector.open(); | |
| id = FanoutConstants.getLocalSocketId(socketCh.socket()); | |
| socketCh.register(selector, SelectionKey.OP_READ); | |
| } catch (Exception e) { | |
| logger.error(MessageFormat.format("failed to open client connection to {0}:{1,number,0}", host, port), e); | |
| try { | |
| Thread.sleep(reconnectTimeout); | |
| } catch (InterruptedException x) { | |
| } | |
| continue; | |
| } | |
| } | |
| // read/write | |
| try { | |
| selector.select(clientTimeout); | |
| Iterator<SelectionKey> i = selector.selectedKeys().iterator(); | |
| while (i.hasNext()) { | |
| SelectionKey key = i.next(); | |
| i.remove(); | |
| if (key.isReadable()) { | |
| // read message | |
| String content = read(); | |
| String[] lines = content.split("\n"); | |
| for (String reply : lines) { | |
| logger.trace(MessageFormat.format("fanout client {0} received: {1}", id, reply)); | |
| if (!processReply(reply)) { | |
| logger.error(MessageFormat.format("fanout client {0} received unknown message", id)); | |
| } | |
| } | |
| } else if (key.isWritable()) { | |
| // resubscribe | |
| if (resubscribe) { | |
| resubscribe = false; | |
| logger.info(MessageFormat.format("fanout client {0} re-subscribing to {1} channels", id, subscriptions.size())); | |
| for (String subscription : subscriptions) { | |
| write("subscribe " + subscription); | |
| } | |
| } | |
| socketCh.register(selector, SelectionKey.OP_READ); | |
| } | |
| } | |
| } catch (IOException e) { | |
| logger.error(MessageFormat.format("fanout client {0} error: {1}", id, e.getMessage())); | |
| closeChannel(); | |
| if (!isAutomaticReconnect.get()) { | |
| isRunning.set(false); | |
| continue; | |
| } | |
| } | |
| } | |
| closeChannel(); | |
| resetState(); | |
| } | |
| protected void resetState() { | |
| readBuffer.clear(); | |
| writeBuffer.clear(); | |
| isRunning.set(false); | |
| isConnected.set(false); | |
| } | |
| private void closeChannel() { | |
| try { | |
| if (socketCh != null) { | |
| socketCh.close(); | |
| socketCh = null; | |
| selector.close(); | |
| selector = null; | |
| isConnected.set(false); | |
| } | |
| } catch (IOException x) { | |
| } | |
| } | |
| protected boolean processReply(String reply) { | |
| String[] fields = reply.split("!", 2); | |
| if (fields.length == 1) { | |
| try { | |
| long time = Long.parseLong(fields[0]); | |
| Date date = new Date(time); | |
| firePong(date); | |
| } catch (Exception e) { | |
| } | |
| return true; | |
| } else if (fields.length == 2) { | |
| String channel = fields[0]; | |
| String message = fields[1]; | |
| if (FanoutConstants.CH_DEBUG.equals(channel)) { | |
| // debug messages are for internal use | |
| if (FanoutConstants.MSG_CONNECTED.equals(message)) { | |
| isConnected.set(true); | |
| resubscribe = subscriptions.size() > 0; | |
| if (resubscribe) { | |
| try { | |
| // register for async resubscribe | |
| socketCh.register(selector, SelectionKey.OP_WRITE); | |
| } catch (Exception e) { | |
| logger.error("an error occurred", e); | |
| } | |
| } | |
| } | |
| logger.debug(MessageFormat.format("fanout client {0} < {1}", id, reply)); | |
| } else { | |
| fireAnnouncement(channel, message); | |
| } | |
| return true; | |
| } else { | |
| // unknown message | |
| return false; | |
| } | |
| } | |
| protected void firePong(Date timestamp) { | |
| logger.info(MessageFormat.format("fanout client {0} < pong {1,date,yyyy-MM-dd HH:mm:ss}", id, timestamp)); | |
| for (FanoutListener listener : listeners) { | |
| try { | |
| listener.pong(timestamp); | |
| } catch (Throwable t) { | |
| logger.error("FanoutListener threw an exception!", t); | |
| } | |
| } | |
| } | |
| protected void fireAnnouncement(String channel, String message) { | |
| logger.info(MessageFormat.format("fanout client {0} < announcement {1} {2}", id, channel, message)); | |
| for (FanoutListener listener : listeners) { | |
| try { | |
| listener.announcement(channel, message); | |
| } catch (Throwable t) { | |
| logger.error("FanoutListener threw an exception!", t); | |
| } | |
| } | |
| } | |
| protected synchronized String read() throws IOException { | |
| readBuffer.clear(); | |
| long len = socketCh.read(readBuffer); | |
| if (len == -1) { | |
| logger.error(MessageFormat.format("fanout client {0} lost connection to {1}:{2,number,0}, end of stream", id, host, port)); | |
| socketCh.close(); | |
| return null; | |
| } else { | |
| readBuffer.flip(); | |
| String content = decoder.decode(readBuffer).toString(); | |
| readBuffer.clear(); | |
| return content; | |
| } | |
| } | |
| protected synchronized boolean write(String message) { | |
| try { | |
| logger.info(MessageFormat.format("fanout client {0} > {1}", id, message)); | |
| byte [] bytes = message.getBytes(FanoutConstants.CHARSET); | |
| writeBuffer.clear(); | |
| writeBuffer.put(bytes); | |
| if (bytes[bytes.length - 1] != 0xa) { | |
| writeBuffer.put((byte) 0xa); | |
| } | |
| writeBuffer.flip(); | |
| // loop until write buffer has been completely sent | |
| long written = 0; | |
| long toWrite = writeBuffer.remaining(); | |
| while (written != toWrite) { | |
| written += socketCh.write(writeBuffer); | |
| try { | |
| Thread.sleep(10); | |
| } catch (Exception x) { | |
| } | |
| } | |
| return true; | |
| } catch (IOException e) { | |
| logger.error("fanout client {0} error: {1}", id, e.getMessage()); | |
| } | |
| return false; | |
| } | |
| } |