/** * Copyright 2012 Ronen Hamias, Anton Kharenko * * 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 io.scalecube.socketio.serialization; import java.io.IOException; import java.util.LinkedList; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.netty.util.CharsetUtil; import io.scalecube.socketio.packets.Packet; import io.scalecube.socketio.packets.PacketsFrame; /** * Class which responde for supporting Socket.IO framing as described below. * * <h1>Framing</h1> * <p/> * Certain transports, like websocket or flashsocket, have built-in lightweight * framing mechanisms for sending and receiving messages. * <p/> * For xhr-multipart, the built-in MIME framing is used for the sake of * consistency. * <p/> * When no built-in lightweight framing is available, and multiple messages need * to be delivered (i.e: buffered messages), the following is used: * <p/> * {@code `\ufffd` [message lenth] `\ufffd`} * <p/> * Transports where the framing overhead is expensive (ie: when the xhr-polling * transport tries to send data to the server). * * * @author Anton Kharenko * */ public final class PacketFramer { private static final char DELIMITER = '\ufffd'; private static final byte[] DELIMITER_BYTES = new String( new char[]{DELIMITER}).getBytes(CharsetUtil.UTF_8); private static final int DELIMITER_BYTES_SIZE = DELIMITER_BYTES.length; /** * Don't let anyone instantiate this class. */ private PacketFramer() { } public static ByteBuf encodePacketsFrame(final PacketsFrame packetsFrame) throws IOException { List<Packet> packets = packetsFrame.getPackets(); if (packets.size() == 1) { Packet packet = packets.get(0); return PacketEncoder.encodePacket(packet); } else { CompositeByteBuf compositeByteBuf = PooledByteBufAllocator.DEFAULT.compositeBuffer(packets.size() * 2); int compositeReadableBytes = 0; for (Packet packet : packets) { // Decode packet ByteBuf packetByteBuf = PacketEncoder.encodePacket(packet); // Prepare length prepender int packetLength = getUtf8CharCountByByteCount(packetByteBuf, 0, packetByteBuf.readableBytes()); byte[] packetLengthBytes = String.valueOf(packetLength).getBytes(CharsetUtil.UTF_8); int packetLengthPrependerCapacity = packetLengthBytes.length + DELIMITER_BYTES_SIZE + DELIMITER_BYTES_SIZE; ByteBuf packetLengthPrependerByteBuf = PooledByteBufAllocator.DEFAULT.buffer(packetLengthPrependerCapacity, packetLengthPrependerCapacity); packetLengthPrependerByteBuf.writeBytes(DELIMITER_BYTES); packetLengthPrependerByteBuf.writeBytes(packetLengthBytes); packetLengthPrependerByteBuf.writeBytes(DELIMITER_BYTES); // Update composite byte buffer compositeByteBuf.addComponent(packetLengthPrependerByteBuf); compositeByteBuf.addComponent(packetByteBuf); compositeReadableBytes += packetLengthPrependerByteBuf.readableBytes() + packetByteBuf.readableBytes(); } compositeByteBuf.writerIndex(compositeReadableBytes); return compositeByteBuf; } } private static int getUtf8CharCountByByteCount(final ByteBuf buffer, final int startByteIndex, final int bytesCount) { int charCount = 0; int endByteIndex = startByteIndex + bytesCount; int byteIndex = startByteIndex; while (byteIndex < endByteIndex) { charCount++; // Define next char first byte short charFirstByte = buffer.getUnsignedByte(byteIndex); // Scan first byte of UTF-8 character according to: // http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 if ((charFirstByte >= 0x20) && (charFirstByte <= 0x7F)) { // characters U-00000000 - U-0000007F (same as ASCII) byteIndex++; } else if ((charFirstByte & 0xE0) == 0xC0) { // characters U-00000080 - U-000007FF, mask 110XXXXX byteIndex += 2; } else if ((charFirstByte & 0xF0) == 0xE0) { // characters U-00000800 - U-0000FFFF, mask 1110XXXX byteIndex += 3; } else if ((charFirstByte & 0xF8) == 0xF0) { // characters U-00010000 - U-001FFFFF, mask 11110XXX byteIndex += 4; } else if ((charFirstByte & 0xFC) == 0xF8) { // characters U-00200000 - U-03FFFFFF, mask 111110XX byteIndex += 5; } else if ((charFirstByte & 0xFE) == 0xFC) { // characters U-04000000 - U-7FFFFFFF, mask 1111110X byteIndex += 6; } else { byteIndex++; } } return charCount; } public static List<Packet> decodePacketsFrame(final ByteBuf buffer) throws IOException { List<Packet> packets = new LinkedList<Packet>(); int sequenceNumber = 0; while (buffer.isReadable()) { Packet packet = PacketFramer.decodeNextPacket(buffer); packet.setSequenceNumber(sequenceNumber); sequenceNumber++; packets.add(packet); } return packets; } private static Packet decodeNextPacket(final ByteBuf buffer) throws IOException { Packet packet; if (isDelimiter(buffer, buffer.readerIndex())) { CharSequence packetCharsCountString = decodePacketLength(buffer); final Integer packetCharsCount = Integer.valueOf(packetCharsCountString.toString()); final int packetStartIndex = buffer.readerIndex() + DELIMITER_BYTES_SIZE + packetCharsCountString.length() + DELIMITER_BYTES_SIZE; final int packetBytesCount = getUtf8ByteCountByCharCount(buffer, packetStartIndex, packetCharsCount); ByteBuf frame = buffer.slice(packetStartIndex, packetBytesCount); packet = PacketDecoder.decodePacket(frame); buffer.readerIndex(packetStartIndex + packetBytesCount); } else { packet = PacketDecoder.decodePacket(buffer); buffer.readerIndex(buffer.readableBytes()); } return packet; } private static int getUtf8ByteCountByCharCount(final ByteBuf buffer, final int startIndex, final int charCount) { int bytesCount = 0; for (int charIndex = 0; charIndex < charCount; charIndex++) { // Define next char first byte int charFirstByteIndex = startIndex + bytesCount; short charFirstByte = buffer.getUnsignedByte(charFirstByteIndex); // Scan first byte of UTF-8 character according to: // http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 if ((charFirstByte >= 0x20) && (charFirstByte <= 0x7F)) { // characters U-00000000 - U-0000007F (same as ASCII) bytesCount++; } else if ((charFirstByte & 0xE0) == 0xC0) { // characters U-00000080 - U-000007FF, mask 110XXXXX bytesCount += 2; } else if ((charFirstByte & 0xF0) == 0xE0) { // characters U-00000800 - U-0000FFFF, mask 1110XXXX bytesCount += 3; } else if ((charFirstByte & 0xF8) == 0xF0) { // characters U-00010000 - U-001FFFFF, mask 11110XXX bytesCount += 4; } else if ((charFirstByte & 0xFC) == 0xF8) { // characters U-00200000 - U-03FFFFFF, mask 111110XX bytesCount += 5; } else if ((charFirstByte & 0xFE) == 0xFC) { // characters U-04000000 - U-7FFFFFFF, mask 1111110X bytesCount += 6; } else { bytesCount++; } } return bytesCount; } private static CharSequence decodePacketLength(final ByteBuf buffer) { StringBuilder length = new StringBuilder(); final int scanStartIndex = buffer.readerIndex() + DELIMITER_BYTES_SIZE; final int scanEndIndex = buffer.readerIndex() + buffer.readableBytes(); for (int charIndex = scanStartIndex; charIndex < scanEndIndex; charIndex++) { if (isDelimiter(buffer, charIndex)) { break; } else { length.append((char) buffer.getUnsignedByte(charIndex)); } } return length; } private static boolean isDelimiter(final ByteBuf buffer, final int index) { for (int i = 0; i < DELIMITER_BYTES_SIZE; i++) { if (buffer.getByte(index + i) != DELIMITER_BYTES[i]) { return false; } } return true; } }