前回の記事では、Nanopbを使ってESP-IDFのビルドシステムにgRPCを統合し、ローカル環境での疎通を確認しました。
前回:ESP32でgRPC(Protobuf)通信!Nanopbを使って爆速・軽量な自動ビルド環境を構築する(ローカル疎通)
今回は、舞台をクラウドへ移します。エンドポイントを Azure Functions に構築し、モダンな JSON通信 と gRPCバイナリ通信 を実環境でぶつけ、そのパフォーマンスを徹底比較します。
理論的背景の解説はこちら
実装に入る前に、gRPCの仕組みやJSONとの比較を詳しく知りたい方は、以下の概念図解記事を併せてご覧ください。
マイコン開発の新常識?ESP32でgRPCを動かすための「理論のハブ」
1. 比較検証のシステム構成
今回の検証環境の全体像です。ESP32からインターネット経由でAzureへデータを飛ばします。
Azure Functions側の実装(Python)
Azure側も uv を活用してモダンな環境を構築します。第1回で作成した定義ファイルから生成された sensor_pb2.py をデプロイ先に配置し、リクエストの req.get_body() からバイナリデータを直接パースします。
import azure.functions as func
import logging
import sensor_pb2
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
# --- 1. JSON用エンドポイント(比較用) ---
@app.route(route="esp_data_json")
def http_trigger_json(req: func.HttpRequest) -> func.HttpResponse:
try:
req_body = req.get_json()
logging.info(f"[JSON] Received: {req_body}")
return func.HttpResponse(f"JSON Success", status_code=200)
except ValueError:
return func.HttpResponse("Invalid JSON", status_code=400)
# --- 2. gRPC (Protobuf) 用エンドポイント(本命) ---
@app.route(route="esp_data_grpc")
def http_trigger_grpc(req: func.HttpRequest) -> func.HttpResponse:
try:
# HTTPボディから生のバイナリ(bytes)を直接取得
body = req.get_body()
# Protobufとしてデコード
sensor_msg = sensor_pb2.SensorData()
sensor_msg.ParseFromString(body)
logging.info(f"[gRPC] Decoded: {sensor_msg.device_id}, Temp: {sensor_msg.temperature}")
return func.HttpResponse(f"gRPC Success: {len(body)} bytes", status_code=200)
except Exception as e:
logging.error(f"Decode Error: {e}")
return func.HttpResponse(f"Decode Failed", status_code=500)
2. ESP32側の実装:比較用ループ
ESP32-S3上で、同じ内容のデータをJSON形式とProtobuf形式で交互にHTTPS POST送信するループを実装しました。シリアライズにかかる純粋な処理時間を esp_timer_get_time() で計測し、その「軽さ」を可視化します。
// --- JSONシリアライズ ---
int64_t j_start = esp_timer_get_time();
int j_len = snprintf(json_data, sizeof(json_data),
"{\"device_id\":\"ESP32-S3-LAB\",\"temperature\":%.2f,\"humidity\":%d}", temp, hum);
int64_t j_end = esp_timer_get_time();
ESP_LOGI(TAG, "[JSON] Serialize Time: %lld us", (j_end - j_start));
// --- gRPC (Protobuf) シリアライズ ---
int64_t p_start = esp_timer_get_time();
pb_ostream_t stream = pb_ostream_from_buffer(pb_buffer, sizeof(pb_buffer));
if (pb_encode(&stream, SensorData_fields, &msg)) {
int64_t p_end = esp_timer_get_time();
ESP_LOGI(TAG, "[gRPC] Serialize Time: %lld us", (p_end - p_start));
}
3. 【実測結果】gRPC vs JSON 徹底比較
Azure Functions(HTTPS環境)へ送信した際のログから得られた、平均的な実測データです。
| 項目 | JSON通信 | gRPC (Nanopb) | 改善率 |
|---|---|---|---|
| シリアライズ時間 | 約424 μs | 約157 μs | 2.7倍高速 |
| ペイロードサイズ | 62 bytes | 21 bytes | 約1/3に凝縮 |
| ネットワーク通信時間 | 約1.3 sec | 約1.3 sec | 差なし |
【考察】ネットワークタイムが変わらない「巨象」の正体
実測の結果、トータルの通信時間はどちらも約1.3秒前後でほぼ差が出ませんでした。
これは、通信時間の大部分を TLSハンドシェイク(HTTPSの暗号化確立) が占めているためです。

現代のインターネット環境において、21バイトと62バイトというデータの差は、TLSハンドシェイクという「巨象」の前では誤差に埋もれてしまうのが現実です。
4. なぜ「トータルタイムが変わらなくても」gRPCを選ぶのか
数値だけ見ると「JSONでいいのでは?」と思われがちですが、マイコン開発という制約の多い環境では、数値以上に重要な3つのメリットがあります。
① CPU稼働時間の削減(省電力化の鍵)
シリアライズ時間はgRPCが 約2.7倍も高速 です。
1回あたりの差はわずかですが、数万回の送信を繰り返すバッテリー駆動デバイスにおいて、CPUが「フルパワー」で動く時間を削れることは、Deep Sleepへの移行を早め、稼働寿命の延長に直結します。
② ペイロードサイズ 1/3 のメリット
- 回線コスト: LTE-MやNB-IoTのような従量課金回線では、この数バイトの差が累積コストに効きます。
- 成功率: パケットが小さいほど、電波強度が弱い場所での送信成功率(パケットロス耐性)が向上します。
③ 型安全という「開発効率」
JSONのような文字列パースではなく、設計図(.proto)から生成された構造体に値を流し込むだけで通信が完結します。サーバーとクライアントで共通の定義を使うため、「型違いによるランタイムエラー」をコンパイル段階で防げる堅牢性は、IoT開発において大きな武器になります。
5. 遭遇した技術的課題:Stack Overflow
検証中、A stack overflow in task main has been detected というクラッシュに頻繁に遭遇しました。
原因:
HTTPS通信(TLS)は内部で巨大なバッファ(Mbed TLSの送受信バッファなど)を消費します。app_main のデフォルトスタックサイズ(通常4KB程度)では、この巨大な「器」を支えきれません。
対策:
sdkconfig にて、メインタスクのスタックサイズを 8192以上(今回は安全を見て16KBを推奨) に拡張することで安定稼働しました。gRPC自体のメモリ消費が少ない分、通信レイヤーにリソースを割く余裕を作ることが重要です。
6. まとめ
Azure Functionsとの比較により、「速度(トータルタイム)そのものはHTTPSのオーバーヘッドに支配される」 という不都合な実態が明らかになりました。
しかし、「CPU負荷の低減」と「パケットの極小化」 においてgRPC(Nanopb)が圧倒的に有利であることも同時に証明されました。リソースが限られたエッジデバイスと、パワフルなクラウドを繋ぐ「接合点」として、gRPCは非常に洗練された選択肢と言えます。
📦 ソースコード
比較検証に使用したESP32側のコードと、Azure Functionsのプロジェクトを公開しています。
GitHub: esp32-grpc-vs-json-azure