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[RW]

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/qubitro-mqtt/packet.rb, line 103
def self.create_from_header(byte)
  # Work out the class
  type_id = ((byte & 0xF0) >> 4)
  packet_class = MQTT::PACKET_TYPES[type_id]
  if packet_class.nil?
    raise ProtocolException, "Invalid packet type identifier: #{type_id}"
  end
  
  # 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/qubitro-mqtt/packet.rb, line 119
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/qubitro-mqtt/packet.rb, line 57
def self.parse(buffer)
  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/qubitro-mqtt/packet.rb, line 65
def self.parse_header(buffer)
  # Check that the packet is a long as the minimum packet size
  if buffer.bytesize < 2
    raise ProtocolException, 'Invalid packet: less than 2 bytes long'
  end
  
  # 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
    if buffer.bytesize <= pos
      raise ProtocolException, 'The packet length header is incomplete'
    end
  
    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/qubitro-mqtt/packet.rb, line 27
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/qubitro-mqtt/packet.rb, line 221
def self.read_byte(socket)
  byte = socket.read(1)
  raise ProtocolException, 'Failed to read byte from socket' if byte.nil?
  
  byte.unpack('C').first
end

Public Instance Methods

body_length=(arg) click to toggle source

Set the length of the packet body

# File lib/qubitro-mqtt/packet.rb, line 157
def body_length=(arg)
  @body_length = arg.to_i
end
encode_body() click to toggle source

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

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

Returns a human readable string

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

@deprecated Please use {#id} instead

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

@deprecated Please use {#id=} instead

# File lib/qubitro-mqtt/packet.rb, line 1028
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/qubitro-mqtt/packet.rb, line 162
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/qubitro-mqtt/packet.rb, line 174
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
  if body_length > 268_435_455
    raise 'Error serialising packet: body is more than 256MB'
  end
  
  # 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/qubitro-mqtt/packet.rb, line 137
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/qubitro-mqtt/packet.rb, line 147
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/qubitro-mqtt/packet.rb, line 126
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/qubitro-mqtt/packet.rb, line 209
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/qubitro-mqtt/packet.rb, line 152
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/qubitro-mqtt/packet.rb, line 236
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/qubitro-mqtt/packet.rb, line 231
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/qubitro-mqtt/packet.rb, line 241
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/qubitro-mqtt/packet.rb, line 248
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/qubitro-mqtt/packet.rb, line 268
def shift_bits(buffer)
  buffer.slice!(0...1).unpack('b8').first.split('').map { |b| b == '1' }
end
shift_byte(buffer) click to toggle source

Remove one byte from the front of the string

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

Remove n bytes from the front of buffer

# File lib/qubitro-mqtt/packet.rb, line 273
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/qubitro-mqtt/packet.rb, line 257
def shift_short(buffer)
  bytes = buffer.slice!(0..1)
  bytes.unpack('n').first
end
shift_string(buffer) click to toggle source

Remove string from the front of buffer

# File lib/qubitro-mqtt/packet.rb, line 278
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