Java tutorial
/* * Copyright (c) 2016 "JackWhite20" * * This file is part of JaPS. * * JaPS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.jackwhite20.japs.server; import de.jackwhite20.japs.server.cache.JaPSCache; import de.jackwhite20.japs.server.config.Config; import de.jackwhite20.japs.server.network.Connection; import de.jackwhite20.japs.server.network.initialize.ServerChannelInitializer; import de.jackwhite20.japs.server.network.initialize.ClusterPublisherChannelInitializer; import de.jackwhite20.japs.shared.config.ClusterServer; import de.jackwhite20.japs.shared.net.OpCode; import de.jackwhite20.japs.shared.pipeline.PipelineUtils; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import org.json.JSONObject; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; /** * Created by JackWhite20 on 25.03.2016. */ public class JaPSServer { private static final Logger LOGGER = JaPS.getLogger(); private String host; private int port; private int backlog; private Map<String, List<Connection>> channelSessions = new ConcurrentHashMap<>(); private int workerThreads; private List<ClusterPublisher> clusterPublisher = new ArrayList<>(); private JaPSCache cache; private EventLoopGroup bossGroup; private EventLoopGroup workerGroup; private Channel serverChannel; public JaPSServer(String host, int port, int backlog, boolean debug, int workerThreads, List<ClusterServer> cluster, int cleanupInterval, int snapshotInterval) { this.host = host; this.port = port; this.backlog = backlog; this.workerThreads = workerThreads; this.cache = new JaPSCache(cleanupInterval, snapshotInterval); LOGGER.setLevel((debug) ? Level.FINE : Level.INFO); start(); // Check if there are cluster servers to avoid unnecessary logic execution if (cluster.size() > 0) { new Thread(() -> { while (cluster.size() > 0) { LOGGER.info("Trying to connecting to all cluster servers"); Iterator<ClusterServer> clusterServerIterator = cluster.iterator(); while (clusterServerIterator.hasNext()) { ClusterServer clusterServer = clusterServerIterator.next(); // Remove the own endpoint of this instance (does not work if it is bound to 0.0.0.0) if (clusterServer.port() == port && clusterServer.host().equals(host)) { clusterServerIterator.remove(); continue; } try { ClusterPublisher cb = new ClusterPublisher(clusterServer.host(), clusterServer.port()); if (cb.connect()) { clusterPublisher.add(cb); clusterServerIterator.remove(); cb.write(new JSONObject().put("op", OpCode.OP_CLUSTER_INFO_SET.getCode()) .put("host", host).put("port", port)); LOGGER.log(Level.INFO, "Connected to cluster server {0}:{1}", new Object[] { clusterServer.host(), String.valueOf(clusterServer.port()) }); } else { LOGGER.log(Level.SEVERE, "Could not connect to cluster server {0}:{1}", new Object[] { clusterServer.host(), String.valueOf(clusterServer.port()) }); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not connect to cluster server {0}:{1}", new Object[] { clusterServer.host(), String.valueOf(clusterServer.port()) }); } } if (cluster.size() == 0) { break; } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } LOGGER.info("Cluster servers are connected successfully!"); }).start(); } } public JaPSServer(Config config) { this(config.host(), config.port(), config.backlog(), config.debug(), config.workerThreads(), config.cluster(), config.cleanupInterval(), config.snapshotInterval()); } private void start() { if (PipelineUtils.isEpoll()) { LOGGER.info("Using high performance epoll event notification mechanism"); } else { LOGGER.info("Using normal select/poll event notification mechanism"); } bossGroup = PipelineUtils.newEventLoopGroup(1); workerGroup = PipelineUtils.newEventLoopGroup(workerThreads); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverChannel = serverBootstrap.group(bossGroup, workerGroup).channel(PipelineUtils.getServerChannel()) .childHandler(new ServerChannelInitializer(this)).option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_BACKLOG, backlog).bind(new InetSocketAddress(host, port)).sync() .channel(); } catch (InterruptedException e) { e.printStackTrace(); } LOGGER.log(Level.INFO, "JaPS server started on {0}:{1}", new Object[] { host, String.valueOf(port) }); } public void stop() { LOGGER.info("Server will stop"); serverChannel.close(); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); // Close the cache cache.close(); LOGGER.info("Server stopped!"); } public void subscribeChannel(String channel, Connection connection) { if (channelSessions.containsKey(channel)) { channelSessions.get(channel).add(connection); } else { channelSessions.put(channel, new CopyOnWriteArrayList<>(Collections.singletonList(connection))); } LOGGER.log(Level.FINE, "[{0}] Channel subscribed: {1}", new Object[] { connection.remoteAddress().toString(), channel }); } public void unsubscribeChannel(String channel, Connection connection) { if (channelSessions.containsKey(channel)) { channelSessions.get(channel).remove(connection); LOGGER.log(Level.FINE, "[{0}] Channel unsubscribed: {1}", new Object[] { connection.remoteAddress().toString(), channel }); } } public void removeClient(Connection connection) { for (String s : connection.channels()) { channelSessions.get(s).remove(connection); } if (!connection.channels().isEmpty()) { LOGGER.log(Level.FINE, "[{0}] Channels unsubscribed from {1}: {2}", new Object[] { connection.remoteAddress().toString(), connection.name(), String.join(", ", connection.channels()) }); } } public void broadcast(Connection con, String channel, JSONObject data) { if (channelSessions.containsKey(channel)) { channelSessions.get(channel).stream().forEach(connection -> connection.send(data)); } // Broadcast it to the cluster if possible clusterPubSubBroadcast(con, channel, data); } public void broadcastTo(Connection con, String channel, JSONObject data, String subscriberName) { JSONObject clusterData = new JSONObject(data); if (channelSessions.containsKey(channel)) { // Remove the subscriber name to save bandwidth and remove the unneeded key data.remove("su"); // Get the correct data to send //String broadcastData = data.toString(); // Find the subscribers with that name and route it to these for (Connection filteredConnection : channelSessions.get(channel).stream() .filter(connection -> connection.name().equals(subscriberName)).collect(Collectors.toList())) { filteredConnection.send(data); } } // Broadcast it to the cluster if possible clusterPubSubBroadcast(con, channel, clusterData); } private void clusterPubSubBroadcast(Connection connection, String channel, JSONObject data) { if (clusterPublisher.size() > 0) { JSONObject clusterMessage = new JSONObject(data).put("op", OpCode.OP_BROADCAST.getCode()).put("ch", channel); clusterBroadcast(connection, clusterMessage); } } public void clusterBroadcast(Connection connection, JSONObject data) { // Publish it to all clusters but exclude the server which has sent it // if it comes from another JaPS server but also publish it // if a normal publisher client has sent it clusterPublisher.stream() .filter(cl -> ((connection == null || connection.host() == null) || (connection.host() != null && connection.port() != cl.port && !connection.host().equals(cl.host)))) .forEach(cl -> cl.write(data)); } public JaPSCache cache() { return cache; } public static class ClusterPublisher extends SimpleChannelInboundHandler<JSONObject> { private String host; private int port; private boolean connected; private Channel channel; public ClusterPublisher(String host, int port) { this.host = host; this.port = port; } private boolean connect() { ChannelFuture channelFuture = new Bootstrap().group(PipelineUtils.newEventLoopGroup(1)) .channel(PipelineUtils.getChannel()).handler(new ClusterPublisherChannelInitializer(this)) .option(ChannelOption.TCP_NODELAY, true).connect(host, port); channel = channelFuture.channel(); CountDownLatch countDownLatch = new CountDownLatch(1); channelFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { connected = channelFuture.isSuccess(); countDownLatch.countDown(); } }); try { countDownLatch.await(4, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } return connected; } public void write(JSONObject jsonObject) { channel.writeAndFlush(jsonObject); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, JSONObject jsonObject) throws Exception { } } }