/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2014, Telestax Inc and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
package org.restcomm.media.ice.network.nio;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.restcomm.media.ice.network.ProtocolHandler;
/**
* Non-blocking server that runs on a single thread, alternating between reading
* and writing modes.
*
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
public class NioServer implements Runnable {
private Logger logger = Logger.getLogger(NioServer.class);
private static final int BUFFER_SIZE = 8192;
// The selector that monitors the registered channels
private Selector selector;
// The buffer into which we will read data when it's available
private ByteBuffer buffer;
// Schedules events for posterior IO operations
private EventScheduler scheduler;
// List of operation requests
private List<OperationRequest> operationRequests;
// Registers pending data per channel
private Map<DatagramChannel, List<ByteBuffer>> pendingData;
// Protocol Handler to process incoming packets
protected ProtocolHandler protocolHandler;
// Server execution controls
private Thread schedulerThread;
private Thread serverThread;
private volatile boolean running;
public NioServer(Selector selector) {
this.selector = selector;
this.buffer = ByteBuffer.allocate(BUFFER_SIZE);
this.scheduler = new EventScheduler(this);
this.operationRequests = new LinkedList<OperationRequest>();
this.pendingData = new HashMap<DatagramChannel, List<ByteBuffer>>();
this.running = false;
}
public boolean isRunning() {
return running;
}
public void start() {
if (!this.running) {
this.schedulerThread = new Thread(this.scheduler);
this.serverThread = new Thread(this);
this.running = true;
this.schedulerThread.start();
this.serverThread.start();
}
}
public void stop() {
this.running = false;
}
public void stopNow() {
this.running = false;
this.scheduler.stop();
}
private void cleanup() {
if (this.selector.isOpen()) {
try {
this.selector.close();
} catch (IOException e) {
this.logger.error("Server did not stop in an elegant manner: "+ e.getMessage(), e);
}
}
}
@Override
public void run() {
logger.info("NIO Server started");
while (this.running && this.selector.isOpen()) {
try {
// Process any pending changes
synchronized (this.operationRequests) {
Iterator<OperationRequest> changes = this.operationRequests.iterator();
while (changes.hasNext()) {
// Process the request
OperationRequest request = changes.next();
if (OperationRequest.CHANGE_OPS == request.getRequestType()) {
SelectionKey key = request.getChannel().keyFor(this.selector);
if (key != null && key.isValid()) {
// The null check is necessary because a key can
// be canceled by external agents.
key.interestOps(request.getOperations());
}
}
// Remove the request from the queue
changes.remove();
}
}
// Wait for an event one of the registered channels
this.selector.select();
// Iterate over the set of keys for which events are available
Iterator<SelectionKey> selectedKeys = this.selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey key = (SelectionKey) selectedKeys.next();
selectedKeys.remove();
// Verify if key was not invalidated in a previous operation
if (key.isValid()) {
try {
// Take action based on current key mode
if (key.isReadable()) {
this.read(key);
} else if (key.isWritable()) {
this.write(key);
}
} catch (Exception e) {
this.logger.error("Could not process packet: "+ e.getMessage(), e);
}
}
}
} catch (Exception e) {
this.logger.error(e.getMessage(), e);
}
}
// Cleanup resource after server stops
cleanup();
logger.info("NIO Server stopped");
}
private List<ByteBuffer> registerChannel(DatagramChannel channel) {
List<ByteBuffer> data = new ArrayList<ByteBuffer>();
this.pendingData.put(channel, data);
return data;
}
public void send(DatagramChannel channel, byte[] data) {
synchronized (this.operationRequests) {
// Alternate the channel mode to write
OperationRequest writeRequest = new OperationRequest(channel, OperationRequest.CHANGE_OPS, SelectionKey.OP_WRITE);
this.operationRequests.add(writeRequest);
// Enqueue the data to be written
synchronized (this.pendingData) {
List<ByteBuffer> queue = this.pendingData.get(channel);
if (queue == null) {
queue = registerChannel(channel);
}
queue.add(ByteBuffer.wrap(data));
}
}
// Wake selector thread so it can make the required changes
this.selector.wakeup();
}
private void read(SelectionKey key) throws IOException {
DatagramChannel channel = (DatagramChannel) key.channel();
// Get buffer ready to read new data
this.buffer.clear();
// Read data from channel
int dataLength = 0;
try {
SocketAddress remotePeer = channel.receive(this.buffer);
if (!channel.isConnected() && remotePeer != null) {
channel.connect(remotePeer);
}
dataLength = (remotePeer == null) ? -1 : this.buffer.position();
} catch (IOException e) {
dataLength = -1;
}
// Stop if socket was shutdown or error occurred
if (dataLength == -1) {
channel.close();
key.cancel();
return;
}
// Delegate work to a handler
if (protocolHandler != null) {
// Handler processes the requests and provides an answer
byte[] data = this.buffer.array();
byte[] response = protocolHandler.process(key, data, dataLength);
// Keep reading if handler provided no answer
if (response != null) {
// schedule STUN request to be sent to browser
this.scheduler.schedule(channel, response, response.length);
}
}
}
private void write(SelectionKey key) throws IOException {
DatagramChannel channel = (DatagramChannel) key.channel();
synchronized (this.pendingData) {
// Retrieve data from registered channel
List<ByteBuffer> queue = this.pendingData.get(key.channel());
// Write all pending data
while (!queue.isEmpty()) {
ByteBuffer dataBuffer = queue.get(0);
channel.send(dataBuffer, channel.getRemoteAddress());
channel.write(dataBuffer);
if (dataBuffer.remaining() > 0) {
// Channel buffer is full
// Data will be written in the next write operation
break;
} else {
// All data was passed to the channel
// Remove empty buffer
queue.remove(0);
}
}
// All pending data was written
// Switch channel mode to read
key.interestOps(SelectionKey.OP_READ);
}
}
}