// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com>
package com.iwebpp.wspp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.iwebpp.node.EventEmitter2;
import com.iwebpp.node.Util;
import com.iwebpp.node.net.AbstractSocket;
import com.iwebpp.node.stream.Writable.WriteCB;
import com.iwebpp.wspp.WebSocket.SendOptions;
/**
* HyBi Sender implementation
*/
public class Sender
extends EventEmitter2 {
private static final String TAG = "Sender";
private AbstractSocket _socket;
private boolean firstFragment;
private byte[] _randomMask;
protected Sender(AbstractSocket socket) {
this._socket = socket;
this.firstFragment = true;
this._randomMask = null;
}
@SuppressWarnings("unused")
private Sender(){}
/**
* Sends a close instruction to the remote party.
* @throws Exception
*
* @api public
*/
protected boolean close(int code, Object data, boolean mask) throws Exception {
debug(TAG, "close, code:"+code+",data:"+data+",mask:"+mask);
if (code > 0) {
if (!ErrorCodes.isValidErrorCode(code))
throw new Exception("first argument must be a valid error code number");
}
code = code > 0 ? code : 1000;
///var dataBuffer = new Buffer(2 + (data ? Buffer.byteLength(data) : 0));
ByteBuffer dataBuffer = ByteBuffer.allocate(2 + (data!=null ? Util.chunkByteLength(data, "utf8") : 0));
///writeUInt16BE.call(dataBuffer, code, 0);
dataBuffer.order(ByteOrder.BIG_ENDIAN).putShort(0, (short) (code & 0xffff));
///if (dataBuffer.length > 2) dataBuffer.write(data, 2);
// TBD...
if (dataBuffer.capacity() > 2)
BufferUtil.fastCopy(dataBuffer.capacity()-2, Util.chunkToBuffer(data, "utf8"), dataBuffer, 2);
return this.frameAndSend(0x8, dataBuffer, true, mask, null);
}
/**
* Sends a ping message to the remote party.
* @throws Exception
*
* @api public
*/
protected boolean ping(Object data, SendOptions options) throws Exception {
boolean mask = options!=null && options.mask;
return this.frameAndSend(0x9, data!=null ? data : "", true, mask, null);
}
/**
* Sends a pong message to the remote party.
* @throws Exception
*
* @api public
*/
protected boolean pong(Object data, SendOptions options) throws Exception {
boolean mask = options!=null && options.mask;
return this.frameAndSend(0xa, data!=null ? data : "", true, mask, null);
}
/**
* Sends text or binary data to the remote party.
* @throws Exception
*
* @api public
*/
protected boolean send(Object data, SendOptions options, WriteCB cb) throws Exception {
debug(TAG, "send");
boolean finalFragment = options!=null && options.fin == false ? false : true;
boolean mask = options!=null && options.mask;
int opcode = options!=null && options.binary == false ? 1 : 2;
if (this.firstFragment == false)
opcode = 0;
else
this.firstFragment = false;
if (finalFragment) this.firstFragment = true;
return this.frameAndSend(opcode, data, finalFragment, mask, cb);
}
/**
* Frames and sends a piece of data according to the HyBi WebSocket protocol.
* @throws Exception
*
* @api private
*/
private boolean frameAndSend(int opcode, Object data, boolean finalFragment,
boolean maskData, WriteCB cb) throws Exception {
debug(TAG, "frameAndSend,opcode:"+opcode+",data:"+data+",mask:"+maskData);
if (data != null && data instanceof ByteBuffer) {
ByteBuffer bd = (ByteBuffer)data;
String dstr = "";
for (int i = 0; i < bd.capacity(); i ++)
dstr += " "+bd.get(i);
debug(TAG, dstr);
}
boolean canModifyData = false;
boolean out = false;
/*
if (!data) {
try {
out = this._socket.write(new Buffer([opcode | (finalFragment ? 0x80 : 0), 0 | (maskData ? 0x80 : 0)].concat(maskData ? [0, 0, 0, 0] : [])), 'binary', cb);
}
catch (e) {
if (typeof cb == 'function') cb(e);
else this.emit('error', e);
}
return out;
}*/
if (data == null) {
ByteBuffer tbw;
if (maskData) {
tbw = ByteBuffer.allocate(6);
tbw.put((byte) (opcode | (finalFragment ? 0x80 : 0)));
tbw.put((byte) (0 | (maskData ? 0x80 : 0)));
tbw.putInt(0);
} else {
tbw = ByteBuffer.allocate(2);
tbw.put((byte) (opcode | (finalFragment ? 0x80 : 0)));
tbw.put((byte) (0 | (maskData ? 0x80 : 0)));
}
tbw.flip();
try {
out = this._socket.write(tbw, null, cb);
} catch (Exception e) {
if (cb != null) cb.writeDone(e.toString());
else this.emit("error", e.toString());
}
return out;
}
/*
if (!Buffer.isBuffer(data)) {
canModifyData = true;
if (data && (typeof data.byteLength !== 'undefined' || typeof data.buffer !== 'undefined')) {
data = getArrayBuffer(data);
} else {
data = new Buffer(data);
}
}*/
if (!Util.isBuffer(data)) {
if (Util.isString(data)) {
canModifyData = true;
data = Util.chunkToBuffer(data, "utf8");
} else {
if (cb != null) cb.writeDone("Invalid data");
else this.emit("error", "Invalid data");
return out;
}
}
debug(TAG, "frameAndSend ... 1, data:"+data.toString());
int dataLength = Util.chunkByteLength(data, null);
int dataOffset = maskData ? 6 : 2;
int secondByte = dataLength;
if (dataLength >= 65536) {
dataOffset += 8;
secondByte = 127;
}
else if (dataLength > 125) {
dataOffset += 2;
secondByte = 126;
}
boolean mergeBuffers = dataLength < 32768 || (maskData && !canModifyData);
int totalLength = mergeBuffers ? dataLength + dataOffset : dataOffset;
///var outputBuffer = new Buffer(totalLength);
ByteBuffer outputBuffer = ByteBuffer.allocate(totalLength);
///outputBuffer[0] = finalFragment ? opcode | 0x80 : opcode;
outputBuffer.put(0, (byte) (finalFragment ? opcode | 0x80 : opcode));
switch (secondByte) {
case 126:
///writeUInt16BE.call(outputBuffer, dataLength, 2);
outputBuffer.order(ByteOrder.BIG_ENDIAN).putShort(2, (short) dataLength);
break;
case 127:
///writeUInt32BE.call(outputBuffer, 0, 2);
///writeUInt32BE.call(outputBuffer, dataLength, 6);
outputBuffer.order(ByteOrder.BIG_ENDIAN).putInt(2, 0);
outputBuffer.order(ByteOrder.BIG_ENDIAN).putInt(6, dataLength);
break;
}
if (maskData) {
///outputBuffer[1] = secondByte | 0x80;
outputBuffer.put(1, (byte) (secondByte | 0x80));
byte[] mask = this._randomMask!=null ? this._randomMask : (this._randomMask = getRandomMask());
/*outputBuffer[dataOffset - 4] = mask[0];
outputBuffer[dataOffset - 3] = mask[1];
outputBuffer[dataOffset - 2] = mask[2];
outputBuffer[dataOffset - 1] = mask[3];*/
outputBuffer.put(dataOffset - 4, mask[0]);
outputBuffer.put(dataOffset - 3, mask[1]);
outputBuffer.put(dataOffset - 2, mask[2]);
outputBuffer.put(dataOffset - 1, mask[3]);
if (mergeBuffers) {
BufferUtil.mask((ByteBuffer) data, mask, outputBuffer, dataOffset, dataLength);
try {
BufferUtil.renewBuffer(outputBuffer); debug(TAG, "outputBuffer 3:"+outputBuffer);
out = this._socket.write(outputBuffer, null, cb);
}
catch (Exception e) {
if (cb != null) cb.writeDone(e.toString());
else this.emit("error", e.toString());
}
}
else {
BufferUtil.mask((ByteBuffer) data, mask, (ByteBuffer) data, 0, dataLength);
try {
///this._socket.write(outputBuffer, 'binary');
///out = this._socket.write(data, 'binary', cb);
BufferUtil.renewBuffer(outputBuffer); debug(TAG, "outputBuffer 1:"+outputBuffer);
this._socket.write(outputBuffer, null, null);
BufferUtil.renewBuffer((ByteBuffer)data); debug(TAG, "data 1:"+(ByteBuffer)data);
out = this._socket.write(data, null, cb);
}
catch (Exception e) {
if (cb != null) cb.writeDone(e.toString());
else this.emit("error", e.toString());
}
}
}
else {
///outputBuffer[1] = secondByte;
outputBuffer.put(1, (byte) secondByte);
if (mergeBuffers) {
///data.copy(outputBuffer, dataOffset);
ByteBuffer tfc = (ByteBuffer)data;
BufferUtil.fastCopy(tfc.capacity(), tfc, outputBuffer, dataOffset);
try {
BufferUtil.renewBuffer(outputBuffer); debug(TAG, "outputBuffer 2:"+outputBuffer);
out = this._socket.write(outputBuffer, null, cb);
}
catch (Exception e) {
if (cb != null) cb.writeDone(e.toString());
else this.emit("error", e.toString());
}
}
else {
try {
///this._socket.write(outputBuffer, 'binary');
///out = this._socket.write(data, 'binary', cb);
BufferUtil.renewBuffer(outputBuffer); debug(TAG, "outputBuffer 0:"+outputBuffer);
this._socket.write(outputBuffer, null, null);
BufferUtil.renewBuffer((ByteBuffer)data); debug(TAG, "data 0:"+(ByteBuffer)data);
out = this._socket.write(data, null, cb);
}
catch (Exception e) {
if (cb != null) cb.writeDone(e.toString());
else this.emit("error", e.toString());
}
}
}
debug(TAG, "frameAndSend ... 2");
return out;
}
private static byte[] getRandomMask() {
byte[] ret = new byte[4];
ret[0] = (byte) Math.ceil(Math.random() * 255);
ret[1] = (byte) Math.ceil(Math.random() * 255);
ret[2] = (byte) Math.ceil(Math.random() * 255);
ret[3] = (byte) Math.ceil(Math.random() * 255);
return ret;
}
}