com.neovisionaries.ws.client.WebSocketFrame Maven / Gradle / Ivy
Show all versions of nv-websocket-client Show documentation
/*
* Copyright (C) 2015-2016 Neo Visionaries Inc.
*
* Licensed 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 com.neovisionaries.ws.client;
import static com.neovisionaries.ws.client.WebSocketOpcode.BINARY;
import static com.neovisionaries.ws.client.WebSocketOpcode.CLOSE;
import static com.neovisionaries.ws.client.WebSocketOpcode.CONTINUATION;
import static com.neovisionaries.ws.client.WebSocketOpcode.PING;
import static com.neovisionaries.ws.client.WebSocketOpcode.PONG;
import static com.neovisionaries.ws.client.WebSocketOpcode.TEXT;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* WebSocket frame.
*
* @see RFC 6455, 5. Data Framing
*/
public class WebSocketFrame
{
private boolean mFin;
private boolean mRsv1;
private boolean mRsv2;
private boolean mRsv3;
private int mOpcode;
private boolean mMask;
private byte[] mPayload;
/**
* Get the value of FIN bit.
*
* @return
* The value of FIN bit.
*/
public boolean getFin()
{
return mFin;
}
/**
* Set the value of FIN bit.
*
* @param fin
* The value of FIN bit.
*
* @return
* {@code this} object.
*/
public WebSocketFrame setFin(boolean fin)
{
mFin = fin;
return this;
}
/**
* Get the value of RSV1 bit.
*
* @return
* The value of RSV1 bit.
*/
public boolean getRsv1()
{
return mRsv1;
}
/**
* Set the value of RSV1 bit.
*
* @param rsv1
* The value of RSV1 bit.
*
* @return
* {@code this} object.
*/
public WebSocketFrame setRsv1(boolean rsv1)
{
mRsv1 = rsv1;
return this;
}
/**
* Get the value of RSV2 bit.
*
* @return
* The value of RSV2 bit.
*/
public boolean getRsv2()
{
return mRsv2;
}
/**
* Set the value of RSV2 bit.
*
* @param rsv2
* The value of RSV2 bit.
*
* @return
* {@code this} object.
*/
public WebSocketFrame setRsv2(boolean rsv2)
{
mRsv2 = rsv2;
return this;
}
/**
* Get the value of RSV3 bit.
*
* @return
* The value of RSV3 bit.
*/
public boolean getRsv3()
{
return mRsv3;
}
/**
* Set the value of RSV3 bit.
*
* @param rsv3
* The value of RSV3 bit.
*
* @return
* {@code this} object.
*/
public WebSocketFrame setRsv3(boolean rsv3)
{
mRsv3 = rsv3;
return this;
}
/**
* Get the opcode.
*
*
* WebSocket opcode
*
*
* Value
* Description
*
*
*
*
* 0x0
* Frame continuation
*
*
* 0x1
* Text frame
*
*
* 0x2
* Binary frame
*
*
* 0x3-0x7
* Reserved
*
*
* 0x8
* Connection close
*
*
* 0x9
* Ping
*
*
* 0xA
* Pong
*
*
* 0xB-0xF
* Reserved
*
*
*
*
* @return
* The opcode.
*
* @see WebSocketOpcode
*/
public int getOpcode()
{
return mOpcode;
}
/**
* Set the opcode
*
* @param opcode
* The opcode.
*
* @return
* {@code this} object.
*
* @see WebSocketOpcode
*/
public WebSocketFrame setOpcode(int opcode)
{
mOpcode = opcode;
return this;
}
/**
* Check if this frame is a continuation frame.
*
*
* This method returns {@code true} when the value of the
* opcode is 0x0 ({@link WebSocketOpcode#CONTINUATION}).
*
*
* @return
* {@code true} if this frame is a continuation frame
* (= if the opcode is 0x0).
*/
public boolean isContinuationFrame()
{
return (mOpcode == CONTINUATION);
}
/**
* Check if this frame is a text frame.
*
*
* This method returns {@code true} when the value of the
* opcode is 0x1 ({@link WebSocketOpcode#TEXT}).
*
*
* @return
* {@code true} if this frame is a text frame
* (= if the opcode is 0x1).
*/
public boolean isTextFrame()
{
return (mOpcode == TEXT);
}
/**
* Check if this frame is a binary frame.
*
*
* This method returns {@code true} when the value of the
* opcode is 0x2 ({@link WebSocketOpcode#BINARY}).
*
*
* @return
* {@code true} if this frame is a binary frame
* (= if the opcode is 0x2).
*/
public boolean isBinaryFrame()
{
return (mOpcode == BINARY);
}
/**
* Check if this frame is a close frame.
*
*
* This method returns {@code true} when the value of the
* opcode is 0x8 ({@link WebSocketOpcode#CLOSE}).
*
*
* @return
* {@code true} if this frame is a close frame
* (= if the opcode is 0x8).
*/
public boolean isCloseFrame()
{
return (mOpcode == CLOSE);
}
/**
* Check if this frame is a ping frame.
*
*
* This method returns {@code true} when the value of the
* opcode is 0x9 ({@link WebSocketOpcode#PING}).
*
*
* @return
* {@code true} if this frame is a ping frame
* (= if the opcode is 0x9).
*/
public boolean isPingFrame()
{
return (mOpcode == PING);
}
/**
* Check if this frame is a pong frame.
*
*
* This method returns {@code true} when the value of the
* opcode is 0xA ({@link WebSocketOpcode#PONG}).
*
*
* @return
* {@code true} if this frame is a pong frame
* (= if the opcode is 0xA).
*/
public boolean isPongFrame()
{
return (mOpcode == PONG);
}
/**
* Check if this frame is a data frame.
*
*
* This method returns {@code true} when the value of the
* opcode is in between 0x1 and 0x7.
*
*
* @return
* {@code true} if this frame is a data frame
* (= if the opcode is in between 0x1 and 0x7).
*/
public boolean isDataFrame()
{
return (0x1 <= mOpcode && mOpcode <= 0x7);
}
/**
* Check if this frame is a control frame.
*
*
* This method returns {@code true} when the value of the
* opcode is in between 0x8 and 0xF.
*
*
* @return
* {@code true} if this frame is a control frame
* (= if the opcode is in between 0x8 and 0xF).
*/
public boolean isControlFrame()
{
return (0x8 <= mOpcode && mOpcode <= 0xF);
}
/**
* Get the value of MASK bit.
*
* @return
* The value of MASK bit.
*/
boolean getMask()
{
return mMask;
}
/**
* Set the value of MASK bit.
*
* @param mask
* The value of MASK bit.
*
* @return
* {@code this} object.
*/
WebSocketFrame setMask(boolean mask)
{
mMask = mask;
return this;
}
/**
* Check if this frame has payload.
*
* @return
* {@code true} if this frame has payload.
*/
public boolean hasPayload()
{
return mPayload != null;
}
/**
* Get the payload length.
*
* @return
* The payload length.
*/
public int getPayloadLength()
{
if (mPayload == null)
{
return 0;
}
return mPayload.length;
}
/**
* Get the unmasked payload.
*
* @return
* The unmasked payload. {@code null} may be returned.
*/
public byte[] getPayload()
{
return mPayload;
}
/**
* Get the unmasked payload as a text.
*
* @return
* A string constructed by interrupting the payload
* as a UTF-8 bytes.
*/
public String getPayloadText()
{
if (mPayload == null)
{
return null;
}
return Misc.toStringUTF8(mPayload);
}
/**
* Set the unmasked payload.
*
*
* Note that the payload length of a control frame must be 125 bytes or less.
*
*
* @param payload
* The unmasked payload. {@code null} is accepted.
* An empty byte array is treated in the same way
* as {@code null}.
*
* @return
* {@code this} object.
*/
public WebSocketFrame setPayload(byte[] payload)
{
if (payload != null && payload.length == 0)
{
payload = null;
}
mPayload = payload;
return this;
}
/**
* Set the payload. The given string is converted to a byte array
* in UTF-8 encoding.
*
*
* Note that the payload length of a control frame must be 125 bytes or less.
*
*
* @param payload
* The unmasked payload. {@code null} is accepted.
* An empty string is treated in the same way as
* {@code null}.
*
* @return
* {@code this} object.
*/
public WebSocketFrame setPayload(String payload)
{
if (payload == null || payload.length() == 0)
{
return setPayload((byte[])null);
}
return setPayload(Misc.getBytesUTF8(payload));
}
/**
* Set the payload that conforms to the payload format of close frames.
*
*
* The given parameters are encoded based on the rules described in
* "5.5.1. Close" of RFC 6455.
*
*
*
* Note that the reason should not be too long because the payload
* length of a control frame must be 125 bytes or less.
*
*
* @param closeCode
* The close code.
*
* @param reason
* The reason. {@code null} is accepted. An empty string
* is treated in the same way as {@code null}.
*
* @return
* {@code this} object.
*
* @see RFC 6455, 5.5.1. Close
*
* @see WebSocketCloseCode
*/
public WebSocketFrame setCloseFramePayload(int closeCode, String reason)
{
// Convert the close code to a 2-byte unsigned integer
// in network byte order.
byte[] encodedCloseCode = new byte[] {
(byte)((closeCode >> 8) & 0xFF),
(byte)((closeCode ) & 0xFF)
};
// If a reason string is not given.
if (reason == null || reason.length() == 0)
{
// Use the close code only.
return setPayload(encodedCloseCode);
}
// Convert the reason into a byte array.
byte[] encodedReason = Misc.getBytesUTF8(reason);
// Concatenate the close code and the reason.
byte[] payload = new byte[2 + encodedReason.length];
System.arraycopy(encodedCloseCode, 0, payload, 0, 2);
System.arraycopy(encodedReason, 0, payload, 2, encodedReason.length);
// Use the concatenated string.
return setPayload(payload);
}
/**
* Parse the first two bytes of the payload as a close code.
*
*
* If any payload is not set or the length of the payload is less than 2,
* this method returns 1005 ({@link WebSocketCloseCode#NONE}).
*
*
*
* The value returned from this method is meaningless if this frame
* is not a close frame.
*
*
* @return
* The close code.
*
* @see RFC 6455, 5.5.1. Close
*
* @see WebSocketCloseCode
*/
public int getCloseCode()
{
if (mPayload == null || mPayload.length < 2)
{
return WebSocketCloseCode.NONE;
}
// A close code is encoded in network byte order.
int closeCode = (((mPayload[0] & 0xFF) << 8) | (mPayload[1] & 0xFF));
return closeCode;
}
/**
* Parse the third and subsequent bytes of the payload as a close reason.
*
*
* If any payload is not set or the length of the payload is less than 3,
* this method returns {@code null}.
*
*
*
* The value returned from this method is meaningless if this frame
* is not a close frame.
*
*
* @return
* The close reason.
*/
public String getCloseReason()
{
if (mPayload == null || mPayload.length < 3)
{
return null;
}
return Misc.toStringUTF8(mPayload, 2, mPayload.length - 2);
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder()
.append("WebSocketFrame(FIN=").append(mFin ? "1" : "0")
.append(",RSV1=").append(mRsv1 ? "1" : "0")
.append(",RSV2=").append(mRsv2 ? "1" : "0")
.append(",RSV3=").append(mRsv3 ? "1" : "0")
.append(",Opcode=").append(Misc.toOpcodeName(mOpcode))
.append(",Length=").append(getPayloadLength());
switch (mOpcode)
{
case TEXT:
appendPayloadText(builder);
break;
case BINARY:
appendPayloadBinary(builder);
break;
case CLOSE:
appendPayloadClose(builder);
break;
}
return builder.append(")").toString();
}
private boolean appendPayloadCommon(StringBuilder builder)
{
builder.append(",Payload=");
if (mPayload == null)
{
builder.append("null");
// Nothing more to append.
return true;
}
if (mRsv1)
{
// In the current implementation, mRsv1=true is allowed
// only when Per-Message Compression is applied.
builder.append("compressed");
// Nothing more to append.
return true;
}
// Continue.
return false;
}
private void appendPayloadText(StringBuilder builder)
{
if (appendPayloadCommon(builder))
{
// Nothing more to append.
return;
}
builder.append("\"");
builder.append(getPayloadText());
builder.append("\"");
}
private void appendPayloadClose(StringBuilder builder)
{
builder
.append(",CloseCode=").append(getCloseCode())
.append(",Reason=");
String reason = getCloseReason();
if (reason == null)
{
builder.append("null");
}
else
{
builder.append("\"").append(reason).append("\"");
}
}
private void appendPayloadBinary(StringBuilder builder)
{
if (appendPayloadCommon(builder))
{
// Nothing more to append.
return;
}
for (int i = 0; i < mPayload.length; ++i)
{
builder.append(String.format("%02X ", (0xFF & mPayload[i])));
}
if (mPayload.length != 0)
{
// Remove the last space.
builder.setLength(builder.length() - 1);
}
}
/**
* Create a continuation frame. Note that the FIN bit of the
* returned frame is false.
*
* @return
* A WebSocket frame whose FIN bit is false, opcode is
* {@link WebSocketOpcode#CONTINUATION CONTINUATION} and
* payload is {@code null}.
*/
public static WebSocketFrame createContinuationFrame()
{
return new WebSocketFrame()
.setOpcode(CONTINUATION);
}
/**
* Create a continuation frame. Note that the FIN bit of the
* returned frame is false.
*
* @param payload
* The payload for a newly create frame.
*
* @return
* A WebSocket frame whose FIN bit is false, opcode is
* {@link WebSocketOpcode#CONTINUATION CONTINUATION} and
* payload is the given one.
*/
public static WebSocketFrame createContinuationFrame(byte[] payload)
{
return createContinuationFrame().setPayload(payload);
}
/**
* Create a continuation frame. Note that the FIN bit of the
* returned frame is false.
*
* @param payload
* The payload for a newly create frame.
*
* @return
* A WebSocket frame whose FIN bit is false, opcode is
* {@link WebSocketOpcode#CONTINUATION CONTINUATION} and
* payload is the given one.
*/
public static WebSocketFrame createContinuationFrame(String payload)
{
return createContinuationFrame().setPayload(payload);
}
/**
* Create a text frame.
*
* @param payload
* The payload for a newly created frame.
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#TEXT TEXT} and payload is
* the given one.
*/
public static WebSocketFrame createTextFrame(String payload)
{
return new WebSocketFrame()
.setFin(true)
.setOpcode(TEXT)
.setPayload(payload);
}
/**
* Create a binary frame.
*
* @param payload
* The payload for a newly created frame.
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#BINARY BINARY} and payload is
* the given one.
*/
public static WebSocketFrame createBinaryFrame(byte[] payload)
{
return new WebSocketFrame()
.setFin(true)
.setOpcode(BINARY)
.setPayload(payload);
}
/**
* Create a close frame.
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#CLOSE CLOSE} and payload is
* {@code null}.
*/
public static WebSocketFrame createCloseFrame()
{
return new WebSocketFrame()
.setFin(true)
.setOpcode(CLOSE);
}
/**
* Create a close frame.
*
* @param closeCode
* The close code.
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#CLOSE CLOSE} and payload
* contains a close code.
*
* @see WebSocketCloseCode
*/
public static WebSocketFrame createCloseFrame(int closeCode)
{
return createCloseFrame().setCloseFramePayload(closeCode, null);
}
/**
* Create a close frame.
*
* @param closeCode
* The close code.
*
* @param reason
* The close reason.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#CLOSE CLOSE} and payload
* contains a close code and a close reason.
*
* @see WebSocketCloseCode
*/
public static WebSocketFrame createCloseFrame(int closeCode, String reason)
{
return createCloseFrame().setCloseFramePayload(closeCode, reason);
}
/**
* Create a ping frame.
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#PING PING} and payload is
* {@code null}.
*/
public static WebSocketFrame createPingFrame()
{
return new WebSocketFrame()
.setFin(true)
.setOpcode(PING);
}
/**
* Create a ping frame.
*
* @param payload
* The payload for a newly created frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#PING PING} and payload is
* the given one.
*/
public static WebSocketFrame createPingFrame(byte[] payload)
{
return createPingFrame().setPayload(payload);
}
/**
* Create a ping frame.
*
* @param payload
* The payload for a newly created frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#PING PING} and payload is
* the given one.
*/
public static WebSocketFrame createPingFrame(String payload)
{
return createPingFrame().setPayload(payload);
}
/**
* Create a pong frame.
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#PONG PONG} and payload is
* {@code null}.
*/
public static WebSocketFrame createPongFrame()
{
return new WebSocketFrame()
.setFin(true)
.setOpcode(PONG);
}
/**
* Create a pong frame.
*
* @param payload
* The payload for a newly created frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#PONG PONG} and payload is
* the given one.
*/
public static WebSocketFrame createPongFrame(byte[] payload)
{
return createPongFrame().setPayload(payload);
}
/**
* Create a pong frame.
*
* @param payload
* The payload for a newly created frame.
* Note that a control frame's payload length must be 125 bytes or less
* (RFC 6455, 5.5. Control Frames).
*
* @return
* A WebSocket frame whose FIN bit is true, opcode is
* {@link WebSocketOpcode#PONG PONG} and payload is
* the given one.
*/
public static WebSocketFrame createPongFrame(String payload)
{
return createPongFrame().setPayload(payload);
}
/**
* Mask/unmask payload.
*
*
* The logic of masking/unmasking is described in "5.3.
* Client-to-Server Masking" in RFC 6455.
*
*
* @param maskingKey
* The masking key. If {@code null} is given or the length
* of the masking key is less than 4, nothing is performed.
*
* @param payload
* Payload to be masked/unmasked.
*
* @return
* {@code payload}.
*
* @see 5.3. Client-to-Server Masking
*/
static byte[] mask(byte[] maskingKey, byte[] payload)
{
if (maskingKey == null || maskingKey.length < 4 || payload == null)
{
return payload;
}
for (int i = 0; i < payload.length; ++i)
{
payload[i] ^= maskingKey[i % 4];
}
return payload;
}
static WebSocketFrame compressFrame(WebSocketFrame frame, PerMessageCompressionExtension pmce)
{
// If Per-Message Compression is not enabled.
if (pmce == null)
{
// No compression.
return frame;
}
// If the frame is neither a TEXT frame nor a BINARY frame.
if (frame.isTextFrame() == false &&
frame.isBinaryFrame() == false)
{
// No compression.
return frame;
}
// If the frame is not the final frame.
if (frame.getFin() == false)
{
// The compression must be applied to this frame and
// all the subsequent continuation frames, but the
// current implementation does not support the behavior.
return frame;
}
// If the RSV1 bit is set.
if (frame.getRsv1())
{
// In the current implementation, RSV1=true is allowed
// only as Per-Message Compressed Bit (See RFC 7692,
// 6. Framing). Therefore, RSV1=true here is regarded
// as "already compressed".
return frame;
}
// The plain payload before compression.
byte[] payload = frame.getPayload();
// If the payload is empty.
if (payload == null || payload.length == 0)
{
// No compression.
return frame;
}
// Compress the payload.
byte[] compressed = compress(payload, pmce);
// If the length of the compressed data is not less than
// that of the original plain payload.
if (payload.length <= compressed.length)
{
// It's better not to compress the payload.
return frame;
}
// Replace the plain payload with the compressed data.
frame.setPayload(compressed);
// Set Per-Message Compressed Bit (See RFC 7692, 6. Framing).
frame.setRsv1(true);
return frame;
}
private static byte[] compress(byte[] data, PerMessageCompressionExtension pmce)
{
try
{
// Compress the data.
return pmce.compress(data);
}
catch (WebSocketException e)
{
// Failed to compress the data. Ignore this error and use
// the plain original data. The current implementation
// does not call any listener callback method for this error.
return data;
}
}
static List splitIfNecessary(
WebSocketFrame frame, int maxPayloadSize, PerMessageCompressionExtension pmce)
{
// If the maximum payload size is not specified.
if (maxPayloadSize == 0)
{
// Not split.
return null;
}
// If the total length of the payload is equal to or
// less than the maximum payload size.
if (frame.getPayloadLength() <= maxPayloadSize)
{
// Not split.
return null;
}
// If the frame is a binary frame or a text frame.
if (frame.isBinaryFrame() || frame.isTextFrame())
{
// Try to compress the frame. In the current implementation, binary
// frames and text frames with the FIN bit true can be compressed.
// The compressFrame() method may change the payload and the RSV1
// bit of the given frame.
frame = compressFrame(frame, pmce);
// If the payload length of the frame has become equal to or less
// than the maximum payload size as a result of the compression.
if (frame.getPayloadLength() <= maxPayloadSize)
{
// Not split. (Note that the frame has been compressed)
return null;
}
}
else if (frame.isContinuationFrame() == false)
{
// Control frames (Close/Ping/Pong) are not split.
return null;
}
// Split the frame.
return split(frame, maxPayloadSize);
}
private static List split(WebSocketFrame frame, int maxPayloadSize)
{
// The original payload and the original FIN bit.
byte[] originalPayload = frame.getPayload();
boolean originalFin = frame.getFin();
List frames = new ArrayList();
// Generate the first frame using the existing WebSocketFrame instance.
// Note that the reserved bit 1 and the opcode are untouched.
byte[] payload = Arrays.copyOf(originalPayload, maxPayloadSize);
frame.setFin(false).setPayload(payload);
frames.add(frame);
for (int from = maxPayloadSize; from < originalPayload.length; from += maxPayloadSize)
{
// Prepare the payload of the next continuation frame.
int to = Math.min(from + maxPayloadSize, originalPayload.length);
payload = Arrays.copyOfRange(originalPayload, from, to);
// Create a continuation frame.
WebSocketFrame cont = WebSocketFrame.createContinuationFrame(payload);
frames.add(cont);
}
if (originalFin)
{
// Set the FIN bit of the last frame.
frames.get(frames.size() - 1).setFin(true);
}
return frames;
}
}