概要
IoT機器が送信するデータを効率的に符号化するバイナリ形式として、CBOR(Concise Binary Object Representation)とProtocol Buffers(Protobuf)は代表的な存在です。どちらもJSONと同様にさまざまなデータ型をシリアライズできますが、テキストではなくバイナリ形式のため、より小さなサイズ・高速なパース処理が特徴です。
CBORはIETFが2013年(RFC 7049)・2020年(RFC 8949)に標準化した形式です。JSONのデータモデル(オブジェクト・配列・文字列・数値・真偽値・null)を踏襲しながら、バイナリに変換することでサイズを削減します。CoAPのペイロードとして明示的に推奨されており、IETFの多くのIoT関連RFCで採用されています。スキーマ定義がなくても使える「自己記述型」フォーマットです。
Protocol BuffersはGoogleが2008年にオープンソース化したバイナリシリアライズ形式です。.protoファイルでスキーマを事前定義し、コードジェネレーターでシリアライズ/デシリアライズコードを自動生成します。フィールドが番号(タグ)で管理されるため非常にコンパクトで、Googleの内部システムや高性能gRPCの基盤として使われています。
歴史・背景
CBORの歴史
JSONはそのまま使うにはテキストオーバーヘッドが大きく、特に制約機器・低帯域通信(LoRaWAN等)では問題でした。MessagePack(2010年)が先行してバイナリJSON的な位置付けで登場しましたが、IETFの標準には至りませんでした。
CBORは6LoWPAN・CoAPとの統合を念頭に設計され、2013年にRFC 7049として制定。その後改訂版のRFC 8949(2020年)が現行標準です。IETFのIoT関連規格(SUIT、OSCORE、EDHOC等)では多くがCBORを採用しており、「IoT界のJSON」的な位置付けになっています。
Protocol Buffersの歴史
Googleは2001年頃、社内システムの大量データシリアライズのためにProtobufを開発しました。XMLが遅すぎた時代の解決策です。2008年にオープンソース化(proto2)、2016年にproto3が登場しデフォルトになりました。gRPCのペイロード形式として2015年に普及が加速しました。
技術仕様
CBOR のデータモデル
CBORはJSONの6型に加えて、バイト文字列・タグ付きデータ・浮動小数点(16/32/64bit)・符号なし整数などをネイティブでサポートします。
主要データ型(Major Types):
0: 符号なし整数
1: 符号付き整数
2: バイト文字列
3: テキスト文字列
4: 配列
5: マップ(オブジェクト)
6: タグ(追加意味情報)
7: 浮動小数点・特殊値
エンコード例
JSONとCBORのバイト列比較:
import json
import cbor2
data = {
"device_id": "s01",
"temperature": 25.3,
"online": True
}
json_bytes = json.dumps(data).encode()
# b'{"device_id": "s01", "temperature": 25.3, "online": true}'
# 長さ: 56バイト
cbor_bytes = cbor2.dumps(data)
# 長さ: 約33バイト(約41%削減)
print(f"JSON: {len(json_bytes)} bytes")
print(f"CBOR: {len(cbor_bytes)} bytes")
print(f"CBOR hex: {cbor_bytes.hex()}")
# a3 69 64657669636...: 辞書型(3要素) + キー/値...
CBOR のバイトレイアウト詳細
例: {"a": 1} をCBORでエンコード
a1 # map(1) - 1要素のマップ
61 # text(1) - 1文字のキー
61 # "a"
01 # unsigned(1) - 値1
例: {"temperature": 25.3}
a1 # map(1)
6b # text(11) - 11文字
74656d706572617475726f # "temperature"
f9 3f73 # float16: 25.3(2バイト浮動小数点!)
CBORは16ビット浮動小数点(half precision)もサポートしており、センサー値のような中程度精度の数値には2バイトで表現できます。
Protocol Buffers の仕様
.protoファイルの定義(proto3)
syntax = "proto3";
package iot.sensors;
// センサーデータメッセージ定義
message SensorData {
string device_id = 1;
double temperature = 2;
double humidity = 3;
int64 timestamp_ms = 4;
repeated Alert alerts = 5;
bool online = 6;
}
message Alert {
string type = 1;
float threshold = 2;
float current_value = 3;
}
// OTA マニフェスト
message OtaManifest {
string version = 1;
string url = 2;
bytes sha256 = 3; // バイナリをそのまま表現可能
uint32 size_bytes = 4;
}
ワイヤーフォーマット(Wire Format)
Protobufはフィールド番号(タグ)とワイヤータイプを組み合わせて各フィールドをエンコードします。
Tag = (field_number << 3) | wire_type
Wire types:
0: Varint (int32, int64, bool, enum)
1: 64-bit (fixed64, double)
2: Length-delimited (string, bytes, embedded message)
5: 32-bit (fixed32, float)
例: device_id = "s01" (field 1, wire type 2)
0a # tag: (1 << 3) | 2 = 0x0a
03 # length: 3バイト
73 30 31 # "s01"
例: temperature = 25.3 (field 2, wire type 1)
11 # tag: (2 << 3) | 1 = 0x11
9a 99 99 99 # double 25.3 (リトルエンディアン)
99 99 39 40
サイズ比較
| フォーマット | バイト数 | 注記 |
|---|---|---|
| JSON | 100% (基準) | 可読性最高 |
| CBOR | 50〜70% | スキーマ不要 |
| MessagePack | 55〜75% | スキーマ不要 |
| Protocol Buffers | 30〜50% | スキーマ必須 |
動作原理
CBORのパース(Cライブラリ)
// tinycbor(Qtのオープンソースライブラリ)使用例
#include "cbor.h"
void encode_sensor_data(uint8_t *buf, size_t buf_size,
float temp, float humidity) {
CborEncoder encoder, mapEncoder;
cbor_encoder_init(&encoder, buf, buf_size, 0);
cbor_encoder_create_map(&encoder, &mapEncoder, 3);
cbor_encode_text_stringz(&mapEncoder, "device_id");
cbor_encode_text_stringz(&mapEncoder, "sensor_001");
cbor_encode_text_stringz(&mapEncoder, "temperature");
cbor_encode_float(&mapEncoder, temp);
cbor_encode_text_stringz(&mapEncoder, "humidity");
cbor_encode_float(&mapEncoder, humidity);
cbor_encoder_close_container(&encoder, &mapEncoder);
}
void decode_cbor(const uint8_t *buf, size_t buf_size) {
CborParser parser;
CborValue it;
cbor_parser_init(buf, buf_size, 0, &parser, &it);
CborValue map;
cbor_value_enter_container(&it, &map);
while (!cbor_value_at_end(&map)) {
char key[32];
size_t len = sizeof(key);
cbor_value_copy_text_string(&map, key, &len, &map);
if (strcmp(key, "temperature") == 0) {
float temp;
cbor_value_get_float(&map, &temp);
printf("温度: %.1f\n", temp);
}
cbor_value_advance(&map);
}
}
Protocol Buffersのコード生成
# .protoからC++コードを生成
protoc --cpp_out=. sensor_data.proto
# → sensor_data.pb.h, sensor_data.pb.cc が生成される
# Pythonコードを生成
protoc --python_out=. sensor_data.proto
# → sensor_data_pb2.py が生成される
# nanopb(組み込み向け軽量Cコード生成)
python nanopb/generator/nanopb_generator.py sensor_data.proto
# → sensor_data.pb.h, sensor_data.pb.c が生成される(動的メモリ不使用)
// 生成されたC++コードの使用例
#include "sensor_data.pb.h"
// シリアライズ
iot::sensors::SensorData msg;
msg.set_device_id("sensor_001");
msg.set_temperature(25.3);
msg.set_humidity(60.5);
msg.set_timestamp_ms(1736942400000LL);
std::string serialized;
msg.SerializeToString(&serialized);
// デシリアライズ
iot::sensors::SensorData received;
received.ParseFromString(received_data);
float temp = received.temperature();
# Python での Protobuf 使用
from sensor_data_pb2 import SensorData
import time
# シリアライズ
msg = SensorData()
msg.device_id = "sensor_001"
msg.temperature = 25.3
msg.humidity = 60.5
msg.timestamp_ms = int(time.time() * 1000)
binary = msg.SerializeToString()
print(f"Protobuf: {len(binary)} bytes")
# デシリアライズ
received = SensorData()
received.ParseFromString(binary)
print(f"温度: {received.temperature}")
nanopb による組み込み向け実装
// nanopb(動的メモリ不使用の組み込み向けProtobuf)
#include "pb_encode.h"
#include "pb_decode.h"
#include "sensor_data.pb.h"
bool encode_sensor_data(uint8_t *buffer, size_t buffer_size,
size_t *message_length) {
iot_sensors_SensorData message = iot_sensors_SensorData_init_zero;
strcpy(message.device_id, "sensor_001");
message.temperature = 25.3f;
message.humidity = 60.5f;
message.online = true;
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
bool status = pb_encode(&stream, iot_sensors_SensorData_fields, &message);
*message_length = stream.bytes_written;
return status;
}
用途・ユースケース
LoRaWAN での省サイズ送信
LoRaWANのペイロード上限は最大242バイト(SF7)〜51バイト(SF12)と非常に小さく、バイナリエンコードが必須です。
import struct
import cbor2
# LoRaWAN向け超コンパクトCBOR
data = {
1: 253, # temp × 10 = 25.3℃(整数キーで最小化)
2: 605, # humidity × 10 = 60.5%
3: 87, # battery %
}
payload = cbor2.dumps(data)
# a3 01 18fd 02 19025d 03 18 57
# 9バイト!(JSONの"{"t":25.3,"h":60.5,"b":87}" = 28バイト)
gRPCサービスでのIoTデータ収集
syntax = "proto3";
service SensorService {
// 単一データ送信
rpc SendData (SensorData) returns (SendResponse);
// ストリーミング送信(多数のデータを連続送信)
rpc StreamData (stream SensorData) returns (StreamSummary);
// サーバーストリーミング(コマンド受信)
rpc ReceiveCommands (DeviceInfo) returns (stream Command);
}
# gRPC クライアント(Raspberry Pi から)
import grpc
from sensor_data_pb2_grpc import SensorServiceStub
from sensor_data_pb2 import SensorData, DeviceInfo
channel = grpc.secure_channel(
'grpc.example.com:443',
grpc.ssl_channel_credentials()
)
stub = SensorServiceStub(channel)
# ストリーミング送信
def generate_data():
for _ in range(100):
yield SensorData(
device_id="rpi_001",
temperature=read_temp(),
timestamp_ms=int(time.time() * 1000)
)
time.sleep(1)
response = stub.StreamData(generate_data())
CoAP + CBOR(IoTの王道組み合わせ)
from aiocoap import Context, Message, resource
from aiocoap.numbers.codes import Code
import cbor2
class TemperatureResource(resource.Resource):
async def render_get(self, request):
data = {
"device": "coap_sensor_01",
"temp": read_temperature(),
"unit": "celsius"
}
# CBOR形式で返す(Content-Format: 60)
return Message(
code=Code.CONTENT,
payload=cbor2.dumps(data),
content_format=60 # CBOR
)
実装・開発のポイント
フォーマット選択ガイド
| 状況 | 推奨フォーマット | 理由 |
|---|---|---|
| LoRaWAN / LPWA | CBOR(整数キー) | 最小バイト数 |
| gRPC サービス | Protocol Buffers | gRPCのデファクト |
| CoAP ペイロード | CBOR | IETF標準推奨 |
| MQTTペイロード(帯域制約あり) | MessagePack or CBOR | 既存エコシステム |
| 開発・デバッグ段階 | JSON | 可読性最優先 |
| 大規模Googleインフラ連携 | Protocol Buffers | Googleエコシステム |
スキーマ進化(後方互換性)
Protobufはフィールド番号が変わらなければ後方互換性を維持できます。
// v1.0
message SensorData {
string device_id = 1;
double temperature = 2;
// フィールド3は将来のために予約(reserved)
}
// v2.0(後方互換)
message SensorData {
string device_id = 1;
double temperature = 2;
double humidity = 3; // 追加OK(古いパーサーは無視)
// フィールド4を削除する場合はreservedを使う
reserved 4;
reserved "old_field_name";
}
ベンチマーク(ESP32での実測目安)
| フォーマット | 100バイト相当エンコード | パース |
|---|---|---|
| JSON(cJSON) | 〜500μs | 〜800μs |
| CBOR(tinycbor) | 〜100μs | 〜150μs |
| Protobuf(nanopb) | 〜80μs | 〜100μs |
他技術との比較
| 項目 | CBOR | Protobuf | MessagePack | JSON |
|---|---|---|---|---|
| RFC標準 | ○(8949) | × | × | ○(8259) |
| スキーマ要否 | 不要 | 必要(.proto) | 不要 | 不要 |
| 自己記述性 | ◎ | × | ◎ | ◎ |
| サイズ | 小 | 最小 | 小 | 大 |
| 組み込み適合 | ◎ | ◎(nanopb) | ○ | ○ |
| CoAP対応 | ◎(推奨) | △ | △ | △ |
| gRPC対応 | △ | ◎(標準) | × | △ |
JSONの可読性を活かした開発・デバッグと、CBORやProtobufの効率性を活かした本番運用という使い分けが実践的なアプローチです。CoAPとの組み合わせにはCBOR、高性能サービス間通信にはProtobufが定番です。LoRaWANやLPWAなど低帯域通信では整数キーを使ったCBORが特に有効です。