TadaoYamaokaの日記

山岡忠夫Homeで公開しているプログラムの開発ネタを中心に書いていきます。

SocketでC#とPythonを連携する

先日、gRPCでC#Pythonを連携する方法について書いたが、Pythonで受信したProtobufのデータを処理すると実行速度に問題があることがわかった。
速度が必要なケースでは、Protobufの使用はあきらめた方がよさそうだ。

今回は、シンプルにSocket通信でデータを渡してPythonで処理する方法について記述する。
Socketでデータを送受信する場合、データの中身はただのバイナリになるため、構造化されたデータを送受信する場合は、データのシリアライズとデシリアライズの処理を記述する必要がある。
データが固定長であれば、処理しやすいが、可変長の場合は、送受信のデータにサイズを含めるなどの処理が必要になる。

C#側(クライアント)

サンプルコード:
SocketSample/Program.cs at master · TadaoYamaoka/SocketSample · GitHub

以下のようにしてSocketでサーバに接続する。

TcpClient tcp = new TcpClient("127.0.0.1", 50007);
NetworkStream ns = tcp.GetStream();

送信するデータは、BitConverter.GetBytesでbyte配列に変換して送信する。
通信回数を抑えるため、MemoryStreamに格納してまとめて送信する。

var ms = new MemoryStream();
int size = 4 + 4 + 4 * a.b.Length;
ms.Write(BitConverter.GetBytes(size), 0, 4);
// ...
ns.Write(ms.ToArray(), 0, 4 + size);

応答の受信は、Readで行う。byte配列で受け取るため、BitConverter.ToInt32などで数値など変換する。

byte[] int_bytes = new byte[4];
ns.Read(int_bytes, 0, 4);
size = BitConverter.ToInt32(int_bytes, 0);

最後に接続をクローズする。

ns.Close();
tcp.Close();

Python側(サーバー)

サンプルコード:
SocketSample/SocketServer.py at master · TadaoYamaoka/SocketSample · GitHub

以下のように接続待ち→接続を行う。

s.bind(('127.0.0.1', 50007))
s.listen(1)
conn, addr = s.accept()

受信は、recvを使用するとバイト列が返る。recv_intoを使うとバッファに直接書き込む。バッファにはnumpyのndarrayが使用できる。

size = int.from_bytes(conn.recv(4, socket.MSG_WAITALL), sys.byteorder)
# ...
data = np.empty(size, dtype=np.uint8)
conn.recv_into(data, size, socket.MSG_WAITALL)

応答の送信は、sendallで行う。numpyのndarrayは直接引数に渡せる。

res = np.array([1, 2, 3])
conn.sendall(len(res).to_bytes(4, sys.byteorder))
conn.sendall(res)

シリアライズとデータ加工をCythonで行う

バイト列をデシリアライズして、機械学習で使用するためにデータ加工を行うような場合は、実行速度をできるだけ高速化したい。
そのような場合、Cythonを使うことでデシリアライズとデータ加工をC++で記述できる。
サンプルコード:
SocketSample/Decoder.pyx at master · TadaoYamaoka/SocketSample · GitHub
SocketSample/Decoder_impl.cpp at master · TadaoYamaoka/SocketSample · GitHub
データの受け渡しをnumpyのndarrayにすることで、余分なメモリコピーを抑えることができる。