All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xlightweb.WebSocketConnection Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright (c) xlightweb.org, 2008 - 2010. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
 * The latest copy of this software may be found on http://www.xlightweb.org/
 */
package org.xlightweb;



import java.io.IOException;


import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.xlightweb.AbstractHttpConnection.IMultimodeExecutor;
import org.xlightweb.WebSocketHandlerAdapter.IPostConnectInterceptor;
import org.xlightweb.client.HttpClientConnection;
import org.xlightweb.server.HttpServerConnection;
import org.xsocket.Execution;
import org.xsocket.MaxReadSizeExceededException;
import org.xsocket.connection.IConnectHandler;
import org.xsocket.connection.IDataHandler;
import org.xsocket.connection.IDisconnectHandler;
import org.xsocket.connection.INonBlockingConnection;
import org.xsocket.connection.NonBlockingConnectionPool;




/**
 * NonBlockingWebSocketConnection
 * 
 * 

This is an experimental implementation of the Web Socket protocol draft and subject to change * * * @author grro */ public final class WebSocketConnection implements IWebSocketConnection { private static final Logger LOG = Logger.getLogger(WebSocketConnection.class.getName()); private INonBlockingConnection tcpConnection; // receive timeout public static final int DEFAULT_RECEIVE_TIMEOUT = Integer.MAX_VALUE; private int receiveTimeoutSec = DEFAULT_RECEIVE_TIMEOUT; // protocol handling private final WebSocketProtocolHandler protocolHandler = new WebSocketProtocolHandler(); private final IMultimodeExecutor executor; private final List inQueue = new ArrayList(); private int inQueueVersion = 0; // upgrade headers private final IHttpRequestHeader upgradeRequestHeader; private final IHttpResponseHeader upgradeResponseHeader; // interceptor support private final IPostWriteInterceptor interceptor; // web socket handler private final Object webSocketHandlerGuard = new Object(); private WebSocketHandlerAdapter webSocketHandlerAdapter = null; // attachment management private AtomicReference attachmentRef = new AtomicReference(null); public WebSocketConnection(String uriString) throws IOException { this(uriString, (String) null); } public WebSocketConnection(String uriString, String protocol) throws IOException { this(uriString, protocol, null); } public WebSocketConnection(String uriString, IWebSocketHandler webSocketHandler) throws IOException { this(uriString, null, webSocketHandler); } public WebSocketConnection(String uriString, String protocol, IWebSocketHandler webSocketHandler) throws IOException { this(URI.create(uriString), protocol, webSocketHandler); } private WebSocketConnection(URI uri, String protocol, IWebSocketHandler webSocketHandler) throws IOException { this(connect(uri), uri, protocol, webSocketHandler); } private static HttpClientConnection connect(URI uri) throws IOException { int port = uri.getPort(); if (port == -1) { if (uri.getScheme().toLowerCase().equals("wss")) { port = 443; } else { port = 80; } } return new HttpClientConnection(uri.getHost(), port); } public WebSocketConnection(HttpClientConnection httpConnection, URI uri, String protocol, IWebSocketHandler webSocketHandler) throws IOException { this(performHandshake(httpConnection, uri, protocol), protocol, webSocketHandler); } private WebSocketConnection(HandeshakeResult handeshakeResult, String protocol, IWebSocketHandler webSocketHandler) throws IOException { this(handeshakeResult.con, webSocketHandler, handeshakeResult.upgradeRequestHeader, handeshakeResult.upgradeResponseHeader); } private WebSocketConnection(HttpClientConnection httpConnection, IWebSocketHandler webSocketHandler, IHttpRequestHeader upgradeRequestHeader, IHttpResponseHeader upgradeResponseHeader) throws IOException { this(convertToTcpConnection(httpConnection), null, webSocketHandler, null, upgradeRequestHeader, upgradeResponseHeader); } private static HandeshakeResult performHandshake(HttpClientConnection httpCon, URI uri, String protocol) throws IOException { GetRequest request = new GetRequest(uri.toString()); request.setHeader("Upgrade", "WebSocket"); request.setHeader("Connection", "Upgrade"); request.setHeader("Origin", request.getRequestUrl().toString()); if (protocol != null) { request.setHeader("WebSocket-Protocol", protocol); } IHttpResponse response = httpCon.call(request); if (response.getStatus() != 101) { if (response.getStatus() == 501) { if (response.hasBody()) { throw new UnsupportedProtocolException(response.getBody().toString()); } else { throw new UnsupportedProtocolException(); } } else { throw new IOException(response.getStatus() + " " + response.getReason()); } } return new HandeshakeResult(httpCon, request.getRequestHeader(), response.getResponseHeader()); } private static final class HandeshakeResult { private HttpClientConnection con; private IHttpRequestHeader upgradeRequestHeader; private IHttpResponseHeader upgradeResponseHeader; public HandeshakeResult(HttpClientConnection con, IHttpRequestHeader upgradeRequestHeader, IHttpResponseHeader upgradeResponseHeader) { this.con = con; this.upgradeRequestHeader = upgradeRequestHeader; this.upgradeResponseHeader = upgradeResponseHeader; } } public WebSocketConnection(HttpServerConnection httpConnection, IWebSocketHandler webSocketHandler, IHttpExchange exchange) throws IOException { this(httpConnection, webSocketHandler, new UpgradeResponseSender(exchange)); } private WebSocketConnection(HttpServerConnection httpConnection, IWebSocketHandler webSocketHandler, UpgradeResponseSender upgradeHandler) throws IOException { this(httpConnection, webSocketHandler, upgradeHandler.getRequestHeader(), upgradeHandler.getResponseHeader(), upgradeHandler); } private static INonBlockingConnection convertToTcpConnection(AbstractHttpConnection httpConnection) throws IOException { INonBlockingConnection tcpCon = httpConnection.getUnderlyingTcpConnection(); tcpCon.setHandler(null); return tcpCon; } private WebSocketConnection(HttpServerConnection httpConnection, IWebSocketHandler webSocketHandler, IHttpRequestHeader upgradeRequestHeader, IHttpResponseHeader upgradeResponseHeader, UpgradeResponseSender upgradeResponseSender) throws IOException { this(convertToTcpConnection(httpConnection), upgradeResponseSender, webSocketHandler, upgradeResponseSender, upgradeRequestHeader, upgradeResponseHeader); } private static final class UpgradeResponseSender implements IPostConnectInterceptor, IPostWriteInterceptor { private final IHttpExchange exchange; private final AtomicBoolean isUpgradeSent = new AtomicBoolean(false); private final IHttpResponseHeader responseHeader; public UpgradeResponseSender(IHttpExchange exchange) { this.exchange = exchange; String protocol = exchange.getRequest().getHeader("WebSocket-Protocol"); String webSocketOrigin = exchange.getRequest().getHeader("Origin"); String webSocketLocation; if (exchange.getRequest().isSecure()) { webSocketLocation = "wss://" + exchange.getRequest().getHost() + exchange.getRequest().getRequestURI(); } else { webSocketLocation = "ws://" + exchange.getRequest().getHost() + exchange.getRequest().getRequestURI(); } responseHeader = new HttpResponseHeader(101); responseHeader.setReason("Web Socket Protocol Handshake"); // do not change the setting order! Chrome would not thank you responseHeader.setHeader("Upgrade", "WebSocket"); responseHeader.setHeader("Connection", "Upgrade"); responseHeader.setHeader("WebSocket-Origin", webSocketOrigin); responseHeader.setHeader("WebSocket-Location", webSocketLocation); if (protocol != null) { responseHeader.setHeader("WebSocket-Protocol", protocol); } } public void onConnectException(IOException ioe) { if (ioe instanceof UnsupportedProtocolException) { exchange.sendError(501, ioe.getMessage()); } else { exchange.sendError(501); } } public void onPostConnect() throws IOException { sentUpgradeIfNecessary(); } public void onPreWrite() throws IOException { sentUpgradeIfNecessary() ; } private void sentUpgradeIfNecessary() throws IOException { if (!isUpgradeSent.getAndSet(true)) { exchange.send(new HttpResponse(responseHeader)); } } IHttpRequestHeader getRequestHeader() { return exchange.getRequest().getRequestHeader(); } IHttpResponseHeader getResponseHeader() { return responseHeader; } } private WebSocketConnection(INonBlockingConnection tcpConnection, IPostWriteInterceptor connectInterceptor, IWebSocketHandler webSocketHandler, IPostConnectInterceptor postConnectInterceptor, IHttpRequestHeader upgradeRequestHeader, IHttpResponseHeader upgradeResponseHeader) throws IOException { this.interceptor = connectInterceptor; this.tcpConnection = tcpConnection; this.upgradeRequestHeader = upgradeRequestHeader; this.upgradeResponseHeader = upgradeResponseHeader; executor = HttpUtils.newMultimodeExecutor(tcpConnection.getWorkerpool()); setMessageHandler(webSocketHandler, postConnectInterceptor); protocolHandler.onConnect(tcpConnection); tcpConnection.setHandler(protocolHandler); } /** * {@inheritDoc} */ public String getProtocol() { return getUpgradeResponseHeader().getHeader("WebSocket-Protocol"); } /** * {@inheritDoc} */ public String getWebSocketLocation() { return getUpgradeResponseHeader().getHeader("WebSocket-Location"); } /** * {@inheritDoc} */ public String getWebSocketOrigin() { return getUpgradeResponseHeader().getHeader("WebSocket-Origin"); } public IHttpRequestHeader getUpgradeRequestHeader() { return upgradeRequestHeader; } public IHttpResponseHeader getUpgradeResponseHeader() { return upgradeResponseHeader; } public void destroy() { // destroy (underlying tcp) connection by using connection pool. The underlying connection could be a pooled one) // The connection pool detects automatically if the connection is pooled or not. The connection will be // closed (logically) anyway try { NonBlockingConnectionPool.destroy(tcpConnection); } catch (IOException ioe) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("[" + getId() + "] error occured by destroying htttp connection " + getId() + " " + ioe.toString()); } } } public INonBlockingConnection getUnderlyingTcpConnection() { return tcpConnection; } public int availableMessages() { synchronized (inQueue) { return inQueue.size(); } } int getInQueueVersion() { synchronized (inQueue) { return inQueueVersion; } } @Override public String toString() { return tcpConnection.toString(); } public WebSocketMessage readMessage() throws BufferUnderflowException, SocketTimeoutException { long start = System.currentTimeMillis(); long remainingTime = receiveTimeoutSec; do { synchronized (inQueue) { if (inQueue.isEmpty()) { try { inQueue.wait(remainingTime); } catch (InterruptedException ie) { // Restore the interrupted status Thread.currentThread().interrupt(); } } else { inQueueVersion++; return inQueue.remove(0); } } remainingTime = computeRemainingTime(start, receiveTimeoutSec); } while (remainingTime > 0); if (LOG.isLoggable(Level.FINE)) { LOG.fine("receive timeout " + receiveTimeoutSec + " sec reached. throwing timeout exception"); } throw new SocketTimeoutException("timeout " + receiveTimeoutSec + " sec reached"); } private long computeRemainingTime(long start, int receiveTimeoutSec) { return (start + ((long) receiveTimeoutSec * 1000)) - System.currentTimeMillis(); } public int writeMessage(WebSocketMessage msg) throws IOException { if (interceptor != null) { interceptor.onPreWrite(); } return msg.writeTo(this, null); } public void writeMessage(WebSocketMessage msg, IWebSocketWriteCompleteHandler completeHandler) throws IOException { if (interceptor != null) { interceptor.onPreWrite(); } msg.writeTo(this, completeHandler); } public void closeQuitly() { try { close(); } catch (IOException ioe) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("[" + getId() + "] error occured by closing connection " + getId() + " " + ioe.toString()); } try { NonBlockingConnectionPool.destroy(tcpConnection); } catch (IOException e) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("[" + getId() + "] error occured by closing connection " + getId() + " " + e.toString()); } } } } public void close() throws IOException { destroy(); } void processNonthreaded(Runnable task) { executor.processNonthreaded(task); } void processMultithreaded(Runnable task) { executor.processMultithreaded(task); } public void setMessageHandler(IWebSocketHandler webSocketHandler) throws IOException { setMessageHandler(webSocketHandler, null); } void setMessageHandler(IWebSocketHandler webSocketHandler, IPostConnectInterceptor postConnectInterceptor) throws IOException { synchronized (webSocketHandlerGuard) { if (webSocketHandlerAdapter != null) { webSocketHandlerAdapter.onDisconnect(this); } webSocketHandlerAdapter = new WebSocketHandlerAdapter(webSocketHandler, postConnectInterceptor); webSocketHandlerAdapter.onConnect(this); } } public boolean isOpen() { return tcpConnection.isOpen(); } public boolean isServerSide() { return tcpConnection.isServerSide(); } public INonBlockingConnection getTcpConnection() { return tcpConnection; } public void setAttachment(Object obj) { attachmentRef.set(obj); } public Object getAttachment() { return attachmentRef.get(); } public long getConnectionTimeoutMillis() { return tcpConnection.getConnectionTimeoutMillis(); } public void setConnectionTimeoutMillis(long timeoutMillis) { tcpConnection.setConnectionTimeoutMillis(timeoutMillis); } public long getRemainingMillisToConnectionTimeout() { return tcpConnection.getRemainingMillisToConnectionTimeout(); } public long getIdleTimeoutMillis() { return tcpConnection.getIdleTimeoutMillis(); } public void setIdleTimeoutMillis(long timeoutInMillis) { tcpConnection.setIdleTimeoutMillis(timeoutInMillis); } public long getRemainingMillisToIdleTimeout() { return tcpConnection.getRemainingMillisToIdleTimeout(); } public String getId() { return tcpConnection.getId(); } public int getLocalPort() { return tcpConnection.getLocalPort(); } public InetAddress getLocalAddress() { return tcpConnection.getLocalAddress(); } public InetAddress getRemoteAddress() { return tcpConnection.getRemoteAddress(); } public int getRemotePort() { return tcpConnection.getRemotePort(); } public Object getOption(String name) throws IOException { return tcpConnection.getOption(name); } @SuppressWarnings("unchecked") public Map getOptions() { return tcpConnection.getOptions(); } public void setOption(String name, Object value) throws IOException { tcpConnection.setOption(name, value); } @Execution(Execution.NONTHREADED) private final class WebSocketProtocolHandler implements IConnectHandler, IDataHandler, IDisconnectHandler { // network data private ByteBuffer rawBuffer = null; public boolean onConnect(INonBlockingConnection connection) throws IOException, BufferUnderflowException, MaxReadSizeExceededException { synchronized (webSocketHandlerGuard) { if (webSocketHandlerAdapter != null) { webSocketHandlerAdapter.onConnect(WebSocketConnection.this); } } return true; } public boolean onData(INonBlockingConnection connection) throws IOException, BufferUnderflowException, ClosedChannelException, MaxReadSizeExceededException { if (connection.isOpen()) { // copying available network data into raw data buffer int available = connection.available(); if (available > 0) { ByteBuffer[] data = connection.readByteBufferByLength(available); onData(data); } } return true; } void onData(ByteBuffer[] data) throws IOException { if (rawBuffer == null) { rawBuffer = HttpUtils.merge(data); } else { rawBuffer = HttpUtils.merge(rawBuffer, data); } parse(rawBuffer); if (!rawBuffer.hasRemaining()) { rawBuffer = null; } } void parse(ByteBuffer buffer) throws IOException { while (buffer.hasRemaining()) { WebSocketMessage msg = WebSocketMessage.parse(buffer); if (msg == null) { return; } else { synchronized (inQueue) { inQueueVersion++; inQueue.add(msg); inQueue.notifyAll(); } synchronized (webSocketHandlerGuard) { if (webSocketHandlerAdapter != null) { webSocketHandlerAdapter.onMessage(WebSocketConnection.this); } } } } } public boolean onDisconnect(INonBlockingConnection connection) throws IOException { synchronized (webSocketHandlerGuard) { if (webSocketHandlerAdapter != null) { webSocketHandlerAdapter.onDisconnect(WebSocketConnection.this); } } return true; } } private static interface IPostWriteInterceptor { void onPreWrite() throws IOException ; } }