/* * Copyright (C) 2012-2015 DataStax 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.datastax.driver.core; import com.datastax.driver.core.exceptions.DriverInternalError; import com.datastax.driver.core.exceptions.FrameTooLongException; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.*; import java.util.EnumSet; import java.util.List; /** * A frame for the CQL binary protocol. * <p/> * Each frame contains a fixed size header (8 bytes for V1 and V2, 9 bytes for V3 and V4) * followed by a variable size body. The content of the body depends * on the header opcode value (the body can in particular be empty for some * opcode values). * <p/> * The protocol distinguishes 2 types of frames: requests and responses. Requests * are those frames sent by the clients to the server, response are the ones sent * by the server. Note however that the protocol supports server pushes (events) * so responses does not necessarily come right after a client request. * <p/> * Frames for protocol versions 1+2 are defined as: * <p/> * <pre> * 0 8 16 24 32 * +---------+---------+---------+---------+ * | version | flags | stream | opcode | * +---------+---------+---------+---------+ * | length | * +---------+---------+---------+---------+ * | | * . ... body ... . * . . * . . * +---------------------------------------- * * </pre> * <p/> * Frames for protocol versions 3+4 are defined as: * <p/> * <pre> * 0 8 16 24 32 40 * +---------+---------+---------+---------+---------+ * | version | flags | stream | opcode | * +---------+---------+---------+---------+---------+ * | length | * +---------+---------+---------+---------+ * | | * . ... body ... . * . . * . . * +---------------------------------------- * </pre> * * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v1.spec" * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v2.spec" * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v3.spec" * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec" */ class Frame { final Header header; final ByteBuf body; private Frame(Header header, ByteBuf body) { this.header = header; this.body = body; } private static Frame create(ByteBuf fullFrame) { assert fullFrame.readableBytes() >= 1 : String.format("Frame too short (%d bytes)", fullFrame.readableBytes()); int versionBytes = fullFrame.readByte(); // version first byte is the "direction" of the frame (request or response) ProtocolVersion version = ProtocolVersion.fromInt(versionBytes & 0x7F); int hdrLen = Header.lengthFor(version); assert fullFrame.readableBytes() >= (hdrLen - 1) : String.format("Frame too short (%d bytes)", fullFrame.readableBytes()); int flags = fullFrame.readByte(); int streamId = readStreamid(fullFrame, version); int opcode = fullFrame.readByte(); int length = fullFrame.readInt(); assert length == fullFrame.readableBytes(); Header header = new Header(version, flags, streamId, opcode); return new Frame(header, fullFrame); } private static int readStreamid(ByteBuf fullFrame, ProtocolVersion version) { switch (version) { case V1: case V2: return fullFrame.readByte(); case V3: case V4: case V5: return fullFrame.readShort(); default: throw version.unsupported(); } } static Frame create(ProtocolVersion version, int opcode, int streamId, EnumSet<Header.Flag> flags, ByteBuf body) { Header header = new Header(version, flags, streamId, opcode); return new Frame(header, body); } static class Header { final ProtocolVersion version; final EnumSet<Flag> flags; final int streamId; final int opcode; private Header(ProtocolVersion version, int flags, int streamId, int opcode) { this(version, Flag.deserialize(flags), streamId, opcode); } private Header(ProtocolVersion version, EnumSet<Flag> flags, int streamId, int opcode) { this.version = version; this.flags = flags; this.streamId = streamId; this.opcode = opcode; } /** * Return the expected frame header length in bytes according to the protocol version in use. * * @param version the protocol version in use * @return the expected frame header length in bytes */ static int lengthFor(ProtocolVersion version) { switch (version) { case V1: case V2: return 8; case V3: case V4: case V5: return 9; default: throw version.unsupported(); } } enum Flag { // The order of that enum matters!! COMPRESSED, TRACING, CUSTOM_PAYLOAD, WARNING, USE_BETA; static EnumSet<Flag> deserialize(int flags) { EnumSet<Flag> set = EnumSet.noneOf(Flag.class); Flag[] values = Flag.values(); for (int n = 0; n < 8; n++) { if ((flags & (1 << n)) != 0) set.add(values[n]); } return set; } static int serialize(EnumSet<Flag> flags) { int i = 0; for (Flag flag : flags) i |= 1 << flag.ordinal(); return i; } } } Frame with(ByteBuf newBody) { return new Frame(header, newBody); } static final class Decoder extends ByteToMessageDecoder { private DecoderForStreamIdSize decoder; @Override protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception { if (buffer.readableBytes() < 1) return; // Initialize sub decoder on first message. No synchronization needed as // decode is always called from same thread. if (decoder == null) { int version = buffer.getByte(buffer.readerIndex()); // version first bit is the "direction" of the frame (request or response) version = version & 0x7F; decoder = new DecoderForStreamIdSize(version, version >= 3 ? 2 : 1); } Object frame = decoder.decode(ctx, buffer); if (frame != null) out.add(frame); } static class DecoderForStreamIdSize extends LengthFieldBasedFrameDecoder { // The maximum response frame length allowed. Note that C* does not currently restrict the length of its responses (CASSANDRA-12630). private static final int MAX_FRAME_LENGTH = SystemProperties.getInt("com.datastax.driver.NATIVE_TRANSPORT_MAX_FRAME_SIZE_IN_MB", 256) * 1024 * 1024; // 256 MB private final int protocolVersion; DecoderForStreamIdSize(int protocolVersion, int streamIdSize) { super(MAX_FRAME_LENGTH, /*lengthOffset=*/ 3 + streamIdSize, 4, 0, 0, true); this.protocolVersion = protocolVersion; } @Override protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { // Capture current index in case we need to get the stream id. // If a TooLongFrameException is thrown the readerIndex will advance to the end of // the buffer (or past the frame) so we need the position as we entered this method. int curIndex = buffer.readerIndex(); try { ByteBuf frame = (ByteBuf) super.decode(ctx, buffer); if (frame == null) { return null; } // Do not deallocate `frame` just yet, because it is stored as Frame.body and will be used // in Message.ProtocolDecoder or Frame.Decompressor if compression is enabled (we deallocate // it there). Frame theFrame = Frame.create(frame); // Validate the opcode (this will throw if it's not a response) Message.Response.Type.fromOpcode(theFrame.header.opcode); return theFrame; } catch (CorruptedFrameException e) { throw new DriverInternalError(e); } catch (TooLongFrameException e) { int streamId = protocolVersion > 2 ? buffer.getShort(curIndex + 2) : buffer.getByte(curIndex + 2); throw new FrameTooLongException(streamId); } } } } @ChannelHandler.Sharable static class Encoder extends MessageToMessageEncoder<Frame> { @Override protected void encode(ChannelHandlerContext ctx, Frame frame, List<Object> out) throws Exception { ProtocolVersion protocolVersion = frame.header.version; ByteBuf header = ctx.alloc().ioBuffer(Frame.Header.lengthFor(protocolVersion)); // We don't bother with the direction, we only send requests. header.writeByte(frame.header.version.toInt()); header.writeByte(Header.Flag.serialize(frame.header.flags)); writeStreamId(frame.header.streamId, header, protocolVersion); header.writeByte(frame.header.opcode); header.writeInt(frame.body.readableBytes()); out.add(header); out.add(frame.body); } private void writeStreamId(int streamId, ByteBuf header, ProtocolVersion protocolVersion) { switch (protocolVersion) { case V1: case V2: header.writeByte(streamId); break; case V3: case V4: case V5: header.writeShort(streamId); break; default: throw protocolVersion.unsupported(); } } } static class Decompressor extends MessageToMessageDecoder<Frame> { private final FrameCompressor compressor; Decompressor(FrameCompressor compressor) { assert compressor != null; this.compressor = compressor; } @Override protected void decode(ChannelHandlerContext ctx, Frame frame, List<Object> out) throws Exception { if (frame.header.flags.contains(Header.Flag.COMPRESSED)) { // All decompressors allocate a new buffer for the decompressed data, so this is the last time // we have a reference to the compressed body (and therefore a chance to release it). ByteBuf compressedBody = frame.body; try { out.add(compressor.decompress(frame)); } finally { compressedBody.release(); } } else { out.add(frame); } } } static class Compressor extends MessageToMessageEncoder<Frame> { private final FrameCompressor compressor; Compressor(FrameCompressor compressor) { assert compressor != null; this.compressor = compressor; } @Override protected void encode(ChannelHandlerContext ctx, Frame frame, List<Object> out) throws Exception { // Never compress STARTUP messages if (frame.header.opcode == Message.Request.Type.STARTUP.opcode) { out.add(frame); } else { frame.header.flags.add(Header.Flag.COMPRESSED); // See comment in decode() ByteBuf uncompressedBody = frame.body; try { out.add(compressor.compress(frame)); } finally { uncompressedBody.release(); } } } } }