Protocol Buffersを試してみる
今開発担当をしているプロジェクトで、NATSとprotobuf(Protocol Buffers)を用いたAPI開発をする必要がありました。protobufについて全く何も知らない状態だったので、自分用に調べたことをまとめました。
protobufとは何か
上記が公式ドキュメント。
-
言語依存・プラットフォーム依存がない
-
拡張可能な
-
構造化データのシリアル化メカニズムで
-
構造化方法の定義をすれば、
-
ソースコードが自動生成され、
-
構造化データの読み書きが簡単にできる
-
言語は以下をサポートしている
- C++
- C#
- Dart
- Go
- Java
- Kotlin
- Objective-C
- Python
- Ruby
- PHP(proto3のみ)
試しに使ってみる
言語依存がなく、高速にデータをやりとりでき、拡張性が高いのが嬉しいところとのことだったので、試しにPythonとJavaでデータ読み書きのサンプルを作ってみる。
デバイスの仕様
プロセッサ Intel(R) Core(TM) i7-14700KF (3.40 GHz)
実装 RAM 64.0 GB (63.8 GB 使用可能)
システムの種類 64 ビット オペレーティング システム、x64 ベース プロセッサ
Windowsの仕様
エディション Windows 11 Home
バージョン 24H2
インストール日 2025/02/24
OS ビルド 26100.4946
エクスペリエンス Windows 機能エクスペリエンス パック 1000.26100.197.0
環境構築
protobuf compilerをインストールする
公式ドキュメントのインストールガイドを見ながら進めていく。
上記リリースページのAssetsにOSごとのprotobuf compiler(protocという名前のもの)があるので、適したものを選んでダウンロードする。

解凍して得られるprotoc.exeをC:\protoc\bin\protoc.exeのように配置し、PATHを通す。
コマンドプロンプトを開き、以下が確認できればOK。
protoc --version
libprotoc 32.0
protoファイルを作成する
公式ドキュメントのLanguage Guide(Proto3)を見ながら進めていく。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
{型} {名前} = {ID};の形で使う項目を宣言する。(IDはバイナリエンコードで用いる不変の値)
IDの再利用はNG(副作用が多いため)で、例えばint64でresults_per_pageを定義し直したいときはint64 results_per_page = 3;ではなくint64 results_per_page = 4;のように新しいIDを付与する。
今回はゲーム内チャットの送受信のような想定で、以下のようなprotoファイル(chat.proto)を作成。
syntax = "proto3";
message Chat {
int32 sendingPlayerID = 1;
int32 receivingPlayerID = 2;
string chatContent = 3;
bool spoiler = 4;
}
Python側(書き込み側)を準備する
ランタイムを導入する
Pythonのランタイム導入ではコマンドプロンプトで以下のコマンドを実行する。
py -m pip install protobuf==6.32.0
以下のような表示が出ればOK。
Defaulting to user installation because normal site-packages is not writeable
Collecting protobuf==6.32.0
Downloading protobuf-6.32.0-cp310-abi3-win_amd64.whl.metadata (593 bytes)
Downloading protobuf-6.32.0-cp310-abi3-win_amd64.whl (435 kB)
Installing collected packages: protobuf
Successfully installed protobuf-6.32.0
protoファイルからコードを生成する
protoファイルを配置したディレクトリに移動し、以下を実行
protoc --python_out=. chat.proto
成功するとchat_pb2.pyのようなファイルが生成される。
シリアライズを試す
以下のようなコードを実行してみる。
from chat_pb2 import Chat
from google.protobuf.json_format import MessageToJson
# Chatメッセージのインスタンスを生成する
msg = Chat(
sendingPlayerID=101,
receivingPlayerID=202,
chatContent="Hello!",
spoiler=False
)
# シリアライズ
data = msg.SerializeToString()
with open("chat.bin", "wb") as f:
f.write(data)
print("Wrote chat.bin", len(data), "bytes")
print("As JSON:", MessageToJson(msg))
実行すると以下のような出力が得られる。
Wrote chat.bin 13 bytes
As JSON: {
"sendingPlayerID": 101,
"receivingPlayerID": 202,
"chatContent": "Hello!"
}
ls chat.bin
Directory: C:\Users\issan\Playground\proto-demo\proto
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2025/08/17 0:20 13 chat.bin
バイナリエディタで中を見てみると、シリアライズされているっぽいことが確認できた。

Java側(読み込み側)を準備する
ランタイムを導入する
上記からprotobuf-java-4.32.0.jarを入手する。
protoファイルからコードを生成する
protoファイルを配置したディレクトリに移動し、以下を実行
protoc --java_out=./ chat.proto
成功するとChatOuterClass.javaのようなファイルが生成される。
デシリアライズを試す
以下のようなコードをコンパイルし、実行してみる。
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("../chat.bin"));
ChatOuterClass.Chat c = ChatOuterClass.Chat.parseFrom(bytes);
System.out.printf("from %d to %d: %s (spoiler=%s)%n",
c.getSendingPlayerID(),
c.getReceivingPlayerID(),
c.getChatContent(),
c.getSpoiler());
}
}
以下のようなコードを実行してコンパイルする。
mkdir ./out
javac -cp "lib\protobuf-java-4.32.0.jar" -d out src\ChatOuterClass.java src\Main.java
コードを実行すると以下のような結果が得られた。デシリアライズできているっぽい!
java -cp "out;lib\protobuf-java-4.32.0.jar" Main
from 101 to 202: Hello! (spoiler=false)
その他試すと良さそうなこと
- スキーマが拡張されて、書き出し側・読み込み側の片方だけが新版になっても互換性あるらしい
- v2書き出し→v1読み込みでは拡張項目に影響されず既存データの読み込みができる
- v1書き出し→v2読み込みでは拡張項目にはデフォルト値を充てる
- v2書き出し→v1更新→v2読み込みでも新フィールドが失われないらしい
- v1の形式でデータ整形されることがないらしい
- 実務ではレングスや内部バージョンのようなヘッダ情報をつけるらしい
- 配列の扱い
- enumの扱い
- ファイル読み書きではなくTCP通信で同様の実験
リポジトリ
作成したサンプルを以下のリポジトリで公開しました。
参考
Discussion