Xml-rpc is a remote procedure call method that uses XML passed over HTTP as a carrier. With it, a client can call a server method with parameters (the server is named after a URI) on a remote server and get structured data. Python has its own XMLRPC implementation. Learning XMLRPC can help us quickly understand the implementation and principle of RPC. This article includes the following parts:
- XMLRPC demo
- xmlrpc-API
- XMLRPC – server implementation
- XMLRPC – client implementation
- XMLRPC serialization/deserialization
- summary
XMLRPC demo
XMLRPC can be run directly to start the RPC service:
# python3 -m xmlrpc.server Serving XML-RPC on localhost port 8000 It is advisable to run this example server within a secure, 127.0.0.1 - - [05/May/2021 18:03:16] "POST /RPC2 HTTP/1.1" 200-127.0.0.1 - - [05/May/2021 18:03:16] "POST /RPC2 HTTP/1.1" 200 -Copy the code
Start the RPC client:
# python3 -m xmlrpc.client
20210505T18:03:16
42
512
3
Copy the code
You can see two HTTP requests from the server. Let’s look at the first HTTP request packet captured:
METHOD: POST URL: http://localhost:8000/RPC2 HEADERS accept-encoding: gzip content-length: 120 content-type: Text/XML host: localhost:8000 user-agent: python-xmlrpc /3.8Copy the code
The requested data is:
<? The XML version = '1.0'? > <methodCall> <methodName> currentTime.getCurrentTime </methodName> <params> </params> </methodCall>Copy the code
HTTP response packet:
STATUS: 200 OK HEADERS content-length: 163 content-type: text/xml date: Wed, 05 May 2021 09:40:57 GMT server: Python / 3.8.5 BaseHTTP / 0.6Copy the code
The response data is:
<? The XML version = '1.0'? > <methodResponse> <params> <param> <value> <dateTime.iso8601> 20210505T18:03:16 </dateTime.iso8601> </value> </param> </params> </methodResponse>Copy the code
I won’t post the data packet for the second request, but here is the request XML:
<? The XML version = '1.0'? > <methodCall> <methodName> system.multicall </methodName> <params> <param> <value> <array> <data> <value> <struct> <member> <name> methodName </name> <value> <string> getData </string> </value> </member> <member> <name> params </name> <value> <array> <data> </data> </array> </value> </member> </struct> </value> <value> <struct> <member> <name> methodName </name> <value> <string> pow </string> </value> </member> <member> <name> params </name> <value> <array> <data> <value> <int> 2 </int> </value> <value> <int> 9 </int> </value> </data> </array> </value> </member> </struct> </value> <value> <struct> <member> <name> methodName </name> <value> <string> add </string> </value> </member> <member> <name> params </name> <value> <array> <data> <value> <int> 1 </int> </value> <value> <int> 2 </int> </value> </data> </array> </value> </member> </struct> </value> </data> </array> </value> </param> </params> </methodCall>Copy the code
Here is the response XML:
<? The XML version = '1.0'? > <methodResponse> <params> <param> <value> <array> <data> <value> <array> <data> <value> <string> 42 </string> </value> </data> </array> </value> <value> <array> <data> <value> <int> 512 </int> </value> </data> </array> </value> <value> <array> <data> <value> <int> 3 </int> </value> </data> </array> </value> </data> </array> </value> </param> </params> </methodResponse>Copy the code
Note: In order to present the XMLRPC data in its entirety, I have posted the full XML, which is a bit long.
From the demo, you can see the following features of XMLRPC:
- Data is transmitted using HTTP. use
POST
Method, the URL isRPC2
, is the content-typetext/xml
. - Request/response is encoded using XML. Request to use
methodCall
Tag, used in responsemethodResponse
The label. - Nesting XML data with layers of redundancy can seem cumbersome (which is probably one reason XMLRPC didn’t catch on).
xmlrpc-API
Continuing with the XMLRPC API usage, the server code:
class ExampleService: def getData(self): return '42' class currentTime: @staticmethod def getCurrentTime(): return datetime.datetime.now() with SimpleXMLRPCServer(("localhost", 8000)) as server: server.register_function(pow) server.register_function(lambda x,y: x+y, 'add') server.register_instance(ExampleService(), allow_dotted_names=True) server.register_multicall_functions() print('Serving XML-RPC on localhost port 8000') print('It is advisable to run this example server within a secure, closed network.') try: server.serve_forever() except KeyboardInterrupt: print("\nKeyboard interrupt received, exiting.") sys.exit(0)Copy the code
The server does the following:
- Create an instance server of SimpleXMLRPCServer on port 8000
- Registered to the server
pow
And calledadd
Lambda function interface - Register a service instance(app is more appropriate) with the server. Instance takes two functions:
getData
andcurrentTime.getCurrentTime
- Register the System. Multicall implementation with the server, which supports the consolidation of multiple RPC requests using one HTTP request
- Start the server
The client is used like this:
server = ServerProxy("http://localhost:8000") print(server.currentTime.getCurrentTime()) multi = MultiCall(server) Multi-.getdata () multi-.pow (2,9) multi-.add (1,2) try: for response in multi(): print(response) except Error as v: print("ERROR", v)Copy the code
- Create a service proxy (rPC-client)
- Invoke the implementation of the server
currentTime.getCurrentTime
- Called with MultiCall
getData
.pow
和add
Three RPC interfaces - Send the multicall request and loop out the result of the service call
The difference between an XMLRPC service and an HTTP service is obvious:
- RPC service interfaces are generic functions such as POW, getData, and getCurrentTime; These interfaces are isolated from HTTP request and Response
- The client requires an additional implementation, not a direct HTTP request
At the same time, we should have an intuitive understanding of remote Procedure Call (RPC), a simple explanation is remote function call. By remote: remote across machines, and in our case, remote across processes. As for how to implement remote function calls, that is the function of each RPC framework, today we will first look at the implementation of XMLRPC.
XMLRPC – the realization of the server
Server HTTP protocol implementation
XMLRPC HTTP protocol by SimpleXMLRPCServer and SimpleXMLRPCRequestHandler implementation:
class SimpleXMLRPCServer(socketserver.TCPServer,
SimpleXMLRPCDispatcher):
allow_reuse_address = True
_send_traceback_header = False
def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
logRequests=True, allow_none=False, encoding=None,
bind_and_activate=True, use_builtin_types=False):
self.logRequests = logRequests
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
Copy the code
SimpleXMLRPCServer’s parent class, TCPServer, described in a previous blog post, provides an implementation of TCP services. SimpleXMLRPCRequestHandler was responsible for part of the HTTP protocol implementation, and XMLRPC code must use a POST request to the/RPC2 focus on do_POST method:
class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler):
# rpc-url
rpc_paths = ('/', '/RPC2')
def do_POST(self):
...
max_chunk_size = 10*1024*1024
size_remaining = int(self.headers["content-length"])
L = []
while size_remaining:
chunk_size = min(size_remaining, max_chunk_size)
chunk = self.rfile.read(chunk_size)
if not chunk:
break
L.append(chunk)
size_remaining -= len(L[-1])
data = b''.join(L)
...
response = self.server._marshaled_dispatch(
data, getattr(self, '_dispatch', None), self.path
)
...
self.send_response(200)
self.send_header("Content-type", "text/xml")
self.send_header("Content-length", str(len(response)))
self.end_headers()
self.wfile.write(response)
Copy the code
The do_POST method has three sections:
- Reads request data from an HTTP request. The length of the data is determined by Content-Length
- Call the RPC interface using the server’s _marshaled_dispatch method
- Wrap the interface return value as an HTTP response return
Server RPC protocol implementation
Another parent of SimpleXMLRPCServer, SimpleXMLRPCDispatcher, provides an implementation of the RPC protocol:
class SimpleXMLRPCDispatcher: def __init__(self, allow_none=False, encoding=None, use_builtin_types=False): Self. funcs = {} # Service instance (app) self.instance = None self.allow_none = allow_none self.encoding = encoding or 'utf-8' self.use_builtin_types = use_builtin_types def register_instance(self, instance, allow_dotted_names=False): Self. instance = instance self.allow_dotted_names = allow_dotted_names def register_function(self.instance = instance self.allow_dotted_names = allow_dotted_names Function =None, name=None): name = function.__name__ self.funcs[name] = function return function def register_multicall_functions(self): Registers the XML-rpc multicall method in the system namespace. Registers the xmL-rpc multicall method in the system namespace. self.system_multicall})Copy the code
Instace and function are relatively easy to register, so we can skip the slightly more complicated implementation of multical and see how the registered interface is called in _marshaled_dispatch:
def _marshaled_dispatch(self, data, dispatch_method = None, path = None): try: Params, method = loads(data, loads) use_builtin_types=self.use_builtin_types) # generate response response = self._dispatch(method, Params) # wrap response in a singleton tuple response = (response,) # generate XML response = dumps(response, methodresponse=1, allow_none=self.allow_none, encoding=self.encoding) except Fault as fault: ... except: ... return response.encode(self.encoding, 'xmlcharrefreplace') def _dispatch(self, method, params): try: Func = self.funcs[method] except KeyError: pass else: If func is not None: return func(*params)... if self.instance is not None: if hasattr(self.instance, '_dispatch'): # call the `_dispatch` method on the instance return self.instance._dispatch(method, params) # call the instance's method directly try: func = resolve_dotted_attribute( self.instance, method, self.allow_dotted_names ) except AttributeError: pass else: if func is not None: return func(*params) ...Copy the code
The code is quite long, and it mainly does two things:
- Parse params and Method from the request
- Call a method from func or instance according to method and return it
Multi-call implementation on the server
Once you understand single-Call, it’s easy to look back at the implementation of multi-Call. Registration interface:
def register_multicall_functions(self):
self.funcs.update({'system.multicall' : self.system_multicall})
Copy the code
The funcs dictionary adds a call to system.multicall and handles system_multicall:
def system_multicall(self, call_list): """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...] ) => [[4],... Allows the caller to package multiple XML-RPC calls into a single request. See "" http://www.xmlrpc.com/discuss/msgReader$1208" results = [] # order execute multiple call for call in call_list: method_name = call['methodName'] params = call['params'] ... results.append([self._dispatch(method_name, params)]) ... return resultsCopy the code
System_multicall, like the annotation, takes multiple requests from a request and calls them one by one. Example of call data for system.multicall:
<methodName>
system.multicall
</methodName>
<params>
...
<member>
<name>
methodName
</name>
<value>
<string>
getData
</string>
</value>
</member>
...
<params>
Copy the code
XMLRPC – client implementation
Client HTTP protocol implementation
Clients also need to implement HTTP protocol, mainly in ServerProxy and Transport (SafeTransport implements HTTPS). ServerProxy package Transport object:
class ServerProxy: def __init__(self, uri, transport=None, encoding=None, verbose=False, allow_none=False, use_datetime=False, use_builtin_types=False, *, headers=(), context=None): # get the url type, uri = urllib.parse._splittype(uri) self.__host, self.__handler = urllib.parse._splithost(uri) .. handler = Transport extra_kwargs = {} transport = handler(use_datetime=use_datetime, use_builtin_types=use_builtin_types, headers=headers, **extra_kwargs) self.__transport = transport ... def __request(self, methodname, params): Request = dumps(params, methodName, encoding=self.__encoding, allow_none=self.__allow_none).encode(self.__encoding, 'xmlcharrefreplace') response = self.__transport.request( self.__host, self.__handler, request, verbose=self.__verbose ) return responseCopy the code
Transport implements HTTP details:
class Transport:
"""Handles an HTTP transaction to an XML-RPC server."""
def __init__(self, use_datetime=False, use_builtin_types=False,
*, headers=()):
self._use_datetime = use_datetime
self._use_builtin_types = use_builtin_types
self._connection = (None, None)
self._headers = list(headers)
self._extra_headers = []
def request(self, host, handler, request_body, verbose=False):
http_conn = self.send_request(host, handler, request_body, verbose)
resp = http_conn.getresponse()
if resp.status == 200:
self.verbose = verbose
return self.parse_response(resp)
def send_request(self, host, handler, request_body, debug):
connection = self.make_connection(host)
headers = self._headers + self._extra_headers
...
connection.putrequest("POST", handler)
headers.append(("Content-Type", "text/xml"))
headers.append(("User-Agent", self.user_agent))
self.send_headers(connection, headers)
self.send_content(connection, request_body)
return connection
def make_connection(self, host):
if self._connection and host == self._connection[0]:
return self._connection[1]
# create a HTTP connection object from a host descriptor
chost, self._extra_headers, x509 = self.get_host_info(host)
self._connection = host, http.client.HTTPConnection(chost)
return self._connection[1]
def parse_response(self, response):
stream = response
p, u = self.getparser()
while 1:
data = stream.read(1024)
if not data:
break
if self.verbose:
print("body:", repr(data))
p.feed(data)
if stream is not response:
stream.close()
p.close()
return u.close()
Copy the code
- Use http.client to create an HTTP connection
- Send the HTTP request using send_request
- Parse HTTP requests using parse_response
Client RPC protocol implementation
Wrapping requests with _Method over the HTTP protocol:
class ServerProxy:
def __getattr__(self, name):
# magic method dispatcher
return _Method(self.__request, name)
class _Method:
# some magic to bind an XML-RPC method to an RPC server.
# supports "nested" methods (e.g. examples.getStateName)
def __init__(self, send, name):
self.__send = send
self.__name = name
def __getattr__(self, name):
return _Method(self.__send, "%s.%s" % (self.__name, name))
def __call__(self, *args):
return self.__send(self.__name, args)
Copy the code
You can use the server. The currentTime. GetCurrentTime () sends the request, this is a chain calls. Server. currentTime calls serverProxy. __getattr__ to geta _Method object; __getAttr__ returns a _Method object, and finally uses getCurrentTime() to execute the call method of the method object, The ServerProxy call method is used to send the request.
Client multi-call implementation
Once you understand the client’s single-call implementation, move on to multi-Call, which covers the following three classes:
class _MultiCallMethod: def __init__(self, call_list, name): self.__call_list = call_list self.__name = name def __getattr__(self, name): return _MultiCallMethod(self.__call_list, "%s.%s" % (self.__name, name)) def __call__(self, *args): Self.__call_list.append ((self.__name, args)) # Add a call class to MultiCallIterator: def __init__(self, results): self.results = results def __getitem__(self, i): item = self.results[i] if type(item) == type({}): raise Fault(item['faultCode'], item['faultString']) elif type(item) == type([]): return item[0] else: raise ValueError("unexpected type in multicall result") class MultiCall: def __init__(self, server): self.__server = server self.__call_list = [] def __getattr__(self, name): return _MultiCallMethod(self.__call_list, name) def __call__(self): marshalled_list = [] for name, args in self.__call_list: marshalled_list.append({'methodName' : name, 'params' : Multical return MultiCallIterator(self.__server.system.multicall(marshalled_list))Copy the code
Call, getattr, and getitem; call, getattr, and getitem;
Multi = MultiCall(server) multi-.getdata () multi-.pow (2,9) multi-.add (1,2) for response in multi(): print(response)Copy the code
XMLRPC serialization/deserialization
RPC services need to be transferred across the network, and data between the server and client needs to be serialized/deserialized. Mainly implemented by Marshaller and Unmarshaller classes:
# client.py
class Marshaller:
...
class Unmarshaller:
...
Copy the code
XMLRPC supports the following nine data types:
- array
- base64
- boolean
- date/time
- double
- integer
- string
- struct
- nil
Some data types, such as double and nil, do not exist in Python. The encoding/decoding of these two types of data is as follows:
class Marshaller:
def dump_double(self, value, write):
write("<value><double>")
write(repr(value))
write("</double></value>\n")
dispatch[float] = dump_double
def dump_nil (self, value, write):
if not self.allow_none:
raise TypeError("cannot marshal None unless allow_none is enabled")
write("<value><nil/></value>")
dispatch[type(None)] = dump_nil
class Unmarshaller:
def end_double(self, data):
self.append(float(data)) # float
self._value = 0
dispatch["double"] = end_double
dispatch["float"] = end_double
def end_nil (self, data):
self.append(None) # None
self._value = 0
dispatch["nil"] = end_nil
Copy the code
summary
In the absence of TCP, XMLRPC is primarily a two-tier model with HTTP at the bottom and XMLRPC at the top. HTTP protocol is responsible for network transmission; The XMLRPC protocol is responsible for converting RPC requests into XML data and then deserializing them into request execution.
Refer to the link
- En.wikipedia.org/wiki/XML-RP…