PythonでバイナリデータをパックしてUDPメッセージを送ってみる
はじめに
PythonでバイナリデータをパックしてUDPメッセージを送るには、struct
モジュールを使う。簡単な使い方はPy MOTW: struct – Working with Binary Dataで確認できる。本エントリでは、struct
モジュールの使い方を押さえ、UDP上のバイナリデータで構成されたプロトコルであるGTPv2のEcho Requestメッセージを試しに送信してみる。
GTPv2のEcho RequestメッセージのプロトコルフォーマットはドコモのネットワークにGTP接続(レイヤー2)するためのメモのエントリで参照した以下のドキュメントで確認できる。
- GTPv2-C: 3GPP TS29.274 v11.5.0
- ドコモ 技術的条件集
structモジュールでバイナリデータをパック、アンパック
前準備として、struct
モジュールの使い方を押さえる。
structモジュールはデータのパック方法として、関数レベルとクラスレベルの2つを用意している。データをパックするときには、フォーマット文字列(例えば、データがネットワークバイトオーダで4オクテットである、とか)を指定する必要がある。Py MOTW: struct – Working with Binary Dataによると、関数レベルのパッキングを利用する場合、フォーマット文字列はパッキングの際に毎度コンパイルされるのに対し、クラスレベルのパッキングを利用する場合、オブジェクトをインスタンス化するときにフォーマット文字列をコンパイルするため、データのパッキングが高速になるとのこと。 よって、ここでは、クラスベースのパッキングをしてみる。
まず、フォーマット文字列として、データがネットワークバイトオーダで4オクテットであることを指定してオブジェクトをインスタンス化するには以下のように、ネットワークバイトオーダを示す"!"と4オクテットであることを示す"i"をコンストラクタの引数に与える。フォーマット文字列の詳細はpythonのマニュアルで確認することができる。
>>> import struct >>> s = struct.Struct("!i") >>> s <Struct object at 0x7f7e09518880>
例えばバイナリデータとして、"0x04 0x03 0x02 0x01"で構成される4オクテットのデータを作り、上記のフォーマット文字列でパッキングするには以下のようにする。
>>> data = (0x04 << 24) + (0x03 << 16) + (0x02 << 8) + 0x01 >>> packed_data = s.pack(data) >>> packed_data b'\x04\x03\x02\x01'
上記の例は、フォーマット文字列として、ネットワークバイトオーダで1オクテットを4つ並べ、以下のようにもできる。
>>> s = struct.Struct("!BBBB") >>> data = [0x04, 0x03, 0x02, 0x01] >>> packed_data = s.pack(*data) >>> packed_data b'\x04\x03\x02\x01'
パックしたデータををUDPで送信するには、socket
モジュールのsendto
の引数にパックしたデータを渡せば良い。
パックしたデータをアンパックするときもパックするときと同様に、まずフォーマット文字列を指定してオブジェクトをインスタンス化し、パックされたデータをunpack
メソッドの引数に渡せば良い。
>>> s1 = struct.Struct("!BBBB") >>> packed_data = s1.pack(*data) >>> s1.unpack(packed_data) (4, 3, 2, 1)
GTPv2のEcho RequestメッセージをUDPで送信
それでは、試しにGTPv2のEcho Requestメッセージをバイナリデータとして構成し、パッキングし、UDPで送信してみる。 用意したプログラムは下記の2つ。 * gtpv2c.py - GTPv2のメッセージは、ヘッダと情報要素(Information Element, IE)で構成される。本ファイルでは、これらのデータを定義する。 - 本ファイルでは、パッキングするフォーマット文字列とバイナリデータの構築のみを行う。 * send_gtpv2c.py - 本ファイルにおいて、gtpv2c.pyを読み込み、そこで定義されたEcho RequestのヘッダとIEを組み立て、パッキングし、UDPでデータ送信する。
gtpv2c.py
#!/usr/bin/env python3 PORT = 2123 ECHO_REQUEST = 1 # ... RECOVERY_TYPE = 3 # ... def header(msg_type, msg_len, teid=None, seq_no=None): version = 2 p = 0 # piggyback off formatter = "!i" octets = [] octets1_4 = 0 octets1_4 += version << 29 octets1_4 += p << 28 if teid != None: octets1_4 += 1 << 27 octets1_4 += msg_type << 16 octets1_4 += msg_len octets.append(octets1_4) if teid != None: formatter += "i" octets.append(teid) if seq_no != None: formatter += "i" octets.append(seq_no << 8) return [ formatter, octets ] def recovery_ie(ins_id, recovery_val): ie_len = 5 formatter = 'ib' octets = [] octets1_4 = 0 octets1_4 += RECOVERY_TYPE << 24 octets1_4 += 1 << 8 octets1_4 += ins_id octets.append(octets1_4) octets.append(recovery_val) return [ ie_len, formatter, octets ]
send_gtpv2c.py
#!/usr/bin/env python3 import socket import time from contextlib import closing import gtpv2c import struct if __name__ == '__main__': host = '127.0.0.1' port = gtpv2c.PORT sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) with closing(sock): ie_len, ie_f, ie_msg = gtpv2c.recovery_ie(0, 100) hdr_f, hdr_msg = gtpv2c.header(gtpv2c.ECHO_REQUEST, ie_len, None, 1234) s = struct.Struct(hdr_f + ie_f) msg = hdr_msg + ie_msg packed_data = s.pack(*msg) sock.sendto(packed_data, (host, port))
サンプルプログラムの実行
以下のようにコマンドを実行し、ローカルホストにgtpv2 echoメッセージを送信し、
$ chmod +x send_gtpv2c.py $ ./send_gtpv2c.py