Jump to content

How to implement JSON-RPC protocol


NotionCommotion

Recommended Posts

I am creating an incredible math server application which has the ability to subtract two numbers.  I think I have the math down, but need some help with the strategy for how the client should react to the servers response.

 

I have no idea whether this is correct, but am thinking of something like the following:

  1. Client creates a socket connection to the server.
  2. Upon server’s acceptance of the connection, client initiates sets $this->callbacks=[].
  3. Client decides to send some big math request to the server.
  4. Based on what it wants to do with the results, client sets $this->callback[]='someCallBackFunction', and then performs a count of the array which shows that the it happens to have three elements.
  5. Client sends the following to the server:  {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
  6. Client listens for the response, and eventually is returned: {"jsonrpc": "2.0", "result": 19, "id": 3}
  7. Client looks up $this->callbacks=[3], unsets it so it cannot be called initiated again, and does something with it.
  8. Maybe after a sufficient duration of time, client unsets any other elements in the array which are old?

Am I on the right track? 

 

What is this requirement typically called so I may google and get more information?

 

Can anyone offer any additional description on how this is typically accomplished?

 

If the server initiates the communication to the communication to the client, is the process basically the same.  I expect the client will have needed to subscribe to the server after initiating the connection, but expect/hope the process is close.

 

Thanks

Link to comment
Share on other sites

That's not really a "handshake". Like in real life, handshakes are used after the initial connection is made to identify both sides to each other and make sure they're all speaking the same language. For you, a handshake would do something like exchange protocol versions to negotiate which one to use - client supports 1,2,3, server supports 2,3,4, best version is 3.

 

Speaking of, protocol is what you're looking at here. This version of the protocol

- Exchanges JSON

- Sends a request in the form {jsonrpc: 2, method: string, params: {...}, id: identifier}

- Sends a response in the form {jsonrpc: 2, result: whatever, id: same identifier}

 

Assuming it fulfills the needs of the application (eg, params being a keyed object instead of an array is up to you) that's a reasonable protocol.

 

What is this requirement typically called so I may google and get more information?

"Requirement"? "Protocol" may be the term you're looking for.

 

Can anyone offer any additional description on how this is typically accomplished?

It varies based on the application, transmission medium, data requirements... What you have there is actually like a JSON version of SOAP.

 

If the server initiates the communication to the communication to the client, is the process basically the same.  I expect the client will have needed to subscribe to the server after initiating the connection, but expect/hope the process is close.

Server making the connection doesn't make sense with your description. The client connects to the server to perform a task, great. The server connects to the client to... what?
Link to comment
Share on other sites

That's not really a "handshake". ... , protocol is what you're looking at here.    "Requirement"? "Protocol" may be the term you're looking for.

Thanks!  At least I have something to search for.  Could it have a more descriptive name?  "Protocol" seems to be used at so many levels.  Maybe "application protocol"?

 

 

It varies based on the application, transmission medium, data requirements... What you have there is actually like a JSON version of SOAP.

I expect you either already know this or it is not really a standard.  It is JSON-RPC 2.0.  The id in the JSON uniquely describes the message sent for a given client.  See http://json-rpc.org/wiki/specification and http://www.jsonrpc.org/specification.

 

Server making the connection doesn't make sense with your description. The client connects to the server to perform a task, great. The server connects to the client to... what?

No, server, doesn't initiate the connection, client does.  Server, however, has the ability to initiate a message using an established connection.  ClientA and ClientB both establish connection to Server, ClientA sends message to Server, and Server sends message to ClientB.

 

Assuming my loosely thought out 8-step strategy is somewhat correct, I would expect I would need to store callback[] in a SplObjectStorage tied to the connection?

 

 

I am sure there are many ways to implement a protocol, however, also expect it has been done many times, and some ways are better than others.  For me:

  • JSON-RPC 2.0 is being used as the protocol.
  • Multiple clients can establish a TCP socket connection to a single server.
  • Clients need to have the ability to send several types of methods with optional parameters to the server, and need to be able to react to the server's response based on the type of method the client had sent to the server.
  • Server need to have the ability to send several types of methods with optional parameters to each individual client (thus subscriptions will be used), and need to be able to react to the client's response based on the type of method the server had sent to the client.

Make sense?  Any advice?  Thanks

Edited by NotionCommotion
Link to comment
Share on other sites

Thanks!  At least I have something to search for.  Could it have a more descriptive name?  "Protocol" seems to be used at so many levels.  Maybe "application protocol"?

...Sure? You're describing a general concept, and even searching for "application protocol" is going to get you too much information.

 

Try thinking of existing mechanisms you know of that are similar (in scope, functionality, medium, whatever) then finding more information about how they work.

 

I expect you either already know this or it is not really a standard.

Right. What I meant was that the method + parameters thing is like how SOAP works - it being an example of something you aren't using.

 

No, server, doesn't initiate the connection, client does...

Now it's a question of what message it's sending. The thing you described earlier really deals with a sender and receiver - it works for client-sender/server-receiver fine, but now the issue is whether it works for server-sender/client-receiver. And that depends on the messages being exchanged, which I'd guess would be like the former's.
Link to comment
Share on other sites

...Sure? You're describing a general concept, and even searching for "application protocol" is going to get you too much information.

Your definitely correct.  Couldn't find anything...

 

Now it's a question of what message it's sending. The thing you described earlier really deals with a sender and receiver - it works for client-sender/server-receiver fine, but now the issue is whether it works for server-sender/client-receiver. And that depends on the messages being exchanged, which I'd guess would be like the former's.

 

Let me give you more definition.

 

There is one server, one superClient, and multiple normalClients.

 

The normalClients can make requests to the server, and gets a response if they want one.

 

The superClient can make requests to the server which in turn gets forwarded to a specific normalClient, and gets a response if it wants one.

 

Below is my attempt.

 

Thoughts?

<?php
require 'vendor/autoload.php';

$port = isset($argv[1])?$argv[1]:1337;
$clientList = new SplObjectStorage();
$guidMap=[];

$commandsForClientRequests = new commandsForClientRequests();
$commandsForClientResponses = new commandsForClientResponses();

$loop = new \React\EventLoop\StreamSelectLoop();
$socket = new React\Socket\Server($loop);
$socket->on('connection', function (\React\Socket\ConnectionInterface $client) use (&$clientList, $guidMap, $commandsForClientRequests, $commandsForClientResponses){
    $clientList->attach($client, ['guid'=>null,'callback'=>[]]);
    $client->on('data', function($data) use (&$clientList, $client, $guidMap, $commandsForClientRequests, $commandsForClientResponses){
        $data=json_decode($data);
        //I will add a little data validation...
        if(empty($data->method)) {
            /* A response to a message set from the server and looks like the following:
            {"jsonrpc": "2.0", "result": 19, "id": 3}
            Modify server state and then respond to super-client the results
            */
            $callback=$clientList[$client]['callback'][$data->id]; //['method'=>'subtract','id'=>123]
            $rs=$commandsForClientResponses($callback['method'],$data->result);  //Returns server state plus normalClient state as applicable
            if(!empty($callback['id'])) {
                //Only reply to non-notification requests
                $client->send(["jsonrpc"=>"2.0", "result"=>[$data->result], "id"=>$callback['id']]);
            }
        }
        else {
            // A new message from a client
            switch($data->method) {
                case 'forward':
                    /* This is a special message coming from super-client which forwards the sub-message to client identified by guid 1234abcd, and looks like the following:
                    {"jsonrpc": "2.0", "method": "forward", "params": {"guid": "1234abcd", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}, "id": 123}
                    */
                    if(isset($guidMap[$data->params->guid])) {
                        $forward=[];
                        $forward['jsonrpc']=$data->jsonrpc;
                        $forward['method']=$data->params->method;
                        $forward['params']=$data->params->params;
                        if(!empty($data->id)) {
                            //Superclient wants a response
                            $count=count($clientList[$client]['callback'])+1;
                            $clientList[$client]['callback'][]=['method'=>$data->params->method,'id'=>$count];
                            $forward['id']=$count;
                        }
                        $guidMap[$data->params->guid]->send($forward);
                    }
                    elseif(!empty($data->id)) {
                        $client->send(["jsonrpc"=>"2.0", "error"=>['code'=>-32601, 'message'=>'Client not found'], "id"=>$data->id]);
                    }
                    break;
                case 'registrar':
                    /* This is a special message which registrars normal client identified by guid 1234abcd, and looks like the following:
                    {"jsonrpc": "2.0", "method": "registrar", "params": {"guid": "1234abcd"}, "id": 123}
                    */
                    $clientList[$client]=['guid'=>$data->params->guid,'callback'=>[]];
                    $guidMap[$data->params->guid] = $client;
                    if(!empty($data->id)) {
                        $client->send(["jsonrpc"=>"2.0", "result"=>['success'=>true], "id"=>$data->id]);
                    }
                    break;
                default:
                    //Deal with normal requests coming from normal clients...
                    $rs=$commandsForClientRequests($data->method,$data->params);
                    if(!empty($data->id)) {
                        //Only reply to non-notification requests
                        $client->send(["jsonrpc"=>"2.0", "result"=>$rs, "id"=>$data->id]);
                    }
            }
        }
    });
});

$socket->listen($port, '0.0.0.0');
$loop->run();
Edited by NotionCommotion
Link to comment
Share on other sites

Thread renamed from "How to implement handshake between client and server?"

 

The superClient can make requests to the server which in turn gets forwarded to a specific normalClient, and gets a response if it wants one.

Reminds me of NAT or TCP/IP masquerading. My computer here at home has the address 10.1.1.5, which is a private network that nobody outside the network can send packets to. I'm able to be on the internet because my router performs IP masquerading on outbound packets, rewriting the packet such that when a response returns the router recognizes it as using masquerading and routing it back to my computer.

 

With that said, your clients are already somehow able to identify other clients connected to the server - something which the internet cannot do. So you have two options:

a) Masquerading. The super client sends a request to the server including which client to route to, the server uses masquerading to rewrite the request and send to the desired client as if it were normal communication, the client (potentially) replies like normal, then the server remembers the masquerading and sends the reply to the original client. That's probably more complicated than you need.

b) Forwarding. When the server is not a direct recipient of a message, it acts as a somewhat dumb router (aka a switch) and simply forwards the message to whichever clients are requested. Super client A issues a request to client B, server routes it to client B, that responds knowing it goes to client A, and the server routes that one too.

 

(b) is probably the best option here. It does add a requirement of knowing where a request/response needs to be routed to, added at the top-level object (which is how TCP/IP does it), but you could make that optional and default to being between the server and client. JSON-RPC doesn't give explicit support for that, but adding two new members would still be compatible with the spec - and there's no reason why you can't extend from it if you remain compatible (not to mention that it's your protocol).

Link to comment
Share on other sites

Super client A...

I don't believe relevant, but there is only one super client which happens to be a sockets client created for one time use by an Ajax request to a normal HTTP server.  I will need to make that client's send method blocking which I have never done, but expect I can figure it out.

 

 

JSON-RPC doesn't give explicit support for that, but adding two new members would still be compatible with the spec - and there's no reason why you can't extend from it if you remain compatible (not to mention that it's your protocol).

I suppose I could add two new members, but my intention was to just make the method name "forward", and include the intended normal clients guid as well as the method and parameters to send to that client in the main params field.  Something like the following.  As such, I am not adding any members.  Am I missing the meaning of what you are saying?

{"jsonrpc": "2.0", "method": "forward", "params": {"guid": "1234abcd", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}, "id": 123}

I recognize it is a bunch of script, but would appreciate any critic of the script that you or others are able to provide.

 

Thanks

Link to comment
Share on other sites

I don't believe relevant, but there is only one super client...

Okay, then "Client A, which is a super client,...".

 

 

I suppose I could add two new members, but my intention was to just make the method name "forward", and include the intended normal clients guid as well as the method and parameters to send to that client in the main params field.  Something like the following.  As such, I am not adding any members.  Am I missing the meaning of what you are saying?

{"jsonrpc": "2.0", "method": "forward", "params": {"guid": "1234abcd", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}, "id": 123}

 

But what happens from there? Server recognizes it as a forwarded request, pulls the method and params out and sends it as a new request to whichever client, then that client responds normally... How does the server know to (repackage? and) forward it to the first client? It could assume clients are responding to requests in order, but that means blocking and you don't want that. So somehow the response from the receiving client must be tagged in such a way that the server recognizes it as a response to a forwarded request, which implies something in the original request so that the client knows it needs to tag its response... which is the sender/receiver target stuff I said.
Link to comment
Share on other sites

But what happens from there? Server recognizes it as a forwarded request, pulls the method and params out and sends it as a new request to whichever client, then that client responds normally... How does the server know to (repackage? and) forward it to the first client? It could assume clients are responding to requests in order, but that means blocking and you don't want that. So somehow the response from the receiving client must be tagged in such a way that the server recognizes it as a response to a forwarded request, which implies something in the original request so that the client knows it needs to tag its response... which is the sender/receiver target stuff I said.

 

Typically, servers do not initiate requests, but only respond to clients' request.

 

1. The one exception is when the superclient sends a forward request to the server...

{"jsonrpc": "2.0", "method": "forward", "params": {"guid": "1234abcd", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}, "id": 123}

which causes the server to save information about the request...

$count=count($clientList[$client]['callback'])+1;   //which happens to be 321
$clientList[$client]['callback'][]=['method'=>$data->params->method,'id'=>$count];.
// or given the request...  $clientList[$client]['callback'][]=['method'=>'subtract','id'=>321];.

and initiate a new request to the client which has a GUID of 1234abcd.

{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 321}

2. The client responds with JSON per the standard which must not have a method property and must have a result property.

{"jsonrpc": "2.0", "result": 19, "id": 321}

3. The server sees that either there is no method property or there is a result property (or to be extra sure, maybe both, but why bother), and knows this is the one exception, and must be a response to a forwarded request.

It then finds the original method sent to the client as well as the original request id sent to the server based on the returned request id...

$callback=$clientList[$client]['callback'][$data->id]; //['method'=>'subtract','id'=>123]

deals with it...

$rs=$commandsForClientResponses($callback['method'],$data->result);  //Returns server state plus normalClient state as applicable

and if a original request id exists, returns the

if(!empty($callback['id']))
   $client->send(["jsonrpc"=>"2.0", "result"=>[$data->result], "id"=>$callback['id']]);
}

I think I am good.  Maybe not?

 

PS. Am I using the term "member" correctly?  I am using it based on your 05:04 PM post, but might have misinterpreted its use.

EDIT. I think I did misinterpret their use, and have edited this post to replace the term "member" with "property".

 

Thanks!

Edited by NotionCommotion
Link to comment
Share on other sites

Note that you should just use an ever-increasing counter variable for your id rather than doing a count() on your callbacks array. If you're going to be removing the callbacks when done and processing requests/responses asynchronously then you may end up doubling up on the ID numbers.

 

For example, if your code ended up executing something like this:

$callbacks = [];

sendRequest('something'); //generate and send id=1
sendRequest('something else'); //generate and send id=2;
completeRequest('something'); //remove id=1;
sendRequest('yet another thing'); //generate and send id=2;
completeRequest('something else'); //calls the 'yet another thing' callback instead.
Link to comment
Share on other sites

Note that you should just use an ever-increasing counter variable for your id rather than doing a count() on your callbacks array.

 

How would you recommend implementing an ever-increasing counter variable for the id?  Use a separate counter, add a flag to the callback array, or something else?

 

I guess I answered my question as I wrote this to reply.  Option 1 is probably the way to go as it doesn't needlessly keep in memory a bunch of obsolete data in the array.

 

 

Option 1

if(empty($data->method)) {
    // A response to a message set from the receiver
    if(isset($clientList[$client]['callback'][$data->id])) {
        $callback=$clientList[$client]['callback'][$data->id]; //['method'=>'subtract','id'=>123]
        // Deal with the response...

        // Unset the response so it can't be implemented again
        unset($clientList[$client]['callback'][$data->id]);
    }
    else {
        //error. Response already received
    }
}
else {
    switch($data->method) {
        case 'forward':
            // A request to forward...
            if(!empty($data->id)) {
                //Superclient wants a response
                $count=++$clientList[$client]['counter'];
                $clientList[$client]['callback'][$count]=['method'=>$data->params->method,'id'=>$data->id];
                $forward['id']=$count;
            }
            // Forward the request
            $guidMap[$data->params->guid]->send($forward);
            break;
    }
}

Option 2

if(empty($data->method)) {
    // A response to a message set from the receiver
    $callback=$clientList[$client]['callback'][$data->id]; //['method'=>'subtract','id'=>123, 'available'=true/false]
    if($callback['available']) {
        // Deal with the response...

        // Set flag to false so it can't be implemented again
        $clientList[$client]['callback'][$data->id]['available']=false;
    }
    else {
        //error. Response already received
    }
}
else {
    switch($data->method) {
        case 'forward':
            // A request to forward...
            if(!empty($data->id)) {
                //Superclient wants a response
                $count=count($clientList[$client]['callback'])+1;
                $clientList[$client]['callback'][]=['method'=>$data->params->method,'id'=>$count, 'avaiable'=>true];
                $forward['id']=$count;
            }
            // Forward the request
            $guidMap[$data->params->guid]->send($forward);
            break;
    }
}
Link to comment
Share on other sites

Um, so, question,

 

How are you coordinating these IDs across clients? Given that the server needs to know which ID corresponds to which client.

$clientList = new SplObjectStorage();

$socket->on('connection', function (\React\Socket\ConnectionInterface $client) use (&$clientList, $guidMap, $commandsForClientRequests, $commandsForClientResponses){
    $clientList->attach($client, ['guid'=>null,'callback'=>[]]);
    $client->on('data', function($data) use (&$clientList, $client, $guidMap, $commandsForClientRequests, $commandsForClientResponses){
        $count=++$clientList[$client]['counter'];
        $clientList[$client]['callback'][$count]=['method'=>$data->params->method,'id'=>$data->id];
    });
});

Should work, right?

Link to comment
Share on other sites

Yeah, I was getting confused there for a bit.

 

Anyway, I think kicken is talking about the $data->id. The clients (being the ones generating those IDs) would use a sequential number, and the easiest way to go about that would be with a static variable somewhere:

static $id = 0;
$message = [
	"id" => ++$id,
	...
];
Really, though, the client can do whatever it wants as long as the ID is scalar (for use as array keys) and "guaranteed" to be unique during the lifetime of that particular message's request/response cycle.
Link to comment
Share on other sites

I'm good at confusing!

 

Agree, the clients will need a unique id.  I like your use of a static variable.

 

The server also needs to generate a unique id.

  1. SuperClient sends to Server a unique ID (I will refer to it as superClientID), a method (forward), and params (the GUID of the intended NormalClient along with the method and parameters to send to that NormalClient).
  2. Server determines a new ID unique for just the intended NormalClient (I will refer to it as serverID), uses serverID to save superClientID along with the method sent to NormalClient, and sends NormalClient the method, parameters, and serverID.
  3. NormalClient processes the method/parameters, and return a response along with serverID.
  4. Server uses the received serverID to determine the method it sent as well as the superClientID, processes the response, and replies to SuperClient using superClientID.

Two possible variants are to make serverID unique across NormalClients and to not have Server save the method sent to NormalClient, but to have NormalClient return the method, but I don't think I will do either.

 

Yeah, it is a little confusing...  It seems like it should work fine, but I am always nervous when I makeup some complicated approach without first reading how others have done it.

 

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.