尚未翻译
本页面尚未翻译成中文,因此显示英文原文。 帮助翻译
xmlrpc Module Complexity¶
The xmlrpc package makes remote procedure calls over HTTP. xmlrpc.client marshals Python
values into an XML document and sends each call as one POST; xmlrpc.server parses the request,
dispatches it to a registered function, and marshals the result back. A request is marshalled
whole before it is sent, and a call returns only once the whole response has been turned into
Python objects; the server reads the whole request body before it parses any of it.
n is the characters in one XML-RPC document, a request or a response body; for a call it
counts both. Marshalling and parsing are both linear in it, and it grows with the values'
elements and string characters, counting a shared reference once for every place it appears.
b is the bytes in a Binary payload, c is the calls queued in a MultiCall, f is the
registered functions plus the attributes dir() finds on a registered instance, p is the
dotted segments in a method name, h is one method's docstring characters, and t is the
docstring characters of every listed method plus the server documentation text. Method-name
lookups are dictionary lookups and count as O(1). No bound includes the network round trip or
the work the called method itself does.
Complexity Reference¶
ServerProxy¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.client.ServerProxy(uri, transport=None, encoding=None, verbose=False, allow_none=False, use_datetime=False, use_builtin_types=False, *, headers=(), context=None) |
O(1) | O(1) | Parses the URI and builds a transport; no connection is opened until the first call |
proxy.name, proxy.name.sub |
O(1) | O(1) | Builds a callable stub for the remote name; sends nothing |
proxy.name(*args) |
O(n) | O(n) | One HTTP POST, plus its round trip: the whole request is marshalled, and the whole response parsed, before the call returns |
ServerProxy.system.listMethods(), ServerProxy.system.methodSignature(name), ServerProxy.system.methodHelp(name) |
O(n) | O(n) | Ordinary calls; SimpleXMLRPCServer answers them after register_introspection_functions(), and its side is priced under SimpleXMLRPCDispatcher |
proxy('close')(), with ServerProxy(...) as proxy |
O(1) | O(1) | Closes the transport's connection |
proxy('transport') |
O(1) | O(1) | The transport the proxy sends through |
xmlrpc.client.Transport, xmlrpc.client.SafeTransport |
O(1) | O(1) | Keep one HTTP (or HTTPS) connection and send the next call over it while the server leaves it open |
MultiCall¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.client.MultiCall(server) |
O(1) | O(1) | Wraps a ServerProxy |
multicall.name(*args) |
O(1) | O(1) | Queues the call locally; sends nothing |
multicall() |
O(n) | O(n) | Sends all c queued calls as one system.multicall request: one round trip instead of c |
Iterating the result, result[i] |
O(1) per result | O(1) | A call that failed raises Fault when its position is reached; the results before it are still returned |
dumps and loads¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.client.dumps(params, methodname=None, methodresponse=None, encoding=None, allow_none=False) |
O(n) | O(n) | n = the document returned; a reference cycle raises TypeError, and None needs allow_none |
xmlrpc.client.loads(data, use_datetime=False, use_builtin_types=False) |
O(n) | O(n) | Returns (params, methodname); a fault document raises Fault |
Binary and DateTime¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.client.Binary(data=None) |
O(1) for bytes, O(b) for bytearray |
O(1) for bytes, O(b) for bytearray |
A bytes argument is kept as it is; a bytearray is copied |
Binary.data |
O(1) | O(1) | The stored bytes object |
Binary.encode(out) |
O(b) | O(b) | Writes the base64 form to out.write() |
Binary.decode(bytes) |
O(b) | O(b) | Base64-decodes into data |
xmlrpc.client.DateTime(value=0) |
O(1) | O(1) | Stores a YYYYMMDDTHH:MM:SS string; the default 0 means the current local time |
DateTime.decode(string), DateTime.encode(out) |
O(1) | O(1) | Replace or write that string |
Constants and exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.client.Fault(faultCode, faultString), Fault.faultCode, Fault.faultString |
O(1) | O(1) | What a call raises when the server method raised; any exception other than Fault arrives as code 1 |
xmlrpc.client.ProtocolError, ProtocolError.url, ProtocolError.errcode, ProtocolError.errmsg, ProtocolError.headers |
O(1) | O(1) | Raised for an HTTP status other than 200 |
xmlrpc.client.MAXINT, xmlrpc.client.MININT |
O(1) | O(1) | Marshalling an int outside them raises OverflowError |
xmlrpc.client.Error, xmlrpc.client.ResponseError |
O(1) | O(1) | The base class, and what loads() raises for XML with no XML-RPC params, fault or method name in it |
SimpleXMLRPCServer¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.server.SimpleXMLRPCServer(addr, requestHandler=SimpleXMLRPCRequestHandler, logRequests=True, allow_none=False, encoding=None, bind_and_activate=True, use_builtin_types=False) |
O(1) | O(1) | Binds and listens; serves one request at a time |
SimpleXMLRPCServer.register_function(function=None, name=None) |
O(1) | O(1) | One dictionary entry; also usable as a decorator |
SimpleXMLRPCServer.register_instance(instance, allow_dotted_names=False) |
O(1) | O(1) | Replaces any earlier instance; names are looked up on it at call time |
SimpleXMLRPCServer.register_introspection_functions() |
O(1) | O(1) | Adds system.listMethods, system.methodSignature and system.methodHelp |
SimpleXMLRPCServer.register_multicall_functions() |
O(1) | O(1) | Adds system.multicall |
| Dispatching a request | O(n) | O(n) | Reads the whole body, parses it, looks up the name, and marshals the result |
| Dispatching to the registered instance | O(p) | O(p) | Only for a name no registered function has: one getattr per dotted segment with allow_dotted_names=True, a single one without, and a segment starting with _ is refused; an instance that defines _dispatch receives the name instead |
SimpleXMLRPCDispatcher¶
The server side of the system.* methods. SimpleXMLRPCServer and CGIXMLRPCRequestHandler
both inherit them.
| Operation | Time | Space | Notes |
|---|---|---|---|
SimpleXMLRPCDispatcher.system_listMethods() |
O(f log f) | O(f) | Sorted names, rebuilt per call; a registered instance's dir() is walked each time unless it defines _listMethods or _dispatch |
SimpleXMLRPCDispatcher.system_methodHelp(method_name) |
O(p + h) | O(p + h) | The method's docstring, or '' for an unknown name; p counts only when the name is resolved on the instance |
SimpleXMLRPCDispatcher.system_methodSignature(method_name) |
O(1) | O(1) | Always 'signatures not supported' |
SimpleXMLRPCDispatcher.system_multicall(call_list) |
O(n) | O(n) | One dispatch per call; a call that raises becomes a fault entry and the rest still run |
SimpleXMLRPCRequestHandler¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.server.SimpleXMLRPCRequestHandler |
O(1) | O(1) | Speaks HTTP/1.0 unless protocol_version is overridden, so the connection closes after each response |
SimpleXMLRPCRequestHandler.rpc_paths |
O(1) | O(1) | Includes '/' and '/RPC2' by default; a POST to a path outside it gets a 404 without being parsed, and an empty tuple accepts every path |
SimpleXMLRPCRequestHandler.encode_threshold |
O(1) | O(1) | A response longer than it (1,400 bytes by default) is gzip-compressed, O(n), when the client accepts gzip, as ServerProxy does |
CGIXMLRPCRequestHandler¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.server.CGIXMLRPCRequestHandler(allow_none=False, encoding=None, use_builtin_types=False) |
O(1) | O(1) | The same dispatcher with no socket |
CGIXMLRPCRequestHandler.register_function(function=None, name=None), CGIXMLRPCRequestHandler.register_instance(instance), CGIXMLRPCRequestHandler.register_introspection_functions(), CGIXMLRPCRequestHandler.register_multicall_functions() |
O(1) | O(1) | As on SimpleXMLRPCServer |
CGIXMLRPCRequestHandler.handle_request(request_text=None) |
O(n) | O(n) | Reads the request from stdin when request_text is omitted, and writes headers and response to stdout |
Documenting servers¶
| Operation | Time | Space | Notes |
|---|---|---|---|
xmlrpc.server.DocXMLRPCServer(addr, requestHandler=DocXMLRPCRequestHandler, ...), xmlrpc.server.DocXMLRPCRequestHandler |
O(1) | O(1) | A SimpleXMLRPCServer that also answers GET with an HTML page |
xmlrpc.server.DocCGIXMLRPCRequestHandler() |
O(1) | O(1) | The CGI equivalent |
DocXMLRPCServer.set_server_title(server_title), DocXMLRPCServer.set_server_name(server_name), DocXMLRPCServer.set_server_documentation(server_documentation) |
O(1) | O(1) | Stored for the next page |
DocCGIXMLRPCRequestHandler.set_server_title(server_title), DocCGIXMLRPCRequestHandler.set_server_name(server_name), DocCGIXMLRPCRequestHandler.set_server_documentation(server_documentation) |
O(1) | O(1) | Stored for the next page |
| A GET for the documentation page | O(f log f + t) | O(f + t) | Rebuilt on every request from the method list, each method's docstring and the server documentation |
Making Calls¶
One Round Trip per Call¶
Attribute access on a proxy costs nothing; the call is what sends. Each call marshals its
arguments, waits for the server, and parses the complete response, so c calls are c round trips.
A MultiCall queues them and sends one request.
import threading
from xmlrpc.client import MultiCall, ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
with SimpleXMLRPCServer(('127.0.0.1', 0), logRequests=False) as server:
server.register_function(pow)
server.register_multicall_functions()
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
with ServerProxy(f'http://{host}:{port}/') as proxy: # O(1) - no connection yet
assert proxy.pow(2, 10) == 1024 # O(n) plus one round trip
batch = MultiCall(proxy)
for exponent in range(5):
batch.pow(2, exponent) # O(1) - queued, not sent
assert list(batch()) == [1, 2, 4, 8, 16] # O(n), one round trip for all five
server.shutdown()
Connection Reuse¶
The transport keeps its connection and sends the next call over it, but only if the server left
it open. SimpleXMLRPCRequestHandler speaks HTTP/1.0 and closes the connection after every
response, so each call pays for a new one. A handler that sets protocol_version to HTTP/1.1
keeps it.
import threading
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
class CountingServer(SimpleXMLRPCServer):
connections = 0
def get_request(self):
CountingServer.connections += 1
return super().get_request()
class KeepAlive(SimpleXMLRPCRequestHandler):
protocol_version = 'HTTP/1.1'
def connections_for_ten_calls(handler):
CountingServer.connections = 0
with CountingServer(('127.0.0.1', 0), requestHandler=handler, logRequests=False) as server:
server.register_function(abs)
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
with ServerProxy(f'http://{host}:{port}/') as proxy:
for number in range(10):
assert proxy.abs(-number) == number
server.shutdown()
return CountingServer.connections
assert connections_for_ten_calls(SimpleXMLRPCRequestHandler) == 10 # one per call
assert connections_for_ten_calls(KeepAlive) == 1 # reused
Faults and Protocol Errors¶
An exception in the server method comes back as a Fault, so the call still costs its round
trip. A Fault the method raises itself keeps its code; anything else arrives as code 1. An HTTP
error, such as a path outside rpc_paths, is a ProtocolError instead.
import threading
from xmlrpc.client import Fault, ProtocolError, ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
def withdraw(amount):
if amount > 100:
raise Fault(4, 'insufficient funds')
return 100 - amount
with SimpleXMLRPCServer(('127.0.0.1', 0), logRequests=False) as server:
server.register_function(withdraw)
server.register_function(int)
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
with ServerProxy(f'http://{host}:{port}/') as proxy:
assert proxy.withdraw(30) == 70
try:
proxy.withdraw(500)
except Fault as fault:
assert (fault.faultCode, fault.faultString) == (4, 'insufficient funds')
else:
raise AssertionError('no fault was raised')
try:
proxy.int('not a number')
except Fault as fault:
assert fault.faultCode == 1 and 'ValueError' in fault.faultString
else:
raise AssertionError('no fault was raised')
with ServerProxy(f'http://{host}:{port}/elsewhere') as proxy:
try:
proxy.withdraw(30)
except ProtocolError as error:
assert error.errcode == 404
else:
raise AssertionError('a path outside rpc_paths was served')
server.shutdown()
Marshalling Without a Server¶
dumps() and loads() are the two halves of every call, usable on their own. Both are linear in
the document. dumps() writes a value out once for every place it is referenced, so a structure
that shares one list many times marshals to a document that repeats it; a reference cycle raises
TypeError instead of recursing forever.
from xmlrpc.client import MAXINT, dumps, loads
request = dumps(({'name': 'Alice', 'tags': ['a', 'b']}, 42), 'store') # O(n)
params, method = loads(request) # O(n)
assert method == 'store'
assert params == ({'name': 'Alice', 'tags': ['a', 'b']}, 42)
shared = ['x' * 100]
twice = dumps(([shared, shared],)) # the shared list is written out twice
once = dumps(([shared],))
assert twice.count('x' * 100) == 2 and once.count('x' * 100) == 1
cycle = []
cycle.append(cycle)
try:
dumps((cycle,))
except TypeError as error:
assert 'recursive' in str(error)
else:
raise AssertionError('a cycle was marshalled')
try:
dumps((MAXINT + 1,))
except OverflowError:
pass
else:
raise AssertionError('an int past MAXINT was marshalled')
assert loads(dumps((None,), allow_none=True))[0] == (None,)
Binary and DateTime¶
Binary and DateTime are the wrappers for base64 and ISO 8601 values. loads() returns them by
default; use_builtin_types=True returns bytes and datetime instead. Binary(data) keeps a
bytes argument as it is, and copies a bytearray.
import datetime
import io
from xmlrpc.client import Binary, DateTime, dumps, loads
payload = b'\x00\x01' * 1000
wrapped = Binary(payload) # O(1) - the same bytes object
assert wrapped.data is payload
assert Binary(bytearray(payload)).data is not payload # O(b) copy
out = io.StringIO()
wrapped.encode(out) # O(b) base64
assert out.getvalue().startswith('<value><base64>')
(value,), _ = loads(dumps((wrapped,))) # O(n)
assert isinstance(value, Binary) and value.data == payload
(raw,), _ = loads(dumps((wrapped,)), use_builtin_types=True)
assert raw == payload
moment = datetime.datetime(2024, 5, 17, 12, 30, 0)
stamp = DateTime(moment) # O(1)
assert stamp.value == '20240517T12:30:00'
(back,), _ = loads(dumps((stamp,)), use_builtin_types=True)
assert back == moment
Serving¶
Registration and Dispatch¶
Registering is a dictionary entry. A call's name is looked up among the registered functions
first; only a name none of them has falls through to the registered instance, where it is
resolved with getattr unless the instance defines its own _dispatch. There, a name with a
segment starting with _ is refused, and dotted names reach nested objects only with
allow_dotted_names=True, which exposes whatever those objects can reach.
import threading
from xmlrpc.client import Fault, ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
class Service:
def greet(self):
return 'from the instance'
def version(self):
return '1.0'
def _internal(self):
return 'hidden'
with SimpleXMLRPCServer(('127.0.0.1', 0), logRequests=False) as server:
@server.register_function(name='greet') # O(1)
def greet():
return 'from the function'
server.register_instance(Service()) # O(1)
server.register_introspection_functions()
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
with ServerProxy(f'http://{host}:{port}/') as proxy:
assert proxy.greet() == 'from the function' # functions come first
assert proxy.version() == '1.0' # O(p) getattr on the instance
try:
proxy._internal()
except Fault as fault:
assert 'not supported' in fault.faultString
else:
raise AssertionError('a private method was called')
methods = proxy.system.listMethods() # O(f log f) on the server
assert methods == sorted(methods)
assert {'greet', 'version', 'system.listMethods'} <= set(methods)
server.shutdown()
One Request at a Time¶
SimpleXMLRPCServer handles each request to completion before it accepts the next, so one slow
method delays every other client. Mixing in socketserver.ThreadingMixIn runs each request in its
own thread.
import socketserver
import threading
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
class ThreadedServer(socketserver.ThreadingMixIn, SimpleXMLRPCServer):
daemon_threads = True
both_inside = threading.Barrier(2, timeout=10)
def rendezvous():
both_inside.wait() # returns only when two calls are running at once
return True
with ThreadedServer(('127.0.0.1', 0), logRequests=False) as server:
server.register_function(rendezvous)
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
results = []
def call():
with ServerProxy(f'http://{host}:{port}/') as proxy:
results.append(proxy.rendezvous())
clients = [threading.Thread(target=call) for _ in range(2)]
for client in clients:
client.start()
for client in clients:
client.join()
assert results == [True, True]
server.shutdown()
Handling a Request Through CGI¶
CGIXMLRPCRequestHandler is the same dispatcher without a socket: handle_request() parses one
request and writes the HTTP headers and response to standard output.
import contextlib
import io
import sys
from xmlrpc.client import dumps, loads
from xmlrpc.server import CGIXMLRPCRequestHandler
handler = CGIXMLRPCRequestHandler()
handler.register_function(len) # O(1)
stdout = io.TextIOWrapper(io.BytesIO(), encoding='utf-8')
with contextlib.redirect_stdout(stdout):
handler.handle_request(dumps(([1, 2, 3],), 'len')) # O(n)
sys.stdout.flush()
output = stdout.buffer.getvalue()
headers, _, body = output.partition(b'\n\n')
assert b'Content-Type: text/xml' in headers
assert loads(body)[0] == (3,)
Documentation Pages¶
DocXMLRPCServer answers GET with an HTML page built from the registered methods. The page is
rebuilt on every request: the methods are listed and sorted, and each docstring is rendered.
import threading
import urllib.request
from xmlrpc.server import DocXMLRPCServer
def add(a, b):
"""Addition."""
return a + b
with DocXMLRPCServer(('127.0.0.1', 0), logRequests=False) as server:
server.register_function(add)
server.set_server_title('Calculator') # O(1)
server.set_server_name('Calculator service')
server.set_server_documentation('Arithmetic over XML-RPC.')
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
with urllib.request.urlopen(f'http://{host}:{port}/') as response: # O(f log f + t)
page = response.read().decode('utf-8')
assert '<title>Python: Calculator</title>' in page
assert 'Addition.' in page
server.shutdown()
Common Patterns¶
Batching Many Small Calls¶
A MultiCall turns c round trips into one. A call that fails does not stop the batch: its
Fault is raised when the result is reached, and the results around it are still there.
import threading
from xmlrpc.client import Fault, MultiCall, ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
with SimpleXMLRPCServer(('127.0.0.1', 0), logRequests=False) as server:
server.register_function(int)
server.register_multicall_functions()
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
with ServerProxy(f'http://{host}:{port}/') as proxy:
batch = MultiCall(proxy)
for text in ['1', 'two', '3']:
batch.int(text) # O(1) each
results = batch() # O(n), one round trip
assert results[0] == 1 # O(1)
try:
results[1]
except Fault as fault:
assert 'ValueError' in fault.faultString
else:
raise AssertionError('the failed call returned a value')
assert results[2] == 3
server.shutdown()
Performance Best Practices¶
✅ Do:
- Batch independent calls with
MultiCall: c calls become one round trip - Keep one
ServerProxyper server and reuse it; a new proxy starts a new connection - Set
protocol_version = 'HTTP/1.1'on the request handler when clients make many calls, so they can reuse the connection - Mix in
socketserver.ThreadingMixInwhen a method can be slow, so it does not hold up other clients
❌ Avoid:
- A loop of small remote calls where one
MultiCall, or one call taking a list, would do - Very large arguments or results: the request is built whole in memory, and the whole result is materialized before the call returns
- Sharing one large object many times inside an argument; it is marshalled once per reference
allow_dotted_names=Trueon anything reachable from an untrusted network
Version Notes¶
- All Python 3:
SimpleXMLRPCRequestHandlerspeaks HTTP/1.0, so every call opens a new connection unless the handler setsprotocol_version = 'HTTP/1.1'
Related Modules¶
- http -
http.clientandhttp.server, which carry every call - socketserver -
ThreadingMixInand the serving loopSimpleXMLRPCServerinherits - json - the other common text format for structured data
- xml - the XML packages;
xmlrpcparses withxml.parsers.expat