CBOR 데이터 직렬화 포맷
Concise Binary Object Representation(CBOR) 은 데이터 직렬화 포맷으로, 데이터 인코딩에 사용됩니다. JSON 과 유사하지만, 문자열 포맷인 JSON 과 달리 CBOR 는 이진 포맷입니다. 메모리 제약이 큰 IoT 디바이스에서 주로 사용되고, AWS 나 GCP 같은 클라우드 IoT 서비스에서도 지원하고 있습니다. 같은 용도로 사용되는 포맷으로는 MessagePack 이 있습니다.
여기에서는 CBOR 스펙에 대한 이해보다는 라이브러리를 사용해 실무에 적용할 수 있는 예시에 초점을 맞춥니다.
예제에서는 가속도 센서 데이터와 자이로 센서 데이터를 전송하도록 하겠습니다. 대부분의 IoT 디바이스들이 C 언어로 구현되기 때문에 예제는 C 언어로 구현합니다.
안정적인 네트워킹을 위해서는 프레이밍 포맷이 추가되어야 하지만, CBOR 사용방법에 집중하기 위해 관련 내용은 포함하지 않습니다.
CBOR 라이브러리
예제에서는 C 로 구현된 라이브러리를 사용합니다. 다음 링크에서 여러 라이브러리를 찾아볼 수 있습니다.
데이터 인코딩 하기
송신 데이터는 다음과 같이 구성합니다:
{
"time": 1660530996,
"data": {
"acc": {
"x": 0.,
"y": 0.,
"z": 9.81
},
"gyro": {
"x": 6.3,
"y": 0.,
"z": 0.
}
}
}
아래와 같이 데이터를 인코딩합니다.
uint8_t buf[BUFSIZE];
cbor_writer_t writer;
cbor_writer_init(&writer, buf, sizeof(buf));
cbor_encode_map_indefinite(&writer);
/*1*/cbor_encode_text_string(&writer, "time");
cbor_encode_unsigned_integer(&writer, unixtime);
/*2*/cbor_encode_text_string(&writer, "data");
cbor_encode_map(&writer, 2);
/*1*/cbor_encode_text_string(&writer, "acc");
cbor_encode_map(&writer, 3);
/*1*/cbor_encode_text_string(&writer, "x");
cbor_encode_float(&writer, acc.x);
/*2*/cbor_encode_text_string(&writer, "y");
cbor_encode_float(&writer, acc.y);
/*3*/cbor_encode_text_string(&writer, "z");
cbor_encode_float(&writer, acc.z);
/*2*/cbor_encode_text_string(&writer, "gyro");
cbor_encode_map(&writer, 3);
/*1*/cbor_encode_text_string(&writer, "x");
cbor_encode_float(&writer, gyro.x);
/*2*/cbor_encode_text_string(&writer, "y");
cbor_encode_float(&writer, gyro.y);
/*3*/cbor_encode_text_string(&writer, "z");
cbor_encode_float(&writer, gyro.z);
cbor_encode_break(&writer);
com_send(cbor_writer_get_encoded(&writer), cbor_writer_len(&writer));
이렇게 인코딩된 데이터는 약 60바이트가 됩니다(“약”이라고 한 이유는 데이터 타입과 실제값에 따라 인코딩 사이즈가 달라지기 때문입니다):
bf61741a62f9b1346464617461a26361
6363a36178f900006179f90000617afa
411cf5c3646779726fa36178fa40c999
9a6179f90000617af90000ff
실제값이 작은 정수일 때 인코딩 사이즈에서 이득을 볼 수 있습니다. 4-byte 정수라도 그 값이 작다면 사용하지 않은 메모리 공간은 인코딩에서 생략하기 때문입니다.
이해를 돕기 위해 키는 모두 문자열을 사용했지만, 어떤 데이터 타입이든 키로 사용할 수 있습니다.
예제에서 사용된 xxx_indefinite()
는 데이터 길이를 지정하지 않습니다. 그렇기
때문에 cbor_encode_break()
로 그 끝을 명시해줘야 합니다. 예제에서는 가장
바깥쪽의 map 길이가 2 이기 때문에 cbor_encode_map_indefinite()
대신
cbor_encode_map(&writer, 2)
을 사용하고 cbor_encode_break()
를 제거할 수
있습니다.
다음 함수들을 사용해 데이터 타입별로 인코딩할 수 있습니다:
- 정수
cbor_encode_unsigned_integer()
cbor_encode_negative_integer()
- 부동소수점
cbor_encode_float()
cbor_encode_double()
- 문자열
cbor_encode_text_string()
- 바이트열
cbor_encode_byte_string()
- 배열
cbor_encode_array()
- 맵 혹은 딕셔너리
cbor_encode_map()
- 불린
cbor_encode_bool()
- 널
cbor_encode_null()
- undefined
cbor_encode_undefined()
데이터 디코딩 하기
위에서 전송한 인코딩된 데이터는 아래와 같이 디코딩할 수 있습니다:
union cbor_value {
int8_t i8;
int16_t i16;
int32_t i32;
int64_t i64;
float f32;
double f64;
uint8_t *bin;
char *arr;
uint8_t arr_copy[MTU];
} val;
cbor_reader_t reader;
cbor_item_t items[MAX_ITEMS];
size_t n;
cbor_reader_init(&reader, items, sizeof(items) / sizeof(*items));
cbor_parse(&reader, received, received_bytes, &n);
for (size_t i = 0; i < n; i++) {
cbor_item_t *item = items + i;
cbor_decode(&reader, item, &val, sizeof(val));
}
서로 다른 데이터 타입을 구분하지 않고 사용하기 위해 union 을 사용했습니다.
달리, 아래와 같이 cbor_iterate()
를 사용하여 전송받은 메시지를 사용자 정의
자료구조로 변환할 수 있습니다:
static void do_unknown(cbor_reader_t const *reader, cbor_item_t const *item, void *udt) { }
static void do_timestamp(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &p->time, sizeof(p->time));
}
static void do_acc_x(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &(p->data.acc.x), sizeof(p->data.acc.x));
}
static void do_acc_y(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &(p->data.acc.y), sizeof(p->data.acc.y));
}
static void do_acc_z(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &(p->data.acc.z), sizeof(p->data.acc.z));
}
static void do_gyr_x(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &(p->data.gyro.x), sizeof(p->data.gyro.x));
}
static void do_gyr_y(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &(p->data.gyro.y), sizeof(p->data.gyro.y));
}
static void do_gyr_z(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
struct udt *p = (struct udt *)udt;
cbor_decode(reader, item, &(p->data.gyro.z), sizeof(p->data.gyro.z));
}
static struct converter {
void const *primary;
void const *secondary;
void (*run)(cbor_reader_t const *reader, cbor_item_t const *item, void *udt);
} converters[] = {
{ .primary = "t", .secondary = NULL, .run = do_timestamp },
{ .primary = "x", .secondary = "acc", .run = do_acc_x },
{ .primary = "y", .secondary = "acc", .run = do_acc_y },
{ .primary = "z", .secondary = "acc", .run = do_acc_z },
{ .primary = "x", .secondary = "gyro", .run = do_gyr_x },
{ .primary = "y", .secondary = "gyro", .run = do_gyr_y },
{ .primary = "z", .secondary = "gyro", .run = do_gyr_z },
};
static void (*get_converter(void const *primary, size_t primary_len,
void const *secondary, size_t secondary_len))
(cbor_reader_t const *reader, cbor_item_t const *item, void *udt)
{
if ((primary == NULL && secondary == NULL) ||
(primary_len == 0 && secondary_len == 0)) {
return do_unknown;
}
for (size_t i = 0; i < sizeof(converters) / sizeof(converters[0]); i++) {
struct converter const *p = &converters[i];
if (p->primary && memcmp(p->primary, primary, primary_len)) {
continue;
} else if (p->secondary && memcmp(p->secondary, secondary, secondary_len)) {
continue;
}
if (p->primary || p->secondary) {
return p->run;
}
}
return do_unknown;
}
static void convert(cbor_reader_t const *reader, cbor_item_t const *item,
cbor_item_t const *parent, void *udt)
{
void const *primary = NULL;
void const *secondary = NULL;
size_t primary_len = 0;
size_t secondary_len = 0;
if (parent != NULL && parent->type == CBOR_ITEM_MAP) {
if ((item - parent) % 2) { /* key */
return;
}
if (parent->offset > 1) {
secondary = cbor_decode_pointer(reader, parent-1);
secondary_len = (parent-1)->size;
}
primary = cbor_decode_pointer(reader, item-1);
primary_len = (item-1)->size;
}
get_converter(primary, primary_len, secondary, secondary_len)
(reader, item, udt);
}
void example(void const *data, size_t datasize, void *udt)
{
cbor_reader_t reader;
cbor_item_t items[MAX_ITEMS];
size_t n;
cbor_reader_init(&reader, items, sizeof(items) / sizeof(*items));
cbor_error_t err = cbor_parse(&reader, data, datasize, &n);
if (err == CBOR_SUCCESS || err == CBOR_BREAK) {
cbor_iterate(&reader, items, n, 0, convert, udt);
}
}
전체 코드는 여기에서 볼 수 있습니다.