// --------------------------------------------------------------------------- // jWebSocket - Copyright (c) 2010 Innotrade GmbH // --------------------------------------------------------------------------- // This program 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 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 Lesser General Public License for // more details. // You should have received a copy of the GNU Lesser General Public License along // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>. // --------------------------------------------------------------------------- package org.jwebsocket.client.java; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javolution.util.FastList; import javolution.util.FastMap; import org.jwebsocket.api.WebSocketClient; import org.jwebsocket.api.WebSocketClientEvent; import org.jwebsocket.api.WebSocketClientListener; import org.jwebsocket.api.WebSocketPacket; import org.jwebsocket.api.WebSocketStatus; import org.jwebsocket.client.token.WebSocketClientTokenEvent; import org.jwebsocket.kit.RawPacket; import org.jwebsocket.kit.WebSocketException; import org.jwebsocket.kit.WebSocketHandshake; /** * Base {@code WebSocket} implementation based on * http://weberknecht.googlecode.com by Roderick Baier. This uses thread model * for handling WebSocket connection which is defined by the <tt>WebSocket</tt> * protocol specification. {@linkplain http://www.whatwg.org/specs/web-socket-protocol/} * {@linkplain http://www.w3.org/TR/websockets/} * * @author Roderick Baier * @author agali * @author puran * @version $Id:$ */ public class BaseWebSocket implements WebSocketClient { /** WebSocket connection url */ private URI url = null; /** list of the listeners registered */ private List<WebSocketClientListener> listeners = new FastList<WebSocketClientListener>(); /** flag for connection test */ private volatile boolean connected = false; private boolean isBinaryData = false; /** TCP socket */ private Socket socket = null; /** IO streams */ private InputStream input = null; private PrintStream output = null; /** Data receiver */ private WebSocketReceiver receiver = null; private WebSocketHandshake handshake = null; /** represents the WebSocket status */ private WebSocketStatus status = WebSocketStatus.CLOSED; /** * Base constructor */ public BaseWebSocket() { } /** * {@inheritDoc} */ @Override public void open(String uriString) throws WebSocketException { URI uri = null; try { uri = new URI(uriString); } catch (URISyntaxException e) { throw new WebSocketException("Error parsing WebSocket URL:" + uriString, e); } this.url = uri; handshake = new WebSocketHandshake(url); try { socket = createSocket(); input = socket.getInputStream(); output = new PrintStream(socket.getOutputStream()); output.write(handshake.getHandshake()); boolean handshakeComplete = false; boolean header = true; int len = 1000; byte[] buffer = new byte[len]; int pos = 0; ArrayList<String> handshakeLines = new ArrayList<String>(); byte[] serverResponse = new byte[16]; while (!handshakeComplete) { status = WebSocketStatus.CONNECTING; int b = input.read(); buffer[pos] = (byte) b; pos += 1; if (!header) { serverResponse[pos - 1] = (byte) b; if (pos == 16) { handshakeComplete = true; } } else if (buffer[pos - 1] == 0x0A && buffer[pos - 2] == 0x0D) { String line = new String(buffer, "UTF-8"); if (line.trim().equals("")) { header = false; } else { handshakeLines.add(line.trim()); } buffer = new byte[len]; pos = 0; } } handshake.verifyServerStatusLine(handshakeLines.get(0)); handshake.verifyServerResponse(serverResponse); handshakeLines.remove(0); Map<String, String> headers = new FastMap<String, String>(); for (String line : handshakeLines) { String[] keyValue = line.split(": ", 2); headers.put(keyValue[0], keyValue[1]); } handshake.verifyServerHandshakeHeaders(headers); receiver = new WebSocketReceiver(input); // TODO: Add event parameter // notifyOpened(null); receiver.start(); connected = true; status = WebSocketStatus.OPEN; } catch (WebSocketException wse) { throw wse; } catch (IOException ioe) { throw new WebSocketException("error while connecting: " + ioe.getMessage(), ioe); } } @Override public void send(byte[] data) throws WebSocketException { if (!connected) { throw new WebSocketException("error while sending binary data: not connected"); } try { if (isBinaryData) { output.write(0x80); // TODO: what if frame is longer than 255 characters (8bit?) Refer to IETF spec! output.write(data.length); output.write(data); } else { output.write(0x00); output.write(data); output.write(0xff); } output.flush(); } catch (IOException ioe) { throw new WebSocketException("error while sending binary data: ", ioe); } } /** * {@inheritDoc} */ @Override public void send(String aData, String aEncoding) throws WebSocketException { byte[] data; try { data = aData.getBytes(aEncoding); send(data); } catch (UnsupportedEncodingException e) { throw new WebSocketException("Encoding exception while sending the data:" + e.getMessage(), e); } } /** * {@inheritDoc} */ @Override public void send(WebSocketPacket dataPacket) throws WebSocketException { send(dataPacket.getByteArray()); } public void handleReceiverError() { try { if (connected) { status = WebSocketStatus.CLOSING; close(); } } catch (WebSocketException wse) { // TODO: don't use printStackTrace wse.printStackTrace(); } } @Override public synchronized void close() throws WebSocketException { if (!connected) { return; } sendCloseHandshake(); if (receiver.isRunning()) { receiver.stopit(); } try { // input.close(); // output.close(); socket.shutdownInput(); socket.shutdownOutput(); socket.close(); status = WebSocketStatus.CLOSED; } catch (IOException ioe) { throw new WebSocketException("error while closing websocket connection: ", ioe); } // TODO: add event notifyClosed(null); } private void sendCloseHandshake() throws WebSocketException { if (!connected) { throw new WebSocketException("error while sending close handshake: not connected"); } try { output.write(0xff00); // TODO: check if final CR/LF is required/valid! output.write("\r\n".getBytes()); // TODO: shouldn't we put a flush here? } catch (IOException ioe) { throw new WebSocketException("error while sending close handshake", ioe); } connected = false; } private Socket createSocket() throws WebSocketException { String scheme = url.getScheme(); String host = url.getHost(); int port = url.getPort(); socket = null; if (scheme != null && scheme.equals("ws")) { if (port == -1) { port = 80; } try { socket = new Socket(host, port); } catch (UnknownHostException uhe) { throw new WebSocketException("unknown host: " + host, uhe); } catch (IOException ioe) { throw new WebSocketException("error while creating socket to " + url, ioe); } } else if (scheme != null && scheme.equals("wss")) { if (port == -1) { port = 443; } try { SocketFactory factory = SSLSocketFactory.getDefault(); socket = factory.createSocket(host, port); } catch (UnknownHostException uhe) { throw new WebSocketException("unknown host: " + host, uhe); } catch (IOException ioe) { throw new WebSocketException("error while creating secure socket to " + url, ioe); } } else { throw new WebSocketException("unsupported protocol: " + scheme); } return socket; } /** * {@inheritDoc } */ @Override public boolean isConnected() { if (connected && status.equals(WebSocketStatus.OPEN)) { return true; } return false; } /** * {@inheritDoc } */ public WebSocketStatus getConnectionStatus() { return status; } /** * @return the client socket */ public Socket getConnectionSocket() { return socket; } /** * {@inheritDoc} */ @Override public void addListener(WebSocketClientListener aListener) { listeners.add(aListener); } /** * {@inheritDoc} */ @Override public void removeListener(WebSocketClientListener aListener) { listeners.remove(aListener); } /** * {@inheritDoc} */ @Override public List<WebSocketClientListener> getListeners() { return Collections.unmodifiableList(listeners); } /** * {@inheritDoc} */ @Override public void notifyOpened(WebSocketClientEvent aEvent) { for (WebSocketClientListener lListener : getListeners()) { lListener.processOpened(aEvent); } } /** * {@inheritDoc} */ @Override public void notifyPacket(WebSocketClientEvent aEvent, WebSocketPacket aPacket) { for (WebSocketClientListener lListener : getListeners()) { lListener.processPacket(aEvent, aPacket); } } /** * {@inheritDoc} */ @Override public void notifyClosed(WebSocketClientEvent aEvent) { for (WebSocketClientListener lListener : getListeners()) { lListener.processClosed(aEvent); } } class WebSocketReceiver extends Thread { private InputStream input = null; private volatile boolean stop = false; public WebSocketReceiver(InputStream input) { this.input = input; } @Override public void run() { boolean frameStart = false; ByteArrayOutputStream baos = new ByteArrayOutputStream(); notifyOpened(null); while (!stop) { try { int b = input.read(); // TODO support binary frames if (b == 0x00) { frameStart = true; } else if (b == 0xff && frameStart == true) { frameStart = false; WebSocketClientEvent lWSCE = new WebSocketClientTokenEvent(); RawPacket lPacket = new RawPacket(baos.toByteArray()); baos.reset(); notifyPacket(lWSCE, lPacket); } else if (frameStart == true) { // messageBytes.add((byte) b); baos.write(b); } else if (b == -1) { handleError(); } } catch (IOException ioe) { handleError(); } } } public void stopit() { stop = true; } public boolean isRunning() { return !stop; } private void handleError() { stopit(); } } }