
com.xqbase.tuna.ConnectorImpl Maven / Gradle / Ivy
Show all versions of tuna-core Show documentation
package com.xqbase.tuna;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedSelectorException;
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.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import com.xqbase.tuna.util.ByteArrayQueue;
abstract class Attachment {
SelectionKey selectionKey;
abstract void closeChannel();
void finishClose() {
selectionKey.cancel();
closeChannel();
}
}
/**
* The encapsulation of a {@link SocketChannel} and its {@link SelectionKey},
* which corresponds to a TCP Socket.
*/
class Client extends Attachment {
private static final int STATUS_CLOSED = 0;
private static final int STATUS_IDLE = 1;
private static final int STATUS_BUSY = 2;
private static final int STATUS_DISCONNECTING = 3;
int bufferSize = Connection.MAX_BUFFER_SIZE;
int status = STATUS_IDLE;
boolean resolving = false;
ByteArrayQueue queue = new ByteArrayQueue();
Connection connection;
SocketChannel socketChannel;
Client(Connection connection) {
this.connection = connection;
connection.setHandler(new ConnectionHandler() {
@Override
public void send(byte[] b, int off, int len) {
write(b, off, len);
}
@Override
public void setBufferSize(int bufferSize) {
boolean blocked = Client.this.bufferSize == 0;
boolean toBlock = bufferSize <= 0;
Client.this.bufferSize = Math.max(0,
Math.min(bufferSize, Connection.MAX_BUFFER_SIZE));
if ((blocked ^ toBlock) && !resolving && isOpen() &&
(selectionKey.interestOps() & SelectionKey.OP_CONNECT) == 0) {
// may be called before resolve
interestOps();
}
}
@Override
public void disconnect() {
if (status == STATUS_IDLE) {
// must be resolved
finishClose();
} else if (status == STATUS_BUSY) {
status = STATUS_DISCONNECTING;
}
}
@Override
public void disconnectNow() {
if (isOpen()) {
finishClose();
}
}
});
}
void add(Selector selector, int ops) {
try {
selectionKey = socketChannel.register(selector, ops, this);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* registers a {@link Client} and connects to a remote address
*
* @see ConnectorImpl#connect(Connection, String, int)
* @throws IOException may throw "Network is unreachable" or "Protocol family unavailable",
* and then socketChannel will be closed, and selectionKey will not be created
*/
void connect(Selector selector, InetSocketAddress socketAddress) throws IOException {
resolving = false;
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.socket().setTcpNoDelay(true);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
socketChannel.connect(socketAddress);
add(selector, SelectionKey.OP_CONNECT);
} catch (IOException e) {
// May throw "Network is unreachable" or "Protocol family unavailable",
// and then socketChannel will be closed, and selectionKey will not be created
closeChannel();
throw e;
}
}
void interestOps() {
selectionKey.interestOps((bufferSize == 0 ? 0 : SelectionKey.OP_READ) |
(status == STATUS_IDLE ? 0 : SelectionKey.OP_WRITE));
}
void write() throws IOException {
int len = queue.length();
int fromLen = len;
while (len > 0) {
int bytesWritten = socketChannel.write(ByteBuffer.wrap(queue.array(),
queue.offset(), len));
if (bytesWritten == 0) {
unblock(len, fromLen);
return;
}
len = queue.remove(bytesWritten).length();
}
unblock(len, fromLen);
if (status == STATUS_DISCONNECTING) {
finishClose();
} else {
status = STATUS_IDLE;
interestOps();
}
}
void write(byte[] b, int off, int len) {
if (status != STATUS_IDLE) {
int fromLen = queue.length();
block(queue.add(b, off, len).length(), fromLen);
return;
}
int bytesWritten;
try {
bytesWritten = socketChannel.write(ByteBuffer.wrap(b, off, len));
} catch (IOException e) {
startClose();
return;
}
if (len > bytesWritten) {
int fromLen = queue.length();
block(queue.add(b, off + bytesWritten, len - bytesWritten).length(), fromLen);
status = STATUS_BUSY;
interestOps();
}
}
void startConnect() {
status = STATUS_BUSY;
}
void finishConnect() {
Socket socket = socketChannel.socket();
InetSocketAddress local = (InetSocketAddress) socket.getLocalSocketAddress();
InetSocketAddress remote = (InetSocketAddress) socket.getRemoteSocketAddress();
connection.onConnect(new ConnectionSession(local, remote));
}
void startClose() {
if (isOpen()) {
finishClose();
// Call "close()" before "onDisconnect()"
// to avoid recursive "disconnect()".
connection.onDisconnect();
}
}
@Override
void closeChannel() {
try {
socketChannel.close();
} catch (IOException e) {/**/}
status = STATUS_CLOSED;
}
boolean isOpen() {
return status != STATUS_CLOSED;
}
private boolean blocking = false;
private void block(int len, int fromLen) {
if (fromLen > 0) {
blocking = true;
connection.onQueue(len);
}
}
private void unblock(int len, int fromLen) {
if (len == fromLen) {
return;
}
boolean fromBlocking = blocking;
if (len == 0) {
blocking = false;
}
if (fromBlocking) {
connection.onQueue(len);
}
}
}
/**
* The encapsulation of a {@link ServerSocketChannel} and its {@link SelectionKey},
* which corresponds to a TCP Server Socket
*/
class Server extends Attachment {
ServerConnection serverConnection;
ServerSocketChannel serverSocketChannel;
/**
* Opens a listening port and binds to a given address.
* @param addr - The IP address to bind and the port to listen.
* @throws IOException If an I/O error occurs when opening the port.
*/
Server(ServerConnection serverConnection,
InetSocketAddress addr) throws IOException {
this.serverConnection = serverConnection;
bind(addr);
}
void bind(InetSocketAddress addr) throws IOException {
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
serverSocketChannel.socket().bind(addr);
} catch (IOException e) {
closeChannel();
throw e;
}
}
@Override
void closeChannel() {
try {
serverSocketChannel.close();
} catch (IOException e) {/**/}
}
}
class Timer implements Comparable {
long uptime;
long id;
@Override
public int compareTo(Timer o) {
int result = Long.compare(uptime, o.uptime);
return result == 0 ? Long.compare(id, o.id) : result;
}
@Override
public String toString() {
return new Date(uptime) + "/" + id;
}
}
class Registrable {
private SelectableChannel channel;
private int interestOps;
private Attachment att;
/** @throws IOException if ServerSocket fails to Bind again */
Registrable(SelectionKey key) throws IOException {
channel = key.channel();
interestOps = key.interestOps();
att = (Attachment) key.attachment();
key.cancel(); // Need to cancel ?
if (!(att instanceof Server)) {
return;
}
// Rebuild ServerSocketChannel
Server server = (Server) att;
InetSocketAddress addr = (InetSocketAddress) server.serverSocketChannel.
socket().getLocalSocketAddress();
server.closeChannel();
server.bind(addr);
channel = server.serverSocketChannel;
}
void register(Selector selector) throws IOException {
if ((interestOps & SelectionKey.OP_CONNECT) != 0) {
((Client) att).startClose();
// Log.w("Removed a Connection-Pending Channel, interestOps = " + interestOps);
return;
}
// Client may be closed by closing of Connection-Pending Channel
if (att instanceof Client && !((Client) att).isOpen()) {
return;
}
att.selectionKey = channel.register(selector, interestOps, att);
}
}
/**
* The encapsulation of {@link Selector},
* which makes {@link Client} and {@link Server} working.
*/
public class ConnectorImpl implements Connector, TimerHandler, EventQueue, Executor, AutoCloseable {
private static Pattern hostName = Pattern.compile("[a-zA-Z]");
private static long nextId = 0;
private volatile Selector selector;
private boolean interrupted = false;
private byte[] buffer = new byte[Connection.MAX_BUFFER_SIZE];
private Map timerMap = new TreeMap<>();
private Queue eventQueue = new ConcurrentLinkedQueue<>();
private ExecutorService executor = Executors.newCachedThreadPool();
{
try {
selector = Selector.open();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** @throws IOException if no IP address for the host
could be found */
@Override
public void connect(Connection connection,
InetSocketAddress socketAddress) throws IOException {
Client client = new Client(connection);
client.startConnect();
if (!socketAddress.isUnresolved()) {
client.connect(selector, socketAddress);
return;
}
String host = socketAddress.getHostName();
int port = socketAddress.getPort();
if (host.indexOf(':') >= 0 || !hostName.matcher(host).find()) {
// Connect immediately for IPv6 or IPv4 Address
client.connect(selector,
new InetSocketAddress(InetAddress.getByName(host), port));
return;
}
client.resolving = true;
execute(() -> {
try {
// Resolve in Executor then Connect later
InetAddress addr = InetAddress.getByName(host);
invokeLater(() -> {
try {
client.connect(selector, new InetSocketAddress(addr, port));
} catch (IOException e) {
// Call "onDisconnect()" when Connecting Failure
connection.onDisconnect();
}
});
} catch (IOException e) {
// Call "onDisconnect()" when Resolving Failure
invokeLater(connection::onDisconnect);
}
});
}
@Override
public Connector.Closeable add(ServerConnection serverConnection,
InetSocketAddress socketAddress) throws IOException {
Server server = new Server(serverConnection, socketAddress);
try {
server.selectionKey = server.serverSocketChannel.
register(selector, SelectionKey.OP_ACCEPT, server);
} catch (IOException e) {
throw new RuntimeException(e);
}
return () -> {
if (server.selectionKey.isValid()) {
server.finishClose();
}
};
}
/** Consume events until interrupted */
public void doEvents() {
while (!isInterrupted()) {
Iterator> it = timerMap.entrySet().iterator();
if (it.hasNext()) {
long timeout = it.next().getKey().uptime - System.currentTimeMillis();
doEvents(timeout > 0 ? timeout : 0);
} else {
doEvents(-1);
}
}
}
private void invokeQueue() {
long now = System.currentTimeMillis();
List runnables = new ArrayList<>();
Iterator> it = timerMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
if (entry.getKey().uptime > now) {
break;
}
runnables.add(entry.getValue());
it.remove();
}
// call run() after iteration since run() may change timerMap
for (Runnable runnable : runnables) {
runnable.run();
}
Runnable runnable;
while ((runnable = eventQueue.poll()) != null) {
runnable.run();
}
}
private static boolean shortTime(long millis) {
return millis >= 0 && millis < 16;
}
private int epollCount = 0;
private void checkEpoll(long timeout, long t, int keySize) {
if (keySize > 0 || shortTime(timeout) ||
!shortTime(System.currentTimeMillis() - t) ||
!eventQueue.isEmpty() ||
Thread.currentThread().isInterrupted()) {
epollCount = 0;
return;
}
Set keys = selector.keys();
// "select()" may exit immediately due to a Broken Connection ?
/* for (SelectionKey key : keys) {
Object att = key.attachment();
if (!(att instanceof Client)) {
continue;
}
Client client = (Client) att;
if (!client.socketChannel.isConnected() &&
!client.socketChannel.isConnectionPending()) {
Log.w("Abort Registering New Selector, timeout = " + timeout +
", t0 = " + Time.toString(t, true) + ", t1 = " +
Time.toString(System.currentTimeMillis(), true));
client.startClose();
epollCount = 0;
return;
}
} */
// E-Poll Spin Detected
epollCount ++;
if (epollCount < 256) {
return;
}
epollCount = 0;
/* Log.w("Epoll Spin Detected, timeout = " + timeout +
", t0 = " + Time.toString(t, true) + ", t1 = " +
Time.toString(System.currentTimeMillis(), true) +
", keys = " + keys.size()); */
// Log.w("Begin Registering New Selector " + selector + " ...");
List regs = new ArrayList<>();
for (SelectionKey key : keys) {
if (!key.isValid()) {
// Log.w("Invaid SelectionKey Detected");
continue;
}
try {
regs.add(new Registrable(key));
} catch (IOException e) {
// Ignored if ServerSocket fails to Bind again
}
}
try {
selector.close();
} catch (IOException e) {/**/}
try {
selector = Selector.open();
for (Registrable reg : regs) {
reg.register(selector);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// Log.w("End Registering New Selector " + selector);
}
/**
* Consumes all events raised by registered Clients and Servers,
* including network events (accept/connect/read/write) and user-defined events.
*
* @param timeout Block for up to timeout milliseconds, or -1 to block indefinitely,
* or 0 without blocking.
* @return true if NETWORK events consumed;
* false if no NETWORK events raised,
* whether or not user-defined events raised.
*/
public boolean doEvents(long timeout) {
long t = System.currentTimeMillis();
int keySize;
try {
keySize = timeout == 0 ? selector.selectNow() :
timeout < 0 ? selector.select() : selector.select(timeout);
} catch (IOException e) {
throw new RuntimeException(e);
}
checkEpoll(timeout, t, keySize);
if (keySize == 0) {
invokeQueue();
return false;
}
Set selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
Server server = (Server) key.attachment();
SocketChannel socketChannel;
try {
socketChannel = server.serverSocketChannel.accept();
if (socketChannel == null) {
continue;
}
socketChannel.configureBlocking(false);
socketChannel.socket().setTcpNoDelay(true);
} catch (IOException e) {
throw new RuntimeException(e);
}
Client client = new Client(server.serverConnection.get());
client.socketChannel = socketChannel;
client.add(selector, SelectionKey.OP_READ);
client.finishConnect();
continue;
}
Client client = (Client) key.attachment();
try {
if (key.isReadable()) {
int bytesRead = client.socketChannel.
read(ByteBuffer.wrap(buffer, 0, client.bufferSize));
if (bytesRead > 0) {
client.connection.onRecv(buffer, 0, bytesRead);
// may be closed by "onRecv"
if (!key.isValid()) {
continue;
}
} else if (bytesRead < 0) {
client.startClose();
// Disconnected, so skip onQueue and onConnect
continue;
}
}
if (key.isWritable()) {
client.write();
} else if (key.isConnectable() && client.socketChannel.finishConnect()) {
client.finishConnect();
// "onConnect()" might call "disconnect()"
if (client.isOpen()) {
client.write();
}
}
} catch (IOException e) {
client.startClose();
}
}
selectedKeys.clear();
invokeQueue();
return true;
}
@Override
public TimerHandler.Closeable postAtTime(Runnable runnable, long uptime) {
Timer timer = new Timer();
timer.uptime = uptime;
timer.id = nextId;
nextId ++;
timerMap.put(timer, runnable);
return () -> timerMap.remove(timer);
}
@Override
public void invokeLater(Runnable runnable) {
eventQueue.offer(runnable);
try {
selector.wakeup();
} catch (ClosedSelectorException e) {/**/}
}
@Override
public void execute(Runnable runnable) {
executor.execute(runnable);
}
/**
* Interrupts {@link #doEvents()} or {@link #doEvents(long)}.
* Can be called outside main thread.
*/
public void interrupt() {
invokeLater(() -> interrupted = true);
}
/**
* Tests whether the connector is interrupted,
* and then the "interrupted" status is cleared.
*/
public boolean isInterrupted() {
boolean interrupted_ = interrupted;
interrupted = false;
return interrupted_;
}
/**
* Unregisters, closes all Clients and Servers,
* then closes the Connector itself.
*/
@Override
public void close() {
executor.shutdown();
for (SelectionKey key : selector.keys()) {
((Attachment) key.attachment()).finishClose();
}
try {
selector.close();
} catch (IOException e) {/**/}
}
}