This recipe shows how to create JSON RPC client and server objects. The aim is to mimic the standard python XML-RPC API both on the client and server sides, but using JSON marshalling. It depends on cjson (http://pypi.python.org/pypi/python-cjson) for the encoding/decoding of JSON data. This recipe tries to reuse the code of XML-RPC as much as possible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | # Heavily based on the XML-RPC implementation in python.
# Based on the json-rpc specs: http://json-rpc.org/wiki/specification
# The main deviation is on the error treatment. The official spec
# would set the 'error' attribute to a string. This implementation
# sets it to a dictionary with keys: message/traceback/type
import cjson
import SocketServer
import sys
import traceback
try:
import fcntl
except ImportError:
fcntl = None
###
### Server code
###
import SimpleXMLRPCServer
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
def _marshaled_dispatch(self, data, dispatch_method = None):
id = None
try:
req = cjson.decode(data)
method = req['method']
params = req['params']
id = req['id']
if dispatch_method is not None:
result = dispatch_method(method, params)
else:
result = self._dispatch(method, params)
response = dict(id=id, result=result, error=None)
except:
extpe, exv, extrc = sys.exc_info()
err = dict(type=str(extpe),
message=str(exv),
traceback=''.join(traceback.format_tb(extrc)))
response = dict(id=id, result=None, error=err)
try:
return cjson.encode(response)
except:
extpe, exv, extrc = sys.exc_info()
err = dict(type=str(extpe),
message=str(exv),
traceback=''.join(traceback.format_tb(extrc)))
response = dict(id=id, result=None, error=err)
return cjson.encode(response)
class SimpleJSONRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
# Class attribute listing the accessible path components;
# paths not on this list will result in a 404 error.
rpc_paths = ('/', '/JSON')
class SimpleJSONRPCServer(SocketServer.TCPServer,
SimpleJSONRPCDispatcher):
"""Simple JSON-RPC server.
Simple JSON-RPC server that allows functions and a single instance
to be installed to handle requests. The default implementation
attempts to dispatch JSON-RPC calls to the functions or instance
installed in the server. Override the _dispatch method inhereted
from SimpleJSONRPCDispatcher to change this behavior.
"""
allow_reuse_address = True
def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
logRequests=True):
self.logRequests = logRequests
SimpleJSONRPCDispatcher.__init__(self, allow_none=True, encoding=None)
SocketServer.TCPServer.__init__(self, addr, requestHandler)
# [Bug #1222790] If possible, set close-on-exec flag; if a
# method spawns a subprocess, the subprocess shouldn't have
# the listening socket open.
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
flags |= fcntl.FD_CLOEXEC
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
###
### Client code
###
import xmlrpclib
class ResponseError(xmlrpclib.ResponseError):
pass
class Fault(xmlrpclib.ResponseError):
pass
def _get_response(file, sock):
data = ""
while 1:
if sock:
response = sock.recv(1024)
else:
response = file.read(1024)
if not response:
break
data += response
file.close()
return data
class Transport(xmlrpclib.Transport):
def _parse_response(self, file, sock):
return _get_response(file, sock)
class SafeTransport(xmlrpclib.SafeTransport):
def _parse_response(self, file, sock):
return _get_response(file, sock)
class ServerProxy:
def __init__(self, uri, id=None, transport=None, use_datetime=0):
# establish a "logical" server connection
# get the url
import urllib
type, uri = urllib.splittype(uri)
if type not in ("http", "https"):
raise IOError, "unsupported JSON-RPC protocol"
self.__host, self.__handler = urllib.splithost(uri)
if not self.__handler:
self.__handler = "/JSON"
if transport is None:
if type == "https":
transport = SafeTransport(use_datetime=use_datetime)
else:
transport = Transport(use_datetime=use_datetime)
self.__transport = transport
self.__id = id
def __request(self, methodname, params):
# call a method on the remote server
request = cjson.encode(dict(id=self.__id, method=methodname,
params=params))
data = self.__transport.request(
self.__host,
self.__handler,
request,
verbose=False
)
response = cjson.decode(data)
if response["id"] != self.__id:
raise ResponseError("Invalid request id (is: %s, expected: %s)" \
% (response["id"], self.__id))
if response["error"] is not None:
raise Fault("JSON Error", response["error"])
return response["result"]
def __repr__(self):
return (
"<ServerProxy for %s%s>" %
(self.__host, self.__handler)
)
__str__ = __repr__
def __getattr__(self, name):
# magic method dispatcher
return xmlrpclib._Method(self.__request, name)
if __name__ == '__main__':
if not len(sys.argv) > 1:
import socket
print 'Running JSON-RPC server on port 8000'
server = SimpleJSONRPCServer(("localhost", 8000))
server.register_function(pow)
server.register_function(lambda x,y: x+y, 'add')
server.serve_forever()
else:
remote = ServerProxy(sys.argv[1])
print 'Using connection', remote
print repr(remote.add(1, 2))
aaa = remote.add
print repr(remote.pow(2, 4))
print aaa(5, 6)
try:
# Invalid parameters
aaa(5, "toto")
print "Successful execution of invalid code"
except Fault:
pass
try:
# Invalid parameters
aaa(5, 6, 7)
print "Successful execution of invalid code"
except Fault:
pass
try:
# Invalid method name
print repr(remote.powx(2, 4))
print "Successful execution of invalid code"
except Fault:
pass
|
It is possible to make this code work with javascript client scripts. Read the comment at the beginning for details on the deviations wrt the JSON-RPC standard.
To try this recipe, on one terminal run: python ./json.py This will spawn a JSON-RPC server listening on port 8000. In another terminal, run: python ./json.py http://localhost:8000 This will create a few JSON-RPC to the server.
Use SocketServer.ThreadingMixIn to make the server multi-threaded (see http://docs.python.org/lib/node632.html).
cjson is buggy, use jsonlib or demjson instead. The linked code uses cjson for JSON serialization/deserialization. cjson is very buggy - please use jsonlib[1] or demjson[2] (in strict mode) instead.
[1] http://pypi.python.org/pypi/jsonlib/
[2] http://pypi.python.org/pypi/demjson
WSGI. This is a lot easier to do with WSGI. I wrote up a tutorial linked from here: http://blog.ianbicking.org/2008/04/02/json-rpc-webob-example/