RPCを利用して、別スクリプトから関数を実行する

# 1. はじめに
この記事では、RPCと呼ばれるAPIのアーキテクチャスタイルを利用して、Pythonで定義した関数を、JavaScriptからリモートで呼び出し、結果を受け取るアプリを作成する。

PythonとJavaScriptという異なる言語間でも、シンプルなプロトコルを使って関数をローカルで呼び出すように使える。

# 2. RPCとは？
RPCとは、APIのアーキテクチャスタイルの１つであり、他のプログラムで定義された関数を、ローカル関数の様に呼び出すことが出来る仕組みを指す。つまり、関数呼び出しをネットワーク越しにやる仕組みである。

RPCはネットワークの詳細ではなく、関数呼び出しの観点から考えることが出来るようにすることで、ネットワークの複雑さを抽象化し、異なるシステムに分散されたソフトウェアを作成する手順を容易にする。

RPCが行われると、呼び出し引数がパッケージ化され、ネットワークを介してサーバーに送信される。サーバーは引数をアンパックし、必要な手順を実行し、クライアントに結果を送り返す

# 3. RPCを利用したアプリの作成
ここでは、RPCを利用してPythonで定義された関数を、JavaScriptからリモートで呼び出し、結果を受け取るアプリを作成する。
 
データの授受は、JSONを通して行なわれ、以下のフォーマットを含む。

```javascript
// 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アプリの構成図は以下の通りである。

```mermaid
sequenceDiagram
  participant JS as JavaScript (Client)
  participant Server as Python RPC Server

  JS->>Server: JSON Request (method, params)
  Server-->>JS: JSON Response (result, id)
```

# 5. サーバー側のコード
サーバーはPythonで実装されている。このコードは、ソケットを通じてRPCリクエストを受け取り、`FunctionRegistry`を呼び出す。

## 5.1. RPC Server
実装コードは以下の通りである。

```python
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
実装コードは以下の通りである。

```python       
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)`のリモート呼び出しを行う例である。

```javascript
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. サーバー実行結果
```text
接続が確立されました。
Closing current connection
```

## 7.2. クライアント実行結果
```text
Connected to server!
Response from server: { status: 'success', result: 55, id: 1 }
Disconnected from server
```

結果から、クライアントとサーバーがともに正常に動作し、期待された結果（フィボナッチ数列の10番目の値）を取得し、クライアントに返していることがわかる。

# 8. 最後に
今回は非常にシンプルなRPCサーバーを構築したが、今後はHTTPベースに乗せたり、gRPCのようなバイナリプロトコルへの展開も考えられる。