Python ZeroMQ 公開鍵通信

PythonのZeroMQライブラリpyzmqを使って、公開鍵を使った通信をしてみました。

ZeroMQ

  • MQTTはBrokerが必要になりますが、ZeroMQはBrokerなしで軽量で高速に1対1通信ができるので便利です。 ZeroはBrokerなし、管理なし、コストなし。
  • MQTTは、Brokerを立ててN:N通信や1:Nの時に便利。 AWS IoT CoreもMQTT。
  • ZeroMQはデバイス内でデータ交換をする時に、Brokerや管理(Administration)など不要なので軽量にできて便利。
  • セキュリティは、MQTTはTLSに対応しているので強固。 ZeroMQは公開鍵を使ったCurveMQ、ZAPがあるけど、2017から更新されていないらしいのでそこまで強固でなさそう。(2024/4/1更新のHiveMQサイトで書かれていました。
  • MQTTはBrokerがServerで他は全てClientになり、ClientはBrokerに接続する。
  • ZeroMQTTは一方がServerで他がClient。 Serverが先に立ち上がって、そこにClientが接続する。

CurveMQ

  • ZeroMQに追加されたCurveMQを使って公開鍵をmmapで受け取り通信しています。
  • zmq_rep.pyがserverとしてbindしています。
  • zmpのバージョンは25.1.2。
zmq_rep.py
import os
import zmq
import mmap

context = zmq.Context()
socket = context.socket(zmq.REP)

def server():
    global socket
    server_public_key, server_secret_key = zmq.curve_keypair()
    socket.curve_secretkey = server_secret_key
    socket.curve_publickey = server_public_key
    _pub_key_size=len(server_public_key)
    #print(_pub_key_size)
    file_path = 'example.txt'

    if not os.path.exists(file_path):
        with open(file_path, 'wb') as file:
            file.write(40*b"\0")
    with open(file_path, 'r+b') as file:
        mm = mmap.mmap(file.fileno(), _pub_key_size, access=mmap.ACCESS_WRITE)
        mm[0:_pub_key_size] = server_public_key
        #print("server pub key",server_public_key)    
        #print("in mmap:",mm.readline())
        mm.close()
    socket.curve_server = True
    try:
        socket.bind("tcp://*:50000")
    except zmq.error.ZMQError as e:
        print("ZMQError:", e)
        os._exit(1)
    while True:
        message = socket.recv()
        print("Received request: %s" % message)
        socket.send(b"World")
 
if __name__ == "__main__":
    server()

clientの方は鍵ペアを作ってsocket.client_secretekeyとsocket.client_publickeyに格納しておくだけでいいようです。 これをしないと通信しません。

zmq_req.py
import zmq
import mmap
import time

print(zmq.__version__)
context = zmq.Context()
socket = context.socket(zmq.REQ)
client_secret_key = b"client_secret_key"  # クライアントの秘密鍵
client_public_key, client_secret_key = zmq.curve_keypair()  # クライアントの公開鍵と秘密鍵を生成

socket.curve_secretkey = client_secret_key
socket.curve_publickey = client_public_key
file_path='./example.txt'
with open(file_path, 'r+b') as file:
    mm = mmap.mmap(file.fileno(),0, access=mmap.ACCESS_READ)
    socket.curve_serverkey  = mm.readline()
    mm.close()
socket.connect("tcp://localhost:50000")

if __name__ == "__main__":
    for i in range(5):
        socket.send(b"Hello")
        message = socket.recv()
        print("Received reply: %s" % message)

#ここから下はdisconnectした時の挙動確認用なので通常不要。 
    socket.disconnect("tcp://localhost:50000")
    time.sleep(0.1)
    socket.connect("tcp://localhost:50000")
    for i in range(5):
        socket.send(b"Hello-2")
        message = socket.recv()
        print("Received reply2: %s" % message)

close()動作せず

  • 何故かsocket.close()してもcontext.term()してもsocketは消えませんでした。 なので上のコードではdisconnectして再度connectしています。
  • socket.close()にはlingerがあり、何も指定しないと無限に待つ可能性があるらしいので、100msや1msにしてみましたが結果は同じでcloseしませんでした。
socket.close(linger=100)
context.term()
if socket:
    print("Still exists")
else:
    print("Null")

通信方法

  • REQ-REP
    • 毎回一方からMessage付きでRequestを送り、Requestを受け取った方が、Messageを受け取り、Messageを付けてReplyする。
    • socket一つでデータのやり取りができる。
    • <<今回は1つのデバイス内のPython間のデータ交換なので、REQ-REPを使っています。>>
  • PUB-SUB
    • 一方がMessageをトピック付きでPublishして相手のキューに溜まる。
    • Publishを受けた側がトピックごとに受け取り処理が出来る。
    • 1Serverー多Clientの場合に使用
    • 双方向にデータをやり取りするには、異なるポート番号のsocket2個を作る必要がある。
  • PUSH-PULL
    • 複数のプロセス間でタスクを分散処理する場合に使う
    • PUSHでタスクを送信し、PULLでタスクを受信して処理する
  • RADIO-DISH(まだ策定中)
    • N:N通信に使用
    • RADIOをブロードキャスト送信してDISHが受信
    • 詳細は理解できていないので、今後必要となったら書き足します。

最大転送量

MQTTはPayloadに最大256Mバイト入れる事ができますが、ZeroMQはどのくらいか分からなかったので色々調べてみました。 どうもLimitを設定してLimitオーバーしたらDisconnectする事は出来るみたいですが、初期値はリミットは無いようでした。

  • Small Size MessageはLarger Messageと違い方法で転送される。
  • Small Size Messageは30バイト
  • Larger Messageは定義なし。
    • ZeroMQ Version3で MaxMsgSize が -1 でNo Limitになる。 2024年4月現在、ZeroMQの最新バージョンは4.3.5 (2023年10月リリースのStable版)なので、No Limitは使えるはず。
    • ZMQ_MAXMSGSIZEは、int64型なので、18Eバイトになる? なので実際はRAMメモリによる?
  • ファイル転送で大きなファイルは送れないので、分割する必要がある。
  • 転送データが大きいときはMultipartで転送した方がメモリ消費を少なく出来る。(ZeroMQ.org)

max_vsm_size

max_vsm_size = 29 Virtual Synchronized Machines(VSM)の最大サイズでSmall Size Messageのサイズでもない。 デフォルトは1Mバイト。

実験

30バイトと言っているサイトもあったので482バイト送って試してみましたが、ちゃんと受信できました。 なのでやはりNo Limitと思います。

Received reply: {‘PLC_PRG.VarTimeCur0’: {‘val’: 20000, ‘type’: ‘Int64’, ‘timestamp’: ‘2024-04-18T02:13:19.421000’}, ‘PLC_PRG.VarTimeCur1’: {‘val’: 280, ‘type’: ‘Int64’, ‘timestamp’: ‘2024-04-18T02:13:19.421000’}, ‘GVL.MyVariable’: {‘val’: 6789, ‘type’: ‘Int16’, ‘timestamp’: ‘2024-04-18T02:13:19.421000’}, ‘GVL.MyString’: {‘val’: ‘HELLO ICHIRI’, ‘type’: ‘String’, ‘timestamp’: ‘2024-04-18T02:13:19.421000’}, ‘GV

その他

HTTP

ZeroMQ over HTTPをしようとしたら、ZMQ_STREAMにして自分でHTTPの内容をParseして、返信のHeaderを作って送らないといけないみたい。

send_multipart()

こんな風に使うんだな。

            socket.send_multipart([
                request_id, b'HTTP/1.1 400 (Bad Request)\r\n\r\n',
                request_id, b'',
            ])

コメント