From 99adb5b0cfa9f54962eb82c536a3b714e4a377e7 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sun, 23 Aug 2015 11:48:57 +0530 Subject: [PATCH 01/15] Work on #74 Initial logging server example --- reactor/pom.xml | 18 ++++ .../main/java/com/iluwatar/reactor/App.java | 42 ++++++++ .../java/com/iluwatar/reactor/AppClient.java | 62 ++++++++++++ .../java/com/iluwatar/reactor/NioReactor.java | 96 +++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 reactor/pom.xml create mode 100644 reactor/src/main/java/com/iluwatar/reactor/App.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/AppClient.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/NioReactor.java diff --git a/reactor/pom.xml b/reactor/pom.xml new file mode 100644 index 000000000..0f3271a9c --- /dev/null +++ b/reactor/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.5.0 + + reactor + + + junit + junit + test + + + diff --git a/reactor/src/main/java/com/iluwatar/reactor/App.java b/reactor/src/main/java/com/iluwatar/reactor/App.java new file mode 100644 index 000000000..d5cd05fec --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/App.java @@ -0,0 +1,42 @@ +package com.iluwatar.reactor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import com.iluwatar.reactor.NioReactor.NioChannelEventHandler; + +public class App { + + public static void main(String[] args) { + try { + new NioReactor(6666, new LoggingServer()).start(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + static class LoggingServer implements NioChannelEventHandler { + + @Override + public void onReadable(SocketChannel channel) { + ByteBuffer requestBuffer = ByteBuffer.allocate(1024); + try { + int byteCount = channel.read(requestBuffer); + if (byteCount > 0) { + byte[] logRequestContents = new byte[byteCount]; + byte[] array = requestBuffer.array(); + System.arraycopy(array, 0, logRequestContents, 0, byteCount); + doLogging(new String(logRequestContents)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void doLogging(String log) { + // do logging at server side + System.out.println(log); + } + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java new file mode 100644 index 000000000..a5d871462 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java @@ -0,0 +1,62 @@ +package com.iluwatar.reactor; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; + +public class AppClient { + + public static void main(String[] args) { + new LoggingClient("Client 1", 6666).start(); + } + + + /* + * A logging client that sends logging requests to logging server + */ + static class LoggingClient { + + private int serverPort; + private String clientName; + + public LoggingClient(String clientName, int serverPort) { + this.clientName = clientName; + this.serverPort = serverPort; + } + + public void start() { + Socket socket = null; + try { + socket = new Socket(InetAddress.getLocalHost(), serverPort); + OutputStream outputStream = socket.getOutputStream(); + PrintWriter writer = new PrintWriter(outputStream); + writeLogs(writer); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private void writeLogs(PrintWriter writer) { + for (int i = 0; i < 10; i++) { + writer.println(clientName + " - Log request: " + i); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + writer.flush(); + } + } + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java new file mode 100644 index 000000000..b2952397c --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java @@ -0,0 +1,96 @@ +package com.iluwatar.reactor; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Set; + +public class NioReactor { + + private int port; + private Selector selector; + private ServerSocketChannel serverSocketChannel; + private NioChannelEventHandler nioEventhandler; + + public NioReactor(int port, NioChannelEventHandler handler) { + this.port = port; + this.nioEventhandler = handler; + } + + + public void start() throws IOException { + startReactor(); + requestLoop(); + } + + private void startReactor() throws IOException { + selector = Selector.open(); + serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.socket().bind(new InetSocketAddress(port)); + serverSocketChannel.configureBlocking(false); + SelectionKey acceptorKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + acceptorKey.attach(new Acceptor()); + System.out.println("Reactor started listening on port: " + port); + } + + private void requestLoop() throws IOException { + while (true) { + selector.select(); + Set keys = selector.selectedKeys(); + for (SelectionKey key : keys) { + dispatchEvent(key); + } + keys.clear(); + } + } + + private void dispatchEvent(SelectionKey key) throws IOException { + Object handler = key.attachment(); + if (handler != null) { + ((EventHandler)handler).handle(); + } + } + + interface EventHandler { + void handle() throws IOException; + } + + private class Acceptor implements EventHandler { + + public void handle() throws IOException { + // non-blocking accept as acceptor will only be called when accept event is available + SocketChannel clientChannel = serverSocketChannel.accept(); + if (clientChannel != null) { + new ChannelHandler(clientChannel).handle(); + } + System.out.println("Connection established with a client"); + } + } + + public static interface NioChannelEventHandler { + void onReadable(SocketChannel channel); + } + + private class ChannelHandler implements EventHandler { + + private SocketChannel clientChannel; + private SelectionKey selectionKey; + + public ChannelHandler(SocketChannel clientChannel) throws IOException { + this.clientChannel = clientChannel; + clientChannel.configureBlocking(false); + selectionKey = clientChannel.register(selector, 0); + selectionKey.attach(this); + selectionKey.interestOps(SelectionKey.OP_READ); + selector.wakeup(); + } + + public void handle() throws IOException { + // only read events are supported. + nioEventhandler.onReadable(clientChannel); + } + } +} From d3f2ea22ac422ddac4d7d9733c45e3ebc46e6001 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sun, 23 Aug 2015 18:51:24 +0530 Subject: [PATCH 02/15] Work on #74, enhanced reactor to allow multiple channels --- .../main/java/com/iluwatar/reactor/App.java | 31 +++++++- .../java/com/iluwatar/reactor/AppClient.java | 9 ++- .../java/com/iluwatar/reactor/NioReactor.java | 79 ++++++++++++------- 3 files changed, 84 insertions(+), 35 deletions(-) diff --git a/reactor/src/main/java/com/iluwatar/reactor/App.java b/reactor/src/main/java/com/iluwatar/reactor/App.java index d5cd05fec..4c7b06e9d 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/App.java @@ -1,7 +1,11 @@ package com.iluwatar.reactor; import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import com.iluwatar.reactor.NioReactor.NioChannelEventHandler; @@ -10,12 +14,35 @@ public class App { public static void main(String[] args) { try { - new NioReactor(6666, new LoggingServer()).start(); + NioReactor reactor = new NioReactor(); + + reactor + .registerChannel(tcpChannel(6666)) + .registerChannel(tcpChannel(6667)) + .start(); + + reactor.registerHandler(new LoggingServer()); } catch (IOException e) { e.printStackTrace(); } } - + + private static SelectableChannel udpChannel(int port) throws IOException { + DatagramChannel channel = DatagramChannel.open(); + channel.socket().bind(new InetSocketAddress(port)); + channel.configureBlocking(false); + System.out.println("Bound UDP socket at port: " + port); + return channel; + } + + private static SelectableChannel tcpChannel(int port) throws IOException { + ServerSocketChannel channel = ServerSocketChannel.open(); + channel.socket().bind(new InetSocketAddress(port)); + channel.configureBlocking(false); + System.out.println("Bound TCP socket at port: " + port); + return channel; + } + static class LoggingServer implements NioChannelEventHandler { @Override diff --git a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java index a5d871462..1181745fb 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java @@ -9,14 +9,15 @@ import java.net.Socket; public class AppClient { public static void main(String[] args) { - new LoggingClient("Client 1", 6666).start(); + new Thread(new LoggingClient("Client 1", 6666)).start(); + new Thread(new LoggingClient("Client 2", 6667)).start(); } /* * A logging client that sends logging requests to logging server */ - static class LoggingClient { + static class LoggingClient implements Runnable { private int serverPort; private String clientName; @@ -26,7 +27,7 @@ public class AppClient { this.serverPort = serverPort; } - public void start() { + public void run() { Socket socket = null; try { socket = new Socket(InetAddress.getLocalHost(), serverPort); @@ -51,7 +52,7 @@ public class AppClient { for (int i = 0; i < 10; i++) { writer.println(clientName + " - Log request: " + i); try { - Thread.sleep(1000); + Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java index b2952397c..734ea086f 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java @@ -1,44 +1,63 @@ package com.iluwatar.reactor; import java.io.IOException; -import java.net.InetSocketAddress; +import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.util.List; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +/* + * Abstractions + * --------------- + * + * 1 - Dispatcher + * 2 - Synchronous Event De-multiplexer + * 3 - Event + * 4 - Event Handler & concrete event handler (application business logic) + * 5 - Selector + */ public class NioReactor { - private int port; private Selector selector; - private ServerSocketChannel serverSocketChannel; - private NioChannelEventHandler nioEventhandler; - - public NioReactor(int port, NioChannelEventHandler handler) { - this.port = port; - this.nioEventhandler = handler; + private Acceptor acceptor; + private List eventHandlers = new CopyOnWriteArrayList<>(); + + public NioReactor() throws IOException { + this.acceptor = new Acceptor(); + this.selector = Selector.open(); } + public NioReactor registerChannel(SelectableChannel channel) throws IOException { + SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT); + key.attach(acceptor); + return this; + } + + public void registerHandler(NioChannelEventHandler handler) { + eventHandlers.add(handler); + } public void start() throws IOException { - startReactor(); - requestLoop(); + new Thread( new Runnable() { + @Override + public void run() { + try { + System.out.println("Reactor started, waiting for events..."); + eventLoop(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); } - private void startReactor() throws IOException { - selector = Selector.open(); - serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.socket().bind(new InetSocketAddress(port)); - serverSocketChannel.configureBlocking(false); - SelectionKey acceptorKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - acceptorKey.attach(new Acceptor()); - System.out.println("Reactor started listening on port: " + port); - } - - private void requestLoop() throws IOException { + private void eventLoop() throws IOException { while (true) { - selector.select(); + selector.select(1000); Set keys = selector.selectedKeys(); for (SelectionKey key : keys) { dispatchEvent(key); @@ -50,21 +69,21 @@ public class NioReactor { private void dispatchEvent(SelectionKey key) throws IOException { Object handler = key.attachment(); if (handler != null) { - ((EventHandler)handler).handle(); + ((EventHandler)handler).handle(key.channel()); } } interface EventHandler { - void handle() throws IOException; + void handle(SelectableChannel channel) throws IOException; } private class Acceptor implements EventHandler { - public void handle() throws IOException { + public void handle(SelectableChannel channel) throws IOException { // non-blocking accept as acceptor will only be called when accept event is available - SocketChannel clientChannel = serverSocketChannel.accept(); + SocketChannel clientChannel = ((ServerSocketChannel)channel).accept(); if (clientChannel != null) { - new ChannelHandler(clientChannel).handle(); + new ChannelHandler(clientChannel).handle(clientChannel); } System.out.println("Connection established with a client"); } @@ -88,9 +107,11 @@ public class NioReactor { selector.wakeup(); } - public void handle() throws IOException { + public void handle(SelectableChannel channel) throws IOException { // only read events are supported. - nioEventhandler.onReadable(clientChannel); + for (NioChannelEventHandler eventHandler : eventHandlers) { + eventHandler.onReadable(clientChannel); + } } } } From ec8203a196106ac6cbb732775af153ff4b15dfd6 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Wed, 26 Aug 2015 20:12:17 +0530 Subject: [PATCH 03/15] Ongoing work on #74 introduced better abstractions in reactor - tcp and udp mode --- .../iluwatar/reactor/AbstractNioChannel.java | 75 ++++++++ .../main/java/com/iluwatar/reactor/App.java | 61 ++----- .../java/com/iluwatar/reactor/AppClient.java | 63 ++++++- .../com/iluwatar/reactor/ChannelHandler.java | 9 + .../java/com/iluwatar/reactor/Dispatcher.java | 8 + .../com/iluwatar/reactor/LoggingHandler.java | 24 +++ .../iluwatar/reactor/NioDatagramChannel.java | 47 +++++ .../java/com/iluwatar/reactor/NioReactor.java | 161 +++++++++++------- .../reactor/NioServerSocketChannel.java | 52 ++++++ .../reactor/SameThreadDispatcher.java | 14 ++ .../reactor/ThreadPoolDispatcher.java | 27 +++ reactor/todo.txt | 4 + 12 files changed, 429 insertions(+), 116 deletions(-) create mode 100644 reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java create mode 100644 reactor/todo.txt diff --git a/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java new file mode 100644 index 000000000..9f6040ade --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java @@ -0,0 +1,75 @@ +package com.iluwatar.reactor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +public abstract class AbstractNioChannel { + + private SelectableChannel channel; + private ChannelHandler handler; + private Map> channelToPendingWrites = new ConcurrentHashMap<>(); + private NioReactor reactor; + + public AbstractNioChannel(ChannelHandler handler, SelectableChannel channel) { + this.handler = handler; + this.channel = channel; + } + + public void setReactor(NioReactor reactor) { + this.reactor = reactor; + } + + public SelectableChannel getChannel() { + return channel; + } + + public abstract int getInterestedOps(); + + public abstract ByteBuffer read(SelectionKey key) throws IOException; + + public void setHandler(ChannelHandler handler) { + this.handler = handler; + } + + public ChannelHandler getHandler() { + return handler; + } + + // Called from the context of reactor thread + public void write(SelectionKey key) throws IOException { + Queue pendingWrites = channelToPendingWrites.get(key.channel()); + while (true) { + ByteBuffer pendingWrite = pendingWrites.poll(); + if (pendingWrite == null) { + System.out.println("No more pending writes"); + reactor.changeOps(key, SelectionKey.OP_READ); + break; + } + + doWrite(pendingWrite, key); + } + } + + protected abstract void doWrite(ByteBuffer pendingWrite, SelectionKey key) throws IOException; + + public void write(ByteBuffer buffer, SelectionKey key) { + Queue pendingWrites = this.channelToPendingWrites.get(key.channel()); + if (pendingWrites == null) { + synchronized (this.channelToPendingWrites) { + pendingWrites = this.channelToPendingWrites.get(key.channel()); + if (pendingWrites == null) { + pendingWrites = new ConcurrentLinkedQueue<>(); + this.channelToPendingWrites.put(key.channel(), pendingWrites); + } + } + } + pendingWrites.add(buffer); + reactor.changeOps(key, SelectionKey.OP_WRITE); + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/App.java b/reactor/src/main/java/com/iluwatar/reactor/App.java index 4c7b06e9d..36aa5290d 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/App.java @@ -1,69 +1,32 @@ package com.iluwatar.reactor; import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectableChannel; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; - -import com.iluwatar.reactor.NioReactor.NioChannelEventHandler; public class App { public static void main(String[] args) { try { - NioReactor reactor = new NioReactor(); - + NioReactor reactor = new NioReactor(new ThreadPoolDispatcher(2)); + LoggingHandler loggingHandler = new LoggingHandler(); reactor - .registerChannel(tcpChannel(6666)) - .registerChannel(tcpChannel(6667)) + .registerChannel(tcpChannel(6666, loggingHandler)) + .registerChannel(tcpChannel(6667, loggingHandler)) + .registerChannel(udpChannel(6668, loggingHandler)) .start(); - - reactor.registerHandler(new LoggingServer()); } catch (IOException e) { e.printStackTrace(); } } - private static SelectableChannel udpChannel(int port) throws IOException { - DatagramChannel channel = DatagramChannel.open(); - channel.socket().bind(new InetSocketAddress(port)); - channel.configureBlocking(false); - System.out.println("Bound UDP socket at port: " + port); + private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { + NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); + channel.bind(); return channel; } - - private static SelectableChannel tcpChannel(int port) throws IOException { - ServerSocketChannel channel = ServerSocketChannel.open(); - channel.socket().bind(new InetSocketAddress(port)); - channel.configureBlocking(false); - System.out.println("Bound TCP socket at port: " + port); + + private static AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { + NioDatagramChannel channel = new NioDatagramChannel(port, handler); + channel.bind(); return channel; } - - static class LoggingServer implements NioChannelEventHandler { - - @Override - public void onReadable(SocketChannel channel) { - ByteBuffer requestBuffer = ByteBuffer.allocate(1024); - try { - int byteCount = channel.read(requestBuffer); - if (byteCount > 0) { - byte[] logRequestContents = new byte[byteCount]; - byte[] array = requestBuffer.array(); - System.arraycopy(array, 0, logRequestContents, 0, byteCount); - doLogging(new String(logRequestContents)); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void doLogging(String log) { - // do logging at server side - System.out.println(log); - } - } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java index 1181745fb..3d7323a55 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java @@ -1,16 +1,22 @@ package com.iluwatar.reactor; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.net.DatagramPacket; +import java.net.DatagramSocket; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketException; public class AppClient { public static void main(String[] args) { - new Thread(new LoggingClient("Client 1", 6666)).start(); - new Thread(new LoggingClient("Client 2", 6667)).start(); +// new Thread(new LoggingClient("Client 1", 6666)).start(); +// new Thread(new LoggingClient("Client 2", 6667)).start(); + new Thread(new UDPLoggingClient(6668)).start(); } @@ -33,7 +39,7 @@ public class AppClient { socket = new Socket(InetAddress.getLocalHost(), serverPort); OutputStream outputStream = socket.getOutputStream(); PrintWriter writer = new PrintWriter(outputStream); - writeLogs(writer); + writeLogs(writer, socket.getInputStream()); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); @@ -48,8 +54,8 @@ public class AppClient { } } - private void writeLogs(PrintWriter writer) { - for (int i = 0; i < 10; i++) { + private void writeLogs(PrintWriter writer, InputStream inputStream) throws IOException { + for (int i = 0; i < 1; i++) { writer.println(clientName + " - Log request: " + i); try { Thread.sleep(100); @@ -57,6 +63,53 @@ public class AppClient { e.printStackTrace(); } writer.flush(); + byte[] data = new byte[1024]; + int read = inputStream.read(data, 0, data.length); + if (read == 0) { + System.out.println("Read zero bytes"); + } else { + System.out.println(new String(data, 0, read)); + } + } + } + } + + static class UDPLoggingClient implements Runnable { + private int port; + + public UDPLoggingClient(int port) { + this.port = port; + } + + @Override + public void run() { + DatagramSocket socket = null; + try { + socket = new DatagramSocket(); + for (int i = 0; i < 1; i++) { + String message = "UDP Client" + " - Log request: " + i; + try { + DatagramPacket packet = new DatagramPacket(message.getBytes(), message.getBytes().length, new InetSocketAddress(InetAddress.getLocalHost(), port)); + socket.send(packet); + + byte[] data = new byte[1024]; + DatagramPacket reply = new DatagramPacket(data, data.length); + socket.receive(reply); + if (reply.getLength() == 0) { + System.out.println("Read zero bytes"); + } else { + System.out.println(new String(reply.getData(), 0, reply.getLength())); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } catch (SocketException e1) { + e1.printStackTrace(); + } finally { + if (socket != null) { + socket.close(); + } } } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java new file mode 100644 index 000000000..055e8edd6 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java @@ -0,0 +1,9 @@ +package com.iluwatar.reactor; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; + +public interface ChannelHandler { + + void handleChannelRead(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key); +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java new file mode 100644 index 000000000..1bc14c55f --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java @@ -0,0 +1,8 @@ +package com.iluwatar.reactor; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; + +public interface Dispatcher { + void onChannelReadEvent(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key); +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java new file mode 100644 index 000000000..3744c3d5a --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java @@ -0,0 +1,24 @@ +package com.iluwatar.reactor; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; + +public class LoggingHandler implements ChannelHandler { + + @Override + public void handleChannelRead(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key) { + byte[] data = readBytes.array(); + doLogging(data); + sendEchoReply(channel, data, key); + } + + private void sendEchoReply(AbstractNioChannel channel, byte[] data, SelectionKey key) { + ByteBuffer buffer = ByteBuffer.wrap("Data logged successfully".getBytes()); + channel.write(buffer, key); + } + + private void doLogging(byte[] data) { + // assuming UTF-8 :( + System.out.println(new String(data)); + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java new file mode 100644 index 000000000..2f655f192 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java @@ -0,0 +1,47 @@ +package com.iluwatar.reactor; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; + +public class NioDatagramChannel extends AbstractNioChannel { + + private int port; + + public NioDatagramChannel(int port, ChannelHandler handler) throws IOException { + super(handler, DatagramChannel.open()); + this.port = port; + } + + @Override + public int getInterestedOps() { + return SelectionKey.OP_READ; + } + + @Override + public ByteBuffer read(SelectionKey key) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); + getChannel().receive(buffer); + return buffer; + } + + @Override + public DatagramChannel getChannel() { + return (DatagramChannel) super.getChannel(); + } + + public void bind() throws IOException { + getChannel().socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + getChannel().configureBlocking(false); + System.out.println("Bound UDP socket at port: " + port); + } + + @Override + protected void doWrite(ByteBuffer pendingWrite, SelectionKey key) throws IOException { + pendingWrite.flip(); + getChannel().write(pendingWrite); + } +} \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java index 734ea086f..05aa609d1 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java @@ -1,46 +1,39 @@ package com.iluwatar.reactor; import java.io.IOException; -import java.nio.channels.SelectableChannel; +import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; -import java.util.List; +import java.util.Iterator; +import java.util.Queue; import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ConcurrentLinkedQueue; /* * Abstractions * --------------- - * - * 1 - Dispatcher * 2 - Synchronous Event De-multiplexer - * 3 - Event - * 4 - Event Handler & concrete event handler (application business logic) - * 5 - Selector */ public class NioReactor { private Selector selector; - private Acceptor acceptor; - private List eventHandlers = new CopyOnWriteArrayList<>(); + private Dispatcher dispatcher; + private Queue pendingChanges = new ConcurrentLinkedQueue<>(); - public NioReactor() throws IOException { - this.acceptor = new Acceptor(); + public NioReactor(Dispatcher dispatcher) throws IOException { + this.dispatcher = dispatcher; this.selector = Selector.open(); } - public NioReactor registerChannel(SelectableChannel channel) throws IOException { - SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT); - key.attach(acceptor); + public NioReactor registerChannel(AbstractNioChannel channel) throws IOException { + SelectionKey key = channel.getChannel().register(selector, channel.getInterestedOps()); + key.attach(channel); + channel.setReactor(this); return this; } - public void registerHandler(NioChannelEventHandler handler) { - eventHandlers.add(handler); - } - public void start() throws IOException { new Thread( new Runnable() { @Override @@ -52,66 +45,110 @@ public class NioReactor { e.printStackTrace(); } } - }).start(); + }, "Reactor Main").start(); } private void eventLoop() throws IOException { while (true) { - selector.select(1000); + // honor any pending requests first + processPendingChanges(); + + selector.select(); + Set keys = selector.selectedKeys(); - for (SelectionKey key : keys) { - dispatchEvent(key); + + Iterator iterator = keys.iterator(); + + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + if (!key.isValid()) { + iterator.remove(); + continue; + } + processKey(key); } keys.clear(); } } - private void dispatchEvent(SelectionKey key) throws IOException { - Object handler = key.attachment(); - if (handler != null) { - ((EventHandler)handler).handle(key.channel()); + private void processPendingChanges() { + Iterator iterator = pendingChanges.iterator(); + while (iterator.hasNext()) { + Command command = iterator.next(); + System.out.println("Processing pending change: " + command); + command.execute(); + iterator.remove(); } } - interface EventHandler { - void handle(SelectableChannel channel) throws IOException; - } - - private class Acceptor implements EventHandler { - - public void handle(SelectableChannel channel) throws IOException { - // non-blocking accept as acceptor will only be called when accept event is available - SocketChannel clientChannel = ((ServerSocketChannel)channel).accept(); - if (clientChannel != null) { - new ChannelHandler(clientChannel).handle(clientChannel); - } - System.out.println("Connection established with a client"); + private void processKey(SelectionKey key) throws IOException { + if (key.isAcceptable()) { + acceptConnection(key); + } else if (key.isReadable()) { + System.out.println("Key is readable"); + read(key); + } else if (key.isWritable()) { + System.out.println("Key is writable"); + write(key); } } - - public static interface NioChannelEventHandler { - void onReadable(SocketChannel channel); + + private void write(SelectionKey key) throws IOException { + AbstractNioChannel channel = (AbstractNioChannel) key.attachment(); + channel.write(key); } - - private class ChannelHandler implements EventHandler { - - private SocketChannel clientChannel; - private SelectionKey selectionKey; - public ChannelHandler(SocketChannel clientChannel) throws IOException { - this.clientChannel = clientChannel; - clientChannel.configureBlocking(false); - selectionKey = clientChannel.register(selector, 0); - selectionKey.attach(this); - selectionKey.interestOps(SelectionKey.OP_READ); - selector.wakeup(); - } - - public void handle(SelectableChannel channel) throws IOException { - // only read events are supported. - for (NioChannelEventHandler eventHandler : eventHandlers) { - eventHandler.onReadable(clientChannel); + private void read(SelectionKey key) { + ByteBuffer readBytes; + try { + readBytes = ((AbstractNioChannel)key.attachment()).read(key); + dispatchReadEvent(key, readBytes); + } catch (IOException e) { + try { + key.channel().close(); + } catch (IOException e1) { + e1.printStackTrace(); } } } -} + + private void dispatchReadEvent(SelectionKey key, ByteBuffer readBytes) { + dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readBytes, key); + } + + private void acceptConnection(SelectionKey key) throws IOException { + ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ); + readKey.attach(key.attachment()); + } + + interface Command { + void execute(); + } + + public void changeOps(SelectionKey key, int interestedOps) { + pendingChanges.add(new ChangeKeyOpsCommand(key, interestedOps)); + selector.wakeup(); + } + + class ChangeKeyOpsCommand implements Command { + private SelectionKey key; + private int interestedOps; + + public ChangeKeyOpsCommand(SelectionKey key, int interestedOps) { + this.key = key; + this.interestedOps = interestedOps; + } + + public void execute() { + key.interestOps(interestedOps); + } + + @Override + public String toString() { + return "Change of ops to: " + interestedOps; + } + } +} \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java b/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java new file mode 100644 index 000000000..66affdb8d --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java @@ -0,0 +1,52 @@ +package com.iluwatar.reactor; + +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.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +public class NioServerSocketChannel extends AbstractNioChannel { + + private int port; + + public NioServerSocketChannel(int port, ChannelHandler handler) throws IOException { + super(handler, ServerSocketChannel.open()); + this.port = port; + } + + @Override + public int getInterestedOps() { + return SelectionKey.OP_ACCEPT; + } + + @Override + public ServerSocketChannel getChannel() { + return (ServerSocketChannel) super.getChannel(); + } + + @Override + public ByteBuffer read(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + int read = socketChannel.read(buffer); + if (read == -1) { + throw new IOException("Socket closed"); + } + return buffer; + } + + public void bind() throws IOException { + ((ServerSocketChannel)getChannel()).socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + ((ServerSocketChannel)getChannel()).configureBlocking(false); + System.out.println("Bound TCP socket at port: " + port); + } + + @Override + protected void doWrite(ByteBuffer pendingWrite, SelectionKey key) throws IOException { + System.out.println("Writing on channel"); + ((SocketChannel)key.channel()).write(pendingWrite); + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java new file mode 100644 index 000000000..9b8029de4 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java @@ -0,0 +1,14 @@ +package com.iluwatar.reactor; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; + +public class SameThreadDispatcher implements Dispatcher { + + @Override + public void onChannelReadEvent(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key) { + if (channel.getHandler() != null) { + channel.getHandler().handleChannelRead(channel, readBytes, key); + } + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java new file mode 100644 index 000000000..2f44e4372 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java @@ -0,0 +1,27 @@ +package com.iluwatar.reactor; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ThreadPoolDispatcher extends SameThreadDispatcher { + + private ExecutorService exectorService; + + public ThreadPoolDispatcher(int poolSize) { + this.exectorService = Executors.newFixedThreadPool(poolSize); + } + + @Override + public void onChannelReadEvent(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key) { + exectorService.execute(new Runnable() { + + @Override + public void run() { + ThreadPoolDispatcher.super.onChannelReadEvent(channel, readBytes, key); + } + }); + } + +} diff --git a/reactor/todo.txt b/reactor/todo.txt new file mode 100644 index 000000000..af06a1892 --- /dev/null +++ b/reactor/todo.txt @@ -0,0 +1,4 @@ +* Make UDP channel work (connect is required) +* Cleanup +* Document - Javadoc +* Better design?? Get review of @iluwatar From b94c1d37d2248513d98778a4f95ee8290e3806d8 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Wed, 2 Sep 2015 12:28:52 +0530 Subject: [PATCH 04/15] Work on #74, server mode works with both UDP and TCP channels --- .../iluwatar/reactor/AbstractNioChannel.java | 17 ++++--- .../java/com/iluwatar/reactor/AppClient.java | 4 +- .../com/iluwatar/reactor/ChannelHandler.java | 3 +- .../java/com/iluwatar/reactor/Dispatcher.java | 3 +- .../com/iluwatar/reactor/LoggingHandler.java | 25 ++++++++--- .../iluwatar/reactor/NioDatagramChannel.java | 45 ++++++++++++++++--- .../java/com/iluwatar/reactor/NioReactor.java | 14 +++--- .../reactor/NioServerSocketChannel.java | 5 ++- .../reactor/SameThreadDispatcher.java | 5 +-- .../reactor/ThreadPoolDispatcher.java | 5 +-- 10 files changed, 83 insertions(+), 43 deletions(-) diff --git a/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java index 9f6040ade..f55cea073 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java @@ -1,7 +1,6 @@ package com.iluwatar.reactor; import java.io.IOException; -import java.nio.ByteBuffer; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.util.Map; @@ -13,7 +12,7 @@ public abstract class AbstractNioChannel { private SelectableChannel channel; private ChannelHandler handler; - private Map> channelToPendingWrites = new ConcurrentHashMap<>(); + private Map> channelToPendingWrites = new ConcurrentHashMap<>(); private NioReactor reactor; public AbstractNioChannel(ChannelHandler handler, SelectableChannel channel) { @@ -31,7 +30,7 @@ public abstract class AbstractNioChannel { public abstract int getInterestedOps(); - public abstract ByteBuffer read(SelectionKey key) throws IOException; + public abstract Object read(SelectionKey key) throws IOException; public void setHandler(ChannelHandler handler) { this.handler = handler; @@ -43,9 +42,9 @@ public abstract class AbstractNioChannel { // Called from the context of reactor thread public void write(SelectionKey key) throws IOException { - Queue pendingWrites = channelToPendingWrites.get(key.channel()); + Queue pendingWrites = channelToPendingWrites.get(key.channel()); while (true) { - ByteBuffer pendingWrite = pendingWrites.poll(); + Object pendingWrite = pendingWrites.poll(); if (pendingWrite == null) { System.out.println("No more pending writes"); reactor.changeOps(key, SelectionKey.OP_READ); @@ -56,10 +55,10 @@ public abstract class AbstractNioChannel { } } - protected abstract void doWrite(ByteBuffer pendingWrite, SelectionKey key) throws IOException; + protected abstract void doWrite(Object pendingWrite, SelectionKey key) throws IOException; - public void write(ByteBuffer buffer, SelectionKey key) { - Queue pendingWrites = this.channelToPendingWrites.get(key.channel()); + public void write(Object data, SelectionKey key) { + Queue pendingWrites = this.channelToPendingWrites.get(key.channel()); if (pendingWrites == null) { synchronized (this.channelToPendingWrites) { pendingWrites = this.channelToPendingWrites.get(key.channel()); @@ -69,7 +68,7 @@ public abstract class AbstractNioChannel { } } } - pendingWrites.add(buffer); + pendingWrites.add(data); reactor.changeOps(key, SelectionKey.OP_WRITE); } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java index 3d7323a55..188b64ea8 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java @@ -14,8 +14,8 @@ import java.net.SocketException; public class AppClient { public static void main(String[] args) { -// new Thread(new LoggingClient("Client 1", 6666)).start(); -// new Thread(new LoggingClient("Client 2", 6667)).start(); + new Thread(new LoggingClient("Client 1", 6666)).start(); + new Thread(new LoggingClient("Client 2", 6667)).start(); new Thread(new UDPLoggingClient(6668)).start(); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java index 055e8edd6..e84c506f9 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java @@ -1,9 +1,8 @@ package com.iluwatar.reactor; -import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; public interface ChannelHandler { - void handleChannelRead(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key); + void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java index 1bc14c55f..15fe7774c 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java @@ -1,8 +1,7 @@ package com.iluwatar.reactor; -import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; public interface Dispatcher { - void onChannelReadEvent(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key); + void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java index 3744c3d5a..fc7efaeed 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java @@ -3,16 +3,31 @@ package com.iluwatar.reactor; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; +import com.iluwatar.reactor.NioDatagramChannel.DatagramPacket; + public class LoggingHandler implements ChannelHandler { @Override - public void handleChannelRead(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key) { - byte[] data = readBytes.array(); - doLogging(data); - sendEchoReply(channel, data, key); + public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) { + if (readObject instanceof ByteBuffer) { + byte[] data = ((ByteBuffer)readObject).array(); + doLogging(data); + sendReply(channel, data, key); + } else if (readObject instanceof DatagramPacket) { + DatagramPacket datagram = (DatagramPacket)readObject; + byte[] data = datagram.getData().array(); + doLogging(data); + sendReply(channel, datagram, key); + } } - private void sendEchoReply(AbstractNioChannel channel, byte[] data, SelectionKey key) { + private void sendReply(AbstractNioChannel channel, DatagramPacket datagram, SelectionKey key) { + DatagramPacket replyPacket = new DatagramPacket(ByteBuffer.wrap("Data logged successfully".getBytes())); + replyPacket.setReceiver(datagram.getSender()); + channel.write(replyPacket, key); + } + + private void sendReply(AbstractNioChannel channel, byte[] data, SelectionKey key) { ByteBuffer buffer = ByteBuffer.wrap("Data logged successfully".getBytes()); channel.write(buffer, key); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java index 2f655f192..4d1690792 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java @@ -3,6 +3,7 @@ package com.iluwatar.reactor; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; @@ -22,10 +23,12 @@ public class NioDatagramChannel extends AbstractNioChannel { } @Override - public ByteBuffer read(SelectionKey key) throws IOException { + public Object read(SelectionKey key) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); - getChannel().receive(buffer); - return buffer; + SocketAddress sender = getChannel().receive(buffer); + DatagramPacket packet = new DatagramPacket(buffer); + packet.setSender(sender); + return packet; } @Override @@ -40,8 +43,38 @@ public class NioDatagramChannel extends AbstractNioChannel { } @Override - protected void doWrite(ByteBuffer pendingWrite, SelectionKey key) throws IOException { - pendingWrite.flip(); - getChannel().write(pendingWrite); + protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { + DatagramPacket pendingPacket = (DatagramPacket) pendingWrite; + getChannel().send(pendingPacket.getData(), pendingPacket.getReceiver()); + } + + static class DatagramPacket { + private SocketAddress sender; + private ByteBuffer data; + private SocketAddress receiver; + + public DatagramPacket(ByteBuffer data) { + this.data = data; + } + + public SocketAddress getSender() { + return sender; + } + + public void setSender(SocketAddress sender) { + this.sender = sender; + } + + public SocketAddress getReceiver() { + return receiver; + } + + public void setReceiver(SocketAddress receiver) { + this.receiver = receiver; + } + + public ByteBuffer getData() { + return data; + } } } \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java index 05aa609d1..f10ea4b82 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java @@ -1,7 +1,6 @@ package com.iluwatar.reactor; import java.io.IOException; -import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; @@ -75,7 +74,6 @@ public class NioReactor { Iterator iterator = pendingChanges.iterator(); while (iterator.hasNext()) { Command command = iterator.next(); - System.out.println("Processing pending change: " + command); command.execute(); iterator.remove(); } @@ -85,10 +83,8 @@ public class NioReactor { if (key.isAcceptable()) { acceptConnection(key); } else if (key.isReadable()) { - System.out.println("Key is readable"); read(key); } else if (key.isWritable()) { - System.out.println("Key is writable"); write(key); } } @@ -99,10 +95,10 @@ public class NioReactor { } private void read(SelectionKey key) { - ByteBuffer readBytes; + Object readObject; try { - readBytes = ((AbstractNioChannel)key.attachment()).read(key); - dispatchReadEvent(key, readBytes); + readObject = ((AbstractNioChannel)key.attachment()).read(key); + dispatchReadEvent(key, readObject); } catch (IOException e) { try { key.channel().close(); @@ -112,8 +108,8 @@ public class NioReactor { } } - private void dispatchReadEvent(SelectionKey key, ByteBuffer readBytes) { - dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readBytes, key); + private void dispatchReadEvent(SelectionKey key, Object readObject) { + dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readObject, key); } private void acceptConnection(SelectionKey key) throws IOException { diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java b/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java index 66affdb8d..ebd8f0ef3 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java @@ -45,8 +45,9 @@ public class NioServerSocketChannel extends AbstractNioChannel { } @Override - protected void doWrite(ByteBuffer pendingWrite, SelectionKey key) throws IOException { + protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { + ByteBuffer pendingBuffer = (ByteBuffer) pendingWrite; System.out.println("Writing on channel"); - ((SocketChannel)key.channel()).write(pendingWrite); + ((SocketChannel)key.channel()).write(pendingBuffer); } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java index 9b8029de4..024441b7c 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java @@ -1,14 +1,13 @@ package com.iluwatar.reactor; -import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; public class SameThreadDispatcher implements Dispatcher { @Override - public void onChannelReadEvent(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key) { + public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { if (channel.getHandler() != null) { - channel.getHandler().handleChannelRead(channel, readBytes, key); + channel.getHandler().handleChannelRead(channel, readObject, key); } } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java index 2f44e4372..e9e4ac34c 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java @@ -1,6 +1,5 @@ package com.iluwatar.reactor; -import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -14,12 +13,12 @@ public class ThreadPoolDispatcher extends SameThreadDispatcher { } @Override - public void onChannelReadEvent(AbstractNioChannel channel, ByteBuffer readBytes, SelectionKey key) { + public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { exectorService.execute(new Runnable() { @Override public void run() { - ThreadPoolDispatcher.super.onChannelReadEvent(channel, readBytes, key); + ThreadPoolDispatcher.super.onChannelReadEvent(channel, readObject, key); } }); } From 940a62bc01833ec11bba664af5914687e5a9e59e Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Wed, 2 Sep 2015 15:08:34 +0530 Subject: [PATCH 05/15] Work on #74, added unit test cases --- .../main/java/com/iluwatar/reactor/App.java | 27 +++++++++++----- .../java/com/iluwatar/reactor/AppClient.java | 31 +++++++++++++++---- .../java/com/iluwatar/reactor/Dispatcher.java | 1 + .../java/com/iluwatar/reactor/NioReactor.java | 24 ++++++++++++-- .../reactor/SameThreadDispatcher.java | 5 +++ .../reactor/ThreadPoolDispatcher.java | 17 ++++++++-- .../java/com/iluwatar/reactor/AppTest.java | 27 ++++++++++++++++ 7 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 reactor/src/test/java/com/iluwatar/reactor/AppTest.java diff --git a/reactor/src/main/java/com/iluwatar/reactor/App.java b/reactor/src/main/java/com/iluwatar/reactor/App.java index 36aa5290d..7ce27a78b 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/App.java @@ -4,19 +4,32 @@ import java.io.IOException; public class App { + private NioReactor reactor; + public static void main(String[] args) { try { - NioReactor reactor = new NioReactor(new ThreadPoolDispatcher(2)); - LoggingHandler loggingHandler = new LoggingHandler(); - reactor - .registerChannel(tcpChannel(6666, loggingHandler)) - .registerChannel(tcpChannel(6667, loggingHandler)) - .registerChannel(udpChannel(6668, loggingHandler)) - .start(); + new App().start(); } catch (IOException e) { e.printStackTrace(); } } + + public void start() throws IOException { + reactor = new NioReactor(new ThreadPoolDispatcher(2)); + + LoggingHandler loggingHandler = new LoggingHandler(); + + reactor + .registerChannel(tcpChannel(6666, loggingHandler)) + .registerChannel(tcpChannel(6667, loggingHandler)) + .registerChannel(udpChannel(6668, loggingHandler)) + .start(); + } + + public void stop() { + reactor.stop(); + } + private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); diff --git a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java index 188b64ea8..2ffb6c0de 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/AppClient.java @@ -10,15 +10,34 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; public class AppClient { - + private ExecutorService service = Executors.newFixedThreadPool(3); + public static void main(String[] args) { - new Thread(new LoggingClient("Client 1", 6666)).start(); - new Thread(new LoggingClient("Client 2", 6667)).start(); - new Thread(new UDPLoggingClient(6668)).start(); + new AppClient().start(); } + public void start() { + service.execute(new LoggingClient("Client 1", 6666)); + service.execute(new LoggingClient("Client 2", 6667)); + service.execute(new UDPLoggingClient(6668)); + } + + public void stop() { + service.shutdown(); + if (!service.isTerminated()) { + service.shutdownNow(); + try { + service.awaitTermination(1000, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } /* * A logging client that sends logging requests to logging server @@ -55,7 +74,7 @@ public class AppClient { } private void writeLogs(PrintWriter writer, InputStream inputStream) throws IOException { - for (int i = 0; i < 1; i++) { + for (int i = 0; i < 4; i++) { writer.println(clientName + " - Log request: " + i); try { Thread.sleep(100); @@ -86,7 +105,7 @@ public class AppClient { DatagramSocket socket = null; try { socket = new DatagramSocket(); - for (int i = 0; i < 1; i++) { + for (int i = 0; i < 4; i++) { String message = "UDP Client" + " - Log request: " + i; try { DatagramPacket packet = new DatagramPacket(message.getBytes(), message.getBytes().length, new InetSocketAddress(InetAddress.getLocalHost(), port)); diff --git a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java index 15fe7774c..7c05a6c1d 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java @@ -4,4 +4,5 @@ import java.nio.channels.SelectionKey; public interface Dispatcher { void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); + void stop(); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java index f10ea4b82..6ee0cb989 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java @@ -9,6 +9,9 @@ import java.util.Iterator; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /* * Abstractions @@ -20,6 +23,7 @@ public class NioReactor { private Selector selector; private Dispatcher dispatcher; private Queue pendingChanges = new ConcurrentLinkedQueue<>(); + private ExecutorService reactorService = Executors.newSingleThreadExecutor(); public NioReactor(Dispatcher dispatcher) throws IOException { this.dispatcher = dispatcher; @@ -34,7 +38,7 @@ public class NioReactor { } public void start() throws IOException { - new Thread( new Runnable() { + reactorService.execute(new Runnable() { @Override public void run() { try { @@ -44,11 +48,27 @@ public class NioReactor { e.printStackTrace(); } } - }, "Reactor Main").start(); + }); + } + + public void stop() { + reactorService.shutdownNow(); + selector.wakeup(); + try { + reactorService.awaitTermination(4, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + dispatcher.stop(); } private void eventLoop() throws IOException { while (true) { + + if (Thread.interrupted()) { + break; + } + // honor any pending requests first processPendingChanges(); diff --git a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java index 024441b7c..c27050a15 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java @@ -10,4 +10,9 @@ public class SameThreadDispatcher implements Dispatcher { channel.getHandler().handleChannelRead(channel, readObject, key); } } + + @Override + public void stop() { + // no-op + } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java index e9e4ac34c..600cb4da4 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java @@ -3,18 +3,19 @@ package com.iluwatar.reactor; import java.nio.channels.SelectionKey; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; public class ThreadPoolDispatcher extends SameThreadDispatcher { - private ExecutorService exectorService; + private ExecutorService executorService; public ThreadPoolDispatcher(int poolSize) { - this.exectorService = Executors.newFixedThreadPool(poolSize); + this.executorService = Executors.newFixedThreadPool(poolSize); } @Override public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { - exectorService.execute(new Runnable() { + executorService.execute(new Runnable() { @Override public void run() { @@ -22,5 +23,15 @@ public class ThreadPoolDispatcher extends SameThreadDispatcher { } }); } + + @Override + public void stop() { + executorService.shutdownNow(); + try { + executorService.awaitTermination(1000, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } diff --git a/reactor/src/test/java/com/iluwatar/reactor/AppTest.java b/reactor/src/test/java/com/iluwatar/reactor/AppTest.java new file mode 100644 index 000000000..17ce0b912 --- /dev/null +++ b/reactor/src/test/java/com/iluwatar/reactor/AppTest.java @@ -0,0 +1,27 @@ +package com.iluwatar.reactor; + +import java.io.IOException; + +import org.junit.Test; + +public class AppTest { + + @Test + public void testApp() throws IOException { + App app = new App(); + app.start(); + + AppClient client = new AppClient(); + client.start(); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + client.stop(); + + app.stop(); + } +} From e5ea9f5c0d1ee162a3733bf086421a51b396b5f0 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Wed, 2 Sep 2015 15:21:09 +0530 Subject: [PATCH 06/15] Work on #74, added reactor to parent pom --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a154164e..ce298d61b 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,7 @@ half-sync-half-async step-builder layers + reactor @@ -196,4 +197,4 @@ - \ No newline at end of file + From 7ac262b880f9ab5b52dd000a5728c965109b9d8c Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Fri, 4 Sep 2015 17:43:01 +0530 Subject: [PATCH 07/15] Work on #74, repackaged and added javadocs --- .../iluwatar/reactor/AbstractNioChannel.java | 74 ------ .../main/java/com/iluwatar/reactor/App.java | 45 ---- .../com/iluwatar/reactor/ChannelHandler.java | 8 - .../java/com/iluwatar/reactor/Dispatcher.java | 8 - .../com/iluwatar/reactor/LoggingHandler.java | 39 --- .../iluwatar/reactor/NioDatagramChannel.java | 80 ------ .../java/com/iluwatar/reactor/NioReactor.java | 170 ------------ .../reactor/SameThreadDispatcher.java | 18 -- .../reactor/ThreadPoolDispatcher.java | 37 --- .../java/com/iluwatar/reactor/app/App.java | 106 ++++++++ .../iluwatar/reactor/{ => app}/AppClient.java | 2 +- .../iluwatar/reactor/app/LoggingHandler.java | 62 +++++ .../reactor/framework/AbstractNioChannel.java | 150 +++++++++++ .../reactor/framework/ChannelHandler.java | 25 ++ .../reactor/framework/Dispatcher.java | 38 +++ .../reactor/framework/NioDatagramChannel.java | 156 +++++++++++ .../reactor/framework/NioReactor.java | 242 ++++++++++++++++++ .../NioServerSocketChannel.java | 38 ++- .../framework/SameThreadDispatcher.java | 43 ++++ .../framework/ThreadPoolDispatcher.java | 55 ++++ .../iluwatar/reactor/{ => app}/AppTest.java | 2 +- reactor/todo.txt | 9 + 22 files changed, 925 insertions(+), 482 deletions(-) delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/App.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/NioReactor.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java delete mode 100644 reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/app/App.java rename reactor/src/main/java/com/iluwatar/reactor/{ => app}/AppClient.java (99%) create mode 100644 reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java rename reactor/src/main/java/com/iluwatar/reactor/{ => framework}/NioServerSocketChannel.java (52%) create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java create mode 100644 reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java rename reactor/src/test/java/com/iluwatar/reactor/{ => app}/AppTest.java (91%) diff --git a/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java deleted file mode 100644 index f55cea073..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/AbstractNioChannel.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.iluwatar.reactor; - -import java.io.IOException; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; - -public abstract class AbstractNioChannel { - - private SelectableChannel channel; - private ChannelHandler handler; - private Map> channelToPendingWrites = new ConcurrentHashMap<>(); - private NioReactor reactor; - - public AbstractNioChannel(ChannelHandler handler, SelectableChannel channel) { - this.handler = handler; - this.channel = channel; - } - - public void setReactor(NioReactor reactor) { - this.reactor = reactor; - } - - public SelectableChannel getChannel() { - return channel; - } - - public abstract int getInterestedOps(); - - public abstract Object read(SelectionKey key) throws IOException; - - public void setHandler(ChannelHandler handler) { - this.handler = handler; - } - - public ChannelHandler getHandler() { - return handler; - } - - // Called from the context of reactor thread - public void write(SelectionKey key) throws IOException { - Queue pendingWrites = channelToPendingWrites.get(key.channel()); - while (true) { - Object pendingWrite = pendingWrites.poll(); - if (pendingWrite == null) { - System.out.println("No more pending writes"); - reactor.changeOps(key, SelectionKey.OP_READ); - break; - } - - doWrite(pendingWrite, key); - } - } - - protected abstract void doWrite(Object pendingWrite, SelectionKey key) throws IOException; - - public void write(Object data, SelectionKey key) { - Queue pendingWrites = this.channelToPendingWrites.get(key.channel()); - if (pendingWrites == null) { - synchronized (this.channelToPendingWrites) { - pendingWrites = this.channelToPendingWrites.get(key.channel()); - if (pendingWrites == null) { - pendingWrites = new ConcurrentLinkedQueue<>(); - this.channelToPendingWrites.put(key.channel(), pendingWrites); - } - } - } - pendingWrites.add(data); - reactor.changeOps(key, SelectionKey.OP_WRITE); - } -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/App.java b/reactor/src/main/java/com/iluwatar/reactor/App.java deleted file mode 100644 index 7ce27a78b..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/App.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.iluwatar.reactor; - -import java.io.IOException; - -public class App { - - private NioReactor reactor; - - public static void main(String[] args) { - try { - new App().start(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void start() throws IOException { - reactor = new NioReactor(new ThreadPoolDispatcher(2)); - - LoggingHandler loggingHandler = new LoggingHandler(); - - reactor - .registerChannel(tcpChannel(6666, loggingHandler)) - .registerChannel(tcpChannel(6667, loggingHandler)) - .registerChannel(udpChannel(6668, loggingHandler)) - .start(); - } - - public void stop() { - reactor.stop(); - } - - - private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { - NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); - channel.bind(); - return channel; - } - - private static AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { - NioDatagramChannel channel = new NioDatagramChannel(port, handler); - channel.bind(); - return channel; - } -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java deleted file mode 100644 index e84c506f9..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/ChannelHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.iluwatar.reactor; - -import java.nio.channels.SelectionKey; - -public interface ChannelHandler { - - void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key); -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java deleted file mode 100644 index 7c05a6c1d..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/Dispatcher.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.iluwatar.reactor; - -import java.nio.channels.SelectionKey; - -public interface Dispatcher { - void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); - void stop(); -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java deleted file mode 100644 index fc7efaeed..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/LoggingHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.iluwatar.reactor; - -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; - -import com.iluwatar.reactor.NioDatagramChannel.DatagramPacket; - -public class LoggingHandler implements ChannelHandler { - - @Override - public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) { - if (readObject instanceof ByteBuffer) { - byte[] data = ((ByteBuffer)readObject).array(); - doLogging(data); - sendReply(channel, data, key); - } else if (readObject instanceof DatagramPacket) { - DatagramPacket datagram = (DatagramPacket)readObject; - byte[] data = datagram.getData().array(); - doLogging(data); - sendReply(channel, datagram, key); - } - } - - private void sendReply(AbstractNioChannel channel, DatagramPacket datagram, SelectionKey key) { - DatagramPacket replyPacket = new DatagramPacket(ByteBuffer.wrap("Data logged successfully".getBytes())); - replyPacket.setReceiver(datagram.getSender()); - channel.write(replyPacket, key); - } - - private void sendReply(AbstractNioChannel channel, byte[] data, SelectionKey key) { - ByteBuffer buffer = ByteBuffer.wrap("Data logged successfully".getBytes()); - channel.write(buffer, key); - } - - private void doLogging(byte[] data) { - // assuming UTF-8 :( - System.out.println(new String(data)); - } -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java deleted file mode 100644 index 4d1690792..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/NioDatagramChannel.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.iluwatar.reactor; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectionKey; - -public class NioDatagramChannel extends AbstractNioChannel { - - private int port; - - public NioDatagramChannel(int port, ChannelHandler handler) throws IOException { - super(handler, DatagramChannel.open()); - this.port = port; - } - - @Override - public int getInterestedOps() { - return SelectionKey.OP_READ; - } - - @Override - public Object read(SelectionKey key) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(1024); - SocketAddress sender = getChannel().receive(buffer); - DatagramPacket packet = new DatagramPacket(buffer); - packet.setSender(sender); - return packet; - } - - @Override - public DatagramChannel getChannel() { - return (DatagramChannel) super.getChannel(); - } - - public void bind() throws IOException { - getChannel().socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); - getChannel().configureBlocking(false); - System.out.println("Bound UDP socket at port: " + port); - } - - @Override - protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { - DatagramPacket pendingPacket = (DatagramPacket) pendingWrite; - getChannel().send(pendingPacket.getData(), pendingPacket.getReceiver()); - } - - static class DatagramPacket { - private SocketAddress sender; - private ByteBuffer data; - private SocketAddress receiver; - - public DatagramPacket(ByteBuffer data) { - this.data = data; - } - - public SocketAddress getSender() { - return sender; - } - - public void setSender(SocketAddress sender) { - this.sender = sender; - } - - public SocketAddress getReceiver() { - return receiver; - } - - public void setReceiver(SocketAddress receiver) { - this.receiver = receiver; - } - - public ByteBuffer getData() { - return data; - } - } -} \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java deleted file mode 100644 index 6ee0cb989..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/NioReactor.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.iluwatar.reactor; - -import java.io.IOException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -/* - * Abstractions - * --------------- - * 2 - Synchronous Event De-multiplexer - */ -public class NioReactor { - - private Selector selector; - private Dispatcher dispatcher; - private Queue pendingChanges = new ConcurrentLinkedQueue<>(); - private ExecutorService reactorService = Executors.newSingleThreadExecutor(); - - public NioReactor(Dispatcher dispatcher) throws IOException { - this.dispatcher = dispatcher; - this.selector = Selector.open(); - } - - public NioReactor registerChannel(AbstractNioChannel channel) throws IOException { - SelectionKey key = channel.getChannel().register(selector, channel.getInterestedOps()); - key.attach(channel); - channel.setReactor(this); - return this; - } - - public void start() throws IOException { - reactorService.execute(new Runnable() { - @Override - public void run() { - try { - System.out.println("Reactor started, waiting for events..."); - eventLoop(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - } - - public void stop() { - reactorService.shutdownNow(); - selector.wakeup(); - try { - reactorService.awaitTermination(4, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - dispatcher.stop(); - } - - private void eventLoop() throws IOException { - while (true) { - - if (Thread.interrupted()) { - break; - } - - // honor any pending requests first - processPendingChanges(); - - selector.select(); - - Set keys = selector.selectedKeys(); - - Iterator iterator = keys.iterator(); - - while (iterator.hasNext()) { - SelectionKey key = iterator.next(); - if (!key.isValid()) { - iterator.remove(); - continue; - } - processKey(key); - } - keys.clear(); - } - } - - private void processPendingChanges() { - Iterator iterator = pendingChanges.iterator(); - while (iterator.hasNext()) { - Command command = iterator.next(); - command.execute(); - iterator.remove(); - } - } - - private void processKey(SelectionKey key) throws IOException { - if (key.isAcceptable()) { - acceptConnection(key); - } else if (key.isReadable()) { - read(key); - } else if (key.isWritable()) { - write(key); - } - } - - private void write(SelectionKey key) throws IOException { - AbstractNioChannel channel = (AbstractNioChannel) key.attachment(); - channel.write(key); - } - - private void read(SelectionKey key) { - Object readObject; - try { - readObject = ((AbstractNioChannel)key.attachment()).read(key); - dispatchReadEvent(key, readObject); - } catch (IOException e) { - try { - key.channel().close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - } - } - - private void dispatchReadEvent(SelectionKey key, Object readObject) { - dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readObject, key); - } - - private void acceptConnection(SelectionKey key) throws IOException { - ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); - SocketChannel socketChannel = serverSocketChannel.accept(); - socketChannel.configureBlocking(false); - SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ); - readKey.attach(key.attachment()); - } - - interface Command { - void execute(); - } - - public void changeOps(SelectionKey key, int interestedOps) { - pendingChanges.add(new ChangeKeyOpsCommand(key, interestedOps)); - selector.wakeup(); - } - - class ChangeKeyOpsCommand implements Command { - private SelectionKey key; - private int interestedOps; - - public ChangeKeyOpsCommand(SelectionKey key, int interestedOps) { - this.key = key; - this.interestedOps = interestedOps; - } - - public void execute() { - key.interestOps(interestedOps); - } - - @Override - public String toString() { - return "Change of ops to: " + interestedOps; - } - } -} \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java deleted file mode 100644 index c27050a15..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/SameThreadDispatcher.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.iluwatar.reactor; - -import java.nio.channels.SelectionKey; - -public class SameThreadDispatcher implements Dispatcher { - - @Override - public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { - if (channel.getHandler() != null) { - channel.getHandler().handleChannelRead(channel, readObject, key); - } - } - - @Override - public void stop() { - // no-op - } -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java deleted file mode 100644 index 600cb4da4..000000000 --- a/reactor/src/main/java/com/iluwatar/reactor/ThreadPoolDispatcher.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.iluwatar.reactor; - -import java.nio.channels.SelectionKey; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolDispatcher extends SameThreadDispatcher { - - private ExecutorService executorService; - - public ThreadPoolDispatcher(int poolSize) { - this.executorService = Executors.newFixedThreadPool(poolSize); - } - - @Override - public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { - executorService.execute(new Runnable() { - - @Override - public void run() { - ThreadPoolDispatcher.super.onChannelReadEvent(channel, readObject, key); - } - }); - } - - @Override - public void stop() { - executorService.shutdownNow(); - try { - executorService.awaitTermination(1000, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - -} diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/App.java b/reactor/src/main/java/com/iluwatar/reactor/app/App.java new file mode 100644 index 000000000..d7b280465 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/app/App.java @@ -0,0 +1,106 @@ +package com.iluwatar.reactor.app; + +import java.io.IOException; + +import com.iluwatar.reactor.framework.AbstractNioChannel; +import com.iluwatar.reactor.framework.ChannelHandler; +import com.iluwatar.reactor.framework.NioDatagramChannel; +import com.iluwatar.reactor.framework.NioReactor; +import com.iluwatar.reactor.framework.NioServerSocketChannel; +import com.iluwatar.reactor.framework.ThreadPoolDispatcher; + +/** + * This application demonstrates Reactor pattern. It represents a Distributed Logging Service + * where it can listen on multiple TCP or UDP sockets for incoming log requests. + * + *

+ * INTENT + *
+ * The Reactor design pattern handles service requests that are delivered concurrently to an + * application by one or more clients. The application can register specific handlers for processing + * which are called by reactor on specific events. + * + *

+ * PROBLEM + *
+ * Server applications in a distributed system must handle multiple clients that send them service + * requests. Following forces need to be resolved: + *

    + *
  • Availability
  • + *
  • Efficiency
  • + *
  • Programming Simplicity
  • + *
  • Adaptability
  • + *
+ * + *

+ * The application utilizes single thread to listen for requests on all ports. It does not create + * a separate thread for each client, which provides better scalability under load (number of clients + * increase). + * + *

+ * The example uses Java NIO framework to implement the Reactor. + * + * @author npathai + * + */ +public class App { + + private NioReactor reactor; + + /** + * App entry. + */ + public static void main(String[] args) { + try { + new App().start(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Starts the NIO reactor. + * @throws IOException if any channel fails to bind. + */ + public void start() throws IOException { + /* + * The application can customize its event dispatching mechanism. + */ + reactor = new NioReactor(new ThreadPoolDispatcher(2)); + + /* + * This represents application specific business logic that dispatcher will call + * on appropriate events. These events are read and write event in our example. + */ + LoggingHandler loggingHandler = new LoggingHandler(); + + /* + * Our application binds to multiple I/O channels and uses same logging handler to handle + * incoming log requests. + */ + reactor + .registerChannel(tcpChannel(6666, loggingHandler)) + .registerChannel(tcpChannel(6667, loggingHandler)) + .registerChannel(udpChannel(6668, loggingHandler)) + .start(); + } + + /** + * Stops the NIO reactor. This is a blocking call. + */ + public void stop() { + reactor.stop(); + } + + private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { + NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); + channel.bind(); + return channel; + } + + private static AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { + NioDatagramChannel channel = new NioDatagramChannel(port, handler); + channel.bind(); + return channel; + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java similarity index 99% rename from reactor/src/main/java/com/iluwatar/reactor/AppClient.java rename to reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java index 2ffb6c0de..e5a7dd145 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java @@ -1,4 +1,4 @@ -package com.iluwatar.reactor; +package com.iluwatar.reactor.app; import java.io.IOException; import java.io.InputStream; diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java new file mode 100644 index 000000000..6fa95de2d --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java @@ -0,0 +1,62 @@ +package com.iluwatar.reactor.app; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; + +import com.iluwatar.reactor.framework.AbstractNioChannel; +import com.iluwatar.reactor.framework.ChannelHandler; +import com.iluwatar.reactor.framework.NioDatagramChannel.DatagramPacket; + +/** + * Logging server application logic. It logs the incoming requests on standard console and returns + * a canned acknowledgement back to the remote peer. + * + * @author npathai + */ +public class LoggingHandler implements ChannelHandler { + + private static final byte[] ACK = "Data logged successfully".getBytes(); + + /** + * Decodes the received data and logs it on standard console. + */ + @Override + public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) { + /* + * As this channel is attached to both TCP and UDP channels we need to check whether + * the data received is a ByteBuffer (from TCP channel) or a DatagramPacket (from UDP channel). + */ + if (readObject instanceof ByteBuffer) { + byte[] data = ((ByteBuffer)readObject).array(); + doLogging(data); + sendReply(channel, data, key); + } else if (readObject instanceof DatagramPacket) { + DatagramPacket datagram = (DatagramPacket)readObject; + byte[] data = datagram.getData().array(); + doLogging(data); + sendReply(channel, datagram, key); + } else { + throw new IllegalStateException("Unknown data received"); + } + } + + private void sendReply(AbstractNioChannel channel, DatagramPacket incomingPacket, SelectionKey key) { + /* + * Create a reply acknowledgement datagram packet setting the receiver to the sender of incoming message. + */ + DatagramPacket replyPacket = new DatagramPacket(ByteBuffer.wrap(ACK)); + replyPacket.setReceiver(incomingPacket.getSender()); + + channel.write(replyPacket, key); + } + + private void sendReply(AbstractNioChannel channel, byte[] data, SelectionKey key) { + ByteBuffer buffer = ByteBuffer.wrap(ACK); + channel.write(buffer, key); + } + + private void doLogging(byte[] data) { + // assuming UTF-8 :( + System.out.println(new String(data)); + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java new file mode 100644 index 000000000..a4b18179a --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java @@ -0,0 +1,150 @@ +package com.iluwatar.reactor.framework; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * This represents the Handle of Reactor pattern. These are resources managed by OS + * which can be submitted to {@link NioReactor}. + * + *

+ * This class serves has the responsibility of reading the data when a read event occurs and + * writing the data back when the channel is writable. It leaves the reading and writing of + * data on the concrete implementation. It provides a block writing mechanism wherein when + * any {@link ChannelHandler} wants to write data back, it queues the data in pending write queue + * and clears it in block manner. This provides better throughput. + * + * @author npathai + * + */ +public abstract class AbstractNioChannel { + + private SelectableChannel channel; + private ChannelHandler handler; + private Map> channelToPendingWrites = new ConcurrentHashMap<>(); + private NioReactor reactor; + + /** + * Creates a new channel. + * @param handler which will handle events occurring on this channel. + * @param channel a NIO channel to be wrapped. + */ + public AbstractNioChannel(ChannelHandler handler, SelectableChannel channel) { + this.handler = handler; + this.channel = channel; + } + + /** + * Injects the reactor in this channel. + */ + void setReactor(NioReactor reactor) { + this.reactor = reactor; + } + + /** + * @return the wrapped NIO channel. + */ + public SelectableChannel getChannel() { + return channel; + } + + /** + * The operation in which the channel is interested, this operation is be provided to {@link Selector}. + * + * @return interested operation. + * @see SelectionKey + */ + public abstract int getInterestedOps(); + + /** + * Requests the channel to bind. + * + * @throws IOException if any I/O error occurs. + */ + public abstract void bind() throws IOException; + + /** + * Reads the data using the key and returns the read data. + * @param key the key which is readable. + * @return data read. + * @throws IOException if any I/O error occurs. + */ + public abstract Object read(SelectionKey key) throws IOException; + + /** + * @return the handler associated with this channel. + */ + public ChannelHandler getHandler() { + return handler; + } + + /* + * Called from the context of reactor thread when the key becomes writable. + * The channel writes the whole pending block of data at once. + */ + void flush(SelectionKey key) throws IOException { + Queue pendingWrites = channelToPendingWrites.get(key.channel()); + while (true) { + Object pendingWrite = pendingWrites.poll(); + if (pendingWrite == null) { + // We don't have anything more to write so channel is interested in reading more data + reactor.changeOps(key, SelectionKey.OP_READ); + break; + } + + // ask the concrete channel to make sense of data and write it to java channel + doWrite(pendingWrite, key); + } + } + + /** + * Writes the data to the channel. + * + * @param pendingWrite data which was queued for writing in batch mode. + * @param key the key which is writable. + * @throws IOException if any I/O error occurs. + */ + protected abstract void doWrite(Object pendingWrite, SelectionKey key) throws IOException; + + /** + * Queues the data for writing. The data is not guaranteed to be written on underlying channel + * when this method returns. It will be written when the channel is flushed. + * + *

+ * This method is used by the {@link ChannelHandler} to send reply back to the client. + *
+ * Example: + *

+	 * 
+	 * {@literal @}Override
+	 * public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) {
+	 *   byte[] data = ((ByteBuffer)readObject).array();
+	 *   ByteBuffer buffer = ByteBuffer.wrap("Server reply".getBytes());
+	 *   channel.write(buffer, key);
+	 * }
+	 * 
+	 * 
+	 * @param data the data to be written on underlying channel.
+	 * @param key the key which is writable.
+	 */
+	public void write(Object data, SelectionKey key) {
+		Queue pendingWrites = this.channelToPendingWrites.get(key.channel());
+		if (pendingWrites == null) {
+			synchronized (this.channelToPendingWrites) {
+				pendingWrites = this.channelToPendingWrites.get(key.channel());
+				if (pendingWrites == null) {
+					pendingWrites = new ConcurrentLinkedQueue<>();
+					this.channelToPendingWrites.put(key.channel(), pendingWrites);
+				}
+			}
+		}
+		pendingWrites.add(data);
+		reactor.changeOps(key, SelectionKey.OP_WRITE);
+	}
+}
diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java
new file mode 100644
index 000000000..e1df57020
--- /dev/null
+++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java
@@ -0,0 +1,25 @@
+package com.iluwatar.reactor.framework;
+
+import java.nio.channels.SelectionKey;
+
+/**
+ * Represents the EventHandler of Reactor pattern. It handles the incoming events dispatched
+ * to it by the {@link Dispatcher}. This is where the application logic resides.
+ * 
+ * 

+ * A {@link ChannelHandler} is associated with one or many {@link AbstractNioChannel}s, and whenever + * an event occurs on any of the associated channels, the handler is notified of the event. + * + * @author npathai + */ +public interface ChannelHandler { + + /** + * Called when the {@code channel} has received some data from remote peer. + * + * @param channel the channel from which the data is received. + * @param readObject the data read. + * @param key the key from which the data is received. + */ + void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key); +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java new file mode 100644 index 000000000..120a11085 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java @@ -0,0 +1,38 @@ +package com.iluwatar.reactor.framework; + +import java.nio.channels.SelectionKey; + +/** + * Represents the event dispatching strategy. When {@link NioReactor} senses any event on the + * registered {@link AbstractNioChannel}s then it de-multiplexes the event type, read or write + * or connect, and then calls the {@link Dispatcher} to dispatch the event. This decouples the I/O + * processing from application specific processing. + *
+ * Dispatcher should call the {@link ChannelHandler} associated with the channel on which event occurred. + * + *

+ * The application can customize the way in which event is dispatched such as using the reactor thread to + * dispatch event to channels or use a worker pool to do the non I/O processing. + * + * @see SameThreadDispatcher + * @see ThreadPoolDispatcher + * + * @author npathai + */ +public interface Dispatcher { + /** + * This hook method is called when read event occurs on particular channel. The data read + * is provided in readObject. The implementation should dispatch this read event + * to the associated {@link ChannelHandler} of channel. + * + * @param channel on which read event occurred + * @param readObject object read by channel + * @param key on which event occurred + */ + void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); + + /** + * Stops the dispatching events and cleans up any acquired resources such as threads. + */ + void stop(); +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java new file mode 100644 index 000000000..2666f05b8 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java @@ -0,0 +1,156 @@ +package com.iluwatar.reactor.framework; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; + +/** + * A wrapper over {@link DatagramChannel} which can read and write data on a DatagramChannel. + * + * @author npathai + */ +public class NioDatagramChannel extends AbstractNioChannel { + + private int port; + + /** + * Creates a {@link DatagramChannel} which will bind at provided port and use handler to handle + * incoming events on this channel. + *

+ * Note the constructor does not bind the socket, {@link #bind()} method should be called for binding + * the socket. + * + * @param port the port to be bound to listen for incoming datagram requests. + * @param handler the handler to be used for handling incoming requests on this channel. + * @throws IOException if any I/O error occurs. + */ + public NioDatagramChannel(int port, ChannelHandler handler) throws IOException { + super(handler, DatagramChannel.open()); + this.port = port; + } + + @Override + public int getInterestedOps() { + /* there is no need to accept connections in UDP, so the channel shows interest in + * reading data. + */ + return SelectionKey.OP_READ; + } + + /** + * Reads and returns a {@link DatagramPacket} from the underlying channel. + * @return the datagram packet read having the sender address. + */ + @Override + public DatagramPacket read(SelectionKey key) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); + SocketAddress sender = getChannel().receive(buffer); + + /* + * It is required to create a DatagramPacket because we need to preserve which + * socket address acts as destination for sending reply packets. + */ + DatagramPacket packet = new DatagramPacket(buffer); + packet.setSender(sender); + + return packet; + } + + /** + * @return the underlying datagram channel. + */ + @Override + public DatagramChannel getChannel() { + return (DatagramChannel) super.getChannel(); + } + + /** + * Binds UDP socket on the provided port. + * + * @throws IOException if any I/O error occurs. + */ + @Override + public void bind() throws IOException { + getChannel().socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + getChannel().configureBlocking(false); + System.out.println("Bound UDP socket at port: " + port); + } + + /** + * Writes the pending {@link DatagramPacket} to the underlying channel sending data to + * the intended receiver of the packet. + */ + @Override + protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { + DatagramPacket pendingPacket = (DatagramPacket) pendingWrite; + getChannel().send(pendingPacket.getData(), pendingPacket.getReceiver()); + } + + /** + * Write the outgoing {@link DatagramPacket} to the channel. The intended receiver of the + * datagram packet must be set in the data using {@link DatagramPacket#setReceiver(SocketAddress)}. + */ + @Override + public void write(Object data, SelectionKey key) { + super.write(data, key); + } + + /** + * Container of data used for {@link NioDatagramChannel} to communicate with remote peer. + */ + public static class DatagramPacket { + private SocketAddress sender; + private ByteBuffer data; + private SocketAddress receiver; + + /** + * Creates a container with underlying data. + * + * @param data the underlying message to be written on channel. + */ + public DatagramPacket(ByteBuffer data) { + this.data = data; + } + + /** + * @return the sender address. + */ + public SocketAddress getSender() { + return sender; + } + + /** + * Sets the sender address of this packet. + * @param sender the sender address. + */ + public void setSender(SocketAddress sender) { + this.sender = sender; + } + + /** + * @return the receiver address. + */ + public SocketAddress getReceiver() { + return receiver; + } + + /** + * Sets the intended receiver address. This must be set when writing to the channel. + * @param receiver the receiver address. + */ + public void setReceiver(SocketAddress receiver) { + this.receiver = receiver; + } + + /** + * @return the underlying message that will be written on channel. + */ + public ByteBuffer getData() { + return data; + } + } +} \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java new file mode 100644 index 000000000..b92f4a9ba --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java @@ -0,0 +1,242 @@ +package com.iluwatar.reactor.framework; + +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +/** + * This class acts as Synchronous Event De-multiplexer and Initiation Dispatcher of Reactor pattern. + * Multiple handles i.e. {@link AbstractNioChannel}s can be registered to the reactor and it blocks + * for events from all these handles. Whenever an event occurs on any of the registered handles, + * it synchronously de-multiplexes the event which can be any of read, write or accept, and + * dispatches the event to the appropriate {@link ChannelHandler} using the {@link Dispatcher}. + * + *

+ * Implementation: + * A NIO reactor runs in its own thread when it is started using {@link #start()} method. + * {@link NioReactor} uses {@link Selector} as a mechanism for achieving Synchronous Event De-multiplexing. + * + *

+ * NOTE: This is one of the way to implement NIO reactor and it does not take care of all possible edge cases + * which may be required in a real application. This implementation is meant to demonstrate the fundamental + * concepts that lie behind Reactor pattern. + * + * @author npathai + * + */ +public class NioReactor { + + private Selector selector; + private Dispatcher dispatcher; + /** + * All the work of altering the SelectionKey operations and Selector operations are performed in + * the context of main event loop of reactor. So when any channel needs to change its readability + * or writability, a new command is added in the command queue and then the event loop picks up + * the command and executes it in next iteration. + */ + private Queue pendingCommands = new ConcurrentLinkedQueue<>(); + private ExecutorService reactorMain = Executors.newSingleThreadExecutor(); + + /** + * Creates a reactor which will use provided {@code dispatcher} to dispatch events. + * The application can provide various implementations of dispatcher which suits its + * needs. + * + * @param dispatcher a non-null dispatcher used to dispatch events on registered channels. + * @throws IOException if any I/O error occurs. + */ + public NioReactor(Dispatcher dispatcher) throws IOException { + this.dispatcher = dispatcher; + this.selector = Selector.open(); + } + + /** + * Starts the reactor event loop in a new thread. + * + * @throws IOException if any I/O error occurs. + */ + public void start() throws IOException { + reactorMain.execute(new Runnable() { + @Override + public void run() { + try { + System.out.println("Reactor started, waiting for events..."); + eventLoop(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + /** + * Stops the reactor and related resources such as dispatcher. + */ + public void stop() { + reactorMain.shutdownNow(); + selector.wakeup(); + try { + reactorMain.awaitTermination(4, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + dispatcher.stop(); + } + + /** + * Registers a new channel (handle) with this reactor after which the reactor will wait for events + * on this channel. While registering the channel the reactor uses {@link AbstractNioChannel#getInterestedOps()} + * to know about the interested operation of this channel. + * + * @param channel a new handle on which reactor will wait for events. The channel must be bound + * prior to being registered. + * @return this + * @throws IOException if any I/O error occurs. + */ + public NioReactor registerChannel(AbstractNioChannel channel) throws IOException { + SelectionKey key = channel.getChannel().register(selector, channel.getInterestedOps()); + key.attach(channel); + channel.setReactor(this); + return this; + } + + private void eventLoop() throws IOException { + while (true) { + + // Honor interrupt request + if (Thread.interrupted()) { + break; + } + + // honor any pending commands first + processPendingCommands(); + + /* + * Synchronous event de-multiplexing happens here, this is blocking call which + * returns when it is possible to initiate non-blocking operation on any of the + * registered channels. + */ + selector.select(); + + /* + * Represents the events that have occurred on registered handles. + */ + Set keys = selector.selectedKeys(); + + Iterator iterator = keys.iterator(); + + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + if (!key.isValid()) { + iterator.remove(); + continue; + } + processKey(key); + } + keys.clear(); + } + } + + private void processPendingCommands() { + Iterator iterator = pendingCommands.iterator(); + while (iterator.hasNext()) { + Runnable command = iterator.next(); + command.run(); + iterator.remove(); + } + } + + /* + * Initiation dispatcher logic, it checks the type of event and notifier application + * specific event handler to handle the event. + */ + private void processKey(SelectionKey key) throws IOException { + if (key.isAcceptable()) { + onChannelAcceptable(key); + } else if (key.isReadable()) { + onChannelReadable(key); + } else if (key.isWritable()) { + onChannelWritable(key); + } + } + + private void onChannelWritable(SelectionKey key) throws IOException { + AbstractNioChannel channel = (AbstractNioChannel) key.attachment(); + channel.flush(key); + } + + private void onChannelReadable(SelectionKey key) { + try { + // reads the incoming data in context of reactor main loop. Can this be improved? + Object readObject = ((AbstractNioChannel)key.attachment()).read(key); + + dispatchReadEvent(key, readObject); + } catch (IOException e) { + try { + key.channel().close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + + /* + * Uses the application provided dispatcher to dispatch events to respective handlers. + */ + private void dispatchReadEvent(SelectionKey key, Object readObject) { + dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readObject, key); + } + + private void onChannelAcceptable(SelectionKey key) throws IOException { + ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ); + readKey.attach(key.attachment()); + } + + /** + * Queues the change of operations request of a channel, which will change the interested + * operations of the channel sometime in future. + *

+ * This is a non-blocking method and does not guarantee that the operations are changed when + * this method returns. + * + * @param key the key for which operations are to be changed. + * @param interestedOps the new interest operations. + */ + public void changeOps(SelectionKey key, int interestedOps) { + pendingCommands.add(new ChangeKeyOpsCommand(key, interestedOps)); + selector.wakeup(); + } + + /** + * A command that changes the interested operations of the key provided. + */ + class ChangeKeyOpsCommand implements Runnable { + private SelectionKey key; + private int interestedOps; + + public ChangeKeyOpsCommand(SelectionKey key, int interestedOps) { + this.key = key; + this.interestedOps = interestedOps; + } + + public void run() { + key.interestOps(interestedOps); + } + + @Override + public String toString() { + return "Change of ops to: " + interestedOps; + } + } +} \ No newline at end of file diff --git a/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java similarity index 52% rename from reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java rename to reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java index ebd8f0ef3..92fa9234f 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/NioServerSocketChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java @@ -1,4 +1,4 @@ -package com.iluwatar.reactor; +package com.iluwatar.reactor.framework; import java.io.IOException; import java.net.InetAddress; @@ -8,25 +8,51 @@ import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +/** + * A wrapper over {@link NioServerSocketChannel} which can read and write data on a {@link SocketChannel}. + * + * @author npathai + */ public class NioServerSocketChannel extends AbstractNioChannel { private int port; + /** + * Creates a {@link ServerSocketChannel} which will bind at provided port and use + * handler to handle incoming events on this channel. + *

+ * Note the constructor does not bind the socket, {@link #bind()} method should be called for binding + * the socket. + * + * @param port the port to be bound to listen for incoming requests. + * @param handler the handler to be used for handling incoming requests on this channel. + * @throws IOException if any I/O error occurs. + */ public NioServerSocketChannel(int port, ChannelHandler handler) throws IOException { super(handler, ServerSocketChannel.open()); this.port = port; } + @Override public int getInterestedOps() { + // being a server socket channel it is interested in accepting connection from remote clients. return SelectionKey.OP_ACCEPT; } + /** + * @return the underlying {@link ServerSocketChannel}. + */ @Override public ServerSocketChannel getChannel() { return (ServerSocketChannel) super.getChannel(); } + /** + * Reads and returns {@link ByteBuffer} from the underlying {@link SocketChannel} represented by + * the key. Due to the fact that there is a dedicated channel for each client connection + * we don't need to store the sender. + */ @Override public ByteBuffer read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); @@ -38,12 +64,22 @@ public class NioServerSocketChannel extends AbstractNioChannel { return buffer; } + /** + * Binds TCP socket on the provided port. + * + * @throws IOException if any I/O error occurs. + */ + @Override public void bind() throws IOException { ((ServerSocketChannel)getChannel()).socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); ((ServerSocketChannel)getChannel()).configureBlocking(false); System.out.println("Bound TCP socket at port: " + port); } + /** + * Writes the pending {@link ByteBuffer} to the underlying channel sending data to + * the intended receiver of the packet. + */ @Override protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { ByteBuffer pendingBuffer = (ByteBuffer) pendingWrite; diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java new file mode 100644 index 000000000..2300d7c74 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java @@ -0,0 +1,43 @@ +package com.iluwatar.reactor.framework; + +import java.nio.channels.SelectionKey; + +/** + * Dispatches the events in the context of caller thread. This implementation is a good fit for + * small applications where there are limited clients. Using this implementation limits the scalability + * because the I/O thread performs the application specific processing. + * + *

+ * For real applications use {@link ThreadPoolDispatcher}. + * + * @see ThreadPoolDispatcher + * + * @author npathai + */ +public class SameThreadDispatcher implements Dispatcher { + + /** + * Dispatches the read event in the context of caller thread. + *
+ * Note this is a blocking call. It returns only after the associated handler has handled the + * read event. + */ + @Override + public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { + if (channel.getHandler() != null) { + /* + * Calls the associated handler to notify the read event where application specific code + * resides. + */ + channel.getHandler().handleChannelRead(channel, readObject, key); + } + } + + /** + * No resources to free. + */ + @Override + public void stop() { + // no-op + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java new file mode 100644 index 000000000..b514d1824 --- /dev/null +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java @@ -0,0 +1,55 @@ +package com.iluwatar.reactor.framework; + +import java.nio.channels.SelectionKey; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * An implementation that uses a pool of worker threads to dispatch the events. This provides + * for better scalability as the application specific processing is not performed in the context + * of I/O thread. + * + * @author npathai + * + */ +public class ThreadPoolDispatcher extends SameThreadDispatcher { + + private ExecutorService executorService; + + /** + * Creates a pooled dispatcher with tunable pool size. + * + * @param poolSize number of pooled threads + */ + public ThreadPoolDispatcher(int poolSize) { + this.executorService = Executors.newFixedThreadPool(poolSize); + } + + /** + * Submits the work of dispatching the read event to worker pool, where it gets picked + * up by worker threads. + *
+ * Note that this is a non-blocking call and returns immediately. It is not guaranteed + * that the event has been handled by associated handler. + */ + @Override + public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { + executorService.execute(() -> + ThreadPoolDispatcher.super.onChannelReadEvent(channel, readObject, key)); + } + + /** + * Stops the pool of workers. + */ + @Override + public void stop() { + executorService.shutdownNow(); + try { + executorService.awaitTermination(1000, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/reactor/src/test/java/com/iluwatar/reactor/AppTest.java b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java similarity index 91% rename from reactor/src/test/java/com/iluwatar/reactor/AppTest.java rename to reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java index 17ce0b912..9447aac01 100644 --- a/reactor/src/test/java/com/iluwatar/reactor/AppTest.java +++ b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java @@ -1,4 +1,4 @@ -package com.iluwatar.reactor; +package com.iluwatar.reactor.app; import java.io.IOException; diff --git a/reactor/todo.txt b/reactor/todo.txt index af06a1892..a59af62b9 100644 --- a/reactor/todo.txt +++ b/reactor/todo.txt @@ -2,3 +2,12 @@ * Cleanup * Document - Javadoc * Better design?? Get review of @iluwatar + + +Design view: + +Handles ---> AbstractNioChannel +Selector ---> Synchronous Event Demultiplexer +NioReactor ---> Initiation Dispatcher + + From 363d2c38456a152ee09858d42e9f3b5644d5b2b0 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sun, 6 Sep 2015 14:01:29 +0530 Subject: [PATCH 08/15] Work on #74, improved documentation and minor changes --- .../java/com/iluwatar/reactor/app/App.java | 17 +- .../com/iluwatar/reactor/app/AppClient.java | 153 +++++++++++------- .../iluwatar/reactor/app/LoggingHandler.java | 18 +-- .../reactor/framework/AbstractNioChannel.java | 12 +- .../reactor/framework/ChannelHandler.java | 8 +- .../reactor/framework/Dispatcher.java | 7 +- .../reactor/framework/NioDatagramChannel.java | 5 +- .../reactor/framework/NioReactor.java | 25 ++- .../framework/NioServerSocketChannel.java | 8 +- .../framework/SameThreadDispatcher.java | 2 +- .../framework/ThreadPoolDispatcher.java | 6 +- 11 files changed, 144 insertions(+), 117 deletions(-) diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/App.java b/reactor/src/main/java/com/iluwatar/reactor/app/App.java index d7b280465..947173494 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/App.java @@ -10,8 +10,8 @@ import com.iluwatar.reactor.framework.NioServerSocketChannel; import com.iluwatar.reactor.framework.ThreadPoolDispatcher; /** - * This application demonstrates Reactor pattern. It represents a Distributed Logging Service - * where it can listen on multiple TCP or UDP sockets for incoming log requests. + * This application demonstrates Reactor pattern. The example demonstrated is a Distributed Logging Service + * where it listens on multiple TCP or UDP sockets for incoming log requests. * *

* INTENT @@ -49,13 +49,10 @@ public class App { /** * App entry. + * @throws IOException */ - public static void main(String[] args) { - try { - new App().start(); - } catch (IOException e) { - e.printStackTrace(); - } + public static void main(String[] args) throws IOException { + new App().start(); } /** @@ -70,12 +67,12 @@ public class App { /* * This represents application specific business logic that dispatcher will call - * on appropriate events. These events are read and write event in our example. + * on appropriate events. These events are read events in our example. */ LoggingHandler loggingHandler = new LoggingHandler(); /* - * Our application binds to multiple I/O channels and uses same logging handler to handle + * Our application binds to multiple channels and uses same logging handler to handle * incoming log requests. */ reactor diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java index e5a7dd145..033711569 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java @@ -9,24 +9,43 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.SocketException; +import java.net.UnknownHostException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +/** + * Represents the clients of Reactor pattern. Multiple clients are run concurrently and send logging + * requests to Reactor. + * + * @author npathai + */ public class AppClient { - private ExecutorService service = Executors.newFixedThreadPool(3); - - public static void main(String[] args) { - new AppClient().start(); + private ExecutorService service = Executors.newFixedThreadPool(4); + + /** + * App client entry. + * @throws IOException if any I/O error occurs. + */ + public static void main(String[] args) throws IOException { + AppClient appClient = new AppClient(); + appClient.start(); } - public void start() { - service.execute(new LoggingClient("Client 1", 6666)); - service.execute(new LoggingClient("Client 2", 6667)); - service.execute(new UDPLoggingClient(6668)); + /** + * Starts the logging clients. + * @throws IOException if any I/O error occurs. + */ + public void start() throws IOException { + service.execute(new TCPLoggingClient("Client 1", 6666)); + service.execute(new TCPLoggingClient("Client 2", 6667)); + service.execute(new UDPLoggingClient("Client 3", 6668)); + service.execute(new UDPLoggingClient("Client 4", 6668)); } - + + /** + * Stops logging clients. This is a blocking call. + */ public void stop() { service.shutdown(); if (!service.isTerminated()) { @@ -39,49 +58,49 @@ public class AppClient { } } - /* - * A logging client that sends logging requests to logging server + private static void artificialDelayOf(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * A logging client that sends requests to Reactor on TCP socket. */ - static class LoggingClient implements Runnable { + static class TCPLoggingClient implements Runnable { private int serverPort; private String clientName; - public LoggingClient(String clientName, int serverPort) { + /** + * Creates a new TCP logging client. + * + * @param clientName the name of the client to be sent in logging requests. + * @param port the port on which client will send logging requests. + */ + public TCPLoggingClient(String clientName, int serverPort) { this.clientName = clientName; this.serverPort = serverPort; } public void run() { - Socket socket = null; - try { - socket = new Socket(InetAddress.getLocalHost(), serverPort); + try (Socket socket = new Socket(InetAddress.getLocalHost(), serverPort)) { OutputStream outputStream = socket.getOutputStream(); PrintWriter writer = new PrintWriter(outputStream); - writeLogs(writer, socket.getInputStream()); + sendLogRequests(writer, socket.getInputStream()); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); - } finally { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } } } - private void writeLogs(PrintWriter writer, InputStream inputStream) throws IOException { + private void sendLogRequests(PrintWriter writer, InputStream inputStream) throws IOException { for (int i = 0; i < 4; i++) { writer.println(clientName + " - Log request: " + i); - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } writer.flush(); + byte[] data = new byte[1024]; int read = inputStream.read(data, 0, data.length); if (read == 0) { @@ -89,46 +108,56 @@ public class AppClient { } else { System.out.println(new String(data, 0, read)); } + + artificialDelayOf(100); } } - } - - static class UDPLoggingClient implements Runnable { - private int port; - public UDPLoggingClient(int port) { - this.port = port; + } + + /** + * A logging client that sends requests to Reactor on UDP socket. + */ + static class UDPLoggingClient implements Runnable { + private String clientName; + private InetSocketAddress remoteAddress; + + /** + * Creates a new UDP logging client. + * + * @param clientName the name of the client to be sent in logging requests. + * @param port the port on which client will send logging requests. + * @throws UnknownHostException if localhost is unknown + */ + public UDPLoggingClient(String clientName, int port) throws UnknownHostException { + this.clientName = clientName; + this.remoteAddress = new InetSocketAddress(InetAddress.getLocalHost(), port); } - + @Override public void run() { - DatagramSocket socket = null; - try { - socket = new DatagramSocket(); + try (DatagramSocket socket = new DatagramSocket()) { for (int i = 0; i < 4; i++) { - String message = "UDP Client" + " - Log request: " + i; - try { - DatagramPacket packet = new DatagramPacket(message.getBytes(), message.getBytes().length, new InetSocketAddress(InetAddress.getLocalHost(), port)); - socket.send(packet); - - byte[] data = new byte[1024]; - DatagramPacket reply = new DatagramPacket(data, data.length); - socket.receive(reply); - if (reply.getLength() == 0) { - System.out.println("Read zero bytes"); - } else { - System.out.println(new String(reply.getData(), 0, reply.getLength())); - } - } catch (IOException e) { - e.printStackTrace(); + + String message = clientName + " - Log request: " + i; + DatagramPacket request = new DatagramPacket(message.getBytes(), + message.getBytes().length, remoteAddress); + + socket.send(request); + + byte[] data = new byte[1024]; + DatagramPacket reply = new DatagramPacket(data, data.length); + socket.receive(reply); + if (reply.getLength() == 0) { + System.out.println("Read zero bytes"); + } else { + System.out.println(new String(reply.getData(), 0, reply.getLength())); } + + artificialDelayOf(100); } - } catch (SocketException e1) { + } catch (IOException e1) { e1.printStackTrace(); - } finally { - if (socket != null) { - socket.close(); - } } } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java index 6fa95de2d..eed26b078 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java @@ -9,7 +9,7 @@ import com.iluwatar.reactor.framework.NioDatagramChannel.DatagramPacket; /** * Logging server application logic. It logs the incoming requests on standard console and returns - * a canned acknowledgement back to the remote peer. + * a canned acknowledgement back to the remote peer. * * @author npathai */ @@ -23,17 +23,15 @@ public class LoggingHandler implements ChannelHandler { @Override public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) { /* - * As this channel is attached to both TCP and UDP channels we need to check whether + * As this handler is attached with both TCP and UDP channels we need to check whether * the data received is a ByteBuffer (from TCP channel) or a DatagramPacket (from UDP channel). */ if (readObject instanceof ByteBuffer) { - byte[] data = ((ByteBuffer)readObject).array(); - doLogging(data); - sendReply(channel, data, key); + doLogging(((ByteBuffer)readObject)); + sendReply(channel, key); } else if (readObject instanceof DatagramPacket) { DatagramPacket datagram = (DatagramPacket)readObject; - byte[] data = datagram.getData().array(); - doLogging(data); + doLogging(datagram.getData()); sendReply(channel, datagram, key); } else { throw new IllegalStateException("Unknown data received"); @@ -50,13 +48,13 @@ public class LoggingHandler implements ChannelHandler { channel.write(replyPacket, key); } - private void sendReply(AbstractNioChannel channel, byte[] data, SelectionKey key) { + private void sendReply(AbstractNioChannel channel, SelectionKey key) { ByteBuffer buffer = ByteBuffer.wrap(ACK); channel.write(buffer, key); } - private void doLogging(byte[] data) { + private void doLogging(ByteBuffer data) { // assuming UTF-8 :( - System.out.println(new String(data)); + System.out.println(new String(data.array(), 0, data.limit())); } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java index a4b18179a..24862644d 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java @@ -55,7 +55,7 @@ public abstract class AbstractNioChannel { } /** - * The operation in which the channel is interested, this operation is be provided to {@link Selector}. + * The operation in which the channel is interested, this operation is provided to {@link Selector}. * * @return interested operation. * @see SelectionKey @@ -63,15 +63,17 @@ public abstract class AbstractNioChannel { public abstract int getInterestedOps(); /** - * Requests the channel to bind. + * Binds the channel on provided port. * * @throws IOException if any I/O error occurs. */ public abstract void bind() throws IOException; /** - * Reads the data using the key and returns the read data. - * @param key the key which is readable. + * Reads the data using the key and returns the read data. The underlying channel should be fetched using + * {@link SelectionKey#channel()}. + * + * @param key the key on which read event occurred. * @return data read. * @throws IOException if any I/O error occurs. */ @@ -106,7 +108,7 @@ public abstract class AbstractNioChannel { /** * Writes the data to the channel. * - * @param pendingWrite data which was queued for writing in batch mode. + * @param pendingWrite the data to be written on channel. * @param key the key which is writable. * @throws IOException if any I/O error occurs. */ diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java index e1df57020..0aae9db75 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java @@ -7,7 +7,7 @@ import java.nio.channels.SelectionKey; * to it by the {@link Dispatcher}. This is where the application logic resides. * *

- * A {@link ChannelHandler} is associated with one or many {@link AbstractNioChannel}s, and whenever + * A {@link ChannelHandler} can be associated with one or many {@link AbstractNioChannel}s, and whenever * an event occurs on any of the associated channels, the handler is notified of the event. * * @author npathai @@ -15,11 +15,11 @@ import java.nio.channels.SelectionKey; public interface ChannelHandler { /** - * Called when the {@code channel} has received some data from remote peer. + * Called when the {@code channel} receives some data from remote peer. * - * @param channel the channel from which the data is received. + * @param channel the channel from which the data was received. * @param readObject the data read. - * @param key the key from which the data is received. + * @param key the key on which read event occurred. */ void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java index 120a11085..c563ef9d3 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java @@ -5,7 +5,7 @@ import java.nio.channels.SelectionKey; /** * Represents the event dispatching strategy. When {@link NioReactor} senses any event on the * registered {@link AbstractNioChannel}s then it de-multiplexes the event type, read or write - * or connect, and then calls the {@link Dispatcher} to dispatch the event. This decouples the I/O + * or connect, and then calls the {@link Dispatcher} to dispatch the read events. This decouples the I/O * processing from application specific processing. *
* Dispatcher should call the {@link ChannelHandler} associated with the channel on which event occurred. @@ -24,6 +24,9 @@ public interface Dispatcher { * This hook method is called when read event occurs on particular channel. The data read * is provided in readObject. The implementation should dispatch this read event * to the associated {@link ChannelHandler} of channel. + * + *

+ * The type of readObject depends on the channel on which data was received. * * @param channel on which read event occurred * @param readObject object read by channel @@ -32,7 +35,7 @@ public interface Dispatcher { void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); /** - * Stops the dispatching events and cleans up any acquired resources such as threads. + * Stops dispatching events and cleans up any acquired resources such as threads. */ void stop(); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java index 2666f05b8..f338ce4a3 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java @@ -48,12 +48,13 @@ public class NioDatagramChannel extends AbstractNioChannel { @Override public DatagramPacket read(SelectionKey key) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); - SocketAddress sender = getChannel().receive(buffer); + SocketAddress sender = ((DatagramChannel)key.channel()).receive(buffer); /* * It is required to create a DatagramPacket because we need to preserve which * socket address acts as destination for sending reply packets. */ + buffer.flip(); DatagramPacket packet = new DatagramPacket(buffer); packet.setSender(sender); @@ -91,7 +92,7 @@ public class NioDatagramChannel extends AbstractNioChannel { } /** - * Write the outgoing {@link DatagramPacket} to the channel. The intended receiver of the + * Writes the outgoing {@link DatagramPacket} to the channel. The intended receiver of the * datagram packet must be set in the data using {@link DatagramPacket#setReceiver(SocketAddress)}. */ @Override diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java index b92f4a9ba..273898ae3 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java @@ -22,11 +22,11 @@ import java.util.concurrent.TimeUnit; *

* Implementation: * A NIO reactor runs in its own thread when it is started using {@link #start()} method. - * {@link NioReactor} uses {@link Selector} as a mechanism for achieving Synchronous Event De-multiplexing. + * {@link NioReactor} uses {@link Selector} for realizing Synchronous Event De-multiplexing. * *

- * NOTE: This is one of the way to implement NIO reactor and it does not take care of all possible edge cases - * which may be required in a real application. This implementation is meant to demonstrate the fundamental + * NOTE: This is one of the ways to implement NIO reactor and it does not take care of all possible edge cases + * which are required in a real application. This implementation is meant to demonstrate the fundamental * concepts that lie behind Reactor pattern. * * @author npathai @@ -64,16 +64,13 @@ public class NioReactor { * @throws IOException if any I/O error occurs. */ public void start() throws IOException { - reactorMain.execute(new Runnable() { - @Override - public void run() { + reactorMain.execute(() -> { try { System.out.println("Reactor started, waiting for events..."); eventLoop(); } catch (IOException e) { e.printStackTrace(); } - } }); } @@ -92,11 +89,11 @@ public class NioReactor { } /** - * Registers a new channel (handle) with this reactor after which the reactor will wait for events - * on this channel. While registering the channel the reactor uses {@link AbstractNioChannel#getInterestedOps()} + * Registers a new channel (handle) with this reactor. Reactor will start waiting for events on this channel + * and notify of any events. While registering the channel the reactor uses {@link AbstractNioChannel#getInterestedOps()} * to know about the interested operation of this channel. * - * @param channel a new handle on which reactor will wait for events. The channel must be bound + * @param channel a new channel on which reactor will wait for events. The channel must be bound * prior to being registered. * @return this * @throws IOException if any I/O error occurs. @@ -111,7 +108,7 @@ public class NioReactor { private void eventLoop() throws IOException { while (true) { - // Honor interrupt request + // honor interrupt request if (Thread.interrupted()) { break; } @@ -189,7 +186,7 @@ public class NioReactor { } /* - * Uses the application provided dispatcher to dispatch events to respective handlers. + * Uses the application provided dispatcher to dispatch events to application handler. */ private void dispatchReadEvent(SelectionKey key, Object readObject) { dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readObject, key); @@ -207,10 +204,10 @@ public class NioReactor { * Queues the change of operations request of a channel, which will change the interested * operations of the channel sometime in future. *

- * This is a non-blocking method and does not guarantee that the operations are changed when + * This is a non-blocking method and does not guarantee that the operations have changed when * this method returns. * - * @param key the key for which operations are to be changed. + * @param key the key for which operations have to be changed. * @param interestedOps the new interest operations. */ public void changeOps(SelectionKey key, int interestedOps) { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java index 92fa9234f..ae54af643 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java @@ -24,8 +24,8 @@ public class NioServerSocketChannel extends AbstractNioChannel { * Note the constructor does not bind the socket, {@link #bind()} method should be called for binding * the socket. * - * @param port the port to be bound to listen for incoming requests. - * @param handler the handler to be used for handling incoming requests on this channel. + * @param port the port on which channel will be bound to accept incoming connection requests. + * @param handler the handler that will handle incoming requests on this channel. * @throws IOException if any I/O error occurs. */ public NioServerSocketChannel(int port, ChannelHandler handler) throws IOException { @@ -36,7 +36,7 @@ public class NioServerSocketChannel extends AbstractNioChannel { @Override public int getInterestedOps() { - // being a server socket channel it is interested in accepting connection from remote clients. + // being a server socket channel it is interested in accepting connection from remote peers. return SelectionKey.OP_ACCEPT; } @@ -58,6 +58,7 @@ public class NioServerSocketChannel extends AbstractNioChannel { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int read = socketChannel.read(buffer); + buffer.flip(); if (read == -1) { throw new IOException("Socket closed"); } @@ -83,7 +84,6 @@ public class NioServerSocketChannel extends AbstractNioChannel { @Override protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { ByteBuffer pendingBuffer = (ByteBuffer) pendingWrite; - System.out.println("Writing on channel"); ((SocketChannel)key.channel()).write(pendingBuffer); } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java index 2300d7c74..b5392ac8f 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java @@ -8,7 +8,7 @@ import java.nio.channels.SelectionKey; * because the I/O thread performs the application specific processing. * *

- * For real applications use {@link ThreadPoolDispatcher}. + * For better performance use {@link ThreadPoolDispatcher}. * * @see ThreadPoolDispatcher * diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java index b514d1824..8624b878e 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java @@ -7,8 +7,8 @@ import java.util.concurrent.TimeUnit; /** * An implementation that uses a pool of worker threads to dispatch the events. This provides - * for better scalability as the application specific processing is not performed in the context - * of I/O thread. + * better scalability as the application specific processing is not performed in the context + * of I/O (reactor) thread. * * @author npathai * @@ -46,7 +46,7 @@ public class ThreadPoolDispatcher extends SameThreadDispatcher { public void stop() { executorService.shutdownNow(); try { - executorService.awaitTermination(1000, TimeUnit.SECONDS); + executorService.awaitTermination(4, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } From 57ff154e0a9dac01a7a929eee3de0629c261c4ab Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sun, 6 Sep 2015 14:09:00 +0530 Subject: [PATCH 09/15] Changed version --- reactor/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor/pom.xml b/reactor/pom.xml index 0f3271a9c..516a4b93c 100644 --- a/reactor/pom.xml +++ b/reactor/pom.xml @@ -5,7 +5,7 @@ com.iluwatar java-design-patterns - 1.5.0 + 1.7.0 reactor From 30f60651952056e775dd7194c882e23263df4b9e Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sun, 6 Sep 2015 14:10:31 +0530 Subject: [PATCH 10/15] Removed todo file --- reactor/todo.txt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 reactor/todo.txt diff --git a/reactor/todo.txt b/reactor/todo.txt deleted file mode 100644 index a59af62b9..000000000 --- a/reactor/todo.txt +++ /dev/null @@ -1,13 +0,0 @@ -* Make UDP channel work (connect is required) -* Cleanup -* Document - Javadoc -* Better design?? Get review of @iluwatar - - -Design view: - -Handles ---> AbstractNioChannel -Selector ---> Synchronous Event Demultiplexer -NioReactor ---> Initiation Dispatcher - - From 9e401b0f34776f6357b2691ed28cf884e3d2e538 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sun, 6 Sep 2015 14:14:38 +0530 Subject: [PATCH 11/15] Fixed version number --- reactor/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor/pom.xml b/reactor/pom.xml index 516a4b93c..599376e32 100644 --- a/reactor/pom.xml +++ b/reactor/pom.xml @@ -5,7 +5,7 @@ com.iluwatar java-design-patterns - 1.7.0 + 1.6.0 reactor From 8d429525dcb752db7854f0695630c1a98eb17b76 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Sat, 12 Sep 2015 17:46:24 +0530 Subject: [PATCH 12/15] Work on #74, updated javadocs, reformatted code to google style guide, added missing final modifiers --- .../java/com/iluwatar/reactor/app/App.java | 145 ++++--- .../com/iluwatar/reactor/app/AppClient.java | 253 +++++------ .../iluwatar/reactor/app/LoggingHandler.java | 81 ++-- .../reactor/framework/AbstractNioChannel.java | 256 +++++------ .../reactor/framework/ChannelHandler.java | 22 +- .../reactor/framework/Dispatcher.java | 56 +-- .../reactor/framework/NioDatagramChannel.java | 266 ++++++------ .../reactor/framework/NioReactor.java | 401 +++++++++--------- .../framework/NioServerSocketChannel.java | 135 +++--- .../framework/SameThreadDispatcher.java | 47 +- .../framework/ThreadPoolDispatcher.java | 71 ++-- .../com/iluwatar/reactor/app/AppTest.java | 50 ++- 12 files changed, 908 insertions(+), 875 deletions(-) diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/App.java b/reactor/src/main/java/com/iluwatar/reactor/app/App.java index 947173494..fcc327b34 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/App.java @@ -10,20 +10,18 @@ import com.iluwatar.reactor.framework.NioServerSocketChannel; import com.iluwatar.reactor.framework.ThreadPoolDispatcher; /** - * This application demonstrates Reactor pattern. The example demonstrated is a Distributed Logging Service - * where it listens on multiple TCP or UDP sockets for incoming log requests. + * This application demonstrates Reactor pattern. The example demonstrated is a Distributed Logging + * Service where it listens on multiple TCP or UDP sockets for incoming log requests. * *

- * INTENT - *
- * The Reactor design pattern handles service requests that are delivered concurrently to an + * INTENT
+ * The Reactor design pattern handles service requests that are delivered concurrently to an * application by one or more clients. The application can register specific handlers for processing * which are called by reactor on specific events. * *

- * PROBLEM - *
- * Server applications in a distributed system must handle multiple clients that send them service + * PROBLEM
+ * Server applications in a distributed system must handle multiple clients that send them service * requests. Following forces need to be resolved: *

    *
  • Availability
  • @@ -33,8 +31,28 @@ import com.iluwatar.reactor.framework.ThreadPoolDispatcher; *
* *

- * The application utilizes single thread to listen for requests on all ports. It does not create - * a separate thread for each client, which provides better scalability under load (number of clients + * PARTICIPANTS
+ *

    + *
  • Synchronous Event De-multiplexer
  • {@link NioReactor} plays the role of synchronous event + * de-multiplexer. It waits for events on multiple channels registered to it in an event loop. + * + *

    + *

  • Initiation Dispatcher
  • {@link NioReactor} plays this role as the application specific + * {@link ChannelHandler}s are registered to the reactor. + * + *

    + *

  • Handle
  • {@link AbstractNioChannel} acts as a handle that is registered to the reactor. + * When any events occur on a handle, reactor calls the appropriate handler. + * + *

    + *

  • Event Handler
  • {@link ChannelHandler} acts as an event handler, which is bound to a + * channel and is called back when any event occurs on any of its associated handles. Application + * logic resides in event handlers. + *
+ * + *

+ * The application utilizes single thread to listen for requests on all ports. It does not create a + * separate thread for each client, which provides better scalability under load (number of clients * increase). * *

@@ -45,59 +63,60 @@ import com.iluwatar.reactor.framework.ThreadPoolDispatcher; */ public class App { - private NioReactor reactor; + private NioReactor reactor; - /** - * App entry. - * @throws IOException - */ - public static void main(String[] args) throws IOException { - new App().start(); - } - - /** - * Starts the NIO reactor. - * @throws IOException if any channel fails to bind. - */ - public void start() throws IOException { - /* - * The application can customize its event dispatching mechanism. - */ - reactor = new NioReactor(new ThreadPoolDispatcher(2)); - - /* - * This represents application specific business logic that dispatcher will call - * on appropriate events. These events are read events in our example. - */ - LoggingHandler loggingHandler = new LoggingHandler(); - - /* - * Our application binds to multiple channels and uses same logging handler to handle - * incoming log requests. - */ - reactor - .registerChannel(tcpChannel(6666, loggingHandler)) - .registerChannel(tcpChannel(6667, loggingHandler)) - .registerChannel(udpChannel(6668, loggingHandler)) - .start(); - } - - /** - * Stops the NIO reactor. This is a blocking call. - */ - public void stop() { - reactor.stop(); - } + /** + * App entry. + * + * @throws IOException + */ + public static void main(String[] args) throws IOException { + new App().start(); + } - private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { - NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); - channel.bind(); - return channel; - } - - private static AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { - NioDatagramChannel channel = new NioDatagramChannel(port, handler); - channel.bind(); - return channel; - } + /** + * Starts the NIO reactor. + * + * @throws IOException if any channel fails to bind. + */ + public void start() throws IOException { + /* + * The application can customize its event dispatching mechanism. + */ + reactor = new NioReactor(new ThreadPoolDispatcher(2)); + + /* + * This represents application specific business logic that dispatcher will call on appropriate + * events. These events are read events in our example. + */ + LoggingHandler loggingHandler = new LoggingHandler(); + + /* + * Our application binds to multiple channels and uses same logging handler to handle incoming + * log requests. + */ + reactor.registerChannel(tcpChannel(6666, loggingHandler)).registerChannel(tcpChannel(6667, loggingHandler)) + .registerChannel(udpChannel(6668, loggingHandler)).start(); + } + + /** + * Stops the NIO reactor. This is a blocking call. + * + * @throws InterruptedException if interrupted while stopping the reactor. + */ + public void stop() throws InterruptedException { + reactor.stop(); + } + + private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { + NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); + channel.bind(); + return channel; + } + + private static AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { + NioDatagramChannel channel = new NioDatagramChannel(port, handler); + channel.bind(); + return channel; + } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java index 033711569..c50e4d3e7 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java @@ -17,148 +17,149 @@ import java.util.concurrent.TimeUnit; /** * Represents the clients of Reactor pattern. Multiple clients are run concurrently and send logging * requests to Reactor. - * + * * @author npathai */ public class AppClient { - private ExecutorService service = Executors.newFixedThreadPool(4); + private final ExecutorService service = Executors.newFixedThreadPool(4); - /** - * App client entry. - * @throws IOException if any I/O error occurs. - */ - public static void main(String[] args) throws IOException { - AppClient appClient = new AppClient(); - appClient.start(); - } + /** + * App client entry. + * + * @throws IOException if any I/O error occurs. + */ + public static void main(String[] args) throws IOException { + AppClient appClient = new AppClient(); + appClient.start(); + } - /** - * Starts the logging clients. - * @throws IOException if any I/O error occurs. - */ - public void start() throws IOException { - service.execute(new TCPLoggingClient("Client 1", 6666)); - service.execute(new TCPLoggingClient("Client 2", 6667)); - service.execute(new UDPLoggingClient("Client 3", 6668)); - service.execute(new UDPLoggingClient("Client 4", 6668)); - } + /** + * Starts the logging clients. + * + * @throws IOException if any I/O error occurs. + */ + public void start() throws IOException { + service.execute(new TCPLoggingClient("Client 1", 6666)); + service.execute(new TCPLoggingClient("Client 2", 6667)); + service.execute(new UDPLoggingClient("Client 3", 6668)); + service.execute(new UDPLoggingClient("Client 4", 6668)); + } - /** - * Stops logging clients. This is a blocking call. - */ - public void stop() { - service.shutdown(); - if (!service.isTerminated()) { - service.shutdownNow(); - try { - service.awaitTermination(1000, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - private static void artificialDelayOf(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + /** + * Stops logging clients. This is a blocking call. + */ + public void stop() { + service.shutdown(); + if (!service.isTerminated()) { + service.shutdownNow(); + try { + service.awaitTermination(1000, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } - /** - * A logging client that sends requests to Reactor on TCP socket. - */ - static class TCPLoggingClient implements Runnable { + private static void artificialDelayOf(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } - private int serverPort; - private String clientName; + /** + * A logging client that sends requests to Reactor on TCP socket. + */ + static class TCPLoggingClient implements Runnable { - /** - * Creates a new TCP logging client. - * - * @param clientName the name of the client to be sent in logging requests. - * @param port the port on which client will send logging requests. - */ - public TCPLoggingClient(String clientName, int serverPort) { - this.clientName = clientName; - this.serverPort = serverPort; - } + private final int serverPort; + private final String clientName; - public void run() { - try (Socket socket = new Socket(InetAddress.getLocalHost(), serverPort)) { - OutputStream outputStream = socket.getOutputStream(); - PrintWriter writer = new PrintWriter(outputStream); - sendLogRequests(writer, socket.getInputStream()); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } + /** + * Creates a new TCP logging client. + * + * @param clientName the name of the client to be sent in logging requests. + * @param port the port on which client will send logging requests. + */ + public TCPLoggingClient(String clientName, int serverPort) { + this.clientName = clientName; + this.serverPort = serverPort; + } - private void sendLogRequests(PrintWriter writer, InputStream inputStream) throws IOException { - for (int i = 0; i < 4; i++) { - writer.println(clientName + " - Log request: " + i); - writer.flush(); + public void run() { + try (Socket socket = new Socket(InetAddress.getLocalHost(), serverPort)) { + OutputStream outputStream = socket.getOutputStream(); + PrintWriter writer = new PrintWriter(outputStream); + sendLogRequests(writer, socket.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } - byte[] data = new byte[1024]; - int read = inputStream.read(data, 0, data.length); - if (read == 0) { - System.out.println("Read zero bytes"); - } else { - System.out.println(new String(data, 0, read)); - } + private void sendLogRequests(PrintWriter writer, InputStream inputStream) throws IOException { + for (int i = 0; i < 4; i++) { + writer.println(clientName + " - Log request: " + i); + writer.flush(); - artificialDelayOf(100); - } - } + byte[] data = new byte[1024]; + int read = inputStream.read(data, 0, data.length); + if (read == 0) { + System.out.println("Read zero bytes"); + } else { + System.out.println(new String(data, 0, read)); + } - } + artificialDelayOf(100); + } + } - /** - * A logging client that sends requests to Reactor on UDP socket. - */ - static class UDPLoggingClient implements Runnable { - private String clientName; - private InetSocketAddress remoteAddress; + } - /** - * Creates a new UDP logging client. - * - * @param clientName the name of the client to be sent in logging requests. - * @param port the port on which client will send logging requests. - * @throws UnknownHostException if localhost is unknown - */ - public UDPLoggingClient(String clientName, int port) throws UnknownHostException { - this.clientName = clientName; - this.remoteAddress = new InetSocketAddress(InetAddress.getLocalHost(), port); - } + /** + * A logging client that sends requests to Reactor on UDP socket. + */ + static class UDPLoggingClient implements Runnable { + private final String clientName; + private final InetSocketAddress remoteAddress; - @Override - public void run() { - try (DatagramSocket socket = new DatagramSocket()) { - for (int i = 0; i < 4; i++) { - - String message = clientName + " - Log request: " + i; - DatagramPacket request = new DatagramPacket(message.getBytes(), - message.getBytes().length, remoteAddress); - - socket.send(request); + /** + * Creates a new UDP logging client. + * + * @param clientName the name of the client to be sent in logging requests. + * @param port the port on which client will send logging requests. + * @throws UnknownHostException if localhost is unknown + */ + public UDPLoggingClient(String clientName, int port) throws UnknownHostException { + this.clientName = clientName; + this.remoteAddress = new InetSocketAddress(InetAddress.getLocalHost(), port); + } - byte[] data = new byte[1024]; - DatagramPacket reply = new DatagramPacket(data, data.length); - socket.receive(reply); - if (reply.getLength() == 0) { - System.out.println("Read zero bytes"); - } else { - System.out.println(new String(reply.getData(), 0, reply.getLength())); - } - - artificialDelayOf(100); - } - } catch (IOException e1) { - e1.printStackTrace(); - } - } - } + @Override + public void run() { + try (DatagramSocket socket = new DatagramSocket()) { + for (int i = 0; i < 4; i++) { + + String message = clientName + " - Log request: " + i; + DatagramPacket request = new DatagramPacket(message.getBytes(), message.getBytes().length, remoteAddress); + + socket.send(request); + + byte[] data = new byte[1024]; + DatagramPacket reply = new DatagramPacket(data, data.length); + socket.receive(reply); + if (reply.getLength() == 0) { + System.out.println("Read zero bytes"); + } else { + System.out.println(new String(reply.getData(), 0, reply.getLength())); + } + + artificialDelayOf(100); + } + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java index eed26b078..1f2694b0b 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java @@ -8,53 +8,54 @@ import com.iluwatar.reactor.framework.ChannelHandler; import com.iluwatar.reactor.framework.NioDatagramChannel.DatagramPacket; /** - * Logging server application logic. It logs the incoming requests on standard console and returns - * a canned acknowledgement back to the remote peer. + * Logging server application logic. It logs the incoming requests on standard console and returns a + * canned acknowledgement back to the remote peer. * * @author npathai */ public class LoggingHandler implements ChannelHandler { - private static final byte[] ACK = "Data logged successfully".getBytes(); + private static final byte[] ACK = "Data logged successfully".getBytes(); - /** - * Decodes the received data and logs it on standard console. - */ - @Override - public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) { - /* - * As this handler is attached with both TCP and UDP channels we need to check whether - * the data received is a ByteBuffer (from TCP channel) or a DatagramPacket (from UDP channel). - */ - if (readObject instanceof ByteBuffer) { - doLogging(((ByteBuffer)readObject)); - sendReply(channel, key); - } else if (readObject instanceof DatagramPacket) { - DatagramPacket datagram = (DatagramPacket)readObject; - doLogging(datagram.getData()); - sendReply(channel, datagram, key); - } else { - throw new IllegalStateException("Unknown data received"); - } - } + /** + * Decodes the received data and logs it on standard console. + */ + @Override + public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) { + /* + * As this handler is attached with both TCP and UDP channels we need to check whether the data + * received is a ByteBuffer (from TCP channel) or a DatagramPacket (from UDP channel). + */ + if (readObject instanceof ByteBuffer) { + doLogging(((ByteBuffer) readObject)); + sendReply(channel, key); + } else if (readObject instanceof DatagramPacket) { + DatagramPacket datagram = (DatagramPacket) readObject; + doLogging(datagram.getData()); + sendReply(channel, datagram, key); + } else { + throw new IllegalStateException("Unknown data received"); + } + } - private void sendReply(AbstractNioChannel channel, DatagramPacket incomingPacket, SelectionKey key) { - /* - * Create a reply acknowledgement datagram packet setting the receiver to the sender of incoming message. - */ - DatagramPacket replyPacket = new DatagramPacket(ByteBuffer.wrap(ACK)); - replyPacket.setReceiver(incomingPacket.getSender()); - - channel.write(replyPacket, key); - } + private void sendReply(AbstractNioChannel channel, DatagramPacket incomingPacket, SelectionKey key) { + /* + * Create a reply acknowledgement datagram packet setting the receiver to the sender of incoming + * message. + */ + DatagramPacket replyPacket = new DatagramPacket(ByteBuffer.wrap(ACK)); + replyPacket.setReceiver(incomingPacket.getSender()); - private void sendReply(AbstractNioChannel channel, SelectionKey key) { - ByteBuffer buffer = ByteBuffer.wrap(ACK); - channel.write(buffer, key); - } + channel.write(replyPacket, key); + } - private void doLogging(ByteBuffer data) { - // assuming UTF-8 :( - System.out.println(new String(data.array(), 0, data.limit())); - } + private void sendReply(AbstractNioChannel channel, SelectionKey key) { + ByteBuffer buffer = ByteBuffer.wrap(ACK); + channel.write(buffer, key); + } + + private void doLogging(ByteBuffer data) { + // assuming UTF-8 :( + System.out.println(new String(data.array(), 0, data.limit())); + } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java index 24862644d..09f308731 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java @@ -10,143 +10,145 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; /** - * This represents the Handle of Reactor pattern. These are resources managed by OS - * which can be submitted to {@link NioReactor}. + * This represents the Handle of Reactor pattern. These are resources managed by OS which can + * be submitted to {@link NioReactor}. * *

- * This class serves has the responsibility of reading the data when a read event occurs and - * writing the data back when the channel is writable. It leaves the reading and writing of - * data on the concrete implementation. It provides a block writing mechanism wherein when - * any {@link ChannelHandler} wants to write data back, it queues the data in pending write queue - * and clears it in block manner. This provides better throughput. - * + * This class serves has the responsibility of reading the data when a read event occurs and writing + * the data back when the channel is writable. It leaves the reading and writing of data on the + * concrete implementation. It provides a block writing mechanism wherein when any + * {@link ChannelHandler} wants to write data back, it queues the data in pending write queue and + * clears it in block manner. This provides better throughput. + * * @author npathai * */ public abstract class AbstractNioChannel { - - private SelectableChannel channel; - private ChannelHandler handler; - private Map> channelToPendingWrites = new ConcurrentHashMap<>(); - private NioReactor reactor; - - /** - * Creates a new channel. - * @param handler which will handle events occurring on this channel. - * @param channel a NIO channel to be wrapped. - */ - public AbstractNioChannel(ChannelHandler handler, SelectableChannel channel) { - this.handler = handler; - this.channel = channel; - } - - /** - * Injects the reactor in this channel. - */ - void setReactor(NioReactor reactor) { - this.reactor = reactor; - } - /** - * @return the wrapped NIO channel. - */ - public SelectableChannel getChannel() { - return channel; - } + private final SelectableChannel channel; + private final ChannelHandler handler; + private final Map> channelToPendingWrites = new ConcurrentHashMap<>(); + private NioReactor reactor; - /** - * The operation in which the channel is interested, this operation is provided to {@link Selector}. - * - * @return interested operation. - * @see SelectionKey - */ - public abstract int getInterestedOps(); - - /** - * Binds the channel on provided port. - * - * @throws IOException if any I/O error occurs. - */ - public abstract void bind() throws IOException; - - /** - * Reads the data using the key and returns the read data. The underlying channel should be fetched using - * {@link SelectionKey#channel()}. - * - * @param key the key on which read event occurred. - * @return data read. - * @throws IOException if any I/O error occurs. - */ - public abstract Object read(SelectionKey key) throws IOException; + /** + * Creates a new channel. + * + * @param handler which will handle events occurring on this channel. + * @param channel a NIO channel to be wrapped. + */ + public AbstractNioChannel(ChannelHandler handler, SelectableChannel channel) { + this.handler = handler; + this.channel = channel; + } - /** - * @return the handler associated with this channel. - */ - public ChannelHandler getHandler() { - return handler; - } + /** + * Injects the reactor in this channel. + */ + void setReactor(NioReactor reactor) { + this.reactor = reactor; + } - /* - * Called from the context of reactor thread when the key becomes writable. - * The channel writes the whole pending block of data at once. - */ - void flush(SelectionKey key) throws IOException { - Queue pendingWrites = channelToPendingWrites.get(key.channel()); - while (true) { - Object pendingWrite = pendingWrites.poll(); - if (pendingWrite == null) { - // We don't have anything more to write so channel is interested in reading more data - reactor.changeOps(key, SelectionKey.OP_READ); - break; - } - - // ask the concrete channel to make sense of data and write it to java channel - doWrite(pendingWrite, key); - } - } + /** + * @return the wrapped NIO channel. + */ + public SelectableChannel getChannel() { + return channel; + } - /** - * Writes the data to the channel. - * - * @param pendingWrite the data to be written on channel. - * @param key the key which is writable. - * @throws IOException if any I/O error occurs. - */ - protected abstract void doWrite(Object pendingWrite, SelectionKey key) throws IOException; + /** + * The operation in which the channel is interested, this operation is provided to + * {@link Selector}. + * + * @return interested operation. + * @see SelectionKey + */ + public abstract int getInterestedOps(); - /** - * Queues the data for writing. The data is not guaranteed to be written on underlying channel - * when this method returns. It will be written when the channel is flushed. - * - *

- * This method is used by the {@link ChannelHandler} to send reply back to the client. - *
- * Example: - *

-	 * 
-	 * {@literal @}Override
-	 * public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) {
-	 *   byte[] data = ((ByteBuffer)readObject).array();
-	 *   ByteBuffer buffer = ByteBuffer.wrap("Server reply".getBytes());
-	 *   channel.write(buffer, key);
-	 * }
-	 * 
-	 * 
-	 * @param data the data to be written on underlying channel.
-	 * @param key the key which is writable.
-	 */
-	public void write(Object data, SelectionKey key) {
-		Queue pendingWrites = this.channelToPendingWrites.get(key.channel());
-		if (pendingWrites == null) {
-			synchronized (this.channelToPendingWrites) {
-				pendingWrites = this.channelToPendingWrites.get(key.channel());
-				if (pendingWrites == null) {
-					pendingWrites = new ConcurrentLinkedQueue<>();
-					this.channelToPendingWrites.put(key.channel(), pendingWrites);
-				}
-			}
-		}
-		pendingWrites.add(data);
-		reactor.changeOps(key, SelectionKey.OP_WRITE);
-	}
+  /**
+   * Binds the channel on provided port.
+   * 
+   * @throws IOException if any I/O error occurs.
+   */
+  public abstract void bind() throws IOException;
+
+  /**
+   * Reads the data using the key and returns the read data. The underlying channel should be
+   * fetched using {@link SelectionKey#channel()}.
+   * 
+   * @param key the key on which read event occurred.
+   * @return data read.
+   * @throws IOException if any I/O error occurs.
+   */
+  public abstract Object read(SelectionKey key) throws IOException;
+
+  /**
+   * @return the handler associated with this channel.
+   */
+  public ChannelHandler getHandler() {
+    return handler;
+  }
+
+  /*
+   * Called from the context of reactor thread when the key becomes writable. The channel writes the
+   * whole pending block of data at once.
+   */
+  void flush(SelectionKey key) throws IOException {
+    Queue pendingWrites = channelToPendingWrites.get(key.channel());
+    while (true) {
+      Object pendingWrite = pendingWrites.poll();
+      if (pendingWrite == null) {
+        // We don't have anything more to write so channel is interested in reading more data
+        reactor.changeOps(key, SelectionKey.OP_READ);
+        break;
+      }
+
+      // ask the concrete channel to make sense of data and write it to java channel
+      doWrite(pendingWrite, key);
+    }
+  }
+
+  /**
+   * Writes the data to the channel.
+   * 
+   * @param pendingWrite the data to be written on channel.
+   * @param key the key which is writable.
+   * @throws IOException if any I/O error occurs.
+   */
+  protected abstract void doWrite(Object pendingWrite, SelectionKey key) throws IOException;
+
+  /**
+   * Queues the data for writing. The data is not guaranteed to be written on underlying channel
+   * when this method returns. It will be written when the channel is flushed.
+   * 
+   * 

+ * This method is used by the {@link ChannelHandler} to send reply back to the client.
+ * Example: + * + *

+   * 
+   * {@literal @}Override
+   * public void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key) {
+   *   byte[] data = ((ByteBuffer)readObject).array();
+   *   ByteBuffer buffer = ByteBuffer.wrap("Server reply".getBytes());
+   *   channel.write(buffer, key);
+   * }
+   * 
+   * 
+   * @param data the data to be written on underlying channel.
+   * @param key the key which is writable.
+   */
+  public void write(Object data, SelectionKey key) {
+    Queue pendingWrites = this.channelToPendingWrites.get(key.channel());
+    if (pendingWrites == null) {
+      synchronized (this.channelToPendingWrites) {
+        pendingWrites = this.channelToPendingWrites.get(key.channel());
+        if (pendingWrites == null) {
+          pendingWrites = new ConcurrentLinkedQueue<>();
+          this.channelToPendingWrites.put(key.channel(), pendingWrites);
+        }
+      }
+    }
+    pendingWrites.add(data);
+    reactor.changeOps(key, SelectionKey.OP_WRITE);
+  }
 }
diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java
index 0aae9db75..a4a392a34 100644
--- a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java
+++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java
@@ -7,19 +7,19 @@ import java.nio.channels.SelectionKey;
  * to it by the {@link Dispatcher}. This is where the application logic resides.
  * 
  * 

- * A {@link ChannelHandler} can be associated with one or many {@link AbstractNioChannel}s, and whenever - * an event occurs on any of the associated channels, the handler is notified of the event. - * + * A {@link ChannelHandler} can be associated with one or many {@link AbstractNioChannel}s, and + * whenever an event occurs on any of the associated channels, the handler is notified of the event. + * * @author npathai */ public interface ChannelHandler { - /** - * Called when the {@code channel} receives some data from remote peer. - * - * @param channel the channel from which the data was received. - * @param readObject the data read. - * @param key the key on which read event occurred. - */ - void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key); + /** + * Called when the {@code channel} receives some data from remote peer. + * + * @param channel the channel from which the data was received. + * @param readObject the data read. + * @param key the key on which read event occurred. + */ + void handleChannelRead(AbstractNioChannel channel, Object readObject, SelectionKey key); } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java index c563ef9d3..0ed53f8fc 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java @@ -3,39 +3,41 @@ package com.iluwatar.reactor.framework; import java.nio.channels.SelectionKey; /** - * Represents the event dispatching strategy. When {@link NioReactor} senses any event on the - * registered {@link AbstractNioChannel}s then it de-multiplexes the event type, read or write - * or connect, and then calls the {@link Dispatcher} to dispatch the read events. This decouples the I/O - * processing from application specific processing. - *
- * Dispatcher should call the {@link ChannelHandler} associated with the channel on which event occurred. + * Represents the event dispatching strategy. When {@link NioReactor} senses any event on the + * registered {@link AbstractNioChannel}s then it de-multiplexes the event type, read or write or + * connect, and then calls the {@link Dispatcher} to dispatch the read events. This decouples the + * I/O processing from application specific processing.
+ * Dispatcher should call the {@link ChannelHandler} associated with the channel on which event + * occurred. * *

- * The application can customize the way in which event is dispatched such as using the reactor thread to - * dispatch event to channels or use a worker pool to do the non I/O processing. - * + * The application can customize the way in which event is dispatched such as using the reactor + * thread to dispatch event to channels or use a worker pool to do the non I/O processing. + * * @see SameThreadDispatcher * @see ThreadPoolDispatcher * * @author npathai */ public interface Dispatcher { - /** - * This hook method is called when read event occurs on particular channel. The data read - * is provided in readObject. The implementation should dispatch this read event - * to the associated {@link ChannelHandler} of channel. - * - *

- * The type of readObject depends on the channel on which data was received. - * - * @param channel on which read event occurred - * @param readObject object read by channel - * @param key on which event occurred - */ - void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); - - /** - * Stops dispatching events and cleans up any acquired resources such as threads. - */ - void stop(); + /** + * This hook method is called when read event occurs on particular channel. The data read is + * provided in readObject. The implementation should dispatch this read event to the + * associated {@link ChannelHandler} of channel. + * + *

+ * The type of readObject depends on the channel on which data was received. + * + * @param channel on which read event occurred + * @param readObject object read by channel + * @param key on which event occurred + */ + void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key); + + /** + * Stops dispatching events and cleans up any acquired resources such as threads. + * + * @throws InterruptedException if interrupted while stopping dispatcher. + */ + void stop() throws InterruptedException; } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java index f338ce4a3..089911d10 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java @@ -15,143 +15,147 @@ import java.nio.channels.SelectionKey; */ public class NioDatagramChannel extends AbstractNioChannel { - private int port; + private final int port; - /** - * Creates a {@link DatagramChannel} which will bind at provided port and use handler to handle - * incoming events on this channel. - *

- * Note the constructor does not bind the socket, {@link #bind()} method should be called for binding - * the socket. - * - * @param port the port to be bound to listen for incoming datagram requests. - * @param handler the handler to be used for handling incoming requests on this channel. - * @throws IOException if any I/O error occurs. - */ - public NioDatagramChannel(int port, ChannelHandler handler) throws IOException { - super(handler, DatagramChannel.open()); - this.port = port; - } + /** + * Creates a {@link DatagramChannel} which will bind at provided port and use handler + * to handle incoming events on this channel. + *

+ * Note the constructor does not bind the socket, {@link #bind()} method should be called for + * binding the socket. + * + * @param port the port to be bound to listen for incoming datagram requests. + * @param handler the handler to be used for handling incoming requests on this channel. + * @throws IOException if any I/O error occurs. + */ + public NioDatagramChannel(int port, ChannelHandler handler) throws IOException { + super(handler, DatagramChannel.open()); + this.port = port; + } - @Override - public int getInterestedOps() { - /* there is no need to accept connections in UDP, so the channel shows interest in - * reading data. - */ - return SelectionKey.OP_READ; - } + @Override + public int getInterestedOps() { + /* + * there is no need to accept connections in UDP, so the channel shows interest in reading data. + */ + return SelectionKey.OP_READ; + } - /** - * Reads and returns a {@link DatagramPacket} from the underlying channel. - * @return the datagram packet read having the sender address. - */ - @Override - public DatagramPacket read(SelectionKey key) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(1024); - SocketAddress sender = ((DatagramChannel)key.channel()).receive(buffer); - - /* - * It is required to create a DatagramPacket because we need to preserve which - * socket address acts as destination for sending reply packets. - */ - buffer.flip(); - DatagramPacket packet = new DatagramPacket(buffer); - packet.setSender(sender); - - return packet; - } - - /** - * @return the underlying datagram channel. - */ - @Override - public DatagramChannel getChannel() { - return (DatagramChannel) super.getChannel(); - } - - /** - * Binds UDP socket on the provided port. - * - * @throws IOException if any I/O error occurs. - */ - @Override - public void bind() throws IOException { - getChannel().socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); - getChannel().configureBlocking(false); - System.out.println("Bound UDP socket at port: " + port); - } + /** + * Reads and returns a {@link DatagramPacket} from the underlying channel. + * + * @return the datagram packet read having the sender address. + */ + @Override + public DatagramPacket read(SelectionKey key) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); + SocketAddress sender = ((DatagramChannel) key.channel()).receive(buffer); - /** - * Writes the pending {@link DatagramPacket} to the underlying channel sending data to - * the intended receiver of the packet. - */ - @Override - protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { - DatagramPacket pendingPacket = (DatagramPacket) pendingWrite; - getChannel().send(pendingPacket.getData(), pendingPacket.getReceiver()); - } + /* + * It is required to create a DatagramPacket because we need to preserve which socket address + * acts as destination for sending reply packets. + */ + buffer.flip(); + DatagramPacket packet = new DatagramPacket(buffer); + packet.setSender(sender); - /** - * Writes the outgoing {@link DatagramPacket} to the channel. The intended receiver of the - * datagram packet must be set in the data using {@link DatagramPacket#setReceiver(SocketAddress)}. - */ - @Override - public void write(Object data, SelectionKey key) { - super.write(data, key); - } - - /** - * Container of data used for {@link NioDatagramChannel} to communicate with remote peer. - */ - public static class DatagramPacket { - private SocketAddress sender; - private ByteBuffer data; - private SocketAddress receiver; + return packet; + } - /** - * Creates a container with underlying data. - * - * @param data the underlying message to be written on channel. - */ - public DatagramPacket(ByteBuffer data) { - this.data = data; - } + /** + * @return the underlying datagram channel. + */ + @Override + public DatagramChannel getChannel() { + return (DatagramChannel) super.getChannel(); + } - /** - * @return the sender address. - */ - public SocketAddress getSender() { - return sender; - } - - /** - * Sets the sender address of this packet. - * @param sender the sender address. - */ - public void setSender(SocketAddress sender) { - this.sender = sender; - } + /** + * Binds UDP socket on the provided port. + * + * @throws IOException if any I/O error occurs. + */ + @Override + public void bind() throws IOException { + getChannel().socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + getChannel().configureBlocking(false); + System.out.println("Bound UDP socket at port: " + port); + } - /** - * @return the receiver address. - */ - public SocketAddress getReceiver() { - return receiver; - } - - /** - * Sets the intended receiver address. This must be set when writing to the channel. - * @param receiver the receiver address. - */ - public void setReceiver(SocketAddress receiver) { - this.receiver = receiver; - } + /** + * Writes the pending {@link DatagramPacket} to the underlying channel sending data to the + * intended receiver of the packet. + */ + @Override + protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { + DatagramPacket pendingPacket = (DatagramPacket) pendingWrite; + getChannel().send(pendingPacket.getData(), pendingPacket.getReceiver()); + } - /** - * @return the underlying message that will be written on channel. - */ - public ByteBuffer getData() { - return data; - } - } -} \ No newline at end of file + /** + * Writes the outgoing {@link DatagramPacket} to the channel. The intended receiver of the + * datagram packet must be set in the data using + * {@link DatagramPacket#setReceiver(SocketAddress)}. + */ + @Override + public void write(Object data, SelectionKey key) { + super.write(data, key); + } + + /** + * Container of data used for {@link NioDatagramChannel} to communicate with remote peer. + */ + public static class DatagramPacket { + private SocketAddress sender; + private ByteBuffer data; + private SocketAddress receiver; + + /** + * Creates a container with underlying data. + * + * @param data the underlying message to be written on channel. + */ + public DatagramPacket(ByteBuffer data) { + this.data = data; + } + + /** + * @return the sender address. + */ + public SocketAddress getSender() { + return sender; + } + + /** + * Sets the sender address of this packet. + * + * @param sender the sender address. + */ + public void setSender(SocketAddress sender) { + this.sender = sender; + } + + /** + * @return the receiver address. + */ + public SocketAddress getReceiver() { + return receiver; + } + + /** + * Sets the intended receiver address. This must be set when writing to the channel. + * + * @param receiver the receiver address. + */ + public void setReceiver(SocketAddress receiver) { + this.receiver = receiver; + } + + /** + * @return the underlying message that will be written on channel. + */ + public ByteBuffer getData() { + return data; + } + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java index 273898ae3..89af20630 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java @@ -12,228 +12,225 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; + /** * This class acts as Synchronous Event De-multiplexer and Initiation Dispatcher of Reactor pattern. * Multiple handles i.e. {@link AbstractNioChannel}s can be registered to the reactor and it blocks - * for events from all these handles. Whenever an event occurs on any of the registered handles, - * it synchronously de-multiplexes the event which can be any of read, write or accept, and - * dispatches the event to the appropriate {@link ChannelHandler} using the {@link Dispatcher}. + * for events from all these handles. Whenever an event occurs on any of the registered handles, it + * synchronously de-multiplexes the event which can be any of read, write or accept, and dispatches + * the event to the appropriate {@link ChannelHandler} using the {@link Dispatcher}. * *

- * Implementation: - * A NIO reactor runs in its own thread when it is started using {@link #start()} method. - * {@link NioReactor} uses {@link Selector} for realizing Synchronous Event De-multiplexing. + * Implementation: A NIO reactor runs in its own thread when it is started using {@link #start()} + * method. {@link NioReactor} uses {@link Selector} for realizing Synchronous Event De-multiplexing. * *

- * NOTE: This is one of the ways to implement NIO reactor and it does not take care of all possible edge cases - * which are required in a real application. This implementation is meant to demonstrate the fundamental - * concepts that lie behind Reactor pattern. + * NOTE: This is one of the ways to implement NIO reactor and it does not take care of all possible + * edge cases which are required in a real application. This implementation is meant to demonstrate + * the fundamental concepts that lie behind Reactor pattern. * * @author npathai * */ public class NioReactor { - private Selector selector; - private Dispatcher dispatcher; - /** - * All the work of altering the SelectionKey operations and Selector operations are performed in - * the context of main event loop of reactor. So when any channel needs to change its readability - * or writability, a new command is added in the command queue and then the event loop picks up - * the command and executes it in next iteration. - */ - private Queue pendingCommands = new ConcurrentLinkedQueue<>(); - private ExecutorService reactorMain = Executors.newSingleThreadExecutor(); - - /** - * Creates a reactor which will use provided {@code dispatcher} to dispatch events. - * The application can provide various implementations of dispatcher which suits its - * needs. - * - * @param dispatcher a non-null dispatcher used to dispatch events on registered channels. - * @throws IOException if any I/O error occurs. - */ - public NioReactor(Dispatcher dispatcher) throws IOException { - this.dispatcher = dispatcher; - this.selector = Selector.open(); - } + private final Selector selector; + private final Dispatcher dispatcher; + /** + * All the work of altering the SelectionKey operations and Selector operations are performed in + * the context of main event loop of reactor. So when any channel needs to change its readability + * or writability, a new command is added in the command queue and then the event loop picks up + * the command and executes it in next iteration. + */ + private final Queue pendingCommands = new ConcurrentLinkedQueue<>(); + private final ExecutorService reactorMain = Executors.newSingleThreadExecutor(); - /** - * Starts the reactor event loop in a new thread. - * - * @throws IOException if any I/O error occurs. - */ - public void start() throws IOException { - reactorMain.execute(() -> { - try { - System.out.println("Reactor started, waiting for events..."); - eventLoop(); - } catch (IOException e) { - e.printStackTrace(); - } - }); - } - - /** - * Stops the reactor and related resources such as dispatcher. - */ - public void stop() { - reactorMain.shutdownNow(); - selector.wakeup(); - try { - reactorMain.awaitTermination(4, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - dispatcher.stop(); - } + /** + * Creates a reactor which will use provided {@code dispatcher} to dispatch events. The + * application can provide various implementations of dispatcher which suits its needs. + * + * @param dispatcher a non-null dispatcher used to dispatch events on registered channels. + * @throws IOException if any I/O error occurs. + */ + public NioReactor(Dispatcher dispatcher) throws IOException { + this.dispatcher = dispatcher; + this.selector = Selector.open(); + } - /** - * Registers a new channel (handle) with this reactor. Reactor will start waiting for events on this channel - * and notify of any events. While registering the channel the reactor uses {@link AbstractNioChannel#getInterestedOps()} - * to know about the interested operation of this channel. - * - * @param channel a new channel on which reactor will wait for events. The channel must be bound - * prior to being registered. - * @return this - * @throws IOException if any I/O error occurs. - */ - public NioReactor registerChannel(AbstractNioChannel channel) throws IOException { - SelectionKey key = channel.getChannel().register(selector, channel.getInterestedOps()); - key.attach(channel); - channel.setReactor(this); - return this; - } - - private void eventLoop() throws IOException { - while (true) { - - // honor interrupt request - if (Thread.interrupted()) { - break; - } - - // honor any pending commands first - processPendingCommands(); - - /* - * Synchronous event de-multiplexing happens here, this is blocking call which - * returns when it is possible to initiate non-blocking operation on any of the - * registered channels. - */ - selector.select(); - - /* - * Represents the events that have occurred on registered handles. - */ - Set keys = selector.selectedKeys(); + /** + * Starts the reactor event loop in a new thread. + * + * @throws IOException if any I/O error occurs. + */ + public void start() throws IOException { + reactorMain.execute(() -> { + try { + System.out.println("Reactor started, waiting for events..."); + eventLoop(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } - Iterator iterator = keys.iterator(); - - while (iterator.hasNext()) { - SelectionKey key = iterator.next(); - if (!key.isValid()) { - iterator.remove(); - continue; - } - processKey(key); - } - keys.clear(); - } - } - - private void processPendingCommands() { - Iterator iterator = pendingCommands.iterator(); - while (iterator.hasNext()) { - Runnable command = iterator.next(); - command.run(); - iterator.remove(); - } - } + /** + * Stops the reactor and related resources such as dispatcher. + * + * @throws InterruptedException if interrupted while stopping the reactor. + */ + public void stop() throws InterruptedException { + reactorMain.shutdownNow(); + selector.wakeup(); + reactorMain.awaitTermination(4, TimeUnit.SECONDS); + dispatcher.stop(); + } - /* - * Initiation dispatcher logic, it checks the type of event and notifier application - * specific event handler to handle the event. - */ - private void processKey(SelectionKey key) throws IOException { - if (key.isAcceptable()) { - onChannelAcceptable(key); - } else if (key.isReadable()) { - onChannelReadable(key); - } else if (key.isWritable()) { - onChannelWritable(key); - } - } + /** + * Registers a new channel (handle) with this reactor. Reactor will start waiting for events on + * this channel and notify of any events. While registering the channel the reactor uses + * {@link AbstractNioChannel#getInterestedOps()} to know about the interested operation of this + * channel. + * + * @param channel a new channel on which reactor will wait for events. The channel must be bound + * prior to being registered. + * @return this + * @throws IOException if any I/O error occurs. + */ + public NioReactor registerChannel(AbstractNioChannel channel) throws IOException { + SelectionKey key = channel.getChannel().register(selector, channel.getInterestedOps()); + key.attach(channel); + channel.setReactor(this); + return this; + } - private void onChannelWritable(SelectionKey key) throws IOException { - AbstractNioChannel channel = (AbstractNioChannel) key.attachment(); - channel.flush(key); - } + private void eventLoop() throws IOException { + while (true) { - private void onChannelReadable(SelectionKey key) { - try { - // reads the incoming data in context of reactor main loop. Can this be improved? - Object readObject = ((AbstractNioChannel)key.attachment()).read(key); + // honor interrupt request + if (Thread.interrupted()) { + break; + } - dispatchReadEvent(key, readObject); - } catch (IOException e) { - try { - key.channel().close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - } - } + // honor any pending commands first + processPendingCommands(); - /* - * Uses the application provided dispatcher to dispatch events to application handler. - */ - private void dispatchReadEvent(SelectionKey key, Object readObject) { - dispatcher.onChannelReadEvent((AbstractNioChannel)key.attachment(), readObject, key); - } + /* + * Synchronous event de-multiplexing happens here, this is blocking call which returns when it + * is possible to initiate non-blocking operation on any of the registered channels. + */ + selector.select(); - private void onChannelAcceptable(SelectionKey key) throws IOException { - ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); - SocketChannel socketChannel = serverSocketChannel.accept(); - socketChannel.configureBlocking(false); - SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ); - readKey.attach(key.attachment()); - } + /* + * Represents the events that have occurred on registered handles. + */ + Set keys = selector.selectedKeys(); - /** - * Queues the change of operations request of a channel, which will change the interested - * operations of the channel sometime in future. - *

- * This is a non-blocking method and does not guarantee that the operations have changed when - * this method returns. - * - * @param key the key for which operations have to be changed. - * @param interestedOps the new interest operations. - */ - public void changeOps(SelectionKey key, int interestedOps) { - pendingCommands.add(new ChangeKeyOpsCommand(key, interestedOps)); - selector.wakeup(); - } - - /** - * A command that changes the interested operations of the key provided. - */ - class ChangeKeyOpsCommand implements Runnable { - private SelectionKey key; - private int interestedOps; - - public ChangeKeyOpsCommand(SelectionKey key, int interestedOps) { - this.key = key; - this.interestedOps = interestedOps; - } - - public void run() { - key.interestOps(interestedOps); - } - - @Override - public String toString() { - return "Change of ops to: " + interestedOps; - } - } -} \ No newline at end of file + Iterator iterator = keys.iterator(); + + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + if (!key.isValid()) { + iterator.remove(); + continue; + } + processKey(key); + } + keys.clear(); + } + } + + private void processPendingCommands() { + Iterator iterator = pendingCommands.iterator(); + while (iterator.hasNext()) { + Runnable command = iterator.next(); + command.run(); + iterator.remove(); + } + } + + /* + * Initiation dispatcher logic, it checks the type of event and notifier application specific + * event handler to handle the event. + */ + private void processKey(SelectionKey key) throws IOException { + if (key.isAcceptable()) { + onChannelAcceptable(key); + } else if (key.isReadable()) { + onChannelReadable(key); + } else if (key.isWritable()) { + onChannelWritable(key); + } + } + + private void onChannelWritable(SelectionKey key) throws IOException { + AbstractNioChannel channel = (AbstractNioChannel) key.attachment(); + channel.flush(key); + } + + private void onChannelReadable(SelectionKey key) { + try { + // reads the incoming data in context of reactor main loop. Can this be improved? + Object readObject = ((AbstractNioChannel) key.attachment()).read(key); + + dispatchReadEvent(key, readObject); + } catch (IOException e) { + try { + key.channel().close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + + /* + * Uses the application provided dispatcher to dispatch events to application handler. + */ + private void dispatchReadEvent(SelectionKey key, Object readObject) { + dispatcher.onChannelReadEvent((AbstractNioChannel) key.attachment(), readObject, key); + } + + private void onChannelAcceptable(SelectionKey key) throws IOException { + ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ); + readKey.attach(key.attachment()); + } + + /** + * Queues the change of operations request of a channel, which will change the interested + * operations of the channel sometime in future. + *

+ * This is a non-blocking method and does not guarantee that the operations have changed when this + * method returns. + * + * @param key the key for which operations have to be changed. + * @param interestedOps the new interest operations. + */ + public void changeOps(SelectionKey key, int interestedOps) { + pendingCommands.add(new ChangeKeyOpsCommand(key, interestedOps)); + selector.wakeup(); + } + + /** + * A command that changes the interested operations of the key provided. + */ + class ChangeKeyOpsCommand implements Runnable { + private SelectionKey key; + private int interestedOps; + + public ChangeKeyOpsCommand(SelectionKey key, int interestedOps) { + this.key = key; + this.interestedOps = interestedOps; + } + + public void run() { + key.interestOps(interestedOps); + } + + @Override + public String toString() { + return "Change of ops to: " + interestedOps; + } + } +} diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java index ae54af643..17f47a394 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java @@ -9,81 +9,82 @@ import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; /** - * A wrapper over {@link NioServerSocketChannel} which can read and write data on a {@link SocketChannel}. + * A wrapper over {@link NioServerSocketChannel} which can read and write data on a + * {@link SocketChannel}. * * @author npathai */ public class NioServerSocketChannel extends AbstractNioChannel { - private int port; + private final int port; - /** - * Creates a {@link ServerSocketChannel} which will bind at provided port and use - * handler to handle incoming events on this channel. - *

- * Note the constructor does not bind the socket, {@link #bind()} method should be called for binding - * the socket. - * - * @param port the port on which channel will be bound to accept incoming connection requests. - * @param handler the handler that will handle incoming requests on this channel. - * @throws IOException if any I/O error occurs. - */ - public NioServerSocketChannel(int port, ChannelHandler handler) throws IOException { - super(handler, ServerSocketChannel.open()); - this.port = port; - } + /** + * Creates a {@link ServerSocketChannel} which will bind at provided port and use + * handler to handle incoming events on this channel. + *

+ * Note the constructor does not bind the socket, {@link #bind()} method should be called for + * binding the socket. + * + * @param port the port on which channel will be bound to accept incoming connection requests. + * @param handler the handler that will handle incoming requests on this channel. + * @throws IOException if any I/O error occurs. + */ + public NioServerSocketChannel(int port, ChannelHandler handler) throws IOException { + super(handler, ServerSocketChannel.open()); + this.port = port; + } - - @Override - public int getInterestedOps() { - // being a server socket channel it is interested in accepting connection from remote peers. - return SelectionKey.OP_ACCEPT; - } - /** - * @return the underlying {@link ServerSocketChannel}. - */ - @Override - public ServerSocketChannel getChannel() { - return (ServerSocketChannel) super.getChannel(); - } - - /** - * Reads and returns {@link ByteBuffer} from the underlying {@link SocketChannel} represented by - * the key. Due to the fact that there is a dedicated channel for each client connection - * we don't need to store the sender. - */ - @Override - public ByteBuffer read(SelectionKey key) throws IOException { - SocketChannel socketChannel = (SocketChannel) key.channel(); - ByteBuffer buffer = ByteBuffer.allocate(1024); - int read = socketChannel.read(buffer); - buffer.flip(); - if (read == -1) { - throw new IOException("Socket closed"); - } - return buffer; - } + @Override + public int getInterestedOps() { + // being a server socket channel it is interested in accepting connection from remote peers. + return SelectionKey.OP_ACCEPT; + } - /** - * Binds TCP socket on the provided port. - * - * @throws IOException if any I/O error occurs. - */ - @Override - public void bind() throws IOException { - ((ServerSocketChannel)getChannel()).socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); - ((ServerSocketChannel)getChannel()).configureBlocking(false); - System.out.println("Bound TCP socket at port: " + port); - } + /** + * @return the underlying {@link ServerSocketChannel}. + */ + @Override + public ServerSocketChannel getChannel() { + return (ServerSocketChannel) super.getChannel(); + } - /** - * Writes the pending {@link ByteBuffer} to the underlying channel sending data to - * the intended receiver of the packet. - */ - @Override - protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { - ByteBuffer pendingBuffer = (ByteBuffer) pendingWrite; - ((SocketChannel)key.channel()).write(pendingBuffer); - } + /** + * Reads and returns {@link ByteBuffer} from the underlying {@link SocketChannel} represented by + * the key. Due to the fact that there is a dedicated channel for each client + * connection we don't need to store the sender. + */ + @Override + public ByteBuffer read(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + int read = socketChannel.read(buffer); + buffer.flip(); + if (read == -1) { + throw new IOException("Socket closed"); + } + return buffer; + } + + /** + * Binds TCP socket on the provided port. + * + * @throws IOException if any I/O error occurs. + */ + @Override + public void bind() throws IOException { + ((ServerSocketChannel) getChannel()).socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + ((ServerSocketChannel) getChannel()).configureBlocking(false); + System.out.println("Bound TCP socket at port: " + port); + } + + /** + * Writes the pending {@link ByteBuffer} to the underlying channel sending data to the intended + * receiver of the packet. + */ + @Override + protected void doWrite(Object pendingWrite, SelectionKey key) throws IOException { + ByteBuffer pendingBuffer = (ByteBuffer) pendingWrite; + ((SocketChannel) key.channel()).write(pendingBuffer); + } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java index b5392ac8f..baacda9f3 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java @@ -4,8 +4,8 @@ import java.nio.channels.SelectionKey; /** * Dispatches the events in the context of caller thread. This implementation is a good fit for - * small applications where there are limited clients. Using this implementation limits the scalability - * because the I/O thread performs the application specific processing. + * small applications where there are limited clients. Using this implementation limits the + * scalability because the I/O thread performs the application specific processing. * *

* For better performance use {@link ThreadPoolDispatcher}. @@ -16,28 +16,25 @@ import java.nio.channels.SelectionKey; */ public class SameThreadDispatcher implements Dispatcher { - /** - * Dispatches the read event in the context of caller thread. - *
- * Note this is a blocking call. It returns only after the associated handler has handled the - * read event. - */ - @Override - public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { - if (channel.getHandler() != null) { - /* - * Calls the associated handler to notify the read event where application specific code - * resides. - */ - channel.getHandler().handleChannelRead(channel, readObject, key); - } - } + /** + * Dispatches the read event in the context of caller thread.
+ * Note this is a blocking call. It returns only after the associated handler has handled the read + * event. + */ + @Override + public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { + /* + * Calls the associated handler to notify the read event where application specific code + * resides. + */ + channel.getHandler().handleChannelRead(channel, readObject, key); + } - /** - * No resources to free. - */ - @Override - public void stop() { - // no-op - } + /** + * No resources to free. + */ + @Override + public void stop() { + // no-op + } } diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java index 8624b878e..9fd539adb 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java @@ -6,50 +6,45 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** - * An implementation that uses a pool of worker threads to dispatch the events. This provides - * better scalability as the application specific processing is not performed in the context - * of I/O (reactor) thread. + * An implementation that uses a pool of worker threads to dispatch the events. This provides better + * scalability as the application specific processing is not performed in the context of I/O + * (reactor) thread. * * @author npathai * */ -public class ThreadPoolDispatcher extends SameThreadDispatcher { +public class ThreadPoolDispatcher implements Dispatcher { - private ExecutorService executorService; + private final ExecutorService executorService; - /** - * Creates a pooled dispatcher with tunable pool size. - * - * @param poolSize number of pooled threads - */ - public ThreadPoolDispatcher(int poolSize) { - this.executorService = Executors.newFixedThreadPool(poolSize); - } + /** + * Creates a pooled dispatcher with tunable pool size. + * + * @param poolSize number of pooled threads + */ + public ThreadPoolDispatcher(int poolSize) { + this.executorService = Executors.newFixedThreadPool(poolSize); + } - /** - * Submits the work of dispatching the read event to worker pool, where it gets picked - * up by worker threads. - *
- * Note that this is a non-blocking call and returns immediately. It is not guaranteed - * that the event has been handled by associated handler. - */ - @Override - public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { - executorService.execute(() -> - ThreadPoolDispatcher.super.onChannelReadEvent(channel, readObject, key)); - } - - /** - * Stops the pool of workers. - */ - @Override - public void stop() { - executorService.shutdownNow(); - try { - executorService.awaitTermination(4, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + /** + * Submits the work of dispatching the read event to worker pool, where it gets picked up by + * worker threads.
+ * Note that this is a non-blocking call and returns immediately. It is not guaranteed that the + * event has been handled by associated handler. + */ + @Override + public void onChannelReadEvent(AbstractNioChannel channel, Object readObject, SelectionKey key) { + executorService.execute(() -> channel.getHandler().handleChannelRead(channel, readObject, key)); + } + /** + * Stops the pool of workers. + * + * @throws InterruptedException if interrupted while stopping pool of workers. + */ + @Override + public void stop() throws InterruptedException { + executorService.shutdown(); + executorService.awaitTermination(4, TimeUnit.SECONDS); + } } diff --git a/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java index 9447aac01..bc51e26de 100644 --- a/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java +++ b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java @@ -4,24 +4,38 @@ import java.io.IOException; import org.junit.Test; +/** + * + * This class tests the Distributed Logging service by starting a Reactor and then sending it + * concurrent logging requests using multiple clients. + * + * @author npathai + */ public class AppTest { - @Test - public void testApp() throws IOException { - App app = new App(); - app.start(); - - AppClient client = new AppClient(); - client.start(); - - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - client.stop(); - - app.stop(); - } + /** + * Test the application. + * + * @throws IOException if any I/O error occurs. + * @throws InterruptedException if interrupted while stopping the application. + */ + @Test + public void testApp() throws IOException, InterruptedException { + App app = new App(); + app.start(); + + AppClient client = new AppClient(); + client.start(); + + // allow clients to send requests. Artificial delay. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + client.stop(); + + app.stop(); + } } From 2ff78184e54883b54d05e83e50f47334b22f8eaa Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Mon, 14 Sep 2015 12:56:17 +0530 Subject: [PATCH 13/15] Work on #74, added class diagram and index.md --- reactor/etc/reactor.png | Bin 0 -> 124359 bytes reactor/etc/reactor.ucls | 207 +++++++++++++++++++++++++++++++++++++++ reactor/index.md | 30 ++++++ 3 files changed, 237 insertions(+) create mode 100644 reactor/etc/reactor.png create mode 100644 reactor/etc/reactor.ucls create mode 100644 reactor/index.md diff --git a/reactor/etc/reactor.png b/reactor/etc/reactor.png new file mode 100644 index 0000000000000000000000000000000000000000..0b00ec98be5ccdbeeb74d71241de065e3d733554 GIT binary patch literal 124359 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 literal 0 HcmV?d00001 diff --git a/reactor/etc/reactor.ucls b/reactor/etc/reactor.ucls new file mode 100644 index 000000000..d072e4029 --- /dev/null +++ b/reactor/etc/reactor.ucls @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reactor/index.md b/reactor/index.md new file mode 100644 index 000000000..7333c74dd --- /dev/null +++ b/reactor/index.md @@ -0,0 +1,30 @@ +--- +layout: pattern +title: Reactor +folder: reactor +permalink: /patterns/reactor/ +categories: Architectural +tags: + - Java + - Difficulty-Expert +--- + +**Intent:** The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. The application can register specific handlers for processing which are called by reactor on specific events. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. + +![Reactor](./etc/reactor.png "Reactor") + +**Applicability:** Use Reactor pattern when + +* a server application needs to handle concurrent service requests from multiple clients. +* a server application needs to be available for receiving requests from new clients even when handling older client requests. +* a server must maximize throughput, minimize latency and use CPU efficiently without blocking. + +**Real world examples:** + +* [Spring Reactor](http://projectreactor.io/) + +**Credits** + +* [Douglas C. Schmidt - Reactor](https://www.dre.vanderbilt.edu/~schmidt/PDF/Reactor.pdf) +* [Doug Lea - Scalable IO in Java](http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf) +* [Netty](http://netty.io/) From e6a4200607614e25aa2457589cc0f65e390c24f3 Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Tue, 15 Sep 2015 13:48:58 +0530 Subject: [PATCH 14/15] Work on #74, increased coverage --- .../java/com/iluwatar/reactor/app/App.java | 23 ++++++++---- .../com/iluwatar/reactor/app/AppTest.java | 35 +++++++++++++++++-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/App.java b/reactor/src/main/java/com/iluwatar/reactor/app/App.java index fcc327b34..975435712 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/App.java @@ -1,9 +1,12 @@ package com.iluwatar.reactor.app; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import com.iluwatar.reactor.framework.AbstractNioChannel; import com.iluwatar.reactor.framework.ChannelHandler; +import com.iluwatar.reactor.framework.Dispatcher; import com.iluwatar.reactor.framework.NioDatagramChannel; import com.iluwatar.reactor.framework.NioReactor; import com.iluwatar.reactor.framework.NioServerSocketChannel; @@ -64,6 +67,7 @@ import com.iluwatar.reactor.framework.ThreadPoolDispatcher; public class App { private NioReactor reactor; + private List channels = new ArrayList<>(); /** * App entry. @@ -71,19 +75,20 @@ public class App { * @throws IOException */ public static void main(String[] args) throws IOException { - new App().start(); + new App().start(new ThreadPoolDispatcher(2)); } /** * Starts the NIO reactor. + * @param threadPoolDispatcher * * @throws IOException if any channel fails to bind. */ - public void start() throws IOException { + public void start(Dispatcher dispatcher) throws IOException { /* * The application can customize its event dispatching mechanism. */ - reactor = new NioReactor(new ThreadPoolDispatcher(2)); + reactor = new NioReactor(dispatcher); /* * This represents application specific business logic that dispatcher will call on appropriate @@ -103,20 +108,26 @@ public class App { * Stops the NIO reactor. This is a blocking call. * * @throws InterruptedException if interrupted while stopping the reactor. + * @throws IOException if any I/O error occurs */ - public void stop() throws InterruptedException { + public void stop() throws InterruptedException, IOException { reactor.stop(); + for (AbstractNioChannel channel : channels) { + channel.getChannel().close(); + } } - private static AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { + private AbstractNioChannel tcpChannel(int port, ChannelHandler handler) throws IOException { NioServerSocketChannel channel = new NioServerSocketChannel(port, handler); channel.bind(); + channels.add(channel); return channel; } - private static AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { + private AbstractNioChannel udpChannel(int port, ChannelHandler handler) throws IOException { NioDatagramChannel channel = new NioDatagramChannel(port, handler); channel.bind(); + channels.add(channel); return channel; } } diff --git a/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java index bc51e26de..2ac9b448a 100644 --- a/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java +++ b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java @@ -4,6 +4,9 @@ import java.io.IOException; import org.junit.Test; +import com.iluwatar.reactor.framework.SameThreadDispatcher; +import com.iluwatar.reactor.framework.ThreadPoolDispatcher; + /** * * This class tests the Distributed Logging service by starting a Reactor and then sending it @@ -14,15 +17,41 @@ import org.junit.Test; public class AppTest { /** - * Test the application. + * Test the application using pooled thread dispatcher. * * @throws IOException if any I/O error occurs. * @throws InterruptedException if interrupted while stopping the application. */ @Test - public void testApp() throws IOException, InterruptedException { + public void testAppUsingThreadPoolDispatcher() throws IOException, InterruptedException { App app = new App(); - app.start(); + app.start(new ThreadPoolDispatcher(2)); + + AppClient client = new AppClient(); + client.start(); + + // allow clients to send requests. Artificial delay. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + client.stop(); + + app.stop(); + } + + /** + * Test the application using same thread dispatcher. + * + * @throws IOException if any I/O error occurs. + * @throws InterruptedException if interrupted while stopping the application. + */ + @Test + public void testAppUsingSameThreadDispatcher() throws IOException, InterruptedException { + App app = new App(); + app.start(new SameThreadDispatcher()); AppClient client = new AppClient(); client.start(); From dbc2acae5ffa2d3770c98f9161a97010379f573e Mon Sep 17 00:00:00 2001 From: Narendra Pathai Date: Wed, 16 Sep 2015 11:39:57 +0530 Subject: [PATCH 15/15] Work on #74, removed author name from all classes. [ci skip]. Author names were added due to default eclipse configuration. --- reactor/src/main/java/com/iluwatar/reactor/app/App.java | 2 -- reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java | 2 -- .../src/main/java/com/iluwatar/reactor/app/LoggingHandler.java | 2 -- .../com/iluwatar/reactor/framework/AbstractNioChannel.java | 3 --- .../java/com/iluwatar/reactor/framework/ChannelHandler.java | 2 -- .../main/java/com/iluwatar/reactor/framework/Dispatcher.java | 2 -- .../com/iluwatar/reactor/framework/NioDatagramChannel.java | 2 -- .../main/java/com/iluwatar/reactor/framework/NioReactor.java | 3 --- .../com/iluwatar/reactor/framework/NioServerSocketChannel.java | 2 -- .../com/iluwatar/reactor/framework/SameThreadDispatcher.java | 2 -- .../com/iluwatar/reactor/framework/ThreadPoolDispatcher.java | 3 --- reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java | 2 -- 12 files changed, 27 deletions(-) diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/App.java b/reactor/src/main/java/com/iluwatar/reactor/app/App.java index 975435712..5c6d91ee8 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/App.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/App.java @@ -61,8 +61,6 @@ import com.iluwatar.reactor.framework.ThreadPoolDispatcher; *

* The example uses Java NIO framework to implement the Reactor. * - * @author npathai - * */ public class App { diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java index c50e4d3e7..659f5da21 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/AppClient.java @@ -17,8 +17,6 @@ import java.util.concurrent.TimeUnit; /** * Represents the clients of Reactor pattern. Multiple clients are run concurrently and send logging * requests to Reactor. - * - * @author npathai */ public class AppClient { private final ExecutorService service = Executors.newFixedThreadPool(4); diff --git a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java index 1f2694b0b..0845303df 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/app/LoggingHandler.java @@ -10,8 +10,6 @@ import com.iluwatar.reactor.framework.NioDatagramChannel.DatagramPacket; /** * Logging server application logic. It logs the incoming requests on standard console and returns a * canned acknowledgement back to the remote peer. - * - * @author npathai */ public class LoggingHandler implements ChannelHandler { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java index 09f308731..4abacd86f 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/AbstractNioChannel.java @@ -19,9 +19,6 @@ import java.util.concurrent.ConcurrentLinkedQueue; * concrete implementation. It provides a block writing mechanism wherein when any * {@link ChannelHandler} wants to write data back, it queues the data in pending write queue and * clears it in block manner. This provides better throughput. - * - * @author npathai - * */ public abstract class AbstractNioChannel { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java index a4a392a34..381738ecd 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ChannelHandler.java @@ -9,8 +9,6 @@ import java.nio.channels.SelectionKey; *

* A {@link ChannelHandler} can be associated with one or many {@link AbstractNioChannel}s, and * whenever an event occurs on any of the associated channels, the handler is notified of the event. - * - * @author npathai */ public interface ChannelHandler { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java index 0ed53f8fc..78aeb84df 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/Dispatcher.java @@ -16,8 +16,6 @@ import java.nio.channels.SelectionKey; * * @see SameThreadDispatcher * @see ThreadPoolDispatcher - * - * @author npathai */ public interface Dispatcher { /** diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java index 089911d10..b55480ffc 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioDatagramChannel.java @@ -10,8 +10,6 @@ import java.nio.channels.SelectionKey; /** * A wrapper over {@link DatagramChannel} which can read and write data on a DatagramChannel. - * - * @author npathai */ public class NioDatagramChannel extends AbstractNioChannel { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java index 89af20630..b818612e5 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java @@ -28,9 +28,6 @@ import java.util.concurrent.TimeUnit; * NOTE: This is one of the ways to implement NIO reactor and it does not take care of all possible * edge cases which are required in a real application. This implementation is meant to demonstrate * the fundamental concepts that lie behind Reactor pattern. - * - * @author npathai - * */ public class NioReactor { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java index 17f47a394..c635a6076 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioServerSocketChannel.java @@ -11,8 +11,6 @@ import java.nio.channels.SocketChannel; /** * A wrapper over {@link NioServerSocketChannel} which can read and write data on a * {@link SocketChannel}. - * - * @author npathai */ public class NioServerSocketChannel extends AbstractNioChannel { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java index baacda9f3..ae995428e 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/SameThreadDispatcher.java @@ -11,8 +11,6 @@ import java.nio.channels.SelectionKey; * For better performance use {@link ThreadPoolDispatcher}. * * @see ThreadPoolDispatcher - * - * @author npathai */ public class SameThreadDispatcher implements Dispatcher { diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java index 9fd539adb..4a240659e 100644 --- a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java +++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java @@ -9,9 +9,6 @@ import java.util.concurrent.TimeUnit; * An implementation that uses a pool of worker threads to dispatch the events. This provides better * scalability as the application specific processing is not performed in the context of I/O * (reactor) thread. - * - * @author npathai - * */ public class ThreadPoolDispatcher implements Dispatcher { diff --git a/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java index 2ac9b448a..752192ef3 100644 --- a/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java +++ b/reactor/src/test/java/com/iluwatar/reactor/app/AppTest.java @@ -11,8 +11,6 @@ import com.iluwatar.reactor.framework.ThreadPoolDispatcher; * * This class tests the Distributed Logging service by starting a Reactor and then sending it * concurrent logging requests using multiple clients. - * - * @author npathai */ public class AppTest {