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

tech.gusavila92.websocketclient.WebSocketClient Maven / Gradle / Ivy

Go to download

A very lightweight WebSocket client library for Java/Android which aims to implement the WebSocket protocol as defined in RFC 6455

There is a newer version: 1.2.2
Show newest version
package tech.gusavila92.websocketclient;

import tech.gusavila92.apache.commons.codec.binary.Base64;
import tech.gusavila92.apache.commons.codec.digest.DigestUtils;
import tech.gusavila92.apache.http.Header;
import tech.gusavila92.apache.http.HttpException;
import tech.gusavila92.apache.http.HttpResponse;
import tech.gusavila92.apache.http.StatusLine;
import tech.gusavila92.apache.http.impl.io.DefaultHttpResponseParser;
import tech.gusavila92.apache.http.impl.io.HttpTransportMetricsImpl;
import tech.gusavila92.apache.http.impl.io.SessionInputBufferImpl;
import tech.gusavila92.apache.http.io.HttpMessageParser;
import tech.gusavila92.websocketclient.common.Utils;
import tech.gusavila92.websocketclient.exceptions.UnknownOpcodeException;
import tech.gusavila92.websocketclient.exceptions.IllegalSchemeException;
import tech.gusavila92.websocketclient.exceptions.InvalidServerHandshakeException;
import tech.gusavila92.websocketclient.model.Payload;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

/**
 * Implements the WebSocket protocol as defined in RFC 6455
 * 
 * @author Gustavo Avila
 *
 */
public abstract class WebSocketClient {
	/**
	 * GUID used when processing Sec-WebSocket-Accept response header
	 */
	private static final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

	/**
	 * Denotes a continuation frame
	 */
	private static final int OPCODE_CONTINUATION = 0x0;

	/**
	 * Denotes a UTF-8 encoded text frame
	 */
	private static final int OPCODE_TEXT = 0x1;

	/**
	 * Denotes a binary frame
	 */
	private static final int OPCODE_BINARY = 0x2;

	/**
	 * Denotes a close frame
	 */
	private static final int OPCODE_CLOSE = 0x8;

	/**
	 * Denotes a Ping frame
	 */
	private static final int OPCODE_PING = 0x9;

	/**
	 * Denotes a Pong frame
	 */
	private static final int OPCODE_PONG = 0xA;

	/**
	 * Global lock for synchronized statements
	 */
	private final Object globalLock;

	/**
	 * Connection URI
	 */
	private final URI uri;

	/**
	 * Cryptographically secure random generator used for the masking key
	 */
	private final SecureRandom secureRandom;

	/**
	 * Timeout in milliseconds to be used while the WebSocket is being connected
	 */
	private int connectTimeout;

	/**
	 * Timeout in milliseconds for considering and idle connection as dead An
	 * idle connection is a connection that has not received data for a long
	 * time
	 */
	private int readTimeout;

	/**
	 * Indicates if a connection must be reopened automatically due to an
	 * IOException
	 */
	private boolean automaticReconnection;

	/**
	 * Time in milliseconds to wait before opening a new WebSocket connection
	 */
	private long waitTimeBeforeReconnection;

	/**
	 * Indicates if the connect() method was called
	 */
	private volatile boolean isRunning;

	/**
	 * Custom headers to be included into the handshake
	 */
	private Map headers;

	/**
	 * Underlying WebSocket connection This instance could change due to an
	 * automatic reconnection Every time an automatic reconnection is fired,
	 * this reference changes
	 */
	private volatile WebSocketConnection webSocketConnection;

	/**
	 * Thread used for reconnection intents
	 */
	private volatile Thread reconnectionThread;

	/**
	 * Initialize all the variables
	 *
	 * @param uri
	 *            URI of the WebSocket server
	 */
	public WebSocketClient(URI uri) {
		this.globalLock = new Object();
		this.uri = uri;
		this.secureRandom = new SecureRandom();
		this.connectTimeout = 0;
		this.readTimeout = 0;
		this.automaticReconnection = false;
		this.waitTimeBeforeReconnection = 0;
		this.isRunning = false;
		webSocketConnection = new WebSocketConnection();
	}

	/**
	 * Called when the WebSocket handshake has been accepted and the WebSocket
	 * is ready to send and receive data
	 */
	public abstract void onOpen();

	/**
	 * Called when a text message has been received
	 *
	 * @param message
	 *            The UTF-8 encoded text received
	 */
	public abstract void onTextReceived(String message);

	/**
	 * Called when a binary message has been received
	 *
	 * @param data
	 *            The binary message received
	 */
	public abstract void onBinaryReceived(byte[] data);

	/**
	 * Called when a ping message has been received
	 *
	 * @param data
	 *            Optional data
	 */
	public abstract void onPingReceived(byte[] data);

	/**
	 * Called when a pong message has been received
	 * 
	 * @param data
	 *            Optional data
	 */
	public abstract void onPongReceived(byte[] data);

	/**
	 * Called when an exception has occurred
	 *
	 * @param e
	 *            The exception that occurred
	 */
	public abstract void onException(Exception e);

	/**
	 * Called when a close code has been received
	 */
	public abstract void onCloseReceived();

	/**
	 * Adds a new header to the set of headers that will be send into the
	 * handshake This header will be added to the set of headers: Host, Upgrade,
	 * Connection, Sec-WebSocket-Key, Sec-WebSocket-Version
	 * 
	 * @param key
	 *            Name of the new header
	 * @param value
	 *            Value of the new header
	 */
	public void addHeader(String key, String value) {
		synchronized (globalLock) {
			if (isRunning) {
				throw new IllegalStateException("Cannot add header while WebSocketClient is running");
			}
			this.headers.put(key, value);
		}
	}

	/**
	 * Set the timeout that will be used while the WebSocket is being connected
	 * If timeout expires before connecting, an IOException will be thrown
	 * 
	 * @param connectTimeout
	 *            Timeout in milliseconds
	 */
	public void setConnectTimeout(int connectTimeout) {
		synchronized (globalLock) {
			if (isRunning) {
				throw new IllegalStateException("Cannot set connect timeout while WebSocketClient is running");
			} else if (connectTimeout < 0) {
				throw new IllegalStateException("Connect timeout must be greater or equal than zero");
			}
			this.connectTimeout = connectTimeout;
		}
	}

	/**
	 * Sets the timeout for considering and idle connection as dead An idle
	 * connection is a connection that has not received data for a long time If
	 * timeout expires, an IOException will be thrown and you should consider
	 * opening a new WebSocket connection, or delegate this functionality to
	 * this WebSocketClient using the method setAutomaticReconnection(true)
	 * 
	 * @param readTimeout
	 *            Read timeout in milliseconds before considering an idle
	 *            connection as dead
	 */
	public void setReadTimeout(int readTimeout) {
		synchronized (globalLock) {
			if (isRunning) {
				throw new IllegalStateException("Cannot set read timeout while WebSocketClient is running");
			} else if (readTimeout < 0) {
				throw new IllegalStateException("Read timeout must be greater or equal than zero");
			}
			this.readTimeout = readTimeout;
		}
	}

	/**
	 * Indicates that a connection must be reopened automatically due to an
	 * IOException. Every time a connection fails due to an IOException,
	 * onException() method is called before establishing a new connection. A
	 * connection will be reopened automatically if an IOException occurred, but
	 * other kinds of Exception will not reopen a connection
	 * 
	 * @param waitTimeBeforeReconnection
	 *            Wait time in milliseconds before trying to establish a new
	 *            WebSocket connection. For performance reasons, you should set
	 *            a wait time greater than zero
	 */
	public void enableAutomaticReconnection(long waitTimeBeforeReconnection) {
		synchronized (globalLock) {
			if (isRunning) {
				throw new IllegalStateException(
						"Cannot enable automatic reconnection while WebSocketClient is running");
			} else if (waitTimeBeforeReconnection < 0) {
				throw new IllegalStateException("Wait time between reconnections must be greater or equal than zero");
			}
			this.automaticReconnection = true;
			this.waitTimeBeforeReconnection = waitTimeBeforeReconnection;
		}
	}

	/**
	 * Indicates that a connection must not be reopened automatically due to an
	 * IOException
	 */
	public void disableAutomaticReconnection() {
		synchronized (globalLock) {
			if (isRunning) {
				throw new IllegalStateException(
						"Cannot disable automatic reconnection while WebSocketClient is running");
			}
			this.automaticReconnection = false;
		}
	}

	/**
	 * Starts a new connection to the WebSocket server
	 */
	public void connect() {
		synchronized (globalLock) {
			if (isRunning) {
				throw new IllegalStateException("WebSocketClient is not reusable");
			}

			this.isRunning = true;
			createAndStartConnectionThread();
		}
	}

	/**
	 * Creates and starts the thread that will handle the WebSocket connection
	 */
	private void createAndStartConnectionThread() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					boolean success = webSocketConnection.createAndConnectTCPSocket();
					if (success) {
						webSocketConnection.startConnection();
					}
				} catch (Exception e) {
					synchronized (globalLock) {
						if (isRunning) {
							webSocketConnection.closeInternal();

							onException(e);

							if (e instanceof IOException && automaticReconnection) {
								createAndStartReconnectionThread();
							}
						}
					}
				}
			}
		}).start();
	}

	/**
	 * Creates and starts the thread that will open a new WebSocket connection
	 */
	private void createAndStartReconnectionThread() {
		reconnectionThread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(waitTimeBeforeReconnection);

					synchronized (globalLock) {
						if (isRunning) {
							webSocketConnection = new WebSocketConnection();
							createAndStartConnectionThread();
						}
					}
				} catch (InterruptedException e) {
					// Expected behavior when the WebSocket connection is closed
				}
			}
		});
		reconnectionThread.start();
	}

	/**
	 * If the close method wasn't called, call onOpen method.
	 */
	private void notifyOnOpen() {
		synchronized (globalLock) {
			if (isRunning) {
				onOpen();
			}
		}
	}

	/**
	 * If the close method wasn't called, call onTextReceived(String message)
	 * method.
	 */
	private void notifyOnTextReceived(String message) {
		synchronized (globalLock) {
			if (isRunning) {
				onTextReceived(message);
			}
		}
	}

	/**
	 * If the close method wasn't called, call onBinaryReceived(byte[] data)
	 * method.
	 */
	private void notifyOnBinaryReceived(byte[] data) {
		synchronized (globalLock) {
			if (isRunning) {
				onBinaryReceived(data);
			}
		}
	}

	/**
	 * If the close method wasn't called, call onPingReceived(byte[] data)
	 * method.
	 */
	private void notifyOnPingReceived(byte[] data) {
		synchronized (globalLock) {
			if (isRunning) {
				onPingReceived(data);
			}
		}
	}

	/**
	 * If the close method wasn't called, call onPongReceived(byte[] data)
	 * method.
	 */
	private void notifyOnPongReceived(byte[] data) {
		synchronized (globalLock) {
			if (isRunning) {
				onPongReceived(data);
			}
		}
	}

	/**
	 * If the close method wasn't called, call onException(Exception e) method.
	 */
	private void notifyOnException(Exception e) {
		synchronized (globalLock) {
			if (isRunning) {
				onException(e);
			}
		}
	}

	/**
	 * If the close method wasn't called, call onCloseReceived() method.
	 */
	private void notifyOnCloseReceived() {
		synchronized (globalLock) {
			if (isRunning) {
				onCloseReceived();
			}
		}
	}

	/**
	 * Sends a text message If the WebSocket is not connected yet, message will
	 * be send the next time the connection is opened
	 *
	 * @param message
	 *            Message that will be send to the WebSocket server
	 */
	public void send(String message) {
		byte[] data = message.getBytes(Charset.forName("UTF-8"));
		final Payload payload = new Payload(OPCODE_TEXT, data);

		new Thread(new Runnable() {
			@Override
			public void run() {
				webSocketConnection.sendInternal(payload);
			}

		}).start();
	}

	/**
	 * Sends a binary message If the WebSocket is not connected yet, message
	 * will be send the next time the connection is opened
	 *
	 * @param data
	 *            Binary data that will be send to the WebSocket server
	 */
	public void send(byte[] data) {
		final Payload payload = new Payload(OPCODE_BINARY, data);

		new Thread(new Runnable() {
			@Override
			public void run() {
				webSocketConnection.sendInternal(payload);
			}
		}).start();
	}

	/**
	 * Closes the WebSocket connection
	 */
	public void close() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (globalLock) {
					isRunning = false;

					if (reconnectionThread != null) {
						reconnectionThread.interrupt();
					}

					webSocketConnection.closeInternal();
				}
			}
		}).start();
	}

	/**
	 * This represents an existing WebSocket connection
	 * 
	 * @author Gustavo Avila
	 *
	 */
	private class WebSocketConnection {
		/**
		 * Flag indicating if there are pending changes waiting to be read by
		 * the writer thread. It is used to avoid a missed signal between
		 * threads
		 */
		private volatile boolean pendingMessages;

		/**
		 * Flag indicating if the closeInternal() method was called
		 */
		private volatile boolean isClosed;

		/**
		 * Data waiting to be read from the writer thread
		 */
		private final LinkedList outBuffer;

		/**
		 * This will act as a lock for synchronized statements
		 */
		private final Object internalLock;

		/**
		 * Writer thread
		 */
		private final Thread writerThread;

		/**
		 * TCP socket for the underlying WebSocket connection
		 */
		private Socket socket;

		/**
		 * Socket input stream
		 */
		private BufferedInputStream bis;

		/**
		 * Socket output stream
		 */
		private BufferedOutputStream bos;

		/**
		 * Initialize the variables that will be used during a valid WebSocket
		 * connection
		 */
		private WebSocketConnection() {
			this.pendingMessages = false;
			this.isClosed = false;
			this.outBuffer = new LinkedList();
			this.internalLock = new Object();

			this.writerThread = new Thread(new Runnable() {
				@Override
				public void run() {
					synchronized (internalLock) {
						while (true) {
							if (!pendingMessages) {
								try {
									internalLock.wait();
								} catch (InterruptedException e) {
									// This should never happen
								}
							}

							pendingMessages = false;

							if (socket.isClosed()) {
								return;
							} else {
								while (outBuffer.size() > 0) {
									Payload payload = outBuffer.removeFirst();
									int opcode = payload.getOpcode();
									byte[] data = payload.getData();

									try {
										send(opcode, data);
									} catch (IOException e) {
										// Reader thread will notify this
										// exception
										// This thread just need to stop
										return;
									}
								}
							}
						}
					}
				}
			});
		}

		/**
		 * Creates and connects a TCP socket for the underlying connection
		 *
		 * @return true is the socket was successfully connected, false
		 *         otherwise
		 * @throws IOException
		 */
		private boolean createAndConnectTCPSocket() throws IOException {
			synchronized (internalLock) {
				if (!isClosed) {
					String scheme = uri.getScheme();
					int port = uri.getPort();
					if (scheme != null) {
						if (scheme.equals("ws")) {
							SocketFactory socketFactory = SocketFactory.getDefault();
							socket = socketFactory.createSocket();
							socket.setSoTimeout(readTimeout);

							if (port != -1) {
								socket.connect(new InetSocketAddress(uri.getHost(), port), connectTimeout);
							} else {
								socket.connect(new InetSocketAddress(uri.getHost(), 80), connectTimeout);
							}
						} else if (scheme.equals("wss")) {
							SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
							socket = socketFactory.createSocket();
							socket.setSoTimeout(readTimeout);

							if (port != -1) {
								socket.connect(new InetSocketAddress(uri.getHost(), port), connectTimeout);
							} else {
								socket.connect(new InetSocketAddress(uri.getHost(), 443), connectTimeout);
							}
						} else {
							throw new IllegalSchemeException("The scheme component of the URI should be ws or wss");
						}
					} else {
						throw new IllegalSchemeException("The scheme component of the URI cannot be null");
					}

					return true;
				}

				return false;
			}
		}

		/**
		 * Starts the WebSocket connection
		 *
		 * @throws IOException
		 */
		private void startConnection() throws IOException {
			bos = new BufferedOutputStream(socket.getOutputStream(), 65536);

			byte[] key = new byte[16];
			Random random = new Random();
			random.nextBytes(key);
			String base64Key = Base64.encodeBase64String(key);

			byte[] handshake = createHandshake(base64Key);
			bos.write(handshake);
			bos.flush();

			InputStream inputStream = socket.getInputStream();
			verifyServerHandshake(inputStream, base64Key);

			writerThread.start();

			notifyOnOpen();

			bis = new BufferedInputStream(socket.getInputStream(), 65536);
			read();
		}

		/**
		 * Creates and returns a byte array containing the client handshake
		 *
		 * @param base64Key
		 *            Random generated Sec-WebSocket-Key
		 * @return Byte array containing the client handshake
		 */
		private byte[] createHandshake(String base64Key) {
			StringBuilder builder = new StringBuilder();

			String path = uri.getRawPath();
			String query = uri.getRawQuery();

			String requestUri;
			if (query == null) {
				requestUri = path;
			} else {
				requestUri = path + "?" + query;
			}

			builder.append("GET " + requestUri + " HTTP/1.1");
			builder.append("\r\n");

			String host;
			if (uri.getPort() == -1) {
				host = uri.getHost();
			} else {
				host = uri.getHost() + ":" + uri.getPort();
			}

			builder.append("Host: " + host);
			builder.append("\r\n");

			builder.append("Upgrade: websocket");
			builder.append("\r\n");

			builder.append("Connection: Upgrade");
			builder.append("\r\n");

			builder.append("Sec-WebSocket-Key: " + base64Key);
			builder.append("\r\n");

			builder.append("Sec-WebSocket-Version: 13");
			builder.append("\r\n");

			if (headers != null) {
				for (Map.Entry entry : headers.entrySet()) {
					builder.append(entry.getKey() + ": " + entry.getValue());
					builder.append("\r\n");
				}
			}

			builder.append("\r\n");

			String handshake = builder.toString();
			return handshake.getBytes(Charset.forName("ASCII"));
		}

		/**
		 * Verifies the validity of the server handshake
		 *
		 * @param inputStream
		 *            Socket input stream
		 * @param secWebSocketKey
		 *            Random generated Sec-WebSocket-Key
		 * @throws IOException
		 */
		private void verifyServerHandshake(InputStream inputStream, String secWebSocketKey) throws IOException {
			try {
				SessionInputBufferImpl sessionInputBuffer = new SessionInputBufferImpl(new HttpTransportMetricsImpl(),
						8192);
				sessionInputBuffer.bind(inputStream);
				HttpMessageParser parser = new DefaultHttpResponseParser(sessionInputBuffer);
				HttpResponse response = parser.parse();

				StatusLine statusLine = response.getStatusLine();
				if (statusLine == null) {
					throw new InvalidServerHandshakeException("There is no status line");
				}

				int statusCode = statusLine.getStatusCode();
				if (statusCode != 101) {
					throw new InvalidServerHandshakeException(
							"Invalid status code. Expected 101, received: " + statusCode);
				}

				Header[] upgradeHeader = response.getHeaders("Upgrade");
				if (upgradeHeader.length == 0) {
					throw new InvalidServerHandshakeException("There is no header named Upgrade");
				}
				String upgradeValue = upgradeHeader[0].getValue();
				if (upgradeValue == null) {
					throw new InvalidServerHandshakeException("There is no value for header Upgrade");
				}
				upgradeValue = upgradeValue.toLowerCase();
				if (!upgradeValue.equals("websocket")) {
					throw new InvalidServerHandshakeException(
							"Invalid value for header Upgrade. Expected: websocket, received: " + upgradeValue);
				}

				Header[] connectionHeader = response.getHeaders("Connection");
				if (connectionHeader.length == 0) {
					throw new InvalidServerHandshakeException("There is no header named Connection");
				}
				String connectionValue = connectionHeader[0].getValue();
				if (connectionValue == null) {
					throw new InvalidServerHandshakeException("There is no value for header Connection");
				}
				connectionValue = connectionValue.toLowerCase();
				if (!connectionValue.equals("upgrade")) {
					throw new InvalidServerHandshakeException(
							"Invalid value for header Connection. Expected: upgrade, received: " + connectionValue);
				}

				Header[] secWebSocketAcceptHeader = response.getHeaders("Sec-WebSocket-Accept");
				if (secWebSocketAcceptHeader.length == 0) {
					throw new InvalidServerHandshakeException("There is no header named Sec-WebSocket-Accept");
				}
				String secWebSocketAcceptValue = secWebSocketAcceptHeader[0].getValue();
				if (secWebSocketAcceptValue == null) {
					throw new InvalidServerHandshakeException("There is no value for header Sec-WebSocket-Accept");
				}

				String keyConcatenation = secWebSocketKey + GUID;
				byte[] sha1 = DigestUtils.sha1(keyConcatenation);
				String secWebSocketAccept = Base64.encodeBase64String(sha1);
				if (!secWebSocketAcceptValue.equals(secWebSocketAccept)) {
					throw new InvalidServerHandshakeException(
							"Invalid value for header Sec-WebSocket-Accept. Expected: " + secWebSocketAccept
									+ ", received: " + secWebSocketAcceptValue);
				}
			} catch (HttpException e) {
				throw new InvalidServerHandshakeException(e.getMessage());
			}
		}

		/**
		 * Sends a message to the WebSocket server
		 *
		 * @param opcode
		 *            Message opcode
		 * @param payload
		 *            Message payload
		 * @throws IOException
		 */
		private void send(int opcode, byte[] payload) throws IOException {
			// The position of the data frame in which the next portion of code
			// will start writing bytes
			int nextPosition;

			// The data frame
			byte[] frame;

			// The length of the payload data
			int length = payload.length;

			if (length < 126) {
				// If payload length is less than 126,
				// the frame must have the first two bytes, plus 4 bytes for the
				// masking key
				// plus the length of the payload
				frame = new byte[6 + length];

				// The first two bytes
				frame[0] = (byte) (-128 | opcode);
				frame[1] = (byte) (-128 | length);

				// The masking key will start at position 2
				nextPosition = 2;
			} else if (length < 65536) {
				// If payload length is greater than 126 and less than 65536,
				// the frame must have the first two bytes, plus 2 bytes for the
				// extended payload length,
				// plus 4 bytes for the masking key, plus the length of the
				// payload
				frame = new byte[8 + length];

				// The first two bytes
				frame[0] = (byte) (-128 | opcode);
				frame[1] = -2;

				// Puts the length into the data frame
				byte[] array = Utils.to2ByteArray(length);
				frame[2] = array[0];
				frame[3] = array[1];

				// The masking key will start at position 4
				nextPosition = 4;
			} else {
				// If payload length is greater or equal than 65536,
				// the frame must have the first two bytes, plus 8 bytes for the
				// extended payload length,
				// plus 4 bytes for the masking key, plus the length of the
				// payload
				frame = new byte[14 + length];

				// The first two bytes
				frame[0] = (byte) (-128 | opcode);
				frame[1] = -1;

				// Puts the length into the data frame
				byte[] array = Utils.to8ByteArray(length);
				frame[2] = array[0];
				frame[3] = array[1];
				frame[4] = array[2];
				frame[5] = array[3];
				frame[6] = array[4];
				frame[7] = array[5];
				frame[8] = array[6];
				frame[9] = array[7];

				// The masking key will start at position 10
				nextPosition = 10;
			}

			// Generate a random 4-byte masking key
			byte[] mask = new byte[4];
			secureRandom.nextBytes(mask);

			// Puts the masking key into the data frame
			frame[nextPosition] = mask[0];
			frame[nextPosition + 1] = mask[1];
			frame[nextPosition + 2] = mask[2];
			frame[nextPosition + 3] = mask[3];
			nextPosition += 4;

			// Puts the masked payload data into the data frame
			for (int i = 0; i < length; i++) {
				frame[nextPosition] = ((byte) (payload[i] ^ mask[i % 4]));
				nextPosition++;
			}

			// Sends the data frame
			bos.write(frame);
			bos.flush();
		}

		/**
		 * Listen for changes coming from the WebSocket server
		 *
		 * @throws IOException
		 */
		private void read() throws IOException {
			// The first byte of every data frame
			int firstByte;

			// Loop until there are not more data to be read from the
			// InputStream
			while ((firstByte = bis.read()) != -1) {
				// Data contained in the first byte
				// int fin = (firstByte << 24) >>> 31;
				// int rsv1 = (firstByte << 25) >>> 31;
				// int rsv2 = (firstByte << 26) >>> 31;
				// int rsv3 = (firstByte << 27) >>> 31;
				int opcode = (firstByte << 28) >>> 28;

				// Reads the second byte
				int secondByte = bis.read();
				if (secondByte == -1) {
					throw new IOException("Unexpected end of stream");
				}

				// Data contained in the second byte
				// int mask = (secondByte << 24) >>> 31;
				int payloadLength = (secondByte << 25) >>> 25;

				// If the length of payload data is less than 126, that's the
				// final
				// payload length
				// Otherwise, it must be calculated as follows
				if (payloadLength == 126) {
					// Attempts to read the next 2 bytes
					byte[] nextTwoBytes = new byte[2];
					for (int i = 0; i < 2; i++) {
						byte b = (byte) bis.read();
						if (b == -1) {
							throw new IOException("Unexpected end of stream");
						}
						nextTwoBytes[i] = b;
					}

					// Those last 2 bytes will be interpreted as a 16-bit
					// unsigned
					// integer
					byte[] integer = new byte[] { 0, 0, nextTwoBytes[0], nextTwoBytes[1] };
					payloadLength = Utils.fromByteArray(integer);
				} else if (payloadLength == 127) {
					// Attempts to read the next 8 bytes
					byte[] nextEightBytes = new byte[8];
					for (int i = 0; i < 8; i++) {
						byte b = (byte) bis.read();
						if (b == -1) {
							throw new IOException("Unexpected end of stream");
						}
						nextEightBytes[i] = b;
					}

					// Only the last 4 bytes matter because Java doesn't support
					// arrays with more than 2^31 -1 elements, so a 64-bit
					// unsigned
					// integer cannot be processed
					// Those last 4 bytes will be interpreted as a 32-bit
					// unsigned
					// integer
					byte[] integer = new byte[] { nextEightBytes[4], nextEightBytes[5], nextEightBytes[6],
							nextEightBytes[7] };
					payloadLength = Utils.fromByteArray(integer);
				}

				// Attempts to read the payload data
				byte[] data = new byte[payloadLength];
				for (int i = 0; i < payloadLength; i++) {
					byte b = (byte) bis.read();
					if (b == -1) {
						throw new IOException("Unexpected end of stream");
					}
					data[i] = b;
				}

				// Execute the action depending on the opcode
				switch (opcode) {
				case OPCODE_CONTINUATION:
					// Should be implemented
					break;
				case OPCODE_TEXT:
					notifyOnTextReceived(new String(data, Charset.forName("UTF-8")));
					break;
				case OPCODE_BINARY:
					notifyOnBinaryReceived(data);
					break;
				case OPCODE_CLOSE:
					closeInternal();
					notifyOnCloseReceived();
					return;
				case OPCODE_PING:
					notifyOnPingReceived(data);
					break;
				case OPCODE_PONG:
					notifyOnPongReceived(data);
					break;
				default:
					closeInternal();
					Exception e = new UnknownOpcodeException("Unknown opcode: 0x" + Integer.toHexString(opcode));
					notifyOnException(e);
					return;
				}
			}

			// If there are not more data to be read,
			// and if the connection didn't receive a close frame,
			// an IOException must be thrown because the connection didn't close
			// gracefully
			throw new IOException("Unexpected end of stream");
		}

		/**
		 * Puts the payload into the out buffer and notifies the writer thread
		 * that new data is available
		 * 
		 * @param payload
		 *            Payload to be send to the WebSocket server
		 */
		private void sendInternal(Payload payload) {
			synchronized (internalLock) {
				outBuffer.addLast(payload);
				pendingMessages = true;
				internalLock.notify();
			}
		}

		/**
		 * Closes the underlying WebSocket connection and notifies the writer
		 * thread and the reconnection thread that they must finish
		 */
		private void closeInternal() {
			try {
				synchronized (internalLock) {
					if (!isClosed) {
						isClosed = true;
						if (socket != null) {
							socket.close();
							pendingMessages = true;
							internalLock.notify();
						}
					}
				}
			} catch (IOException e) {
				// This should never happen
			}
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy