JSON-RPC 2.0 Transport: Sockets
| Status: | proposal/draft |
|---|---|
| Date: | 2013-05-03 |
| Author: | Roland Koebler <rk at simple-is-better dot org> |
Table of Contents
Using sockets (TCP, UDP or Unix Domain Sockets) with JSON-RPC is simple: Simply send and receive the JSON-RPC strings. The only problem is to detect when a Request/Response is complete and where the next Request/Response starts. Here, three ways are presented:
- shutdown/close the connection after every Request/Response
- encapsulate JSON-RPC in netstrings
- detect JSON-object/array boundaries by using a streaming JSON-splitter or streaming JSON-decoder
1 shutdown/close after every Request/Response
Use a new connection for every Request/Response.
The client MUST shutdown the writing (SHUT_WR) to signal the end of the Request. The server MUST close the connection to signal the end of the Response.
This is simple, straight-forward and robust. But it may be slow, since every Request needs a new connection.
Client-side:
- open a socket, connect
- send Request, shutdown socket-writing
- receive Response (=receive until close/shutdown)
- close socket
- goto 1
Server-side:
- open socket, bind, listen
- accept a connection
- receive Request (=receive until shutdown)
- send Response
- close connection
- goto 1
2 Netstrings
| See: |
|---|
Encode JSON-RPC objects in netstrings. Since netstrings are a self-delimiting encoding, a single connection can be used for several Requests/Responses.
But note that the server may close a connection e.g. on error or after a timeout, so use appropriate error-handling and always use a new connection as fallback.
Example:
60:{"jsonrpc": "2.0", "method": "first", "params": 42, "id": 1},66:{"jsonrpc": "2.0", "method": "second", "params": [23, 7], "id": 2},
3 pipelined Requests/Responses / JSON-splitter
It's also possible to use a single connection for several Requests/Responses without an additional encoding. Then, (a) a streaming JSON-decoder or (b) a streaming JSON-splitter is needed, which detects the end of a JSON-object/array. Then, several JSON-RPC Requests can simply be pipelined on a single connection.
Streaming JSON-decoders seem to be rare, but a JSON-splitter which only detects the end of a JSON-object/array can easily be implemented (see below).
But note that the server may close a connection e.g. on error or after a timeout, so use appropriate error-handling and always use a new connection as fallback.
JSON-splitter example in Python 2 (should be easily transferable to other languages):
def json_split(s, start=0):
r"""Extract 1st JSON-object/array from a string.
:Returns: (EXTRACTED_JSON_STRING_or_None, i_remainder)
:Raises: ValueError
:Example::
>>> stream = '{"first": "object", "data": "x"} {"second": "object", "data": "y"} ["third", "array"]["fourth", "array"]["incomplete", "arr'
>>> i=0
>>> while True:
... j, i = json_split(stream, i)
... if j is None:
... break
... print j
{"first": "object", "data": "x"}
{"second": "object", "data": "y"}
["third", "array"]
["fourth", "array"]
>>> print stream[i:]
["incomplete", "arr
>>> stream = '{"a": "b", "1": 2, "c": {"1": [1, 2], "3": [{"d": ["}"]}], "2": {"3": 4}}, "xy": "x ] } \\" [ { y"}' * 5
>>> i=0
>>> while True:
... j, i = json_split(stream, i)
... if j is None:
... break
... print j
{"a": "b", "1": 2, "c": {"1": [1, 2], "3": [{"d": ["}"]}], "2": {"3": 4}}, "xy": "x ] } \" [ { y"}
{"a": "b", "1": 2, "c": {"1": [1, 2], "3": [{"d": ["}"]}], "2": {"3": 4}}, "xy": "x ] } \" [ { y"}
{"a": "b", "1": 2, "c": {"1": [1, 2], "3": [{"d": ["}"]}], "2": {"3": 4}}, "xy": "x ] } \" [ { y"}
{"a": "b", "1": 2, "c": {"1": [1, 2], "3": [{"d": ["}"]}], "2": {"3": 4}}, "xy": "x ] } \" [ { y"}
{"a": "b", "1": 2, "c": {"1": [1, 2], "3": [{"d": ["}"]}], "2": {"3": 4}}, "xy": "x ] } \" [ { y"}
"""
state = 0 # 0=search start, 1=array, 11=string inside array
# 2=object, 12=string inside object
i = start # current position in string
b = i # begin of string-part
depth = 0 # array/object-depth-counter
while i < len(s):
# find start of JSON-array or object
if 0 == state:
if s[i] == '[':
state = 1
b = i
depth = 1
elif s[i] == '{':
state = 2
b = i
depth = 1
elif s[i].isspace(): # ignore whitespace
pass
else:
raise ValueError("Invalid character '%s' at %d, expected '[' or '{'." % (s[i], i))
# skip string
elif state > 10:
if s[i] == '\\': # skip char after \
i += 1
elif s[i] == '"': # end of string
state -= 10
# inside array
elif 1 == state:
if s[i] == '"':
state += 10
elif s[i] == '[':
depth += 1
elif s[i] == ']':
depth -= 1
if depth == 0:
return s[b:i+1], i+1
# inside object
elif 2 == state:
if s[i] == '"':
state += 10
elif s[i] == '{':
depth += 1
elif s[i] == '}':
depth -= 1
if depth == 0:
return s[b:i+1], i+1
i += 1
return None, b
