/* * Copyright 2014 the original author or authors. * * 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.nosceon.titanite.examples; import org.nosceon.titanite.WebSocket; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Johan Siebens */ public class ChatBroadcast { private final List<Subscriber> subscribers = new LinkedList<>(); private final Dispatcher dispatcher; public ChatBroadcast() { this.dispatcher = new Dispatcher(ForkJoinPool.commonPool()); } public WebSocket newSubscriber(String nick) { return new Subscriber(nick); } private void publishNewMessage(String nick, String message) { dispatcher.execute(() -> subscribers.forEach(s -> s.sendMessage(nick, message))); } private void subscribe(Subscriber subscriber) { dispatcher.execute(() -> { subscribers.forEach(s -> subscriber.sendAddUser(s.nickName)); subscribers.add(subscriber); subscribers.forEach(s -> s.sendAddUser(subscriber.nickName)); }); } private void unsubscribe(Subscriber subscriber) { dispatcher.execute(() -> { subscribers.remove(subscriber); subscribers.forEach(s -> s.sendRemoveUser(subscriber.nickName)); }); } private class Subscriber implements WebSocket { private String nickName; private Channel channel; private Subscriber(String nickName) { this.nickName = nickName; } @Override public void onReady(Channel channel) { this.channel = channel; subscribe(this); channel.onTextMessage(s -> publishNewMessage(nickName, s)); channel.onClose(() -> unsubscribe(this)); } private void sendAddUser(String nickName) { this.channel.write("{\"addUser\":\"" + nickName + "\"}"); } private void sendRemoveUser(String nickName) { this.channel.write("{\"removeUser\":\"" + nickName + "\"}"); } private void sendMessage(String nick, String message) { this.channel.write("{\"nickname\":\"" + nick + "\", \"message\":\"" + message.replace("\"", "\\\"") + "\"}"); } } public static class Dispatcher implements Executor { private static final int THROUGHPUT = 25; private final Executor executor; private final Queue<Runnable> tasks = new ConcurrentLinkedQueue<>(); private final AtomicBoolean scheduled = new AtomicBoolean(false); public Dispatcher(Executor executor) { this.executor = executor; } @Override public void execute(Runnable command) { tasks.offer(command); trySchedule(); } private void trySchedule() { if (!tasks.isEmpty()) { if (scheduled.compareAndSet(false, true)) { executor.execute(this::processTasks); } } } private void processTasks() { try { int i = 0; while (!tasks.isEmpty() && i < THROUGHPUT) { tasks.poll().run(); i++; } } finally { scheduled.compareAndSet(true, false); trySchedule(); } } } }