package org.threadly.litesockets; import java.io.IOException; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedSelectorException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import org.threadly.concurrent.NoThreadScheduler; import org.threadly.concurrent.SubmitterExecutor; import org.threadly.concurrent.future.ListenableFuture; import org.threadly.litesockets.utils.PortUtils; import org.threadly.util.ArgumentVerifier; import org.threadly.util.Clock; /** * <p>The NoThreadSocketExecuter is a simpler implementation of a {@link SocketExecuter} * that does not create any threads. Since there are no threads operations happen on whatever thread * calls .select(), and only 1 thread at a time should ever call it at a time. Other then * that it should be completely thread safe.</p> * * <p>This is generally the implementation used by clients. It can be used for servers * but only when not servicing many connections at once. How many connections is hardware * and OS defendant. For an average multi-core x86 linux server I a connections not to much more * then 1000 connections would be its limit, though alot depends on how active those connections are.</p> * * <p>It should also be noted that all client read/close callbacks happen on the thread that calls select().</p> * * @author lwahlmeier */ public class NoThreadSocketExecuter extends SocketExecuterCommonBase { private final NoThreadScheduler localNoThreadScheduler; private Selector commonSelector; private volatile boolean wakeUp = false; /** * Constructs a NoThreadSocketExecuter. {@link #start()} must still be called before using it. */ public NoThreadSocketExecuter() { super(new NoThreadScheduler()); localNoThreadScheduler = (NoThreadScheduler)schedulerPool; } /** * This is used to wakeup the {@link Selector} assuming it was called with a timeout on it. * Most all methods in this class that need to do a wakeup do it automatically, but * there are situations where you might want to wake up the thread we are blocked on * manually. */ public void wakeup() { if(commonSelector != null && commonSelector.isOpen()) { wakeUp = true; commonSelector.wakeup(); } } @Override public void setClientOperations(final Client client) { ArgumentVerifier.assertNotNull(client, "Client"); if(!clients.containsKey(client.getChannel())) { clients.remove(client.getChannel()); return; } if(client.isClosed()) { clients.remove(client.getChannel()); ListenableFuture<?> lf = schedulerPool.submit(new RemoveFromSelector(commonSelector, client)); lf.addListener(new Runnable() { @Override public void run() { PortUtils.closeQuietly(client.getChannel()); }}); } else if(client.getChannel().isConnectionPending()) { schedulerPool.execute(new AddToSelector(schedulerPool, client, commonSelector, SelectionKey.OP_CONNECT)); } else if(client.canWrite() && client.canRead()) { schedulerPool.execute(new AddToSelector(schedulerPool, client, commonSelector, SelectionKey.OP_WRITE|SelectionKey.OP_READ)); } else if (client.canRead()){ schedulerPool.execute(new AddToSelector(schedulerPool, client, commonSelector, SelectionKey.OP_READ)); } else if (client.canWrite()){ schedulerPool.execute(new AddToSelector(schedulerPool, client, commonSelector, SelectionKey.OP_WRITE)); } else { schedulerPool.execute(new AddToSelector(schedulerPool, client, commonSelector, 0)); } commonSelector.wakeup(); } @Override public void setUDPServerOperations(final UDPServer udpServer, final boolean enable) { if(checkServer(udpServer)) { if(enable) { if(udpServer.needsWrite()) { schedulerPool.execute(new AddToSelector(schedulerPool, udpServer, commonSelector, SelectionKey.OP_READ|SelectionKey.OP_WRITE)); } else { schedulerPool.execute(new AddToSelector(schedulerPool, udpServer, commonSelector, SelectionKey.OP_READ)); } } else { schedulerPool.execute(new AddToSelector(schedulerPool, udpServer, commonSelector, 0)); } commonSelector.wakeup(); } } @Override protected void startupService() { commonSelector = openSelector(); this.acceptSelector = commonSelector; this.readSelector = commonSelector; this.writeSelector = commonSelector; } @Override protected void shutdownService() { commonSelector.wakeup(); for(final Client client: clients.values()) { client.close(); } for(final Server server: servers.values()) { server.close(); } if(commonSelector != null && commonSelector.isOpen()) { closeSelector(schedulerPool, commonSelector); } if(localNoThreadScheduler.hasTaskReadyToRun()) { localNoThreadScheduler.tick(null); } clients.clear(); servers.clear(); } /** * This will run all ExecuterTasks, check for pending network operations, * then run those operations. There can be a lot of I/O operations so this * could take some time to run. In general it should not be called from things like * GUI threads. * */ public void select() { select(0); } /** * This is the same as the {@link #select()} but it allows you to set a delay. * This delay is the time to wait for socket operations to happen. It will * block the calling thread for up to this amount of time, but it could be less * if any network operation happens (including another thread adding a client/server). * * * @param delay Max time in milliseconds to block for. */ public void select(final int delay) { ArgumentVerifier.assertNotNegative(delay, "delay"); checkRunning(); long startTime = Clock.accurateForwardProgressingMillis(); while(Clock.accurateForwardProgressingMillis()- startTime <= delay && isRunning() && !wakeUp) { try { commonSelector.selectNow(); //We have to do this before we tick for windows localNoThreadScheduler.tick(null); commonSelector.select(Math.min(delay, 50)); if(isRunning()) { for(final SelectionKey key: commonSelector.selectedKeys()) { try { if(key.isAcceptable()) { doServerAccept(servers.get(key.channel())); } else { final Client tmpClient = clients.get(key.channel()); if(key.isConnectable() && tmpClient != null) { doClientConnect(tmpClient, commonSelector); key.cancel(); //Stupid windows bug here. setClientOperations(tmpClient); } else { if (key.isReadable()) { if(tmpClient != null){ stats.addRead(doClientRead(tmpClient, commonSelector)); } else { final Server server = servers.get(key.channel()); if(server != null && server.getServerType() == WireProtocol.UDP) { server.acceptChannel((DatagramChannel)server.getSelectableChannel()); } } } if(key.isWritable()) { if(tmpClient != null){ stats.addWrite(doClientWrite(tmpClient, commonSelector)); } else { final Server server = servers.get(key.channel()); if(server != null) { if(server instanceof UDPServer) { UDPServer us = (UDPServer) server; stats.addWrite(us.doWrite()); setUDPServerOperations(us, true); } } } } } } } catch(CancelledKeyException e) { //Key could be cancelled at any point, we dont really care about it. } } //Also for windows bug, canceled keys are not removed till we select again. //So we just have to at the end of the loop. commonSelector.selectNow(); localNoThreadScheduler.tick(null); } } catch (IOException e) { //There is really nothing to do here but try again, usually this is because of shutdown. } catch(ClosedSelectorException e) { //We do nothing here because the next loop should not happen now. } catch (NullPointerException e) { //There is a bug in some JVMs around this where the select() can throw an NPE from native code. } } wakeUp = false; } @Override public SubmitterExecutor getExecutorFor(final Object obj) { return localNoThreadScheduler; } }