/*******************************************************************************
* gMix open source project - https://svs.informatik.uni-hamburg.de/gmix/
* Copyright (C) 2014 SVS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package userGeneratedContent.testbedPlugIns.layerPlugIns.layer1network.cascade_TCP_v0_001;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.Iterator;
import java.util.LinkedList;
import java.util.Vector;
import staticContent.framework.controller.SubImplementation;
import staticContent.framework.message.MixMessage;
import staticContent.framework.message.Reply;
import staticContent.framework.message.Request;
import staticContent.framework.userDatabase.DatabaseEventListener;
import staticContent.framework.userDatabase.User;
import staticContent.framework.userDatabase.UserAttachment;
import staticContent.framework.util.Util;
public class ClientHandler_TCP_FCFS_async_nio extends SubImplementation implements DatabaseEventListener {
//TODO: add timeout for inactive users
private int port;
private InetAddress bindAddress;
private int backlog;
private int soTimeout;
private int maxConnections;
private Integer numberOfActiveConnections = 0;
private Selector selector = null;
private ServerSocketChannel serverSocketChannel;
//private int queueBlockSize;
private NIOLoop nioLoop;
private ReplyThread replyThread;
private Object sync = new Object();
private int maxRequestLength;
private int maxMessages;
private int messageCounterReplies = 0;
private volatile boolean replyThreadSleeping = false;
// if data sent by a client is available, but the internal message queue is
// full, the data won't be read until space becomes available in the queue.
// in that case, the SelectionKey indicating that data is available will be
// queued in this list and removed from the selector. the reason for
// removing them is to prevent unnecessary iteration of the nio-loop, when
// only readRequests are available, but the queue is full
private LinkedList<SelectionKey> delayedReadRequestEvents = new LinkedList<SelectionKey>();
private LinkedList<Request> delayedRequests = new LinkedList<Request>();
private LinkedList<UserChannelData> usersWithRepliesReady = new LinkedList<UserChannelData>();
@Override
public void constructor() {
this.bindAddress = settings.getPropertyAsInetAddress("GLOBAL_MIX_BIND_ADDRESS");
this.port = settings.getPropertyAsInt("GLOBAL_MIX_BIND_PORT");
this.backlog = settings.getPropertyAsInt("BACKLOG");
this.soTimeout = settings.getPropertyAsInt("SO_TIMEOUT");
this.maxConnections = settings.getPropertyAsInt("MAX_CONNECTIONS");
this.maxRequestLength = settings.getPropertyAsInt("MAX_REQUEST_LENGTH");
this.maxMessages = settings.getPropertyAsInt("MAX_MESSAGES_IN_IOH");
//this.queueBlockSize = settings.getPropertyAsInt("QUEUE_BLOCK_SIZE");
anonNode.getUserDatabase().registerEventListener(this);
//InfoServiceServer.mixAddresses[mix.getIdentifier()] = bindAddress;
//InfoServiceServer.mixPorts[mix.getIdentifier()] = port;
this.nioLoop = new NIOLoop();
if (anonNode.IS_DUPLEX)
this.replyThread = new ReplyThread();
}
@Override
public void initialize() {
// TODO Auto-generated method stub
}
@Override
public void begin() {
this.nioLoop.start();
if (anonNode.IS_DUPLEX)
this.replyThread.start();
}
private class NIOLoop extends Thread {
@Override
public void run() {
openServerSocket();
SelectionKey key = null;
while (true) { // handle read, write and accept events
try {
registerWriteRequests();
int remainingCapacity = anonNode.getRequestInputQueue().remainingCapacity();
boolean requestsDelayed = delayedRequests.size() > 0;
boolean requestQueueFull = remainingCapacity == 0;
// try to hand over as many delayed requests to the recodingScheme as possible
if (requestsDelayed &! requestQueueFull) {
Iterator<Request> requests = delayedRequests.iterator();
while (requests.hasNext()) {
Request request = requests.next();
requests.remove();
anonNode.putInRequestInputQueue(request);
remainingCapacity--;
if (remainingCapacity == 0)
break;
}
}
System.out.println("###!!## c");
boolean readRequestEventsDelayed = delayedReadRequestEvents.size() > 0;
requestQueueFull = remainingCapacity == 0;
// handle delayed read request events (read requests are delayed without reading the
// available data when the request queue is full. this prevents the clients from
// sending even more data (if the data isen't read from the buffer, clients can't
// send more data due to the tc-protocol characteristics))
if (readRequestEventsDelayed &! requestQueueFull) {
Iterator<SelectionKey> selectedKeys = delayedReadRequestEvents.iterator();
while (selectedKeys.hasNext()) {
key = selectedKeys.next();
selectedKeys.remove();
if (!key.isValid())
continue;
assert key.isReadable();
assert key.attachment() != null;
assert key.attachment() instanceof UserChannelData;
UserChannelData userdata = (UserChannelData)key.attachment();
if (!userdata.valid)
continue;
Vector<Request> requests = tryToReadRequests(userdata);
int count = requests.size();
for (int i=0; i<count; i++)
if (i<remainingCapacity) {
anonNode.putInRequestInputQueue(requests.remove(0));
remainingCapacity--;
} else {
Request r = requests.remove(0);
delayedRequests.add(r);
UserChannelData userData = r.getOwner().getAttachment(getThis(), UserChannelData.class);
if (!userData.valid)
continue;
SelectionKey selectionkey = userData.socketChannel.keyFor(selector);
selectionkey.interestOps(SelectionKey.OP_READ);
}
if (remainingCapacity == 0)
break;
}
}
// wait for event(s)
selector.select();
// handle new events
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
boolean wakeUpReplyThread = false;
while (selectedKeys.hasNext()) {
key = selectedKeys.next();
selectedKeys.remove();
if (!key.isValid())
continue;
if (key.isAcceptable()) {
handleAcceptRequest();
} else if (key.isReadable()) {
if (remainingCapacity == 0) { // don't read new data when queue is full. otherwise clients will send even more data
delayedReadRequestEvents.add(key);
} else { // read as much data ass possible; queue rest
UserChannelData userdata = (UserChannelData)key.attachment();
if (!userdata.valid)
continue;
Vector<Request> requests = tryToReadRequests(userdata);
int count = requests.size();
for (int i=0; i<count; i++) {
if (i<remainingCapacity) {
anonNode.putInRequestInputQueue(requests.remove(0));
remainingCapacity--;
} else {
delayedRequests.add(requests.remove(0));
}
}
}
} else if (key.isWritable()) {
synchronized (sync) {
UserChannelData userdata = (UserChannelData)key.attachment();
if (!userdata.valid)
continue;
int repliesWritten = tryToSendReplies(userdata);
messageCounterReplies -= repliesWritten;
if (repliesWritten > 0)
wakeUpReplyThread = true;
}
}
} // end while (handle new events)
if (wakeUpReplyThread) {
synchronized (sync) {
if (replyThreadSleeping)
sync.notifyAll();
}
}
} catch (IOException e) {
e.printStackTrace();
if (key != null && key.attachment() instanceof UserChannelData) {
removeUser((UserChannelData)key.attachment());
key.cancel();
}
synchronized (sync) {
if (replyThreadSleeping)
sync.notifyAll();
}
continue;
}
} // end while
} // end run
} // end NIOLoop
private class ReplyThread extends Thread {
@Override
public void run() {
while (true) { // wait for data from messageProcessor, store it in UserChannelData, add reference on UserChannelData in usersWithRepliesReady and wake up selector (which will send the data later)
Reply[] reply = anonNode.getFromReplyOutputQueue(); // blocking method
for (int i=0; i<reply.length; i++) {
// block if message limit reached
synchronized (sync) {
while (messageCounterReplies >= maxMessages) {
replyThreadSleeping = true;
try {
sync.wait();
} catch (InterruptedException e) {
e.printStackTrace();
continue;
}
replyThreadSleeping = false;
}
UserChannelData userData = reply[i].getOwner().getAttachment(getThis(), UserChannelData.class);
if (!userData.valid)
continue;
messageCounterReplies++;
userData.dataToSend.add(ByteBuffer.wrap(Util.concatArrays(Util.intToByteArray(reply[i].getByteMessage().length), reply[i].getByteMessage())));
usersWithRepliesReady.add(userData);
}
// wake up selector so it can send the data
selector.wakeup();
}
}
}
} // end ReplyThread
private class UserChannelData extends UserAttachment {
ByteBuffer receivedData;
Vector<ByteBuffer> dataToSend = new Vector<ByteBuffer>();
SocketChannel socketChannel;
boolean requestLengthHeaderRead;
boolean valid = true;
public UserChannelData(User owner, Object callingInstance) {
super(owner, callingInstance);
}
public void clear() {
synchronized (sync) {
messageCounterReplies -= dataToSend.size();
}
receivedData.clear();
dataToSend.clear();
try {socketChannel.close();} catch (IOException e) {}
socketChannel = null;
valid = false;
}
} // end UserChannelData
private void removeUser(UserChannelData userData) {
userData.clear();
synchronized (sync) {
while(usersWithRepliesReady.remove(userData));
}
numberOfActiveConnections--;
userDatabase.removeUser(userData.getOwner(), this);
selector.wakeup();
}
private void openServerSocket() {
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress endpoint = new InetSocketAddress(bindAddress, port);
serverSocket.bind(endpoint, backlog);
serverSocket.setSoTimeout(soTimeout);
// generate selector
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println(anonNode +" listening on " +bindAddress +":" +port);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("could not open ServerSocket");
}
}
/**
* Registers all write requests from <code>writeRequests</code> with
* <code>selector</code>.
*
* @see #writeRequests
* @see #selector
*/
private void registerWriteRequests() {
synchronized(sync) {
Iterator<UserChannelData> users = usersWithRepliesReady.iterator();
while (users.hasNext()) {
UserChannelData userData = users.next();
if (!userData.valid)
continue;
SelectionKey selectionkey = userData.socketChannel.keyFor(selector);
selectionkey.interestOps(SelectionKey.OP_WRITE);
}
// delete old writeRequests
usersWithRepliesReady.clear();
}
}
/**
* Handles an accept request. Accepts connections until the maximum number
* of connections is reached (see <code>numberOfActiveConnections</code>,
* <code>maxConnections</code>). Generates <code>User</code> objects and
* adds them to the <code>UserDatabase</code> (if connection accepted).
*
* @throws IOException If an I/O error occurres.
*/
private void handleAcceptRequest() throws IOException {
if (numberOfActiveConnections < maxConnections) {
SocketChannel clientSocketChannel = serverSocketChannel.accept();
clientSocketChannel.configureBlocking(false);
numberOfActiveConnections++;
User user = userDatabase.generateUser();
userDatabase.addUser(user, this);
UserChannelData userData = new UserChannelData(user, this);
userData.socketChannel = clientSocketChannel;
clientSocketChannel.register(selector, SelectionKey.OP_READ, userData);
} else {
System.out.println(anonNode +" connection refused. too many connections ("+numberOfActiveConnections+")");
}
}
private Vector<Request> tryToReadRequests(UserChannelData userData) throws IOException {
Vector<Request> requests = new Vector<Request>();
Request next = tryToReadRequest(userData);
while (next != null) {
requests.add(next);
next = tryToReadRequest(userData);
}
return requests;
}
private Request tryToReadRequest(UserChannelData userData) throws IOException {
if (userData.receivedData == null)
userData.receivedData = ByteBuffer.allocate(4);
// try to read header (containing the length of the message)
if (!userData.requestLengthHeaderRead) {
if (userData.receivedData.position() == 0)
userData.receivedData.limit(4);
if (userData.socketChannel.read(userData.receivedData) == -1) // read data
throw new IOException("warning: lost connection to user " +userData.getOwner());
if (userData.receivedData.hasRemaining()) {
return null;
} else {
userData.receivedData.flip();
byte[] lengthHeader = new byte[4];
userData.receivedData.get(lengthHeader);
int messageLength = Util.byteArrayToInt(lengthHeader);
if (messageLength > maxRequestLength)
throw new IOException("warning: user " +userData.getOwner() +" sent a too large message");
userData.receivedData.clear();
userData.receivedData = ByteBuffer.allocate(messageLength);
userData.requestLengthHeaderRead = true;
}
}
// try to read the message itself
assert userData.requestLengthHeaderRead == true;
if (userData.socketChannel.read(userData.receivedData) == -1) // read data
throw new IOException("warning: lost connection to user " +userData.getOwner());
if (userData.receivedData.hasRemaining()) {
return null;
} else {
userData.receivedData.flip();
byte[] message = userData.receivedData.array();
userData.receivedData.clear();
userData.receivedData = null;
userData.requestLengthHeaderRead = false;
return MixMessage.getInstanceRequest(message, userData.getOwner());
}
}
private int tryToSendReplies(UserChannelData userData) throws IOException {
int repliesSent = 0;
while(tryToSendReply(userData))
repliesSent++;
return repliesSent;
}
private boolean tryToSendReply(UserChannelData userData) throws IOException {
ByteBuffer messageToSend = userData.dataToSend.get(0);
int written = userData.socketChannel.write(messageToSend);
assert written != 0;
if (messageToSend.hasRemaining()) {
return false;
} else {
userData.dataToSend.remove(0);
return true;
}
}
@Override
public void userAdded(User user) {
// no need to do anything
}
@Override
public void userRemoved(User user) {
UserChannelData userData = user.getAttachment(getThis(), UserChannelData.class);
userData.clear();
synchronized (sync) {
while(usersWithRepliesReady.remove(userData));
}
numberOfActiveConnections--;
selector.wakeup();
}
private ClientHandler_TCP_FCFS_async_nio getThis() {
return this;
}
}