Apache Thrift クライアントとサーバー間の通信にunix socketを使用する

Thriftを使用し、PythonとNode.jsそれぞれでバイナリデータを送受信するプログラムを作成しました。
Apache Thrift Pythonでバイナリデータ(画像)の送受信
Apache Thrift Node.jsでバイナリデータ(画像)の送受信

クライアントとサーバー間の通信にはポート番号を指定してソケットを使用しましたが、
この部分をユニックスソケットに変更してみます。



Python


TSocket.pyのソースを見てみると
https://github.com/apache/thrift/blob/master/lib/py/src/transport/TSocket.py
host、portではなく、unix_socketで指定すれば良いようです。

ソケットは書き込み権限があるパスならどこでも良いのですが、
今回は「/tmp/thrift.sock」としました。
ソースを修正してみます。

・client.py


  1. # Thriftコンパイラーで出力したテンプレートをパスに追加
  2. import sys
  3. sys.path.append('gen-py')
  4. # テンプレートのファイル名に使用した「sample」にある「Transfer」サービスをimport
  5. from sample import Transfer
  6. from thrift import Thrift
  7. from thrift.transport import TSocket
  8. from thrift.transport import TTransport
  9. from thrift.protocol import TBinaryProtocol
  10. def main():
  11.     # データのやり取りはソケット通信を使用
  12.     # Make socket
  13.     # transport = TSocket.TSocket('localhost', 9090)
  14.     # unix socket使用
  15.     transport = TSocket.TSocket(unix_socket='/tmp/thrift.sock')
  16.     # Buffering is critical. Raw sockets are very slow
  17.     transport = TTransport.TBufferedTransport(transport)
  18.     # プロトコルの指定
  19.     protocol = TBinaryProtocol.TBinaryProtocol(transport)
  20.     # Transferサービスを使用するクライアントを生成
  21.     client = Transfer.Client(protocol)
  22.     # コネクションオープン
  23.     transport.open()
  24.     # 送信するファイル読み込み
  25.     body = open('lena.png', 'rb').read()
  26.     # Transferサービスのsendを実行しファイル送信
  27.     result = client.send('send.png', body)
  28.     print(result)
  29.     # コネクションクローズ
  30.     transport.close()
  31. if __name__ == '__main__':
  32.     main()




サーバー側もunix_socketを使用するよう変更。

・server.py


  1. # Thriftコンパイラーで出力したテンプレートをパスに追加
  2. import sys
  3. sys.path.append('gen-py')
  4. # テンプレートのファイル名に使用した「sample」にある「Transfer」サービスをimport
  5. from sample import Transfer
  6. from thrift.transport import TSocket
  7. from thrift.transport import TTransport
  8. from thrift.protocol import TBinaryProtocol
  9. from thrift.server import TServer
  10. class TransferHandler:
  11.     def __init__(self):
  12.         self.log = {}
  13.     def send(self, name, body):
  14.         print(name)
  15.         print(len(body))
  16.         # 受信したファイル名で内容を保存
  17.         open(name, 'wb').write(body)
  18.         return 'ok'
  19. if __name__ == '__main__':
  20.     handler = TransferHandler()
  21.     processor = Transfer.Processor(handler)
  22.     #transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)
  23.     # unix socket使用
  24.     transport = TSocket.TServerSocket(unix_socket='/tmp/thrift.sock')
  25.     tfactory = TTransport.TBufferedTransportFactory()
  26.     pfactory = TBinaryProtocol.TBinaryProtocolFactory()
  27.     server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
  28.     # You could do one of these for a multithreaded server
  29.     # server = TServer.TThreadedServer(
  30.     #     processor, transport, tfactory, pfactory)
  31.     # server = TServer.TThreadPoolServer(
  32.     #     processor, transport, tfactory, pfactory)
  33.     print('Starting the server...')
  34.     server.serve()
  35.     print('done.')




サーバー側を起動。

$ python3 server.py
Starting the server...



/tmp/thrift.sockが生成されました。
クライアント側を実行。

$ python3 client.py
ok



TCPソケットと使用したときと同様、画像ファイルの転送が確認できました。




Node.js


Node.jsでもunix socketを使用するよう修正してみます。
connection.jsのソースを見てみると
https://github.com/apache/thrift/blob/master/lib/nodejs/lib/thrift/connection.js

createUDSConnectionを使用すれば良いようです。
ソースを修正します。

・client.js


  1. const fs = require('fs').promises;
  2. // コンパイラで出力したファイルを読み込み
  3. const Transfer = require('./gen-nodejs/Transfer');
  4. const thrift = require('thrift');
  5. const transport = thrift.TBufferedTransport;
  6. const protocol = thrift.TBinaryProtocol;
  7. // unixソケットを使用するよう変更
  8. //const connection = thrift.createConnection('localhost', 9090, {
  9. const connection = thrift.createUDSConnection('/tmp/thrift.sock', {
  10.     transport : transport,
  11.     protocol : protocol
  12. });
  13. connection.on('error', function(err) {
  14.     console.log(err);
  15. });
  16. // データ送信用のクライアント生成
  17. const client = thrift.createClient(Transfer, connection);
  18. async function main() {
  19.     // 送信データ読み込み
  20.     const body = await fs.readFile('lena.png');
  21.     client.send('send.png', body, function(err, response) {
  22.         if (err) {
  23.             console.log(err);
  24.         } else {
  25.             console.log(response);
  26.         }
  27.         connection.end();
  28.     });
  29. }
  30. main();




サーバー側もソースを修正。

・server.js


  1. const fs = require('fs').promises;
  2. // コンパイラで出力したファイルを読み込み
  3. const Transfer = require('./gen-nodejs/Transfer');
  4. const thrift = require('thrift');
  5. const server = thrift.createServer(Transfer, {
  6.     // 送信されたファイルを受信
  7.     send: async function(name, body, result) {
  8.         console.log(name);
  9.         await fs.writeFile(name, body);
  10.         result(null, 'ok');
  11.     }
  12. });
  13. // unixソケットを使用するよう変更
  14. //server.listen(9090);
  15. server.listen('/tmp/thrift.sock');




実行前、/tmp/thrift.sockが残っているとサーバー起動時にエラーとなります。
Python版を実行した後はファイルが残っていたので一旦削除。

$ rm -f /tmp/thrift.sock



サーバー側を起動。

$ node server.js



クライアント側を実行してファイルを送信。

$ node client.js
ok



画像ファイルの転送が確認できました。

Apache Thrift PythonとNode.js間のバイナリデータ(画像)送受信

Thriftを使用し、PythonとNode.jsそれぞれでバイナリデータを送受信するプログラムを作成しました。
Apache Thrift Pythonでバイナリデータ(画像)の送受信
Apache Thrift Node.jsでバイナリデータ(画像)の送受信

Pythonでデータ送信。Node.jsでデータ復元という異なる言語間での動作を試してみます。


通信定義の作成とコンパイル


前回と同じく「Transfer」というサービス、string(ファイル名)とbinary(ファイルデータ)を
送信する定義を使用します。

・sample.thrift

service Transfer {
    string send(1:string name, 2:binary body)
}



定義からプログラムの実装を出力します。
Pythonでの実装の出力は「py」
Node.jsの実装は--genには「js:node」を指定して出力を実行。

// python用
$ thrift --gen py sample.thrift

// Node.js用
$ thrift --gen js:node sample.thrift






Python側のクライアントプログラム


Thrift + Python編で使用したのと全く同じプログラムです。

・client.py


  1. # Thriftコンパイラーで出力したテンプレートをパスに追加
  2. import sys
  3. sys.path.append('gen-py')
  4. # テンプレートのファイル名に使用した「sample」にある「Transfer」サービスをimport
  5. from sample import Transfer
  6. from thrift import Thrift
  7. from thrift.transport import TSocket
  8. from thrift.transport import TTransport
  9. from thrift.protocol import TBinaryProtocol
  10. def main():
  11.     # データのやり取りはソケット通信を使用
  12.     # Make socket
  13.     transport = TSocket.TSocket('localhost', 9090)
  14.     # Buffering is critical. Raw sockets are very slow
  15.     transport = TTransport.TBufferedTransport(transport)
  16.     # プロトコルの指定
  17.     protocol = TBinaryProtocol.TBinaryProtocol(transport)
  18.     # Transferサービスを使用するクライアントを生成
  19.     client = Transfer.Client(protocol)
  20.     # コネクションオープン
  21.     transport.open()
  22.     # 送信するファイル読み込み
  23.     body = open('lena.png', 'rb').read()
  24.     # Transferサービスのsendを実行しファイル送信
  25.     result = client.send('send.png', body)
  26.     print(result)
  27.     # コネクションクローズ
  28.     transport.close()
  29. if __name__ == '__main__':
  30.     main()






Node.js側のサーバープログラム


こちらもThrift + Node.js編で使用したのと同じ内容です。

・server.js


  1. const fs = require('fs').promises;
  2. // コンパイラで出力したファイルを読み込み
  3. const Transfer = require('./gen-nodejs/Transfer');
  4. const thrift = require('thrift');
  5. const server = thrift.createServer(Transfer, {
  6.     // 送信されたファイルを受信
  7.     send: async function(name, body, result) {
  8.         console.log(name);
  9.         await fs.writeFile(name, body);
  10.         result(null, 'ok');
  11.     }
  12. });
  13. server.listen(9090);






実行


まずNode.jsのサーバー側を起動。

$ node server.js



続いてPythonのクライアントから送信実行。

$ python3 client.py




サーバー側のディレクトリにsend.pngという名称で画像ファイルが出力されることを確認できました。
Thriftを使用すれば、異なる言語間で簡単にネットワーク越しのデータ連携が実現できますね。
webサーバーも不要ですし便利です。

Apache Thrift Pythonでバイナリデータ(画像)の送受信

Apache Thrift
https://thrift.apache.org/

Thriftは以前に触ったことがありますが、
ThriftをPythonで使用する
当時と状況が変わっているようなので、改めて動かしてみます。

お題として、ファイル名(文字列)とファイルの内容(バイナリ)を送信。
サーバー側でファイルを復元する機能を作成します。


Thrift compilerの取得とビルド


https://thrift.apache.org/download
こちらのダウンロードからThriftのコンパイラー(データ型の定義から実装を出力する)を取得。
Windows用のバイナリ(thrift-0.17.0.exe)も用意されていますね。

Ubuntuへはこちらの手順でインストールしました。
Ubuntu 22.04にApache Thriftコンパイラーをインストール(thrift-compilerとソースからのビルド)

※aptでもインストールできるはずです。

$ sudo apt install thrift-compiler






通信定義の作成とコンパイル


データのやり取りを行うサービスやメソッド名、データ型の定義を行います。
定義体の記載方法は、チュートリアルに登場するこちらが参考になります。
shared.thrift
tutorial.thrift

「Transfer」というサービス、string(ファイル名)とbinary(ファイルデータ)を
送信する定義は以下のようになりました。

・sample.thrift

service Transfer {
    string send(1:string name, 2:binary body)
}



定義からプログラムの実装を出力します。
Pythonの実装が欲しいので--genには「py」を指定して出力を実行。

$ thrift --gen py sample.thrift



Windowsの場合

> thrift-0.17.0.exe --gen py sample.thrift



gen-pyというディレクトリが自動的に作成され、ひな形のソースコートが出力されます。

b59_01.png

このひな形を利用してプログラム本体を記載していきます。



必要なPythonパッケージのインストール


以前はプログラムに必要な資産全てがコンパイラーから出力されていた気がするのですが、
データ送受信などの共通的なプログラムはpipでインストールする必要がありました。

https://pypi.org/project/thrift/

pipコマンドでインストールします。

$ pip3 install thrift






プログラムサンプル


チュートリアルを参考にプログラムを作成します。
https://thrift.apache.org/tutorial/py.html

まずクライアント側。
画像ファイルを読み込んでサーバー側に送信します。

送信テストに使用したファイルはこちら。

b59_02.png

・client.py


  1. # Thriftコンパイラーで出力したテンプレートをパスに追加
  2. import sys
  3. sys.path.append('gen-py')
  4. # テンプレートのファイル名に使用した「sample」にある「Transfer」サービスをimport
  5. from sample import Transfer
  6. from thrift import Thrift
  7. from thrift.transport import TSocket
  8. from thrift.transport import TTransport
  9. from thrift.protocol import TBinaryProtocol
  10. def main():
  11.     # データのやり取りはソケット通信を使用
  12.     # Make socket
  13.     transport = TSocket.TSocket('localhost', 9090)
  14.     # Buffering is critical. Raw sockets are very slow
  15.     transport = TTransport.TBufferedTransport(transport)
  16.     # プロトコルの指定
  17.     protocol = TBinaryProtocol.TBinaryProtocol(transport)
  18.     # Transferサービスを使用するクライアントを生成
  19.     client = Transfer.Client(protocol)
  20.     # コネクションオープン
  21.     transport.open()
  22.     # 送信するファイル読み込み
  23.     body = open('lena.png', 'rb').read()
  24.     # Transferサービスのsendを実行しファイル送信
  25.     result = client.send('send.png', body)
  26.     print(result)
  27.     # コネクションクローズ
  28.     transport.close()
  29. if __name__ == '__main__':
  30.     main()




次にサーバー側。
受信したファイル名で内容を保存します。

・server.py


  1. # Thriftコンパイラーで出力したテンプレートをパスに追加
  2. import sys
  3. sys.path.append('gen-py')
  4. # テンプレートのファイル名に使用した「sample」にある「Transfer」サービスをimport
  5. from sample import Transfer
  6. from thrift.transport import TSocket
  7. from thrift.transport import TTransport
  8. from thrift.protocol import TBinaryProtocol
  9. from thrift.server import TServer
  10. class TransferHandler:
  11.     def __init__(self):
  12.         self.log = {}
  13.     def send(self, name, body):
  14.         print(name)
  15.         print(len(body))
  16.         # 受信したファイル名で内容を保存
  17.         open(name, 'wb').write(body)
  18.         return 'ok'
  19. if __name__ == '__main__':
  20.     handler = TransferHandler()
  21.     processor = Transfer.Processor(handler)
  22.     transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)
  23.     tfactory = TTransport.TBufferedTransportFactory()
  24.     pfactory = TBinaryProtocol.TBinaryProtocolFactory()
  25.     server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
  26.     # You could do one of these for a multithreaded server
  27.     # server = TServer.TThreadedServer(
  28.     #     processor, transport, tfactory, pfactory)
  29.     # server = TServer.TThreadPoolServer(
  30.     #     processor, transport, tfactory, pfactory)
  31.     print('Starting the server...')
  32.     server.serve()
  33.     print('done.')



サーバー側を起動。


$ python3 server.py
Starting the server...



クライアント側のプログラムを実行します。

$ python3 client.py



実行すると、send.pngというファイルが出力されます。
内容も元の画像と同様ですね。
手軽にシステム間でバイナリデータのやり取りが行えて便利です。

Python asyncio並行処理と終了の待ち合わせ、タスク初期値の設定

以前、Pythonのスレッドで並行処理を行う方法を調べました。
Python スレッドによる並行処理と終了の待ち合わせ、スレッド初期値の設定

Python2しか使用できない環境を想定していましたが、
今回はasyncioが使用できる環境を想定し、同様の内容を試してみます。



最も簡単な例


0から4までの数字を出力するプログラムを2つ同時に動かしてみます。

・sample.py


  1. import asyncio
  2. import time
  3. async def funA():
  4.     for i in range(5):
  5.         print('funA:%d' % i)
  6.         await asyncio.sleep(1)
  7. async def funB():
  8.     for i in range(5):
  9.         print('funB:%d' % i)
  10.         await asyncio.sleep(1)
  11. async def main():
  12.     # A,Bそれぞれをタスクとして作成する
  13.     taskA = asyncio.create_task(funA())
  14.     taskB = asyncio.create_task(funB())
  15.     # タスクを実行 並列で実行される
  16.     await taskA
  17.     await taskB
  18. if __name__ == '__main__':
  19.     asyncio.run(main())




非同期実行の関数は「async」をつけて定義する。
asyncio.runで実行。
並行して実行したいものは、asyncio.create_taskで実行をスケジュール。
Taskオブジェクトをawaitして実行終了を待ち合わせる。という感じでしょうか。

実行結果

$ python3 sample.py
funA:0
funB:0
funA:1
funB:1
funA:2
funB:2
funA:3
funB:3
funA:4
funB:4



funcA, funcBの処理はasyncio.create_taskした時点で開始されます。
await taskA, await taskBで各々の処理が終了しているか待ち合わせるイメージです。




async関数への値の設定


実行する関数へ値を渡したい場合はそのまま引数にとればOKです。

・sample.py


  1. import asyncio
  2. import time
  3. async def worker(name):
  4.     for i in range(5):
  5.         # 引数で得た名前と繰り返し回数を表示
  6.         print('Worker%s:%d' % (name, i))
  7.         await asyncio.sleep(1)
  8. async def main():
  9.     # 引数を渡してタスクを作成
  10.     taskA = asyncio.create_task(worker('A'))
  11.     taskB = asyncio.create_task(worker('B'))
  12.     
  13.     # タスクを実行 並列で実行される
  14.     await taskA
  15.     await taskB
  16.     
  17. if __name__ == '__main__':
  18.     asyncio.run(main())




実行結果

$ python3 sample.py
WorkerA:0
WorkerB:0
WorkerA:1
WorkerB:1
WorkerA:2
WorkerB:2
WorkerA:3
WorkerB:3
WorkerA:4
WorkerB:4



スレッドで実行する際はあれこれ工夫しましたが、asyncioだと簡単ですね。




タスク実行結果の値の受け取り


Python スレッドによる並行処理と終了の待ち合わせ、スレッド初期値の設定
こちらの「スレッド終了の待ち合わせ(join)」で試した内容のasyncio版です。
例では、0から引数に与えられた数字までを合算するプログラムを並行実行。
各々の実行結果から得られた値をさらに合算するサンプルでした。


まず、与えられた数値までの合算を行うサンプル。

  1. import asyncio
  2. import time
  3. async def worker(max):
  4.     # 0から与えられた数値まで合算
  5.     total = 0
  6.     for i in range(max):
  7.         total += i
  8.         # 本来必要ないが、処理に時間がかかる例としてwait
  9.         await asyncio.sleep(0.1)
  10.     
  11.     # 結果を取得
  12.     return total
  13. async def main():
  14.     # 引数を渡してタスクを作成
  15.     taskA = asyncio.create_task(worker(10))
  16.     
  17.     # タスクから戻り値を得られる
  18.     totalA = await taskA
  19.     print(totalA)
  20.     
  21. if __name__ == '__main__':
  22.     asyncio.run(main())




await [タスク]の戻り値として関数からの戻り値が得られます。
実行結果

$ python3 sample.py
45




過去サンプルと同様の処理を記載してみます。

・sample.py


  1. import asyncio
  2. import time
  3. async def worker(name, max):
  4.     # 0から与えられた数値まで合算
  5.     total = 0
  6.     for i in range(max):
  7.         print('Worker%s:%d' % (name, i))
  8.         total += i
  9.         # 本来必要ないが、処理に時間がかかる例としてwait
  10.         await asyncio.sleep(0.1)
  11.     
  12.     # 結果を取得
  13.     return total
  14. async def main():
  15.     # 引数を渡してタスクを作成
  16.     taskA = asyncio.create_task(worker('A', 10))
  17.     taskB = asyncio.create_task(worker('B', 20))
  18.     
  19.     # タスクから戻り値を得られる
  20.     totalA = await taskA
  21.     totalB = await taskB
  22.     print(totalA + totalB)
  23.     
  24. if __name__ == '__main__':
  25.     asyncio.run(main())




実行結果

$ python3 sample.py
WorkerA:0
WorkerB:0
WorkerA:1
WorkerB:1
WorkerA:2
...
WorkerB:18
WorkerB:19
235



2つの加算処理が並列で実行。
各々の計算結果を受け取り、合算値を表示することができました。

スレッドを使用するよりお手軽に実装できますね。

ISO-2022-JP対応 SMTPメール送信テストサーバー PyMailHog 0.0.4

ISO-2022-JPに対応したMailHogのPythonクローン「PyMailHog」
PyMailHog

cssフレームワークにBootstrap 3系を使用していたのですが、
Picnic CSSを使用するよう変更しました。
これに伴い、jqueryに依存する必要がなくなったので、jqueryを削除しています。

加えて、.eml形式でメールデータをダウンロードする機能を追加。
ローカルのメーラーで表示を確認できるようになりました。



スクリーンショット



b42_01.png

b42_02.png

b42_03.png

b42_04.png



使い方



PyMailHogファイルはzipappで実行可能なPython zip書庫としています。
zipapp --- 実行可能な Python zip 書庫を管理する

ダウンロードリンクにあるPyMailHog単体で動作します。
PyMailHog ダウンロード

pythonコマンドに続いてファイル名を指定すれば起動します。

c:\download\> python Hog-0.0.4
$ python3 Hog-0.0.4



Linuxではダウンロードしたファイルに実行権限を付与
そのまま実行することが可能です。

$ chmod +x PyMailHog-0.0.4
$ ./PyMailHog-0.0.4



Ctrl + cキーで終了します。

デフォルトでsmtpポートは1025
httpポートは8025で待ち受けます。

--smtpportオプションでsmtpポート
--httpportオプションでhttpポートを指定可能です。

$ ./PyMailHog --smtpport 1026 --httpport 8080



-hで指定可能なオプションが表示されます。

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
https://symfo.web.fc2.com/

PR

検索フォーム

月別アーカイブ