Protocol Buffers (protobuf.js)
GoogleのProtocol BuffersのJavaScript/TypeScript実装
Protocol Buffers (protobuf.js)
Protocol Buffers(protobuf)は、Googleが開発した言語中立、プラットフォーム中立な構造化データのシリアライゼーション形式です。protobuf.jsはその公式JavaScript/TypeScript実装です。
主な特徴
- 高いパフォーマンス: JSONよりも高速でコンパクト
- 強力な型システム: スキーマベースの型安全性
- クロスプラットフォーム: 多言語間でのデータ交換
- 後方互換性: スキーマの進化をサポート
- コード生成: 静的型付きクライアント生成
- RPCサポート: gRPCとの統合が容易
主なユースケース
- gRPCサービス: 高速なRPC通信
- マイクロサービス: サービス間のデータ交換
- API通信: 効率的なRESTful API
- データストレージ: 構造化データの永続化
- ゲーム開発: リアルタイムデータ同期
- IoTシステム: デバイス間通信
インストール
# メインライブラリ
npm install protobufjs
# 開発時のCLIツール
npm install protobufjs-cli --save-dev
CDNから直接利用:
<script src="//cdn.jsdelivr.net/npm/[email protected]/dist/protobuf.min.js"></script>
基本的な使い方
1. プロトコルバッファの定義
user.protoファイルでスキーマを定義:
// user.proto
syntax = "proto3";
package userpackage;
enum UserStatus {
INACTIVE = 0;
ACTIVE = 1;
SUSPENDED = 2;
}
message Address {
string street = 1;
string city = 2;
string country = 3;
string postal_code = 4;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
UserStatus status = 4;
Address address = 5;
repeated string tags = 6;
map<string, string> metadata = 7;
int64 created_at = 8;
}
2. 動的読み込み(Runtime)
import protobuf from 'protobufjs';
// .protoファイルの読み込み
protobuf.load('user.proto', function(err, root) {
if (err) throw err;
// メッセージ型の取得
const User = root.lookupType('userpackage.User');
// データの作成
const userData = {
id: 1,
name: '田中太郎',
email: '[email protected]',
status: 'ACTIVE',
address: {
street: '1-1-1 新宿',
city: '東京',
country: '日本',
postal_code: '160-0022'
},
tags: ['premium', 'verified'],
metadata: {
source: 'web',
campaign: 'summer2024'
},
created_at: Date.now()
};
// バリデーション
const errMsg = User.verify(userData);
if (errMsg) throw Error(errMsg);
// メッセージの作成
const message = User.create(userData);
// エンコード
const buffer = User.encode(message).finish();
console.log('エンコード後のサイズ:', buffer.length);
// デコード
const decoded = User.decode(buffer);
console.log('デコード結果:', decoded);
// オブジェクトへの変換
const object = User.toObject(decoded, {
longs: String,
enums: String,
bytes: String,
defaults: true
});
console.log('JavaScript Object:', object);
});
3. Promiseベースの読み込み
import protobuf from 'protobufjs';
async function loadSchema() {
try {
const root = await protobuf.load('user.proto');
const User = root.lookupType('userpackage.User');
// メッセージの処理
const message = User.create({
id: 1,
name: '田中太郎',
email: '[email protected]'
});
return User.encode(message).finish();
} catch (error) {
console.error('スキーマ読み込みエラー:', error);
throw error;
}
}
静的コード生成
1. コード生成コマンド
# JavaScript生成
pbjs -t static-module -w commonjs -o user_pb.js user.proto
# TypeScript定義生成
pbts -o user_pb.d.ts user_pb.js
2. 生成されたコードの使用
// 生成されたコードをインポート
import { User, UserStatus } from './user_pb.js';
// 静的メソッドの使用
const user = User.create({
id: 1,
name: '田中太郎',
email: '[email protected]',
status: UserStatus.ACTIVE,
address: {
street: '1-1-1 新宿',
city: '東京',
country: '日本',
postal_code: '160-0022'
},
tags: ['premium', 'verified'],
metadata: {
source: 'web',
campaign: 'summer2024'
},
created_at: Date.now()
});
// エンコード・デコード
const buffer = User.encode(user).finish();
const decoded = User.decode(buffer);
console.log('User ID:', decoded.id);
console.log('User Name:', decoded.name);
console.log('User Status:', decoded.status);
console.log('Address:', decoded.address);
TypeScriptでの使用
1. 型定義付きコード生成
import { User, UserStatus, Address } from './user_pb';
// 型安全なメッセージ作成
const createUser = (userData: Partial<User>): User => {
const user = User.create(userData);
// TypeScriptの型チェックが有効
if (!user.name || !user.email) {
throw new Error('必須フィールドが不足しています');
}
return user;
};
// 使用例
const user = createUser({
id: 1,
name: '田中太郎',
email: '[email protected]',
status: UserStatus.ACTIVE
});
// エンコード・デコード
const buffer: Uint8Array = User.encode(user).finish();
const decoded: User = User.decode(buffer);
2. デコレーターを使った定義
import { Message, Type, Field, OneOf } from 'protobufjs/light';
@Type.d('UserMessage')
export class UserMessage extends Message<UserMessage> {
@Field.d(1, 'int32')
public id: number;
@Field.d(2, 'string')
public name: string;
@Field.d(3, 'string')
public email: string;
@Field.d(4, 'string', 'optional')
public phone?: string;
@OneOf.d('email', 'phone')
public contact: string;
}
// 使用例
const message = new UserMessage({
id: 1,
name: '田中太郎',
email: '[email protected]'
});
const buffer = UserMessage.encode(message).finish();
const decoded = UserMessage.decode(buffer);
高度な使用例
1. カスタムクラスの登録
import protobuf from 'protobufjs';
// カスタムクラスの定義
class CustomUser {
constructor(properties) {
this.id = properties.id;
this.name = properties.name;
this.email = properties.email;
this.createdAt = new Date(properties.created_at);
}
// カスタムメソッド
getDisplayName() {
return `${this.name} (${this.email})`;
}
isActive() {
return this.status === 'ACTIVE';
}
// 静的メソッド
static fromEmail(email) {
return new CustomUser({
id: 0,
name: email.split('@')[0],
email: email,
created_at: Date.now()
});
}
}
// スキーマ読み込み後にカスタムクラスを登録
protobuf.load('user.proto', function(err, root) {
if (err) throw err;
const UserType = root.lookupType('userpackage.User');
// カスタムクラスを登録
UserType.ctor = CustomUser;
// カスタム機能の追加
CustomUser.prototype.validate = function() {
if (!this.email.includes('@')) {
throw new Error('無効なメールアドレス');
}
};
// 使用例
const buffer = /* エンコードされたデータ */;
const user = UserType.decode(buffer);
console.log(user.getDisplayName()); // カスタムメソッドが利用可能
console.log(user.isActive());
user.validate();
});
2. gRPCサービスの実装
// greeter.proto
/*
syntax = "proto3";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayGoodbye (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
*/
import protobuf from 'protobufjs';
// RPCトランスポート実装
function createRpcImpl(baseUrl) {
return function(method, requestData, callback) {
fetch(`${baseUrl}/${method.name}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-protobuf'
},
body: requestData
})
.then(response => response.arrayBuffer())
.then(responseData => {
callback(null, new Uint8Array(responseData));
})
.catch(error => {
callback(error);
});
};
}
// gRPCクライアントの使用
protobuf.load('greeter.proto', function(err, root) {
if (err) throw err;
const Greeter = root.lookupService('Greeter');
const rpcImpl = createRpcImpl('https://api.example.com/grpc');
// サービスインスタンスの作成
const greeter = Greeter.create(rpcImpl);
// RPCコール(コールバック)
greeter.sayHello({ name: '田中太郎' }, function(err, response) {
if (err) {
console.error('RPC エラー:', err);
return;
}
console.log('応答:', response.message);
});
// RPCコール(Promise)
greeter.sayHello({ name: '田中太郎' })
.then(response => {
console.log('応答:', response.message);
})
.catch(error => {
console.error('RPC エラー:', error);
});
});
3. ストリーミング処理
import protobuf from 'protobufjs';
class ProtobufStreamer {
constructor(schema) {
this.schema = schema;
this.buffer = new Uint8Array();
}
// 複数のメッセージを連続してエンコード
encodeStream(messages) {
const encodedMessages = messages.map(msg => {
const encoded = this.schema.encode(msg).finish();
const length = encoded.length;
// 長さ区切りプロトコル(Length-Delimited)
const lengthBuffer = new Uint8Array(4);
const view = new DataView(lengthBuffer.buffer);
view.setUint32(0, length, true); // リトルエンディアン
// 長さ + データ
const result = new Uint8Array(4 + length);
result.set(lengthBuffer);
result.set(encoded, 4);
return result;
});
// 全てのメッセージを結合
const totalLength = encodedMessages.reduce((sum, msg) => sum + msg.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const msg of encodedMessages) {
result.set(msg, offset);
offset += msg.length;
}
return result;
}
// ストリームからメッセージをデコード
decodeStream(streamData) {
const messages = [];
let offset = 0;
while (offset < streamData.length) {
if (offset + 4 > streamData.length) {
break; // 長さ情報が不完全
}
// 長さを読み取り
const lengthBuffer = streamData.slice(offset, offset + 4);
const view = new DataView(lengthBuffer.buffer);
const length = view.getUint32(0, true);
if (offset + 4 + length > streamData.length) {
break; // データが不完全
}
// メッセージデータを読み取り
const messageData = streamData.slice(offset + 4, offset + 4 + length);
const message = this.schema.decode(messageData);
messages.push(message);
offset += 4 + length;
}
return messages;
}
}
// 使用例
protobuf.load('user.proto', function(err, root) {
if (err) throw err;
const User = root.lookupType('userpackage.User');
const streamer = new ProtobufStreamer(User);
// 複数のユーザーをストリーム形式でエンコード
const users = [
{ id: 1, name: '田中太郎', email: '[email protected]' },
{ id: 2, name: '佐藤花子', email: '[email protected]' },
{ id: 3, name: '山田次郎', email: '[email protected]' }
];
const streamData = streamer.encodeStream(users);
console.log('ストリームサイズ:', streamData.length);
// ストリームからデコード
const decodedUsers = streamer.decodeStream(streamData);
console.log('デコードされたユーザー数:', decodedUsers.length);
decodedUsers.forEach(user => {
console.log(`ID: ${user.id}, Name: ${user.name}`);
});
});
パフォーマンスベンチマーク
2024年のベンチマーク結果
// パフォーマンステスト
function performanceTest() {
const testData = {
users: Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `ユーザー${i}`,
email: `user${i}@example.com`,
status: 'ACTIVE',
tags: ['tag1', 'tag2', 'tag3'],
metadata: {
source: 'test',
version: '1.0',
created: Date.now()
}
}))
};
console.log('パフォーマンステスト開始');
// JSON テスト
let start = performance.now();
const jsonString = JSON.stringify(testData);
const jsonParsed = JSON.parse(jsonString);
const jsonTime = performance.now() - start;
// Protocol Buffers テスト
start = performance.now();
const messages = testData.users.map(user => User.create(user));
const encoded = messages.map(msg => User.encode(msg).finish());
const decoded = encoded.map(buf => User.decode(buf));
const protobufTime = performance.now() - start;
console.log('結果:');
console.log(`JSON: ${jsonTime.toFixed(2)}ms, サイズ: ${jsonString.length} bytes`);
console.log(`Protocol Buffers: ${protobufTime.toFixed(2)}ms, サイズ: ${encoded.reduce((sum, buf) => sum + buf.length, 0)} bytes`);
console.log(`Protocol Buffers高速化: ${(jsonTime / protobufTime).toFixed(2)}倍`);
}
// 実際のベンチマーク結果(Node.js 18.x)
/*
Protocol Buffers (static) x 290,096 ops/sec
JSON (string) x 129,381 ops/sec (55.3% slower)
Google Protobuf x 42,050 ops/sec (85.5% slower)
*/
実用的な例
1. WebSocketでの使用
import protobuf from 'protobufjs';
class ProtobufWebSocket {
constructor(url, schema) {
this.ws = new WebSocket(url);
this.ws.binaryType = 'arraybuffer';
this.schema = schema;
this.messageHandlers = new Map();
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
}
handleMessage(data) {
try {
const buffer = new Uint8Array(data);
const message = this.schema.decode(buffer);
const handler = this.messageHandlers.get(message.constructor.name);
if (handler) {
handler(message);
}
} catch (error) {
console.error('メッセージ処理エラー:', error);
}
}
send(message) {
if (this.ws.readyState === WebSocket.OPEN) {
const buffer = this.schema.encode(message).finish();
this.ws.send(buffer);
}
}
onMessage(messageType, handler) {
this.messageHandlers.set(messageType, handler);
}
}
// 使用例
protobuf.load('chat.proto', function(err, root) {
if (err) throw err;
const ChatMessage = root.lookupType('chat.ChatMessage');
const ws = new ProtobufWebSocket('wss://chat.example.com', ChatMessage);
ws.onMessage('ChatMessage', (message) => {
console.log(`${message.user}: ${message.content}`);
});
// メッセージ送信
ws.send(ChatMessage.create({
user: '田中太郎',
content: 'こんにちは!',
timestamp: Date.now()
}));
});
2. Node.jsでのファイル処理
import fs from 'fs';
import protobuf from 'protobufjs';
class ProtobufFileHandler {
constructor(schemaPath) {
this.root = null;
this.loadSchema(schemaPath);
}
async loadSchema(schemaPath) {
try {
this.root = await protobuf.load(schemaPath);
console.log('スキーマが読み込まれました');
} catch (error) {
console.error('スキーマ読み込みエラー:', error);
}
}
writeToFile(filename, messages, messageType) {
const MessageType = this.root.lookupType(messageType);
const encodedMessages = messages.map(msg => {
const message = MessageType.create(msg);
return MessageType.encode(message).finish();
});
// ファイルヘッダー(メッセージ数)
const header = new Uint8Array(4);
const view = new DataView(header.buffer);
view.setUint32(0, encodedMessages.length, true);
// 全データの結合
const totalSize = header.length + encodedMessages.reduce((sum, msg) => sum + msg.length + 4, 0);
const fileData = new Uint8Array(totalSize);
let offset = 0;
// ヘッダーを書き込み
fileData.set(header, offset);
offset += header.length;
// 各メッセージを書き込み
encodedMessages.forEach(msg => {
// メッセージサイズ
const sizeBuffer = new Uint8Array(4);
const sizeView = new DataView(sizeBuffer.buffer);
sizeView.setUint32(0, msg.length, true);
fileData.set(sizeBuffer, offset);
offset += 4;
// メッセージデータ
fileData.set(msg, offset);
offset += msg.length;
});
fs.writeFileSync(filename, fileData);
console.log(`${messages.length}個のメッセージを${filename}に保存しました`);
}
readFromFile(filename, messageType) {
const MessageType = this.root.lookupType(messageType);
const fileData = fs.readFileSync(filename);
const messages = [];
// ヘッダーを読み込み
const headerView = new DataView(fileData.buffer, 0, 4);
const messageCount = headerView.getUint32(0, true);
let offset = 4;
// 各メッセージを読み込み
for (let i = 0; i < messageCount; i++) {
const sizeView = new DataView(fileData.buffer, offset, 4);
const messageSize = sizeView.getUint32(0, true);
offset += 4;
const messageData = fileData.slice(offset, offset + messageSize);
const message = MessageType.decode(messageData);
messages.push(message);
offset += messageSize;
}
console.log(`${filename}から${messages.length}個のメッセージを読み込みました`);
return messages;
}
}
// 使用例
const handler = new ProtobufFileHandler('user.proto');
// データの保存
const users = [
{ id: 1, name: '田中太郎', email: '[email protected]' },
{ id: 2, name: '佐藤花子', email: '[email protected]' },
{ id: 3, name: '山田次郎', email: '[email protected]' }
];
handler.writeToFile('users.pb', users, 'userpackage.User');
// データの読み込み
const loadedUsers = handler.readFromFile('users.pb', 'userpackage.User');
loadedUsers.forEach(user => {
console.log(`ID: ${user.id}, Name: ${user.name}`);
});
他のライブラリとの比較
| 特徴 | Protocol Buffers | JSON | MessagePack | FlatBuffers |
|---|---|---|---|---|
| パフォーマンス | 高 | 中 | 高 | 最高 |
| サイズ | 小 | 大 | 小 | 小 |
| 可読性 | 低 | 高 | 低 | 低 |
| スキーマ | 必要 | 不要 | 不要 | 必要 |
| 型安全性 | 高 | 低 | 低 | 高 |
| 後方互換性 | 優秀 | 制限的 | 制限的 | 優秀 |
トラブルシューティング
よくある問題と解決策
-
スキーマの進化と互換性
// 古いバージョンとの互換性を保つ const message = User.create({ id: 1, name: '田中太郎', // 新しいフィールドは省略可能 // phone: '090-1234-5678' // 新しいフィールド }); -
大きなメッセージの処理
// メッセージサイズの制限 const MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB function safeEncode(message) { const buffer = MessageType.encode(message).finish(); if (buffer.length > MAX_MESSAGE_SIZE) { throw new Error('メッセージサイズが大きすぎます'); } return buffer; } -
エラーハンドリング
function safeDecode(buffer) { try { return MessageType.decode(buffer); } catch (error) { if (error instanceof protobuf.util.ProtocolError) { console.error('必須フィールドが不足:', error.instance); } else { console.error('データ形式エラー:', error); } throw error; } }
まとめ
Protocol Buffers は、高性能で型安全なデータシリアライゼーションを提供する優れたライブラリです。特に以下の場合に効果的です:
利点
- 高いパフォーマンス: JSONより高速でコンパクト
- 強力な型システム: スキーマベースの型安全性
- クロスプラットフォーム: 多言語間での互換性
- 後方互換性: スキーマの進化をサポート
推奨用途
- gRPCサービスでのRPC通信
- マイクロサービス間のデータ交換
- 高頻度のAPI通信でパフォーマンスが重要な場合
- 型安全性が重要なアプリケーション
Protocol Buffers は、JSONの柔軟性を犠牲にしてパフォーマンスと型安全性を得る、エンタープライズレベルのアプリケーションに最適なソリューションです。