使用Python實現RPC框架

前言

本文將會使用Python實現一個最簡單的RPC框架,玩具向,不具備實用意義,但可讓你清醒的理解RPC框架的幾個組成部分,只是比看Python自帶的xmlrpc清晰。python

本文須要一點Python socket基礎。docker

若是你對Python Socket基礎方面的內容不是很熟悉,推薦閱讀Real Python的「Socket Programming in Python (Guide)json

吐槽一下VSCode,在開發一些比較複雜的Python項目時,VSCode的debug功能讓人感到蛋疼,詢問了Windows下使用VSCode的同事,都沒有這樣的問題,不清楚VSCode對Mac的支持是否存在問題,還只是我單純的不會用:(網絡

本文代碼比較簡單,因此仍是使用VSCode進行開發。那咱們開始吧!框架

回顧RPC

  • 客戶端(Client):服務調用方。
  • 客戶端存根(Client Stub):存放服務端地址信息,將客戶端的請求參數數據信息打包成網絡消息,再經過網絡傳輸發送給服務端。
  • 服務端存根(Server Stub):接收客戶端發送過來的請求消息並進行解包,而後再調用本地服務進行處理。
  • 服務端(Server):服務的真正提供者。
  • Network Service:底層傳輸,能夠是 TCP 或 HTTP。

實現jsonrpc

在實現前,簡單理一下總體思路。socket

1.Network Service 直接使用Python Socket相關的API實現 2.傳輸數據使用JSON,在Socket層會被壓成二進制,咱們無需關心ide

模仿xmlrpc,Client與Server都採用Minix多繼承機制來實現,每一個類負責自身的事情,最終暴露出現的只有一個類中有限的方法。oop

先從Client端開始實現。優化

# client.py

import rpcclient

c = rpcclient.RPCClient()
c.connect('127.0.0.1', 5000)
res = c.add(1, 2, c=3)
print(f'res: [{res}]')
複製代碼

實例化rpcclient.RPCClient類,而後調用connect方法連接Server端,隨後直接調用Server端的add方法,該方法的效果就是將傳入的數據進行累加並將累加的結果返回,最後將add方法返回的結果打印出了。ui

RPCClient類繼承於TCPClient類與RPCStub類。

# rpclient.py

class RPCClient(TCPClient, RPCStub):
    pass
複製代碼

其中TCPClient負責經過Socket實現TCP連接並將數據請求過去,而RPCStub類主要將Client端調用Server端方法的相關信息打包,而後調用TCPClient類中的方法發送則可,兩個類一樣實如今rpclient.py文件中,代碼以下。

class TCPClient(object):
    def __init__(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def connect(self, host, port):
        '''連接Server端'''
        self.sock.connect((host, port))

    def send(self, data):
        '''將數據發送到Server端'''
        self.sock.send(data)

    def recv(self, length):
        '''接受Server端回傳的數據'''
        return self.sock.recv(length)
        

class RPCStub(object):
    def __getattr__(self, function):
        def _func(*args, **kwargs):
            d = {'method_name': function, 'method_args': args, 'method_kwargs': kwargs}
            self.send(json.dumps(d).encode('utf-8')) # 發送數據
            data = self.recv(1024) # 接收方法執行後返回的結果
            return data

        setattr(self, function, _func)
        return _func
複製代碼

TCPClient類就是常規的Socket API的操做,無需多言,主要看看RPCStub類。

當咱們在Client端調用res = c.add(1, 2, c=3)時,會執行RPCStub中的__getattr__方法,該方法會將Client端調用的方法、參數等信息經過TCPServer類的send方法發送,發送數據進行了JSON格式化,方便Server端解碼,隨後便調用recv方法等待Server端相應的數據返回。

由於RPCClient類自己沒有add方法,爲了讓用戶作到Client端直接調用Server端方法的形式,先利用__getattr__構建了_func方法,並將其經過setattr方法設置到RPCClient類中,此時該類就有Server端方法對應的映射了。

調用add方法,就調用了對應的_func方法,將數據發送至Server端。

Client端就這樣搞定了,接着來實現Server端,不用緊張,很是簡單。

Server端的使用方式以下。

# server.py

import rpcserver

def add(a, b, c=10):
    sum = a + b + c
    return sum

s = rpcserver.RPCServer()
s.register_function(add) # 註冊方法
s.loop(5000) # 傳入要監聽的端口
複製代碼

實例化rpcserver.RPCServer類,而後經過register_function方法將想被Client端調用的方法傳入,隨後調用loop方法,將要監聽的端口傳入,RPCServer類的實現以下。

# rpcserver.py

class RPCServer(TCPServer, JSONRPC, RPCStub):
    def __init__(self):
        TCPServer.__init__(self)
        JSONRPC.__init__(self)
        RPCStub.__init__(self)

    def loop(self, port):
        # 循環監聽 5000 端口
        self.bind_listen(port)
        print('Server listen 5000 ...')
        while True:
            self.accept_receive_close()

    def on_msg(self, data):
        return self.call_method(data)
複製代碼

RPCServer繼承自TCPServer、JSONRPC、RPCStub,這些類一樣實如今rpcserver.py文件中而且給出了詳細的註釋,因此就詳細解釋了。

class TCPServer(object):
    def __init__(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def bind_listen(self, port):
        self.sock.bind(('0.0.0.0', port))
        self.sock.listen(5)

    def accept_receive_close(self):
        '''獲取Client端信息'''
        (client_socket, address) = self.sock.accept()
        msg = client_socket.recv(1024)
        data = self.on_msg(msg)
        client_socket.sendall(data) # 回傳
        client_socket.close()


class JSONRPC(object):
    def __init__(self):
        self.data = None

    def from_data(self, data):
        '''解析數據'''
        self.data = json.loads(data.decode('utf-8'))

    def call_method(self, data):
        '''解析數據,調用對應的方法變將該方法執行結果返回'''
        self.from_data(data)
        method_name = self.data['method_name']
        method_args = self.data['method_args']
        method_kwargs = self.data['method_kwargs']
        res = self.funs[method_name](*method_args, **method_kwargs)
        data = {"res": res}
        return json.dumps(data).encode('utf-8')


class RPCStub(object):
    def __init__(self):
        self.funs = {}

    def register_function(self, function, name=None):
        '''Server端方法註冊,Client端只可調用被註冊的方法'''
        if name is None:
            name = function.__name__
        self.funs[name] = function
複製代碼

至此,Client端和Server端都寫好了,跑一下吧。

總結一下

經過上述代碼,再次理解一下RPC中這幾個重要的概念,理解的是否是深刻了一下。

  • 客戶端(Client):服務調用方。
  • 客戶端存根(Client Stub):存放服務端地址信息,將客戶端的請求參數數據信息打包成網絡消息,再經過網絡傳輸發送給服務端。
  • 服務端存根(Server Stub):接收客戶端發送過來的請求消息並進行解包,而後再調用本地服務進行處理。
  • 服務端(Server):服務的真正提供者。
  • Network Service:底層傳輸,能夠是 TCP 或 HTTP。

開源的RPC框架確定不是這麼簡單的,其中考慮了特別的邊界條件以及各類優化,但RPC自己確是簡單的。

結尾

最近一直在研究Docker,嘗試經過Go來寫一個玩具docker,後面弄出來,會分享一下Go與docker相關的內容。

下篇文章見,對了,有幫助的話,點「在看」或「讚揚」進行催更吧。

相關文章
相關標籤/搜索