JSON-RPC

JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol. It uses JSON as its data format and is transport-independent.

Reference

specificatio of version 1.0
specification of version 2.0
JSON-RPC 1.2 Proposal , a discussion in the JSON-RPC Google Group, 2008-02-22

Description

APN 2008-07-05: A Tcl implementation of the JSON-RPC client-side API is now part of TclSOAP. Most of the server-side calls are implemented, but not integrated with any http servers.

MaxJarek JSON-RPC is my primary technology for client-server applications. I use Tk GUI on client side and Wibble as JSON-RPC http server. Sample client example:

package require JSONRPC
JSONRPC::create add -proxy "http://www.raboof.com/projects/jayrock/demo.ashx" \
        -params { val1 int val2 int }

puts [::add 2 3456]

Sample server side example (for wibble::jsonrpc::proxy package):

namespace eval ::wibble::json-rpc {
  proc add {a b} {return [expr {$a+$b}]}
}

TclSOAP don't support https transport for JSON-RPC. I have created patch for SOAP::https which is waiting in repo.

wibble::jsonrpc::proxy is my package thats change Wibble in good (for me) SOA application server. jsonrpc-proxy needs same corrects in Wibble core. I am waiting for new Wibble version then proxy package will be public available. :)

AMG: Please let me know what changes you would like in the Wibble core, and I'll see what I can incorporate directly. For anything that I can't just drop in without breaking compatibility and/or adding prerequisites, I may have ideas on how to modify the core to be more receptive to outside customization.

The next version of Wibble, when I have time to work on it, should feature charset support, rather than being tied to ISO8859-1. That's my primary goal. I'm also collecting little fixes and updates. For example, I modified [getrequest] to permit application/json-rpc to be parsed by a zone handler, and I modified [listen] to work better with TLS, both thanks to your suggestions and contributions. Another feature I'd like in the next Wibble is support for virtual hosts, by request from SDW.

MaxJarek: Ok, let's start with Wibble 0.2 :). This is initial version.

# Simple JSONRPC proxy for Wibble
# J. Lewandowski, (MaxJarek)
# Available under the Tcl/Tk license.  https://www.tcl-lang.org/software/tcltk/license.html

package provide wibble::jsonrpc::proxy 0.1

namespace eval ::wibble::zone {

proc jsonrpc-proxy {namespace state} {

  dict set state response status 200
  dict set state response header content-type "" application/json-rpc charset utf-8

  dict with state request {}
  if {$method eq "POST" && [dict exists $header content-type ""] && [dict get $header content-type ""] eq "application/json-rpc"} {

    array set json_req [json::json2dict [dehex $rawpost]]

    # execute method with params
    catch {set result [eval ${namespace}::$json_req(method) $json_req(params)]} err

  } else {

    set err "JSON-RPC error"

  }

  if {![info exists result]} {
        dict set state response content [json::write object id null \
                error [json::write object name [json::write string JSONRPCError] message [json::write string $err]]]
        ::wibble::log "JSON-RPC: $err"
  } else {
        dict set state response content [json::write object result [json::write string $result] id $json_req(id)]
  }

  sendresponse [dict get $state response]

}

}

Example Wibble startup script:

namespace eval ::wibble::json-rpc {
  proc strlen {a} {return "[string length $a]"}
  proc date {} {return [clock seconds]}
  proc add {a b} {return [expr {$a+$b}]}
}

package require wibble
package require json
package require json::write
package require wibble::jsonrpc::proxy

set ::wibble::zonehandlers {}
::wibble::handle /maxgui [list jsonrpc-proxy ::wibble::json-rpc]
::wibble::handle / notfound

wibble::listen 8080
vwait forever

Example client code:

package require JSONRPC
JSONRPC::configure -transport http
JSONRPC::create add -proxy "http://localhost:8080/maxgui" \
        -params { val1 int val2 int }
puts [::add 2 3456]

If you want any new json-rpc method simple insert new funkcion in ::wibble::json-rpc namespace

AMG: I don't see any core modifications here.

One change I made in my development version of Wibble is to add a [dict getnull] command (see [dict get] for the implementation), which simplifies this:

[dict exists $header content-type ""] && [dict get $header content-type ""] eq "application/json-rpc"

to be this:

[dict getnull $header content-type ""] eq "application/json-rpc"

This piece of code:

namespace eval ::wibble::json-rpc {
  proc strlen {a} {return "[string length $a]"}
  proc date {} {return [clock seconds]}
  proc add {a b} {return [expr {$a+$b}]}
}

can also be written:

namespace eval ::wibble::json-rpc {
  proc strlen {a} {string length $a}
  proc date {} {clock seconds}
  proc add {a b} {expr {$a + $b}}
}

It's mostly a question of style.

In your code above, I went ahead and modified the line:

  dict set state response header content-type "" "application/json-rpc; charset=utf-8"

to instead read:

  dict set state response header content-type "" application/json-rpc charset utf-8

This is how header dicts are intended to work. The empty string key maps to the value, and the other keys map to the parameters. This way, Wibble is able to read, interpret, and modify the headers without having to run the HTTP encode and decode routines.

Sadly, setting the charset here doesn't actually work, since Wibble will still convert your content to ISO8859-1. If the content consists solely of 7-bit ASCII, this has no effect, since 7-bit characters are encoded identically across ISO885-1, ASCII, and UTF-8. But this will corrupt anything else. For now, you're better off omitting the charset, or explicitly setting it to "iso8859-1".

Like I said, I intend to fix this in the next release of Wibble, though I'm still trying to figure out how it should work. Here's what I'm thinking, for now anyway.

  • Wibble will have a table mapping between HTTP charset names and Tcl encoding names. When parsing the client's accept-charset header, Wibble will ignore charsets not present in its table. If nothing remains, or if there was no accept-charset to begin with, Wibble will fall back on ISO8859-1.
  • If there's no charset in the content-type header, or the content-type header is missing altogether, Wibble assumes content is a native Tcl string, or that contentchan already has its encoding configured such that [read]ing it will produce a native Tcl string. (Not sure how to handle contentfile.) Wibble will then pick the client's favorite charset (according to the accept-charset header), configure the socket to encode using that charset, then write the content to it.
  • If there is a charset in the content-type header, Wibble assumes content or contentfile is encoded using that charset, or that contentchan is configured without encoding translation and will produce text in that charset when [read]. If that charset is among those advertised with nonzero qvalue in the client's accept-charset header, Wibble will turn off encoding translation for the socket before writing out the content. Otherwise, Wibble will decode it to a native Tcl string then perform as in the above case. By the way, it's an error for to specify an unsupported charset in the content-type header.

Form submission remains a problem [L1 ] [L2 ] [L3 ]. The only approach that makes sense, given the current state of popular web browsers, is to require that all HTML <form>s specify accept-charset="utf-8". This way the browsers will send UTF-8, a known encoding that supports every character. Without an explicit accept-charset, browser behavior varies wildly, and in no case is it possible to reliably autodetect the encoding, not without magical knowledge unavailable to Wibble (e.g. the encoding of the page from which the <form> was submitted) or ridiculous amounts of effort (e.g. statistical analysis of the byte patterns compared with typical frequencies for various encodings). Sadly, I couldn't find any configurations in which the browser actually bothered to tell the server what encoding it was using.

MaxJarek: I know about Wibble problem with encoding content but for JSON-RPC i delete this line with ISO8859-1 encoding. Works and i have utf-8 encoding. Header with charset=utf-8 (in wibble::jsonrpc::proxy) is ignored by Wibble but is important for http transport in SOAP::http.

I don't know how Wibble encoding content should work. For now i adjusting rpc-proxy for Wibble 0.2 with this ISO8859-1 modification. I noticed also thats JSONRPC package is ASCII characters oriented. I patch this and for now intensively testing with my apllications.

AMG: I did similar (delete the ISO8859-1 encoding conversion) to my copy of Wibble for investigation into how different browsers handle different encodings, but that's not really a total solution. This approach requires every zone handler that calls [sendresponse] to first ensure that it has named the charset in its outgoing content-type header and that the content is encoded using that same charset, and it interferes with correct operation of contentfile.

Instead I am contemplating expanding the zone handler system to have multiple phases, such that [sendresponse] doesn't altogether terminate the zone handler search, but rather only the current phase. If a later phase exists, it is given a chance to further process the state dicts, including the response dict. That would be a great place to put postprocessing stuff, for instance common page headers/footers, charset encoding, and compression.

However, I am not quite sure this is the right path to take. For one, the name [sendresponse] is no longer appropriate in this scheme, since it only forces the response to be sent when in the final phase. The bigger question to me is whether I can do better. Wibble is my testbed for innovative web server concepts, so I don't want to commit to a design approach simply because it's the first thing that came to mind. So, let's think about what I have from a different perspective.

The phase system I describe above basically adds a minimal goto. The barrier between phases constitutes a line label, and [sendresponse] goes to the next line label in the sequence. So, it's a basic flow control concept. That raises two questions:

  1. What other flow control concepts are useful?
  2. Can the zone configuration be expressed in a more typical programming style?

Now this has me interested in the possibility of virtual hosts. Vhost configuration is basically a switch statement. When the host is X, do this; when it's Y or Z, do that; otherwise, do the third thing. But it would be really cool if the different hosts had some opportunity to share parts of the configuration, since they perhaps make up the same "site". For instance, some of the hosts may have different docroots but the same header and footer, and all of the hosts may permit compression.

I think it's clear that the zone handler configuration can be viewed as an executable program. The trouble is integrating that concept with the current model, which is basically an interpreter. Well, what kind of interpreter is it? It's multiprocessing; it cycles between the alternative state dicts. If I let Tcl be the interpreter, I'll need to retain that functionality, but implement it in terms of the Tcl core. It turns out Tcl already has the required capability: coroutines!! I have some ideas on how this would actually work, but I want to leave a little space here for your own imagination. But anyway, this means a Wibble configuration would basically be a proc and can use any standard (or custom!) Tcl flow control and decision-making commands. One exciting possibility would be splitting up the configuration across multiple procs which can call each other.

By the way, once we get something hammered out, I will move this discussion to Wibble discussion archive. I'm just writing that now so I don't forget. ;^)

'escargo 2012-01-31' - In answer to your previous question, "What other flow control concepts are useful?" I think if what you have now is equivalent to a 'go-to' statement (restricted to a forward direction), Then the obvious possible additions would be:

  1. Unrestricted go-to
  2. Call/return without parameters
  3. Call/return with parameters
  4. If
  5. If/else
  6. Do/while

I'm not sure you need to loop, but 'if' and unrestricted 'go-to' are sufficient.

It might make sense to see if 'go-to' is the wrong model for flow control, and see if do/while and if/else (yay for structured programming) can do the job.

AMG: I'd like to just have the configuration be executable Tcl, so I get all this stuff for free, without writing my own interpreter.

The big challenge is multiple state dicts, wherein there are multiple copies of the zone handler response search running in parallel. The one use for this feature in the provided set of zone handlers is [indexfile], which forks the search to look both for index.html and for the directory itself. [indexfile] can't know in advance if index.html exists (there might not be a file of that name but instead some other zone handler, e.g. [templatefile], knows how to make it anyway), so it must "branch both ways", generating an extra state dict.

Earlier I said I could model this using coroutines, which would yield to each other in round robin fashion, until one definitively produced a response. [nexthandler] could be the point at which [yield] gets called, though I'm not entirely happy with that arrangement, since [nexthandler] is currently optional. The real problem is starting a second coroutine. I don't know of a way to fork or clone the current coroutine execution, such that the new coroutine starts off not at the beginning of the proc, but at the fork point.

Maybe there's a way, similar to [L4 ] or maybe Goto in Tcl, wherein the points at which [nexthandler] could be called get transformed into line labels, and the invocation of the new coroutine is given the current line label as an argument to which it immediately jumps. For now, this seems like too much trouble, plus it gets really sticky with nested command invocation.

Instead I could drop the feature and instead handle it in the user's zone configuration script via a loop or if statement: Repeatedly run these few zone handler commands, until a success is obtained or all possibilities (index.html or directory listing) are exhausted. Still that feels less elegant than what I currently have.