CBOR 데이터 직렬화 포맷

CBOR 데이터 직렬화 포맷

바이너리 포맷인 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);
	}
}

전체 코드는 여기에서 볼 수 있습니다.