1. はじめに
この記事では、RPCと呼ばれるAPIのアーキテクチャスタイルを利用して、Pythonで定義した関数を、JavaScriptからリモートで呼び出し、結果を受け取るアプリを作成する。
PythonとJavaScriptという異なる言語間でも、シンプルなプロトコルを使って関数をローカルで呼び出すように使える。
2. RPCとは?
RPCとは、APIのアーキテクチャスタイルの1つであり、他のプログラムで定義された関数を、ローカル関数の様に呼び出すことが出来る仕組みを指す。つまり、関数呼び出しをネットワーク越しにやる仕組みである。
RPCはネットワークの詳細ではなく、関数呼び出しの観点から考えることが出来るようにすることで、ネットワークの複雑さを抽象化し、異なるシステムに分散されたソフトウェアを作成する手順を容易にする。
RPCが行われると、呼び出し引数がパッケージ化され、ネットワークを介してサーバーに送信される。サーバーは引数をアンパックし、必要な手順を実行し、クライアントに結果を送り返す
3. RPCを利用したアプリの作成
ここでは、RPCを利用してPythonで定義された関数を、JavaScriptからリモートで呼び出し、結果を受け取るアプリを作成する。
データの授受は、JSONを通して行なわれ、以下のフォーマットを含む。
// Request
{
"method": "fibonacci",
"params": [10],
"id": 1
}
// Response
{
"status": "success",
"result": 55,
"id": 1
}
// Error
{
"status": "error",
"error_type": error_type,
"message": message,
"id": id
}
Pythonで定義される関数は、以下の通り
-
floor(x: float) -> int
小数点以下を切り捨てた整数値を返す -
nth_root(n: int, x: int) -> float
ニュートン法を用いて、x の n 乗根(n√x)を求める。誤差が十分小さくなった時点で収束 -
reverse(string: str) -> str
与えられた文字列を逆順にして返す。 -
validAnagram(str1: str, str2: str) -> bool
str1 と str2 がアナグラム関係であるかを判定する(文字の構成と個数が一致するか) -
sort(strArr: list) -> list
文字列のリストを昇順にソートして返す。 -
is_prime(n: int) -> bool
素数判定を行う。2以上の数に対して、その平方根まで試し割りを行う。 -
fibonacci(n: int) -> int
n 番目のフィボナッチ数を行列累乗法により高速に計算して返す。 -
gcd(a: int, b: int) -> int
ユークリッドの互除法により、a と b の最大公約数を求める。 -
lcm(a: int, b: int) -> int
最小公倍数を求める。lcm(a, b) = abs(a * b) // gcd(a, b) を使用。 -
mod_pow(a: int, b: int, m: int = 1) -> int
繰り返し二乗法による高速べき乗。a^b mod m を効率的に計算。
4. 構成図
今回のRPCアプリの構成図は以下の通りである。
5. サーバー側のコード
サーバーはPythonで実装されている。このコードは、ソケットを通じてRPCリクエストを受け取り、FunctionRegistry
を呼び出す。
5.1. RPC Server
実装コードは以下の通りである。
class RPCServer:
def __init__(self, socket_path, registry=None):
self.socket_path = socket_path
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.registry = registry or FunctionRegistry
# 古いソケットが存在したら削除
if os.path.exists(self.socket_path):
os.remove(self.socket_path)
self.sock.bind(self.socket_path)
self.sock.listen(5)
def format_error_response(self, message: str, id: int, error_type: str = "ServerError"):
return {
"status": "error",
"error_type": error_type,
"message": message,
"id": id
}
def handle_connection(self, conn):
MAX_READ_BYTES = 4096
try:
while True:
data = conn.recv(MAX_READ_BYTES)
if not data:
break
try:
request = json.loads(data.decode())
except Exception as e:
response = self.format_error_response(str(e), -1, "InvalidRequest")
conn.sendall(json.dumps(response).encode('utf-8'))
continue
method = request.get("method")
params = request.get("params", [])
id = request.get("id", -1)
response = self.dispatch(method, params, id)
conn.sendall(json.dumps(response).encode('utf-8'))
finally:
conn.close()
def dispatch(self, method, params, id):
try:
if not hasattr(self.registry, method):
response = self.format_error_response(f"Method `{method}` not found", -1, "InvalidRequest")
return response
func = getattr(self.registry, method)
result = func(*params)
return {
"status": "success",
"result": result,
"id": id
}
except Exception as e:
return {
"status": f"Error: {str(e)}",
"id": id
}
def start(self):
while True:
conn, addr = self.sock.accept()
try:
print(f"接続が確立されました。{addr}")
self.handle_connection(conn)
finally:
print("Closing current connection")
conn.close()
break
5.2. FunctionRegistry
実装コードは以下の通りである。
class FunctionRegistry:
@staticmethod
def floor(x: float) -> int:
return math.floor(x)
@staticmethod
def nth_root(n: int, x: int) -> float:
r = x if x >= 1 else 1.0
epsilon = 1e-10
while True:
next_r = ((n - 1) * r + x / r**(n - 1)) / n
if abs(next_r - r) < epsilon:
return next_r
r = next_r
@staticmethod
def reverse(string: str) -> str:
return string[::-1]
@staticmethod
def validAnagram(str1: str, str2: str) -> bool:
if len(str1) != len(str2):
return False
hashMap = {}
for char in str1:
hashMap[char] = hashMap.get(char, 0) + 1
for char in str2:
if char in hashMap and hashMap[char] > 0:
hashMap[char] -= 1
else:
return False
return True
@staticmethod
def sort(strArr: list) -> list:
return sorted(strArr)
@staticmethod
def is_prime(n: int) -> bool:
MAX = int(n ** 0.5) + 1
for i in range(2, MAX):
if n % i == 0:
return False
return True
@staticmethod
def __pow_matrix(base: list, expo: int) -> int:
def cal_matrix(A: list, B: list) -> list:
return [
[A[0][0]*B[0][0] + A[0][1]*B[1][0],
A[0][0]*B[0][1] + A[0][1]*B[1][1]],
[A[1][0]*B[0][0] + A[1][1]*B[1][0],
A[1][0]*B[0][1] + A[1][1]*B[1][1]]
]
result = [[1, 0], [0, 1]]
while expo > 0:
if expo & 1:
result = cal_matrix(result, base)
base = cal_matrix(base, base)
expo >>= 1
return result
@staticmethod
def fibonacci(n: int) -> int:
if n < 0:
raise ValueError("n must be non-negative")
if n == 0:
return 0
base = [[1, 1], [1, 0]]
result = FunctionRegistry.__pow_matrix(base, n - 1)
return result[0][0]
@staticmethod
def gcd(a: int, b: int) -> int:
if b == 0:
return a
return FunctionRegistry.gcd(b, a % b)
@staticmethod
def lcm(a: int, b: int) -> int:
return abs(a * b) // FunctionRegistry.gcd(a, b)
@staticmethod
def mod_pow(a: int, b: int, m: int = 1) -> int:
result = 1
a %= m
while b > 0:
if b & 1:
result = result * a % m
a = a * a % m
b >>= 1
return result
if __name__ == "__main__":
SOCKET_PATH = "/tmp/rpc_socket"
server = RPCServer(SOCKET_PATH)
server.start()
6. クライアント側のコード
サーバーはNode.jsで実装されている。このコードは、Pythonで動作するRPCサーバーと UNIXドメインソケット を通じて通信を行う。
以下のコードは、fibonacci(10)
のリモート呼び出しを行う例である。
const net = require('net');
const path = '/tmp/rpc_socket'; // Path to the socket file
const client = net.createConnection({ path }, () => {
console.log('Connected to server!');
const request = {
method: 'fibonacci',
params: [10],
id: 1
};
const requestJSON = JSON.stringify(request);
client.write(requestJSON);
})
client.on('data', (data) => {
try {
const response = JSON.parse(data.toString());
console.log('Response from server:', response);
} catch (err) {
console.error('Error parsing response:', err);
}
client.end();
})
client.on('end', () => {
console.log('Disconnected from server');
})
client.on('error', (err) => {
console.error('Error:', err.message);
})
7. 実行結果
fibonacci(10)
を実行した結果、サーバー、クライアントそれぞれ以下のようになった。
7.1. サーバー実行結果
接続が確立されました。
Closing current connection
7.2. クライアント実行結果
Connected to server!
Response from server: { status: 'success', result: 55, id: 1 }
Disconnected from server
結果から、クライアントとサーバーがともに正常に動作し、期待された結果(フィボナッチ数列の10番目の値)を取得し、クライアントに返していることがわかる。
8. 最後に
今回は非常にシンプルなRPCサーバーを構築したが、今後はHTTPベースに乗せたり、gRPCのようなバイナリプロトコルへの展開も考えられる。