/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ package org.apache.mina.transport.tcp; import java.io.IOException; import java.net.SocketAddress; 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.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.mina.api.IoServer; import org.apache.mina.api.IoService; import org.apache.mina.api.IoSession; import org.apache.mina.service.AbstractIoService; import org.apache.mina.service.SelectorProcessor; import org.apache.mina.service.SelectorStrategy; import org.apache.mina.session.WriteRequest; import org.apache.mina.transport.tcp.nio.NioTcpServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * A {@link SelectorProcessor} for processing NIO based {@link IoSession}. * * @author <a href="http://mina.apache.org">Apache MINA Project</a> * */ public class NioSelectorProcessor implements SelectorProcessor { /** * A timeout used for the select, as we need to get out to deal with idle * sessions */ private static final long SELECT_TIMEOUT = 1000L; private SelectorStrategy strategy; private static final Logger LOGGER = LoggerFactory.getLogger(NioSelectorProcessor.class); private Map<SocketAddress, ServerSocketChannel> serverSocketChannels = new ConcurrentHashMap<SocketAddress, ServerSocketChannel>(); private ByteBuffer readBuffer; /** * new binded server to add to the selector {ServerSocketChannel, IoServer} */ private final Queue<Object[]> serversToAdd = new ConcurrentLinkedQueue<Object[]>(); /** server to remove of the selector */ private final Queue<ServerSocketChannel> serversToRemove = new ConcurrentLinkedQueue<ServerSocketChannel>(); /** * new session freshly accepted, placed here for being added to the selector */ private final Queue<NioTcpSession> sessionsToConnect = new ConcurrentLinkedQueue<NioTcpSession>(); /** session to be removed of the selector */ private final Queue<NioTcpSession> sessionsToClose = new ConcurrentLinkedQueue<NioTcpSession>(); /** A queue used to store the sessions to be flushed */ private final Queue<NioTcpSession> flushingSessions = new ConcurrentLinkedQueue<NioTcpSession>(); private Selector selector; // Lock for Selector worker, using default. can look into fairness later private Lock workerLock = new ReentrantLock(); public NioSelectorProcessor(String name, SelectorStrategy strategy) { this.strategy = strategy; // TODO : configurable parameter readBuffer = ByteBuffer.allocate(1024); } /** * Add a bound server channel for starting accepting new client connections. * * @param serverChannel */ private void add(ServerSocketChannel serverChannel, IoServer server) { LOGGER.debug("adding a server channel {} for server {}", serverChannel, server); serversToAdd.add(new Object[] { serverChannel, server }); wakeupWorker(); } private SelectorWorker worker = null; private void wakeupWorker() { workerLock.lock(); try { if (worker == null) { worker = new SelectorWorker(); worker.start(); } } finally { workerLock.unlock(); } if (selector != null) { selector.wakeup(); } } @Override public void bindAndAcceptAddress(IoServer server, SocketAddress address) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(address); serverSocketChannel.configureBlocking(false); serverSocketChannels.put(address, serverSocketChannel); add(serverSocketChannel, server); } @Override public void unbind(SocketAddress address) throws IOException { ServerSocketChannel channel = serverSocketChannels.get(address); channel.socket().close(); channel.close(); if (serverSocketChannels.remove(address) == null) { LOGGER.warn("The server channel for address {} was already unbound", address); } LOGGER.debug("Removing a server channel {}", channel); serversToRemove.add(channel); wakeupWorker(); } @Override public void createSession(IoService service, Object clientSocket) { LOGGER.debug("create session"); SocketChannel socketChannel = (SocketChannel) clientSocket; NioTcpSession session = new NioTcpSession((NioTcpServer) service, socketChannel, strategy.getSelectorForNewSession(this)); // TODO : configure try { socketChannel.configureBlocking(false); } catch (IOException e) { LOGGER.error("Unexpected exception, while configuring socket as non blocking", e); } // event session created session.getFilterChain().processSessionCreated(session); // add the session to the queue for being added to the selector sessionsToConnect.add(session); wakeupWorker(); } /** * The worker processing incoming session creation and destruction requests. * It will also bind new servers. */ private class SelectorWorker extends Thread { // map for finding the keys associated with a given server private Map<ServerSocketChannel, SelectionKey> serverKey = new HashMap<ServerSocketChannel, SelectionKey>(); // map for finding read keys associated with a given session private Map<NioTcpSession, SelectionKey> sessionReadKey = new HashMap<NioTcpSession, SelectionKey>(); @Override public void run() { if (selector == null) { LOGGER.debug("opening a new selector"); try { selector = Selector.open(); } catch (IOException e) { LOGGER.error("IOException while opening a new Selector", e); } } for (;;) { try { // pop server sockets for removing if (serversToRemove.size() > 0) { while (!serversToRemove.isEmpty()) { ServerSocketChannel channel = serversToRemove.poll(); SelectionKey key = serverKey.remove(channel); if (key == null) { LOGGER.error("The server socket was already removed of the selector"); } else { key.cancel(); } } } // pop new server sockets for accepting if (serversToAdd.size() > 0) { while (!serversToAdd.isEmpty()) { Object[] tmp = serversToAdd.poll(); ServerSocketChannel channel = (ServerSocketChannel) tmp[0]; SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT); key.attach(tmp); } } // pop new session for starting read/write if (sessionsToConnect.size() > 0) { while (!sessionsToConnect.isEmpty()) { NioTcpSession session = sessionsToConnect.poll(); SelectionKey key = session.getSocketChannel().register(selector, SelectionKey.OP_READ); key.attach(session); sessionReadKey.put(session, key); session.setConnected(); // fire the event ((AbstractIoService) session.getService()).fireSessionCreated(session); session.getFilterChain().processSessionOpen(session); } } // pop session for close if (sessionsToClose.size() > 0) { while (!sessionsToClose.isEmpty()) { NioTcpSession session = sessionsToClose.poll(); SelectionKey key = sessionReadKey.remove(session); key.cancel(); // needed ? session.getSocketChannel().close(); // fire the event session.getFilterChain().processSessionClosed(session); ((AbstractIoService) session.getService()).fireSessionDestroyed(session); } } LOGGER.debug("selecting..."); int readyCount = selector.select(SELECT_TIMEOUT); LOGGER.debug("... done selecting : {}", readyCount); if (readyCount > 0) { // process selected keys Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { SelectionKey key = selectedKeys.next(); selectedKeys.remove(); if (!key.isValid()) { continue; } selector.selectedKeys().remove(key); if (key.isReadable()) { LOGGER.debug("readable client {}", key); NioTcpSession session = (NioTcpSession) key.attachment(); SocketChannel channel = session.getSocketChannel(); int readCount = channel.read(readBuffer); LOGGER.debug("read {} bytes", readCount); if (readCount < 0) { // session closed by the remote peer LOGGER.debug("session closed by the remote peer"); sessionsToClose.add(session); } else { // we have read some data // limit at the current position & rewind buffer back to start & push to the chain readBuffer.flip(); session.getFilterChain().processMessageReceived(session, readBuffer); } } if (key.isWritable()) { LOGGER.debug("writable session : {}", key.attachment()); NioTcpSession session = (NioTcpSession) key.attachment(); // write from the session write queue Queue<WriteRequest> queue = session.getWriteQueue(); do { // get a write request from the queue WriteRequest wreq = queue.peek(); if (wreq == null) { break; } ByteBuffer buf = (ByteBuffer) wreq.getMessage(); int wrote = session.getSocketChannel().write(buf); LOGGER.debug("wrote {} bytes to {}", wrote, session); if (buf.remaining() == 0) { // completed write request, let's remove // it queue.remove(); } else { // output socket buffer is full, we need // to give up until next selection for // writing break; } } while (!queue.isEmpty()); // if the session is no more interested in writing, we need // to stop listening for OP_WRITE events if (queue.isEmpty()) { // a key registered for read ? (because we can have a // Selector for reads and another for the writes SelectionKey readKey = sessionReadKey.get(session); if (readKey != null) { LOGGER.debug("registering key for only reading"); SelectionKey mykey = session.getSocketChannel().register(selector, SelectionKey.OP_READ, session); sessionReadKey.put(session, mykey); } else { LOGGER.debug("cancel key for writing"); session.getSocketChannel().keyFor(selector).cancel(); } } } if (key.isAcceptable()) { LOGGER.debug("acceptable new client {}", key); ServerSocketChannel serverSocket = (ServerSocketChannel) ((Object[]) key.attachment())[0]; IoServer server = (IoServer) (((Object[]) key.attachment())[1]); // accepted connection SocketChannel newClientChannel = serverSocket.accept(); LOGGER.debug("client accepted"); // and give it's to the strategy strategy.getSelectorForNewSession(NioSelectorProcessor.this).createSession(server, newClientChannel); } } } // registering session with data in the write queue for // writing while (!flushingSessions.isEmpty()) { NioTcpSession session = flushingSessions.poll(); // a key registered for read ? (because we can have a // Selector for reads and another for the writes SelectionKey readKey = sessionReadKey.get(session); if (readKey != null) { // register for read/write SelectionKey key = session.getSocketChannel().register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, session); sessionReadKey.put(session, key); } else { session.getSocketChannel().register(selector, SelectionKey.OP_WRITE, session); } } } catch (IOException e) { LOGGER.error("IOException while selecting selector", e); } // stop the worker if needed workerLock.lock(); try { if (selector.keys().isEmpty()) { worker = null; break; } } finally { workerLock.unlock(); } } } } @Override public void flush(IoSession session) { LOGGER.debug("scheduling session {} for writing", session.toString()); // add the session to the list of session to be registered for writing // wake the selector flushingSessions.add((NioTcpSession) session); wakeupWorker(); } }