package org.threadly.litesockets;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.util.Arrays;
import java.util.concurrent.ScheduledExecutorService;
import org.threadly.concurrent.ConfigurableThreadFactory;
import org.threadly.concurrent.SingleThreadScheduler;
import org.threadly.concurrent.SubmitterExecutor;
import org.threadly.concurrent.SubmitterScheduler;
import org.threadly.concurrent.future.FutureUtils;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.concurrent.wrapper.KeyDistributedExecutor;
import org.threadly.concurrent.wrapper.compatibility.ScheduledExecutorServiceWrapper;
import org.threadly.litesockets.utils.PortUtils;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.ExceptionUtils;
/**
* <p>This is a mutliThreaded implementation of a {@link SocketExecuter}. It uses separate threads to perform Accepts, Reads and Writes.
* Constructing this will create 3 additional threads. Generally only one of these will ever be needed in a process.</p>
*
* <p>This is generally the {@link SocketExecuter} implementation you want to use for servers, especially if they have to deal with more
* then just a few connections. See {@link NoThreadSocketExecuter} for a more efficient implementation when not dealing with many connections.</p>
*
* @author lwahlmeier
*
*/
public class ThreadedSocketExecuter extends SocketExecuterCommonBase {
private final KeyDistributedExecutor clientDistributer;
private final SingleThreadScheduler localAcceptScheduler;
private final SingleThreadScheduler localReadScheduler;
private final SingleThreadScheduler localWriteScheduler;
private volatile long readThreadID;
private final AcceptRunner acceptor = new AcceptRunner();
private final ReadRunner reader = new ReadRunner();
private final WriteRunner writer = new WriteRunner();
/**
* <p>This constructor creates its own {@link SingleThreadScheduler} Threadpool to use for client operations. This is generally
* not recommended unless you are not doing many socket connections/operations. You should really use your own multiThreaded
* thread pool.</p>
*/
public ThreadedSocketExecuter() {
this(new SingleThreadScheduler(
new ConfigurableThreadFactory(
"SocketClientThread", false, true, Thread.currentThread().getPriority(), null, null)));
}
/**
* <p>This is provided to allow people to use java's generic threadpool scheduler {@link ScheduledExecutorService}.</p>
*
* @param exec The {@link ScheduledExecutorService} to be used for client/server callbacks.
*/
public ThreadedSocketExecuter(final ScheduledExecutorService exec) {
this(new ScheduledExecutorServiceWrapper(exec));
}
/**
* <p>Here you can provide a {@link ScheduledExecutorService} for this {@link SocketExecuter}. This will be used
* on accept, read, and close callback events.</p>
*
* @param exec the {@link ScheduledExecutorService} to be used for client/server callbacks.
*/
public ThreadedSocketExecuter(final SubmitterScheduler exec) {
super(new SingleThreadScheduler(new ConfigurableThreadFactory("SocketAccept", false, true, Thread.currentThread().getPriority(), null, null)),
new SingleThreadScheduler(new ConfigurableThreadFactory("SocketReader", false, true, Thread.currentThread().getPriority(), null, null)),
new SingleThreadScheduler(new ConfigurableThreadFactory("SocketWriter", false, true, Thread.currentThread().getPriority(), null, null)),
exec);
localAcceptScheduler = (SingleThreadScheduler) this.acceptScheduler;
localReadScheduler = (SingleThreadScheduler) this.readScheduler;
localWriteScheduler = (SingleThreadScheduler) this.writeScheduler;
clientDistributer = new KeyDistributedExecutor(schedulerPool);
}
@Override
protected void startupService() {
super.startIfNotStarted();
acceptSelector = openSelector();
readSelector = openSelector();
writeSelector = openSelector();
acceptScheduler.execute(acceptor);
readScheduler.execute(reader);
writeScheduler.execute(writer);
}
@Override
protected void shutdownService() {
for(final Client client: clients.values()) {
client.close();
}
for(final Server server: servers.values()) {
server.close();
}
closeSelector(localAcceptScheduler, acceptSelector);
closeSelector(localReadScheduler, readSelector);
closeSelector(localWriteScheduler, writeSelector);
}
@Override
public void setClientOperations(final Client client) {
ArgumentVerifier.assertNotNull(client, "Client");
if(!clients.containsKey(client.getChannel())) {
clients.remove(client.getChannel());
return;
}
synchronized(client) {
if(client.isClosed()) {
clients.remove(client.getChannel());
ListenableFuture<?> lf = readScheduler.submit(new RemoveFromSelector(readSelector, client));
ListenableFuture<?> lf2 = writeScheduler.submit(new RemoveFromSelector(writeSelector, client));
FutureUtils.makeCompleteFuture(Arrays.asList(lf, lf2)).addListener(new Runnable() {
@Override
public void run() {
PortUtils.closeQuietly(client.getChannel());
}});
} else if(!client.getChannel().isConnected() && client.getChannel().isConnectionPending()) {
readScheduler.execute(new AddToSelector(readScheduler, client, readSelector, SelectionKey.OP_CONNECT));
writeScheduler.execute(new AddToSelector(writeScheduler, client, writeSelector, 0));
} else if(client.canWrite() && client.canRead()) {
writeScheduler.execute(new AddToSelector(writeScheduler, client, writeSelector, SelectionKey.OP_WRITE));
readScheduler.execute(new AddToSelector(readScheduler, client, readSelector, SelectionKey.OP_READ));
} else if (client.canRead()){
readScheduler.execute(new AddToSelector(readScheduler, client, readSelector, SelectionKey.OP_READ));
writeScheduler.execute(new AddToSelector(writeScheduler, client, writeSelector, 0));
} else if (client.canWrite()){
writeScheduler.execute(new AddToSelector(writeScheduler, client, writeSelector, SelectionKey.OP_WRITE));
readScheduler.execute(new AddToSelector(readScheduler, client, readSelector, 0));
} else {
writeScheduler.execute(new AddToSelector(writeScheduler, client, writeSelector, 0));
readScheduler.execute(new AddToSelector(readScheduler, client, readSelector, 0));
}
}
readSelector.wakeup();
writeSelector.wakeup();
}
@Override
public void setUDPServerOperations(final UDPServer udpServer, final boolean enable) {
if(checkServer(udpServer)) {
if(enable) {
readScheduler.execute(new AddToSelector(readScheduler, udpServer, readSelector, SelectionKey.OP_READ));
readSelector.wakeup();
if(udpServer.needsWrite()) {
writeScheduler.execute(new AddToSelector(writeScheduler, udpServer, writeSelector, SelectionKey.OP_WRITE));
writeSelector.wakeup();
} else {
writeScheduler.execute(new AddToSelector(writeScheduler, udpServer, writeSelector, 0));
writeSelector.wakeup();
}
} else {
readScheduler.execute(new AddToSelector(readScheduler, udpServer, readSelector, 0));
writeScheduler.execute(new AddToSelector(writeScheduler, udpServer, writeSelector, 0));
readSelector.wakeup();
writeSelector.wakeup();
}
}
}
/**
* Runnable for the Acceptor thread. This runs the acceptSelector on the AcceptorThread.
*/
private class AcceptRunner implements Runnable {
@Override
public void run() {
if(isRunning()) {
try {
acceptSelector.selectedKeys().clear();
acceptSelector.select();
} catch (Exception e) {
stopIfRunning();
return;
}
if(isRunning()) {
for(final SelectionKey sk: acceptSelector.selectedKeys()) {
if(sk.isAcceptable()) {
final ServerSocketChannel server = (ServerSocketChannel) sk.channel();
doServerAccept(servers.get(server));
}
}
}
if(isRunning()) {
acceptScheduler.execute(this);
}
}
}
}
/**
* Runnable for the Read thread. This runs the readSelector on the ReadThread.
*/
private class ReadRunner implements Runnable {
@Override
public void run() {
if(isRunning()) {
if(readThreadID == 0) {
readThreadID = Thread.currentThread().getId();
}
try {
readSelector.selectedKeys().clear();
readSelector.select();
} catch (Exception e) {
stopIfRunning();
return;
}
if(isRunning() && ! readSelector.selectedKeys().isEmpty()) {
for(final SelectionKey sk: readSelector.selectedKeys()) {
final Client client = clients.get(sk.channel());
if(client != null) {
try {
if(sk.isConnectable()) {
doClientConnect(client, readSelector);
sk.cancel();
setClientOperations(client);
} else {
stats.addRead(doClientRead(client, readSelector));
}
} catch(CancelledKeyException e) {
client.close();
ExceptionUtils.handleException(e);
}
} else {
final Server server = servers.get(sk.channel());
if(server != null) {
if(sk.isReadable()) {
final DatagramChannel dgc = (DatagramChannel) sk.channel();
server.acceptChannel(dgc);
}
}
}
}
}
if(isRunning()) {
readScheduler.execute(this);
}
}
}
}
/**
* Runnable for the Write thread. This runs the writeSelector on the WriteThread.
*/
private class WriteRunner implements Runnable {
@Override
public void run() {
if(isRunning()) {
try {
writeSelector.selectedKeys().clear();
writeSelector.select();
} catch (Exception e) {
stopIfRunning();
return;
}
if(isRunning() && ! writeSelector.selectedKeys().isEmpty()) {
for(final SelectionKey sk: writeSelector.selectedKeys()) {
final Client client = clients.get(sk.channel());
if(client != null) {
stats.addWrite(doClientWrite(client, writeSelector));
} else {
final Server server = servers.get(sk.channel());
if(server != null) {
if(server instanceof UDPServer) {
UDPServer us = (UDPServer) server;
us.doWrite();
setUDPServerOperations(us, true);
}
}
}
}
}
if(isRunning()) {
writeScheduler.execute(this);
}
}
}
}
@Override
public SubmitterExecutor getExecutorFor(final Object obj) {
return clientDistributer.getExecutorForKey(obj);
}
}