class MQTT::Packet

Class representing a MQTT Packet Performs binary encoding and decoding of headers

Constants

ATTR_DEFAULTS

Default attribute values

Attributes

body_length[R]

The length of the parsed packet body

flags[RW]

Array of 4 bits in the fixed header

id[RW]

Identifier to link related control packets together

version[R]

The version number of the MQTT protocol to use (default 3.1.0)

Public Class Methods

create_from_header(byte) click to toggle source

Create a new packet object from the first byte of a MQTT packet

# File lib/mqtt/packet.rb, line 101
def self.create_from_header(byte)
  # Work out the class
  type_id = ((byte & 0xF0) >> 4)
  packet_class = MQTT::PACKET_TYPES[type_id]
  raise ProtocolException, "Invalid packet type identifier: #{type_id}" if packet_class.nil?

  # Convert the last 4 bits of byte into array of true/false
  flags = (0..3).map { |i| byte & (2 ** i) != 0 }

  # Create a new packet object
  packet_class.new(flags: flags)
end
new(args = {}) click to toggle source

Create a new empty packet

# File lib/mqtt/packet.rb, line 115
def initialize(args = {})
  # We must set flags before the other values
  @flags = [false, false, false, false]
  update_attributes(ATTR_DEFAULTS.merge(args))
end
parse(buffer) click to toggle source

Parse buffer into new packet object

# File lib/mqtt/packet.rb, line 58
def self.parse(buffer)
  buffer = buffer.dup
  packet = parse_header(buffer)
  packet.parse_body(buffer)
  packet
end
parse_header(buffer) click to toggle source

Parse the header and create a new packet object of the correct type The header is removed from the buffer passed into this function

# File lib/mqtt/packet.rb, line 67
def self.parse_header(buffer)
  # Check that the packet is a long as the minimum packet size
  raise ProtocolException, 'Invalid packet: less than 2 bytes long' if buffer.bytesize < 2

  # Create a new packet object
  bytes = buffer.unpack('C5')
  packet = create_from_header(bytes.first)
  packet.validate_flags

  # Parse the packet length
  body_length = 0
  multiplier = 1
  pos = 1

  loop do
    raise ProtocolException, 'The packet length header is incomplete' if buffer.bytesize <= pos

    digit = bytes[pos]
    body_length += ((digit & 0x7F) * multiplier)
    multiplier *= 0x80
    pos += 1
    break if (digit & 0x80).zero? || pos > 4
  end

  # Store the expected body length in the packet
  packet.instance_variable_set(:@body_length, body_length)

  # Delete the fixed header from the raw packet passed in
  buffer.slice!(0...pos)

  packet
end
read(socket) click to toggle source

Read in a packet from a socket

# File lib/mqtt/packet.rb, line 28
def self.read(socket)
  # Read in the packet header and create a new packet object
  packet = create_from_header(
    read_byte(socket)
  )
  packet.validate_flags

  # Read in the packet length
  multiplier = 1
  body_length = 0
  pos = 1

  loop do
    digit = read_byte(socket)
    body_length += ((digit & 0x7F) * multiplier)
    multiplier *= 0x80
    pos += 1
    break if (digit & 0x80).zero? || pos > 4
  end

  # Store the expected body length in the packet
  packet.instance_variable_set(:@body_length, body_length)

  # Read in the packet body
  packet.parse_body(socket.read(body_length))

  packet
end
read_byte(socket) click to toggle source

Read and unpack a single byte from a socket

# File lib/mqtt/packet.rb, line 224
def self.read_byte(socket)
  byte = socket.read(1)
  raise ProtocolException, 'Failed to read byte from socket' if byte.nil?

  byte.unpack1('C')
end

Public Instance Methods

body_length=(arg) click to toggle source

Set the length of the packet body

# File lib/mqtt/packet.rb, line 160
def body_length=(arg)
  @body_length = arg.to_i
end
dup() click to toggle source
Calls superclass method
# File lib/mqtt/packet.rb, line 121
def dup
  result = super
  result.instance_variable_set(:@flags, @flags.dup)
  result
end
encode_body() click to toggle source

Get serialisation of packet's body (variable header and payload)

# File lib/mqtt/packet.rb, line 174
def encode_body
  '' # No body by default
end
inspect() click to toggle source

Returns a human readable string

# File lib/mqtt/packet.rb, line 219
def inspect
  "\#<#{self.class}>"
end
message_id() click to toggle source

@deprecated Please use {#id} instead

# File lib/mqtt/packet.rb, line 1026
def message_id
  id
end
message_id=(args) click to toggle source

@deprecated Please use {#id=} instead

# File lib/mqtt/packet.rb, line 1031
def message_id=(args)
  self.id = args
end
parse_body(buffer) click to toggle source

Parse the body (variable header and payload) of a packet

# File lib/mqtt/packet.rb, line 165
def parse_body(buffer)
  return if buffer.bytesize == body_length

  raise ProtocolException,
        "Failed to parse packet - input buffer (#{buffer.bytesize}) " \
        "is not the same as the body length header (#{body_length})"
end
to_s() click to toggle source

Serialise the packet

# File lib/mqtt/packet.rb, line 179
def to_s
  # Encode the fixed header
  header = [
    ((type_id.to_i & 0x0F) << 4) |
      (flags[3] ? 0x8 : 0x0) |
      (flags[2] ? 0x4 : 0x0) |
      (flags[1] ? 0x2 : 0x0) |
      (flags[0] ? 0x1 : 0x0)
  ]

  # Get the packet's variable header and payload
  body = encode_body

  # Check that that packet isn't too big
  body_length = body.bytesize
  raise 'Error serialising packet: body is more than 256MB' if body_length > 268_435_455

  # Build up the body length field bytes
  loop do
    digit = (body_length % 128)
    body_length = body_length.div(128)
    # if there are more digits to encode, set the top bit of this digit
    digit |= 0x80 if body_length > 0
    header.push(digit)
    break if body_length <= 0
  end

  # Convert header to binary and add on body
  header.pack('C*') + body
end
type_id() click to toggle source

Get the identifer for this packet type

# File lib/mqtt/packet.rb, line 139
def type_id
  index = MQTT::PACKET_TYPES.index(self.class)
  raise "Invalid packet type: #{self.class}" if index.nil?

  index
end
type_name() click to toggle source

Get the name of the packet type as a string in capitals (like the MQTT specification uses)

Example: CONNACK

# File lib/mqtt/packet.rb, line 150
def type_name
  self.class.name.split('::').last.upcase
end
update_attributes(attr = {}) click to toggle source

Set packet attributes from a hash of attribute names and values

# File lib/mqtt/packet.rb, line 128
def update_attributes(attr = {})
  attr.each_pair do |k, v|
    if v.is_a?(Array) || v.is_a?(Hash)
      send("#{k}=", v.dup)
    else
      send("#{k}=", v)
    end
  end
end
validate_flags() click to toggle source

Check that fixed header flags are valid for types that don't use the flags @private

# File lib/mqtt/packet.rb, line 212
def validate_flags
  return if flags == [false, false, false, false]

  raise ProtocolException, "Invalid flags in #{type_name} packet header"
end
version=(arg) click to toggle source

Set the protocol version number

# File lib/mqtt/packet.rb, line 155
def version=(arg)
  @version = arg.to_s
end

Protected Instance Methods

encode_bits(bits) click to toggle source

Encode an array of bits and return them

# File lib/mqtt/packet.rb, line 239
def encode_bits(bits)
  [bits.map { |b| b ? '1' : '0' }.join].pack('b*')
end
encode_bytes(*bytes) click to toggle source

Encode an array of bytes and return them

# File lib/mqtt/packet.rb, line 234
def encode_bytes(*bytes)
  bytes.pack('C*')
end
encode_short(val) click to toggle source

Encode a 16-bit unsigned integer and return it

# File lib/mqtt/packet.rb, line 244
def encode_short(val)
  raise 'Value too big for short' if val > 0xffff

  [val.to_i].pack('n')
end
encode_string(str) click to toggle source

Encode a UTF-8 string and return it (preceded by the length of the string)

# File lib/mqtt/packet.rb, line 252
def encode_string(str)
  str = str.to_s.encode('UTF-8')

  # Force to binary, when assembling the packet
  str.force_encoding('ASCII-8BIT')
  encode_short(str.bytesize) + str
end
shift_bits(buffer) click to toggle source

Remove 8 bits from the front of buffer

# File lib/mqtt/packet.rb, line 272
def shift_bits(buffer)
  buffer.slice!(0...1).unpack1('b8').split('').map { |b| b == '1' }
end
shift_byte(buffer) click to toggle source

Remove one byte from the front of the string

# File lib/mqtt/packet.rb, line 267
def shift_byte(buffer)
  buffer.slice!(0...1).unpack1('C')
end
shift_data(buffer, bytes) click to toggle source

Remove n bytes from the front of buffer

# File lib/mqtt/packet.rb, line 277
def shift_data(buffer, bytes)
  buffer.slice!(0...bytes)
end
shift_short(buffer) click to toggle source

Remove a 16-bit unsigned integer from the front of buffer

# File lib/mqtt/packet.rb, line 261
def shift_short(buffer)
  bytes = buffer.slice!(0..1)
  bytes.unpack1('n')
end
shift_string(buffer) click to toggle source

Remove string from the front of buffer

# File lib/mqtt/packet.rb, line 282
def shift_string(buffer)
  len = shift_short(buffer)
  str = shift_data(buffer, len)
  # Strings in MQTT v3.1 are all UTF-8
  str.force_encoding('UTF-8')
end