CBOR for JavaScript
Concise Binary Object Representation (CBOR) RFC7049の純粋JavaScript実装。JSONよりもコンパクトで高速なバイナリシリアライゼーションを実現。
CBOR for JavaScript
概要
CBOR(Concise Binary Object Representation)は、RFC7049で定義されたJSONベースのバイナリシリアライゼーションフォーマットです。cbor-jsは、このフォーマットの純粋JavaScript実装で、ネイティブ依存なしでブラウザとNode.js両方で動作します。JSONよりもコンパクトで高速なデータ交換を実現し、特にIoTアプリケーションやリアルタイム通信で威力を発揮します。
主な特徴
- コンパクトなサイズ: JSONよりもサイズが小さく、ネットワーク転送が効率的
- 高速処理: バイナリフォーマットにより高速なエンコード/デコードが可能
- 拡張可能: カスタムデータタイプのサポート
- ブラウザ互換性: WebSocketでのArrayBuffer通信をサポート
- 標準準拠: RFC7049に完全準拠した実装
- ライトウェイト: 依存関係なしで簡単に組み込み可能
モダンな代替ライブラリ
2025年現在、より高性能な代替ライブラリが利用可能です:
- cbor-x: 超高速実装(cbor-jsの3-10倍高速)
- cbor: アクティブに維持されている公式ライブラリ
- CBOR.js (cyberphone): リッチなAPIを持つモダン実装
インストール
cbor-js(オリジナル)
npm install cbor-js
モダンな代替ライブラリ
# 高性能を重視する場合
npm install cbor-x
# 標準実装を使用したい場合
npm install cbor
# リッチなAPIを使用したい場合
npm install cbor2
基本的な使い方
cbor-jsでのシンプルな例
const CBOR = require('cbor-js');
// シンプルなデータのシリアライゼーション
const originalData = {
name: '田中太郎',
age: 30,
isActive: true,
scores: [85, 92, 78],
address: {
city: '東京',
zipCode: '100-0001'
},
tags: null,
balance: 1234.56
};
// エンコード(JavaScriptオブジェクト → ArrayBuffer)
const encoded = CBOR.encode(originalData);
console.log('CBORエンコードサイズ:', encoded.byteLength, 'バイト');
// デコード(ArrayBuffer → JavaScriptオブジェクト)
const decoded = CBOR.decode(encoded);
console.log('デコード結果:', decoded);
// JSONとのサイズ比較
const jsonString = JSON.stringify(originalData);
const jsonBytes = new TextEncoder().encode(jsonString).length;
console.log('JSONサイズ:', jsonBytes, 'バイト');
console.log('サイズ削減率:',
Math.round((1 - encoded.byteLength / jsonBytes) * 100) + '%');
cbor-xでの高性能例
const { encode, decode } = require('cbor-x');
// 大量データの処理
const largeData = {
users: Array.from({length: 1000}, (_, i) => ({
id: i + 1,
name: `ユーザー${i + 1}`,
email: `user${i + 1}@example.com`,
timestamp: Date.now(),
metadata: {
department: 'engineering',
level: 'senior',
active: true
}
})),
totalCount: 1000,
generatedAt: new Date().toISOString()
};
// 高速エンコード
const startTime = performance.now();
const encodedData = encode(largeData);
const encodeTime = performance.now() - startTime;
console.log(`エンコード時間: ${encodeTime.toFixed(2)}ms`);
console.log(`エンコードサイズ: ${encodedData.length} バイト`);
// 高速デコード
const decodeStartTime = performance.now();
const decodedData = decode(encodedData);
const decodeTime = performance.now() - decodeStartTime;
console.log(`デコード時間: ${decodeTime.toFixed(2)}ms`);
console.log(`デコードユーザー数: ${decodedData.users.length}`);
CBOR.js(cyberphone)でのリッチAPI
const CBOR = require('cbor2');
// リッチなAPIでのデータ作成
const cborMap = CBOR.Map()
.set(CBOR.Int(1), CBOR.Float(45.7))
.set(CBOR.Int(2), CBOR.String("こんにちは世界!"))
.set(CBOR.String("array"), CBOR.Array().add(CBOR.String("要素1")).add(CBOR.String("要素2")))
.set(CBOR.String("nested"), CBOR.Map()
.set(CBOR.String("key1"), CBOR.Boolean(true))
.set(CBOR.String("key2"), CBOR.Null())
);
// エンコード
const encoded = cborMap.encode();
console.log('エンコードサイズ:', encoded.length, 'バイト');
// デコード
const decoded = CBOR.decode(encoded);
console.log('デコード結果:', decoded);
// カスタムデータタイプの使用
const bigIntData = CBOR.BigInt("123456789012345678901234567890");
const dateData = CBOR.DateTime(new Date());
const binaryData = CBOR.Bytes(new Uint8Array([1, 2, 3, 4, 5]));
const complexData = CBOR.Map()
.set(CBOR.String("bigint"), bigIntData)
.set(CBOR.String("date"), dateData)
.set(CBOR.String("binary"), binaryData);
const complexEncoded = complexData.encode();
console.log('複合データサイズ:', complexEncoded.length, 'バイト');
WebSocketでの実用例
ブラウザ側コード
class CBORWebSocketClient {
constructor(url) {
this.websocket = new WebSocket(url);
this.websocket.binaryType = 'arraybuffer';
this.messageHandlers = new Map();
this.websocket.onopen = this.onOpen.bind(this);
this.websocket.onmessage = this.onMessage.bind(this);
this.websocket.onclose = this.onClose.bind(this);
this.websocket.onerror = this.onError.bind(this);
}
onOpen(event) {
console.log('WebSocket接続が開かれました');
// 初期化メッセージを送信
this.send({
type: 'init',
timestamp: Date.now(),
clientInfo: {
userAgent: navigator.userAgent,
language: navigator.language
}
});
}
onMessage(event) {
try {
const message = CBOR.decode(event.data);
console.log('メッセージ受信:', message);
// メッセージタイプに応じた処理
if (this.messageHandlers.has(message.type)) {
this.messageHandlers.get(message.type)(message);
}
} catch (error) {
console.error('CBORデコードエラー:', error);
}
}
onClose(event) {
console.log('WebSocket接続が閉じられました');
}
onError(error) {
console.error('WebSocketエラー:', error);
}
send(data) {
if (this.websocket.readyState === WebSocket.OPEN) {
const encoded = CBOR.encode(data);
this.websocket.send(encoded);
}
}
on(messageType, handler) {
this.messageHandlers.set(messageType, handler);
}
// チャットメッセージを送信
sendChatMessage(message) {
this.send({
type: 'chat',
message: message,
timestamp: Date.now(),
userId: localStorage.getItem('userId')
});
}
// ユーザーアクティビティを送信
sendUserActivity(activity) {
this.send({
type: 'activity',
activity: activity,
timestamp: Date.now(),
pageUrl: window.location.href
});
}
}
// 使用例
const client = new CBORWebSocketClient('ws://localhost:8080');
// メッセージハンドラーの登録
client.on('chat', (message) => {
console.log(`チャットメッセージ: ${message.message}`);
displayChatMessage(message);
});
client.on('notification', (message) => {
console.log(`通知: ${message.content}`);
showNotification(message);
});
// ユーザーアクションのトラッキング
document.addEventListener('click', (e) => {
client.sendUserActivity({
type: 'click',
element: e.target.tagName,
x: e.clientX,
y: e.clientY
});
});
Node.jsサーバー側コード
const WebSocket = require('ws');
const CBOR = require('cbor-js');
class CBORWebSocketServer {
constructor(port) {
this.wss = new WebSocket.Server({ port });
this.clients = new Map();
this.wss.on('connection', this.onConnection.bind(this));
console.log(`CBOR WebSocketサーバーがポート${port}で起動しました`);
}
onConnection(ws) {
const clientId = this.generateClientId();
this.clients.set(clientId, {
ws: ws,
info: {},
lastActivity: Date.now()
});
console.log(`クライアント接続: ${clientId}`);
ws.on('message', (data) => {
try {
const message = CBOR.decode(data);
this.handleMessage(clientId, message);
} catch (error) {
console.error('CBORデコードエラー:', error);
}
});
ws.on('close', () => {
console.log(`クライアント切断: ${clientId}`);
this.clients.delete(clientId);
});
ws.on('error', (error) => {
console.error(`クライアントエラー ${clientId}:`, error);
});
}
handleMessage(clientId, message) {
const client = this.clients.get(clientId);
if (!client) return;
client.lastActivity = Date.now();
switch (message.type) {
case 'init':
client.info = message.clientInfo;
this.sendToClient(clientId, {
type: 'welcome',
message: 'サーバーに接続しました',
clientId: clientId,
timestamp: Date.now()
});
break;
case 'chat':
console.log(`チャットメッセージ from ${clientId}: ${message.message}`);
// 全クライアントにブロードキャスト
this.broadcast({
type: 'chat',
message: message.message,
userId: message.userId,
timestamp: message.timestamp
}, clientId);
break;
case 'activity':
console.log(`ユーザーアクティビティ from ${clientId}:`, message.activity);
// アクティビティデータをログやデータベースに保存
this.logActivity(clientId, message.activity);
break;
default:
console.log(`未知のメッセージタイプ: ${message.type}`);
}
}
sendToClient(clientId, data) {
const client = this.clients.get(clientId);
if (client && client.ws.readyState === WebSocket.OPEN) {
const encoded = CBOR.encode(data);
client.ws.send(encoded);
}
}
broadcast(data, excludeClientId = null) {
const encoded = CBOR.encode(data);
this.clients.forEach((client, clientId) => {
if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(encoded);
}
});
}
generateClientId() {
return 'client_' + Math.random().toString(36).substr(2, 9);
}
logActivity(clientId, activity) {
// アクティビティデータをログファイルやデータベースに保存
const logEntry = {
clientId: clientId,
activity: activity,
timestamp: Date.now()
};
// コンソールログでの例
console.log('アクティビティログ:', logEntry);
}
// 定期的なスタッツメッセージを送信
startStatusBroadcast() {
setInterval(() => {
const status = {
type: 'status',
connectedClients: this.clients.size,
serverTime: Date.now(),
uptime: process.uptime()
};
this.broadcast(status);
}, 10000); // 10秒ごと
}
}
// サーバーを起動
const server = new CBORWebSocketServer(8080);
server.startStatusBroadcast();
IoTアプリケーションでの使用
センサーデータの効率的な送信
class IoTDataCollector {
constructor() {
this.sensorData = [];
this.batchSize = 10;
this.sendInterval = 5000; // 5秒
this.startDataCollection();
}
// センサーデータのシミュレーション
generateSensorData() {
return {
deviceId: 'sensor_001',
timestamp: Date.now(),
temperature: Math.round((Math.random() * 30 + 10) * 100) / 100,
humidity: Math.round((Math.random() * 50 + 30) * 100) / 100,
pressure: Math.round((Math.random() * 50 + 1000) * 100) / 100,
batteryLevel: Math.round((Math.random() * 100) * 100) / 100,
location: {
lat: 35.6762 + (Math.random() - 0.5) * 0.01,
lng: 139.6503 + (Math.random() - 0.5) * 0.01
}
};
}
startDataCollection() {
setInterval(() => {
const data = this.generateSensorData();
this.sensorData.push(data);
// バッチサイズに達したら送信
if (this.sensorData.length >= this.batchSize) {
this.sendBatch();
}
}, this.sendInterval);
}
sendBatch() {
const batch = {
batchId: this.generateBatchId(),
timestamp: Date.now(),
deviceType: 'environmental_sensor',
data: this.sensorData.slice()
};
// CBORでエンコード
const encoded = CBOR.encode(batch);
// JSONとのサイズ比較
const jsonSize = new TextEncoder().encode(JSON.stringify(batch)).length;
console.log(`バッチ送信: CBOR(${encoded.byteLength}バイト) vs JSON(${jsonSize}バイト)`);
console.log(`サイズ削減: ${Math.round((1 - encoded.byteLength / jsonSize) * 100)}%`);
// サーバーに送信
this.uploadData(encoded);
// バッファをクリア
this.sensorData = [];
}
uploadData(encodedData) {
fetch('/api/sensor-data', {
method: 'POST',
headers: {
'Content-Type': 'application/cbor',
'Authorization': 'Bearer ' + this.getAuthToken()
},
body: encodedData
})
.then(response => {
if (response.ok) {
console.log('センサーデータのアップロード成功');
} else {
console.error('アップロードエラー:', response.status);
}
})
.catch(error => {
console.error('ネットワークエラー:', error);
});
}
generateBatchId() {
return 'batch_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
}
getAuthToken() {
return localStorage.getItem('iot_auth_token') || 'demo_token';
}
}
// IoTデータコレクターを起動
const collector = new IoTDataCollector();
Express.jsでのIoTデータ受信サーバー
const express = require('express');
const CBOR = require('cbor-js');
const app = express();
// CBORバイナリデータのパースミドルウェア
app.use('/api/sensor-data', (req, res, next) => {
if (req.headers['content-type'] === 'application/cbor') {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => {
try {
const buffer = Buffer.concat(chunks);
const arrayBuffer = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
);
req.body = CBOR.decode(arrayBuffer);
next();
} catch (error) {
console.error('CBORデコードエラー:', error);
res.status(400).json({ error: 'Invalid CBOR data' });
}
});
} else {
next();
}
});
// センサーデータ受信エンドポイント
app.post('/api/sensor-data', (req, res) => {
const batch = req.body;
console.log(`センサーデータバッチ受信:`);
console.log(`- バッチID: ${batch.batchId}`);
console.log(`- デバイスタイプ: ${batch.deviceType}`);
console.log(`- データポイント数: ${batch.data.length}`);
// データの処理と保存
batch.data.forEach(dataPoint => {
processSensorData(dataPoint);
});
// レスポンスをCBORで返す
const response = {
status: 'success',
batchId: batch.batchId,
processedCount: batch.data.length,
timestamp: Date.now()
};
const encodedResponse = CBOR.encode(response);
res.set('Content-Type', 'application/cbor');
res.send(Buffer.from(encodedResponse));
});
// センサーデータの処理
function processSensorData(dataPoint) {
// データバリデーション
if (dataPoint.temperature < -10 || dataPoint.temperature > 50) {
console.warn(`異常な温度値: ${dataPoint.temperature}°C`);
}
// データベースへの保存(シミュレーション)
console.log(`データ保存: ${dataPoint.deviceId} - 温度: ${dataPoint.temperature}°C`);
// アラートのチェック
checkAlerts(dataPoint);
}
function checkAlerts(dataPoint) {
// バッテリー低下アラート
if (dataPoint.batteryLevel < 20) {
console.log(`バッテリー低下アラート: ${dataPoint.deviceId} - ${dataPoint.batteryLevel}%`);
}
// 温度異常アラート
if (dataPoint.temperature > 40) {
console.log(`高温アラート: ${dataPoint.deviceId} - ${dataPoint.temperature}°C`);
}
}
app.listen(3000, () => {
console.log('IoT CBORサーバーがポート3000で起動しました');
});
パフォーマンス最適化
ベンチマークテスト
const { performance } = require('perf_hooks');
// ベンチマーク用のテストデータ生成
function generateTestData(count) {
return {
metadata: {
version: '1.0.0',
timestamp: Date.now(),
source: 'benchmark-test'
},
items: Array.from({length: count}, (_, i) => ({
id: i + 1,
name: `アイテム${i + 1}`,
description: `これはアイテム${i + 1}の説明です。テストデータとして作成されました。`,
price: Math.round(Math.random() * 10000) / 100,
inStock: Math.random() > 0.3,
categories: ['category1', 'category2', 'category3'].slice(0, Math.floor(Math.random() * 3) + 1),
attributes: {
weight: Math.round(Math.random() * 1000) / 100,
color: ['red', 'blue', 'green'][Math.floor(Math.random() * 3)],
size: ['S', 'M', 'L', 'XL'][Math.floor(Math.random() * 4)]
}
}))
};
}
// ベンチマーク実行関数
function runBenchmark() {
const testSizes = [100, 1000, 5000, 10000];
const libraries = [
{ name: 'cbor-js', encode: CBOR.encode, decode: CBOR.decode },
{ name: 'JSON', encode: JSON.stringify, decode: JSON.parse }
];
console.log('=== CBOR vs JSON ベンチマーク ===\n');
testSizes.forEach(size => {
console.log(`テストデータサイズ: ${size}件`);
const testData = generateTestData(size);
libraries.forEach(lib => {
// エンコードベンチマーク
const encodeStart = performance.now();
const encoded = lib.encode(testData);
const encodeTime = performance.now() - encodeStart;
// デコードベンチマーク
const decodeStart = performance.now();
const decoded = lib.decode(encoded);
const decodeTime = performance.now() - decodeStart;
// サイズ測定
let dataSize;
if (lib.name === 'JSON') {
dataSize = new TextEncoder().encode(encoded).length;
} else {
dataSize = encoded.byteLength;
}
console.log(` ${lib.name}:`);
console.log(` エンコード: ${encodeTime.toFixed(2)}ms`);
console.log(` デコード: ${decodeTime.toFixed(2)}ms`);
console.log(` サイズ: ${dataSize}バイト`);
console.log(` データ整合性: ${JSON.stringify(testData).length === JSON.stringify(decoded).length ? 'OK' : 'NG'}`);
});
console.log(''); // 空行
});
}
// ベンチマーク実行
runBenchmark();
ベストプラクティス
1. ライブラリの選択
- cbor-js: シンプルな使用例、ブラウザ互換性を重視する場合
- cbor-x: 最高のパフォーマンスが必要な場合
- cbor: アクティブな維持と標準準拠が必要な場合
2. エラーハンドリング
function safeCBOREncode(data) {
try {
return CBOR.encode(data);
} catch (error) {
console.error('CBORエンコードエラー:', error);
throw new Error('Failed to encode data as CBOR');
}
}
function safeCBORDecode(buffer) {
try {
return CBOR.decode(buffer);
} catch (error) {
console.error('CBORデコードエラー:', error);
throw new Error('Failed to decode CBOR data');
}
}
3. メモリ効率の最適化
// バッファプールでメモリ使用量を最適化
class CBORBufferPool {
constructor() {
this.pool = [];
this.maxSize = 10;
}
get(size) {
if (this.pool.length > 0) {
const buffer = this.pool.pop();
if (buffer.byteLength >= size) {
return buffer.slice(0, size);
}
}
return new ArrayBuffer(size);
}
return(buffer) {
if (this.pool.length < this.maxSize) {
this.pool.push(buffer);
}
}
}
const bufferPool = new CBORBufferPool();
まとめ
CBOR for JavaScriptは、特にIoTアプリケーション、リアルタイム通信、モバイルアプリケーションにおいて、JSONよりもコンパクトで高速なデータ交換を実現する優れたソリューションです。オリジナルのcbor-jsはシンプルで使いやすいですが、2025年現在ではより高性能なcbor-xやアクティブに維持されているcborライブラリを検討することをおすすめします。プロジェクトの要件に応じて適切なライブラリを選択し、バンド幅と処理速度の最適化を図ることが重要です。