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アプリケーションやリアルタイム通信で威力を発揮します。

主な特徴

  1. コンパクトなサイズ: JSONよりもサイズが小さく、ネットワーク転送が効率的
  2. 高速処理: バイナリフォーマットにより高速なエンコード/デコードが可能
  3. 拡張可能: カスタムデータタイプのサポート
  4. ブラウザ互換性: WebSocketでのArrayBuffer通信をサポート
  5. 標準準拠: RFC7049に完全準拠した実装
  6. ライトウェイト: 依存関係なしで簡単に組み込み可能

モダンな代替ライブラリ

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ライブラリを検討することをおすすめします。プロジェクトの要件に応じて適切なライブラリを選択し、バンド幅と処理速度の最適化を図ることが重要です。