Class: MQTT::Packet

Inherits:
Object
  • Object
show all
Defined in:
lib/mqtt/packet.rb

Overview

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

Defined Under Namespace

Classes: Connack, Connect, Disconnect, Pingreq, Pingresp, Puback, Pubcomp, Publish, Pubrec, Pubrel, Suback, Subscribe, Unsuback, Unsubscribe

Constant Summary collapse

ATTR_DEFAULTS =

Default attribute values

{
  version: '3.1.0',
  id: 0,
  body_length: nil
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ Packet

Create a new empty packet



115
116
117
118
119
# 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

Instance Attribute Details

#body_lengthObject

The length of the parsed packet body



18
19
20
# File 'lib/mqtt/packet.rb', line 18

def body_length
  @body_length
end

#flagsObject

Array of 4 bits in the fixed header



15
16
17
# File 'lib/mqtt/packet.rb', line 15

def flags
  @flags
end

#idObject

Identifier to link related control packets together



12
13
14
# File 'lib/mqtt/packet.rb', line 12

def id
  @id
end

#versionObject

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



9
10
11
# File 'lib/mqtt/packet.rb', line 9

def version
  @version
end

Class Method Details

.create_from_header(byte) ⇒ Object

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

Raises:



101
102
103
104
105
106
107
108
109
110
111
112
# 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

.parse(buffer) ⇒ Object

Parse buffer into new packet object



58
59
60
61
62
63
# 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) ⇒ Object

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

Raises:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# 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) ⇒ Object

Read in a packet from a socket



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 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) ⇒ Object

Read and unpack a single byte from a socket

Raises:



224
225
226
227
228
229
# 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

Instance Method Details

#dupObject



121
122
123
124
125
# File 'lib/mqtt/packet.rb', line 121

def dup
  result = super
  result.instance_variable_set(:@flags, @flags.dup)
  result
end

#encode_bodyObject

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



174
175
176
# File 'lib/mqtt/packet.rb', line 174

def encode_body
  '' # No body by default
end

#inspectObject

Returns a human readable string



219
220
221
# File 'lib/mqtt/packet.rb', line 219

def inspect
  "\#<#{self.class}>"
end

#message_idObject

Deprecated.

Please use #id instead



1026
1027
1028
# File 'lib/mqtt/packet.rb', line 1026

def message_id
  id
end

#message_id=(args) ⇒ Object

Deprecated.

Please use #id= instead



1031
1032
1033
# File 'lib/mqtt/packet.rb', line 1031

def message_id=(args)
  self.id = args
end

#parse_body(buffer) ⇒ Object

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

Raises:



165
166
167
168
169
170
171
# 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_sObject

Serialise the packet



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# 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_idObject

Get the identifer for this packet type



139
140
141
142
143
144
# 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_nameObject

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

Example: CONNACK



150
151
152
# File 'lib/mqtt/packet.rb', line 150

def type_name
  self.class.name.split('::').last.upcase
end

#update_attributes(attr = {}) ⇒ Object

Set packet attributes from a hash of attribute names and values



128
129
130
131
132
133
134
135
136
# 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_flagsObject

Check that fixed header flags are valid for types that don’t use the flags

Raises:



212
213
214
215
216
# 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