Zenohは、Pub/Subによる配信と、保存された値や計算結果への問い合わせ(Query)を1つのプロトコルで扱う分散通信ミドルウェアです。ここでは現行版1.10.0を実際に動かしながら、3つの動作モードとルーター(zenohd)の役割、Pub/SubとQueryの最小コード、MQTTやDDSとの違い、ROS 2での使い方までを実装目線で整理します。掲載したコードはmacOS 26.6.2(x86_64)・Python 3.9.6・eclipse-zenoh 1.10.0で実行し、その出力をそのまま載せています。
まとめ:Zenohを採用する前に押さえる要点
Zenohの現行版は1.10.0(2026年8月14日公開)で、本体・Zenoh-Pico・各言語バインディングが同じ版番号で並行リリースされています。ライセンスはApache-2.0とEPL-2.0のデュアルで、実装言語はRustです。
設計上の要点は3つです。第一に、アプリケーションは既定でpeerモードとして起動し、同一ネットワーク内の相手を自動探索して直接つながります。仲介プロセスであるzenohd(Zenohルーター)は必須ではなく、サブネットをまたぐ場合や制約デバイスをclientモードでぶら下げる場合に置きます。第二に、Pub/Subに加えてQuery(問い合わせと応答)が同じキー空間の上に載るため、「流れてくる値を受け取る」処理と「今の値を取りに行く」処理を別ミドルウェアに分けずに済みます。第三に、ROS 2ではrmw_zenoh_cppが公式のRMW実装として提供されており、Kilted KaijuではTier 1に位置付けられています。
逆に注意すべき点は、マルチキャスト探索がサブネットを越えないこと、共有メモリ転送は送受信の双方で有効化が必要なことなど、既定値のままだと期待通りに動かない設定が残る点です。以降でその具体を、設定ファイルの実値と実行結果に沿って説明します。
Zenohの位置づけとキー式の考え方
ZenohはEclipse Foundationのプロジェクトとして開発されている通信ミドルウェアで、データの配信・保存・問い合わせを同じキー空間の上で扱う点に特徴があります。データはキーと値の組で表され、キーはUnixのファイルパスに似た階層表現になります。たとえばmyhome/kitchen/tempのように書き、購読側はmyhome/kitchen/*(1階層)やmyhome/kitchen/**(0階層を含む複数階層)のワイルドカードでまとめて受け取れます。
クライアントライブラリはRustが本家で、その上に主要言語のバインディングが載る構成です。Python・C・C++・Java・Kotlin・TypeScript・Go・C#、および低リソース機器向けのZenoh-Picoが公式リポジトリとして公開されています。実装や機能追加はRust本体から進むため、最新機能を追う用途ではRust、検証やグルー処理ではPythonという使い分けになります。
peer・client・routerの3モードとzenohdの役割
Zenohのプロセスは3つのモードのいずれかで動きます。どれを選ぶかで、探索の仕方と維持するセッション数が変わります。
| モード | 接続の持ち方 | 探索 | 主な用途 |
|---|---|---|---|
| peer(既定) | 相手ごとに直接セッション | マルチキャスト+ゴシップ | 同一NW内のアプリ間通信 |
| client | 常に1セッションのみ | マルチキャスト+connect指定 | 制約デバイス/セッション数の抑制 |
| router | 他ノードの通信を中継 | connect指定でルーター間も連結 | サブネット越え/メッシュ構成 |
既定値はmode: "peer"で、peerモードのアプリはマルチキャストグループ224.0.0.224のUDPポート7446へスカウトメッセージを送り、見つけた相手へ自動接続します。待ち受け側のエンドポイントはrouterがtcp/[::]:7447、peerがtcp/[::]:0が既定です。つまりZenohは「ブローカー必須」ではなく、ルーターを置かない構成が出発点になっています。
ルーターを使う場合は、macOSならHomebrew、Debian系ならEclipseのaptリポジトリを追加してzenohdをインストールし、設定ファイルを渡して起動します。設定はJSON5またはYAMLで、ストレージやRESTプラグインもここで有効化します。
$ brew tap eclipse-zenoh/homebrew-zenoh
$ brew install zenoh
$ zenohd -c zenoh-myhome.json5
Pub/SubとQueryのPython実装
Zenohの理解で最初につまずきやすいのが、Pub/SubとQueryが同じキー空間に同居する点です。ここでは検証用にマルチキャスト探索を切り、TCPエンドポイントを明示した最小構成で両方を動かします。ライブラリはpip install eclipse-zenohで導入できます。
Pub/Sub:配信側と購読側の直結構成
購読側は127.0.0.1:7450で待ち受け、myhome/kitchen/**を購読します。
import json, time, zenoh
conf = zenoh.Config.from_json5(json.dumps({
"mode": "peer",
"listen": {"endpoints": ["tcp/127.0.0.1:7450"]},
"scouting": {"multicast": {"enabled": False}},
}))
def listener(sample):
print(f"[SUB] {sample.key_expr} = {sample.payload.to_string()}", flush=True)
with zenoh.open(conf) as session:
sub = session.declare_subscriber("myhome/kitchen/**", listener)
time.sleep(6)
配信側は同じアドレスへconnectし、1秒間隔で3つの値を送ります。
import json, time, zenoh
conf = zenoh.Config.from_json5(json.dumps({
"mode": "peer",
"connect": {"endpoints": ["tcp/127.0.0.1:7450"]},
"scouting": {"multicast": {"enabled": False}},
}))
with zenoh.open(conf) as session:
pub = session.declare_publisher("myhome/kitchen/temp")
for t in (21, 22, 23):
pub.put(f"{t}")
time.sleep(1)
購読側を先に起動してから配信側を実行すると、次の出力が得られました。ルータープロセスは起動していません。ワイルドカード**を宣言した購読が、具体キーmyhome/kitchen/tempへの配信を受け取っています。
[SUB] myhome/kitchen/temp = 21
[SUB] myhome/kitchen/temp = 22
[SUB] myhome/kitchen/temp = 23
Query:queryableの宣言とgetによる応答取得
Queryは、キーに対する問い合わせに応答する側(queryable)と、問い合わせる側(get)で構成します。応答側はdeclare_queryableでキーを宣言し、受け取ったqueryにreplyを返します。
import json, time, zenoh
conf = zenoh.Config.from_json5(json.dumps({
"mode": "peer",
"listen": {"endpoints": ["tcp/127.0.0.1:7460"]},
"scouting": {"multicast": {"enabled": False}},
}))
def handler(query):
query.reply(query.key_expr, "23")
with zenoh.open(conf) as session:
q = session.declare_queryable("myhome/kitchen/temp", handler)
time.sleep(6)
問い合わせ側はsession.getの戻り値を反復し、応答を順に受け取ります。
import json, zenoh
conf = zenoh.Config.from_json5(json.dumps({
"mode": "peer",
"connect": {"endpoints": ["tcp/127.0.0.1:7460"]},
"scouting": {"multicast": {"enabled": False}},
}))
with zenoh.open(conf) as session:
for reply in session.get("myhome/kitchen/temp"):
print(f"[GET] {reply.ok.key_expr} = {reply.ok.payload.to_string()}", flush=True)
[GET] myhome/kitchen/temp = 23
この応答側をアプリで書く代わりに、zenohdのstorage_managerプラグインでストレージを設定すれば、配信された値をルーターが保持し、同じgetで最新値を取り出せます。公式ドキュメントの入門では、RESTプラグインとメモリボリュームを有効にした設定例が示されています。センサー値を配信しつつ、後から起動したプロセスが現在値を取りに行く、という構成が追加のデータベースなしで組めるのがQueryの利点です。
MQTT・DDSとの違いと使い分け
選定でよく比較されるのがMQTTとDDSです。通信モデルと探索の仕方が異なるため、置き換えではなく適材適所で考えるのが実務的です。
| 観点 | Zenoh | MQTT | DDS |
|---|---|---|---|
| 通信モデル | Pub/Sub+Query+ストレージ | Pub/Sub | Pub/Sub(データ中心) |
| 仲介プロセス | 任意(peerで直接も可) | ブローカー必須 | 不要 |
| 探索 | マルチキャスト+ゴシップ | ブローカーへ接続 | マルチキャスト中心 |
| 広域接続 | ルーター経由で連結 | ブローカー中継 | 既定の探索では不得手 |
MQTTはブローカーを中心に据える構成が前提で、機器からクラウドへ集約する用途に向きます。ブローカーの選定や配信品質の設計はそれ自体が論点になるため、MQTTブローカーとは?仕組み・主要ブローカー比較・構築手順を解説とMQTT QoSとは?0・1・2の違いと選び方をレベル別に解説を参照してください。DDSはブローカー不要で低遅延ですが、既定のマルチキャスト探索はサブネットをまたぐ環境で扱いにくく、越えるにはDiscovery Serverなど別の仕組みを足すことになります。Zenohはこの中間で、近接ノードは直接、離れたノードはルーター経由という構成を1つの設定で切り替えられる点が差です。逆に言えば、既存のMQTTやDDSが安定して動いていて拠点をまたぐ要件も無いなら、Zenohへ移す実利はほとんどありません。
また、既存資産との接続にはブリッジが用意されています。zenoh-plugin-mqtt、zenoh-plugin-dds、zenoh-plugin-ros2ddsがいずれも本体と同じ版番号で並行リリースされており、MQTTブローカー配下の機器やDDSベースのシステムを、既存側を書き換えずにZenohへつなげます。移行判断の前に、まずブリッジで併存させて評価するのが安全です。
ROS 2連携(rmw_zenoh)の設定
ROS 2は通信層をRMW(ROS Middleware)として差し替えられます。rmw_zenoh_cppはros2組織で開発されている公式のRMW実装で、REP-2000のKilted Kaiju(2025年5月〜2026年11月)のミドルウェア対応表ではTier 1、全プラットフォーム・全アーキテクチャ対応として記載されています。同じ表にあるZenohの1.0.4という数字は、REP-2000自身が注記するとおりディストリビューション初回リリース時点の下限値で、以後の更新は追われていません。実際の依存版はzenoh_cpp_vendorの更新に従います。既定のRMWはrmw_fastrtps_cppのままなので、使うには明示的に切り替えます。
$ sudo apt install ros-lyrical-rmw-zenoh-cpp
$ export RMW_IMPLEMENTATION=rmw_zenoh_cpp
$ ros2 run rmw_zenoh_cpp rmw_zenohd
ここで見落としやすいのが、rmw_zenohのノード用セッション設定ではマルチキャスト探索が既定で無効になっている点です。ノードはZenohルーターのゴシップ機能を通じて他ノードの情報を受け取る設計のため、ルーター(rmw_zenohd)を起動しないとノード同士が見つかりません。ルーターを立てずに同一ホスト内で試すなら、環境変数で探索設定を上書きします。
$ export ZENOH_ROUTER_CHECK_ATTEMPTS=-1
$ export ZENOH_CONFIG_OVERRIDE='scouting/multicast/enabled=true'
ZENOH_ROUTER_CHECK_ATTEMPTSは起動時のルーター確認回数で、0なら接続できるまで待ち続け、負値なら確認を飛ばし、未設定なら1回だけ確認します。ルーターが後から起動した場合は自動で接続されます。設定ファイル全体を差し替えたいときはZENOH_ROUTER_CONFIG_URIとZENOH_SESSION_CONFIG_URIで絶対パスを指定します。なお、RMWを切り替える前にはROS 2デーモンを停止しておかないと、別RMWで起動したデーモンからグラフ情報を取得してしまいros2 node listなどが正しく動きません。ROS 2そのものの構成はノードとDDS通信の構造とROS 2世代の選び方、環境構築の手順はUbuntu 22.04へのHumbleインストールとcolconビルドで確認できます。
制約デバイス向けZenoh-Picoとの使い分け
マイコンやRTOS上で動かす場合は、C実装のZenoh-Picoを使います。本体と同じ日に同じ版番号でリリースされるため、組み合わせの管理がしやすい構成です。Zenoh-Picoは機能を絞った移植版なので、ストレージやプラグインを伴う処理はルーター側に寄せ、デバイス側はclientモードでルーターへ1本のセッションを張る設計が基本になります(既定はclientモードで、UDPマルチキャストを使うpeerモードも選べます)。Linuxが載るエッジ機器なら本体版、RTOSやベアメタルで動く機器ならPico、と切り分けるのが実際的です。
導入時につまずきやすい設定
既定値のままでは意図通りに動かない箇所が、実装時のつまずきどころになります。
探索の範囲がまず問題になります。マルチキャストのスカウトはTTLが1で、同一サブネット内に閉じます。別サブネットやクラウド越しのノードをつなぐには、connectのエンドポイントに接続先を明示するか、ゴシップ探索の入口となるルーターを経由させます。本記事の検証コードがマルチキャストを切っても通信できたのは前者で、相手のpeerがlistenしているアドレスをconnectで直接指定しています(ルーターは使っていません)。
暗号化はリンク層のプロトコル選択で行います。設定ファイルがサポート済みとして挙げるリンクはtcp・udp・tls・quic・ws・unixsock-stream・vsockの7種で、transport/link/protocolsに列挙したものだけを受け入れる形に絞れます。ただし既定ビルドに入るのはvsockを除く6種で、vsockはビルド時のフィーチャ指定が要ります。TLSは証明書に加えてenable_mtlsによる相互認証を設定でき、証明書の有効期限切れで自動的にリンクを切る挙動も選べます(listener側から切断させる用途ではmTLSが前提です)。
同一ホスト内の大きなペイロードでは共有メモリ転送が効きますが、ここには前提が2つあります。1つは、共有メモリはshared-memoryフィーチャを有効にしてビルドしたバイナリでのみ使え、そうでなければこのセクションの設定は何の効果も持たないこと。もう1つは、設定のshared_memoryが「対応していることを相手に通知する」だけで、実際に使われるかは相手側の設定にも依存することです。購読側でも有効にしておかないとネットワーク経由へフォールバックするため、送信側だけ設定して速くならないという誤解が起きやすい箇所です。
よくある質問
ZenohとMQTTはどちらを選ぶべきですか?
機器からクラウドへ集約する構成で、既にブローカー運用が確立しているならMQTTを維持するのが妥当です。近接ノード同士の低遅延通信や、拠点をまたぐ動的な接続構成が要件ならZenohが向きます。両方が必要な場合は、zenoh-plugin-mqttでブローカーとZenohを橋渡しして併存できます。
zenohd(Zenohルーター)は必須ですか?
必須ではありません。既定のpeerモードでは同一ネットワーク内のアプリ同士が直接つながります。ルーターが要るのは、サブネットを越える場合、clientモードの機器を集約する場合、ストレージやRESTなどのプラグインを動かす場合です。ただしROS 2のrmw_zenohはノード側のマルチキャスト探索が既定で無効なため、そのままではルーターの起動が前提になります。
ROS 2のどのディストリビューションで使えますか?
rosdistroには humble・jazzy・kilted・lyrical の各ディストリビューション向けにrmw_zenohがリリース登録されており(登録バージョンはそれぞれ0.1.9-1/0.2.10-1/0.6.7-1/0.10.5-1)、ros-<DISTRO>-rmw-zenoh-cppとしてバイナリで導入できます。ディストリビューションが新しいほどrmw_zenoh側の版も進んでいるため、新規採用なら新しい版を選ぶのが無難です。なおREP-2000でTier 1と明記されているのはKilted Kaijuで、REP-2000自体は2025年7月を最後に更新されておらず、Lyrical Luthの節はまだありません。
マイコンなど制約デバイスでも動きますか?
C実装のZenoh-Picoが低リソース機器向けに提供されており、本体と同じ版番号で並行リリースされています。デバイス側はclientモードでルーターへ接続し、保存や集約はルーター側で担う構成が扱いやすい形です。
通信をTLSで暗号化できますか?
できます。リンクプロトコルとしてtlsとquicが用意されており、設定のtransport/link/tlsで証明書と鍵を指定します。enable_mtlsを有効にすればクライアント証明書による相互認証になり、受け入れるプロトコルをprotocolsでtlsとquicだけに制限すれば平文接続を塞げます。