/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion 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 Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.server.http; import aphelion.shared.net.HttpWebSocketServerListener; import aphelion.shared.net.WebSocketTransport.ServerWebSocketImpl; import aphelion.shared.swissarmyknife.ThreadSafe; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedSelectorException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; import org.java_websocket.*; import org.java_websocket.framing.CloseFrame; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.handshake.Handshakedata; /** * * @author Joris */ class HttpWebSocketServer extends WebSocketAdapter implements Runnable { private static final Logger log = Logger.getLogger("aphelion.server.http"); public Thread thread = new Thread(this); private volatile boolean ready = false; private Selector selector; private ConcurrentLinkedQueue<NewChannel> newChannels = new ConcurrentLinkedQueue<>(); private final HttpWebSocketServerListener listener; private final Set<WebSocket> connections = new HashSet<>(); private ByteBuffer buffer; HttpWebSocketServer(HttpWebSocketServerListener listener) { this.listener = listener; if (this.listener == null) { throw new IllegalArgumentException(); } } @ThreadSafe public void startWaitReady() { thread.start(); while (!ready) { try { Thread.sleep(1); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return; } } } @ThreadSafe public void stop() { thread.interrupt(); } public void setDeamon(boolean on) { thread.setDaemon(on); } public Set<WebSocket> connections() { return this.connections; } private boolean registerNewChannel() throws IOException, InterruptedException { NewChannel newChannel = newChannels.poll(); if (newChannel == null) { return false; // done } log.log(Level.INFO, "Registering new websocket connection: {0}", newChannel.sChannel.getRemoteAddress()); newChannel.sChannel.configureBlocking(false); WebSocketImpl conn = new ServerWebSocketImpl(this); newChannel.sChannel.socket().setTcpNoDelay(true); conn.key = newChannel.sChannel.register(selector, SelectionKey.OP_READ, conn); if (!onConnect(conn.key)) { conn.key.cancel(); } else { conn.channel = newChannel.sChannel; ByteBuffer prependData = newChannel.prependData; newChannel.prependData = null; conn.decode(prependData); } return true; } private boolean readable(SelectionKey key, WebSocketImpl conn) throws InterruptedException, IOException { buffer.clear(); int read = conn.channel.read(buffer); buffer.flip(); if (read == -1) { // connection closed conn.eot(); return true; } if (read == 0) { return true; // true = done reading } // Something has been read (up to WebSocket.RCVBUF) // Perhaps there is more in the TCP receive buffer, // but other connections will get a chance first conn.decode(buffer); return false; // false = keep this connection in the selector list } private boolean writable(SelectionKey key, WebSocketImpl conn) throws IOException { if (SocketChannelIOHelper.batch(conn, (ByteChannel) conn.channel)) { if (key.isValid()) { key.interestOps(SelectionKey.OP_READ); } return true; // true = done writing } return false; // false = there is more to write, but give other connections a chance to write something } @Override public void run() { thread.setName("WebSocketServer-" + thread.getId()); buffer = ByteBuffer.allocate(WebSocketImpl.RCVBUF); try { try { selector = Selector.open(); } catch (IOException ex) { log.log(Level.SEVERE, "Unable to open selector", ex); return; } ready = true; while (!thread.isInterrupted()) { SelectionKey key = null; Iterator<SelectionKey> it; try { selector.select(); while (registerNewChannel()) { } it = selector.selectedKeys().iterator(); } catch (InterruptedException ex) { break; } catch (ClosedSelectorException ex) { break; } catch (IOException ex) { log.log(Level.SEVERE, "IOException in select()", ex); break; } while (it.hasNext()) { WebSocketImpl conn = null; key = it.next(); if (!key.isValid()) { continue; } try { if (key.isReadable()) { conn = (WebSocketImpl) key.attachment(); if (readable(key, conn)) { it.remove(); } } if (key.isValid() && key.isWritable()) { conn = (WebSocketImpl) key.attachment(); if (writable(key, conn)) { try { it.remove(); } catch (IllegalStateException ex) { // already removed } } } } catch (ClosedSelectorException ex) { break; } catch (InterruptedException ex) { break; } catch (CancelledKeyException ex) { it.remove(); // an other thread may cancel the key } catch (IOException ex) { log.log(Level.SEVERE, "IOException while parsing selector", ex); key.cancel(); it.remove(); handleIOException(conn, ex); } } } for (WebSocket ws : connections) { ws.close(CloseFrame.NORMAL); } } catch (RuntimeException ex) { log.log(Level.SEVERE, null, ex); onError(null, ex); } } private void handleIOException(WebSocket conn, IOException ex) { onWebsocketError(conn, ex); // conn may be null here try { if (conn != null) { conn.close(CloseFrame.ABNORMAL_CLOSE); } } catch(CancelledKeyException ex2) { onWebsocketClose(conn, CloseFrame.ABNORMAL_CLOSE, null, true); } } @Override public final void onWebsocketMessage(WebSocket conn, String message) { onMessage(conn, message); } @Override public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) { onMessage(conn, blob); } @Override public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) { if (this.connections.add(conn)) { onOpen(conn, (ClientHandshake) handshake); } } @Override public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) { try { selector.wakeup(); } catch (IllegalStateException ex) { } if (this.connections.remove(conn)) { onClose(conn, code, reason, remote); } } /** * @param conn may be null if the error does not belong to a single connection */ @Override public final void onWebsocketError(WebSocket conn, Exception ex) { onError(conn, ex); } @Override public final void onWriteDemand(WebSocket w) { WebSocketImpl conn = (WebSocketImpl) w; conn.key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); try { selector.wakeup(); } catch (IllegalStateException ex) { } } public boolean onConnect(SelectionKey key) { return listener.wssConnect(key); } public void onOpen(WebSocket conn, ClientHandshake handshake) { listener.wssOpen(conn, handshake); } public void onClose(WebSocket conn, int code, String reason, boolean remote) { listener.wssClose(conn, code, reason, remote); } public void onMessage(WebSocket conn, String message) { listener.wssMessage( conn, message); } public void onMessage(WebSocket conn, ByteBuffer message) { listener.wssMessage(conn, message); } public void onError(WebSocket conn, Exception ex) { listener.wssError(conn, ex); } @ThreadSafe void addNewChannel(SocketChannel sChannel, ByteBuffer prependData) { newChannels.add(new NewChannel(sChannel, prependData)); try { selector.wakeup(); } catch (IllegalStateException | NullPointerException ex) { // Thread has not started yet, or it just stopped assert false; } } @Override public void onWebsocketClosing(WebSocket ws, int code, String reason, boolean remote) { } @Override public void onWebsocketCloseInitiated(WebSocket ws, int code, String reason) { } private Socket getSocket( WebSocket conn ) { WebSocketImpl impl = (WebSocketImpl) conn; return ( (SocketChannel) impl.key.channel() ).socket(); } @Override public InetSocketAddress getLocalSocketAddress(WebSocket conn) { return (InetSocketAddress) getSocket( conn ).getLocalSocketAddress(); } @Override public InetSocketAddress getRemoteSocketAddress(WebSocket conn) { return (InetSocketAddress) getSocket( conn ).getRemoteSocketAddress(); } private static final class NewChannel { SocketChannel sChannel; ByteBuffer prependData; NewChannel(SocketChannel sChannel, ByteBuffer prependData) { this.sChannel = sChannel; this.prependData = prependData; } } }