Sign in to follow this  
micaww

rage-rpc: Universal, asynchronous Remote Procedure Call implementation for RAGE

Recommended Posts

Hey everyone. :)

Here are some super annoying RAGE client-server-browser communication pitfalls that most of you will probably recognize:

  • Communication mechanisms between different contexts on RAGE are limited. The server can only communicate to clients, browser instances can only communicate to the local client, and the client can communicate with either. In order words, you can only send messages in a single step. You can't send some data to the server directly from a browser instance or vice-versa. There are a couple of things you could do, like routing every single event through the client or setting up an HTTP server on top of your game server and using AJAX calls within the browser. Let's not go there... that's really messy.
  • Unable to get result from event triggers. RAGE treats all events in a "send it and forget it" fashion. That means you can trigger an event on a local or remote context, such as the server, but then that's it. The only way to see how the server handled the request is to make another event handler on the client, then have the server trigger the event with the result. But what if the remote event is called multiple times? How does the "result" event handler know which request it's seeing the result of? Now imagine the same situation, but you want to ask the server something directly from a CEF instance. Now, you have to send an event to the client to tell it to send an event to the server, only to get the server to send an event back to the client which also should send an event back to your browser. AND you have to keep track of different requests so that the relevant result handler gets called. Oh my god. This can get really complicated, really fast.

Here's a surprise: these are now all issues of the past!

Yesterday, I decided to go at these problems head-on by writing rage-rpc: a universal, asynchronous Remote Procedure Call (RPC) implementation for RAGE. For those of you who are unfamiliar with the term and don't feel like hitting that link (I don't blame you), RPC is a mechanism that lets a context call a function on a remote context, and then retrieve the result of the remote function. It's not as complicated as it sounds, trust me. Here are some feature highlights of rage-rpc:

  • Universal. There is one small library (<5 kb) that you can include in any context: browser, client, or server.
  • Multi-step communication. Any context can send data to any other context. Browser instances can directly message the server. The server can directly send data to any browser instance of a specific player.
  • Asynchronous result retrieval. Registered functions can return any JSON-able value, and it will get immediately passed back to the caller in the form of a Promise. This means that the client can ask the server for something, wait for it to finish executing, and then see the result in-line. No more need to set up multiple events and track requests just to get a simple response from a remote context.

The bottom line is: any context can call a function that resides in any other context and immediately get access to its return value without any extra work.

Enough of the chit-chat. Let's dive into some code examples. I'll post a comment on this post with plain RAGE event versions of each example. Beware.

Installation

You can download the minified JS bundle from the GitHub page or install the package with NPM.

npm install -S rage-rpc

Example 1: Client --> Server

Let's start simple. Say we want to get a list of license plates of all vehicles on the server. We could do this on the client, but it might not always work if some vehicles aren't streamed in. For reliability, we want to ask the server to provide this list for us.

Here's the client-side code:

/* Client */

const rpc = require('rage-rpc');

rpc.callServer('getAllLicensePlates').then(plates => {
  mp.gui.chat.push(plates.join(', '));
});

Yeah. That's it. Seriously. Take a look at the server:

/* Server */

const rpc = require('rage-rpc');

// the callback will be called, and the result will be sent back to the client automatically
rpc.register('getAllLicensePlates', () => mp.vehicles.toArray().map(veh => veh.numberPlate));

That is all. Requests are matched up all under the hood. There are no events for you to handle or request IDs to manage. The client-side code is actually 100% compatible in the CEF environment also. No code changes anywhere.

Example 2: Server --> Browser

Imagine we want to get/set the value of some input field defined in some CEF instance of a player, but we're on the server. There are definitely better examples than this, but this isn't a common issue to begin with.

Let's dive in and start with the browser code. You can define this in any CEF instance.

/* Browser */

// assuming you're loading rage-rpc via script tag, "rpc" will be a global variable.

// or if you're using a bundler like Webpack:
//const rpc = require('rage-rpc'); 

const $input = $('#input');

rpc.register('getInputValue', () => $input.val());
rpc.register('setInputValue', val => $input.val(val));

For client-side, there is no code, but the module just needs to be required somewhere. This is because the browser has to route through the client to get to the server and vice-versa:

/* Client */

require('rage-rpc');

Now for the server-side part:

/* Server */

const rpc = require('rage-rpc');

const player = mp.players.at(0);

rpc.callBrowsers(player, 'getInputValue').then((value, info) => {
  // notice the "info" variable. this is an object that contains various information about the callee such as:
  //    - id: the internal unique ID associated with the request
  //    - player: the player who called this function, if remotely called from CEF or client
  //    - environment: the environment that called this function. one of "cef", "client", or "server". in this case it would be "cef"
  
  console.log(`The value of ${info.player.name}'s input is ${value}!`);
  
  rpc.callBrowsers(player, 'setInputValue', 'pooooop');
});

When you use callBrowsers, the client iterates over every browser instance until it finds a browser that has defined the procedure we're looking for, then it calls the first one it finds. This is why you can define the procedure in any browser instance, because technically each CEF instance is a completely separate context.

You can even use callBrowsers from inside a CEF instance to communicate with other CEF instances. The API is the same as above, but you just leave out the player parameter from callBrowsers.

Changelog

0.1.0

  • ADD: Bundled Typescript definitions
  • IMPROVE: CEF outgoing call returning performance
  • IMRPOVE: callBrowsers performance on all contexts
  • FIX: Some code simplifications

0.0.3

  • FIX: Bug that prevented multiple resources from using RPC at the same time
  • FIX: False alarm for multiple CEF instances receiving the same result
  • ADD: Extra player verification for outgoing server calls

0.0.2

  • FIX: UMD exposing for correct Node.js importing

0.0.1

  • Initial commit

Conclusion

Please try it out and let me know what you think. This system makes communicating with remote contexts a breeze. And remember, you can call code on ANY context from ANY other context with rage-rpc.

Also as a reminder, this is the very first iteration of this library as it was only made and tested within one day. I have some ideas for more features to incorporate in the future.

Check out the GitHub page for more implementation details and API documentation: https://github.com/micaww/rage-rpc

If you have any questions, comments, or issues, contact me on Discord: micaww#3032

You can also use the issue tracker on the GitHub page for feature requests and bugs.

Edited by micaww
Add Changelog
  • Like 6

Share this post


Link to post
Share on other sites

Example 1: Client --> Server

This is the same example as in the original post, but using nothing but native RAGE events. This is just for comparison. Don't use this :D

Here is the client-side code:

/* Client-side */


// BEGIN just the setup code

let currentRequestID = 0; // assigning an ID to each request ensures that we call the correct resolve function
let pendingRequests = {};

mp.events.add('getAllLicensePlates:result', (requestID, plates) => {
  if(pendingRequests[requestID]){
    pendingRequests[requestID](JSON.parse(plates)); // resolve the promise with our value
  }
});

function getAllLicensePlates(){
  return new Promise(resolve => {
    pendingRequests[currentRequestID] = resolve;
    mp.events.callRemote('getAllLicensePlates', currentRequestID);
    currentRequestID++;
  });
}
// END setup code

// now we can use our function throughout the client, but you have to include this setup code in every file you use it in

getAllLicensePlates().then(plates => {
  // do what you want with the license plates
  mp.gui.chat.push(plates.join(', '));
});

AxHlvMi8fFTP9PRuIEmnek3TLaswLoyY-xDFjJ1NeFpBE9z-OIJAUOvXXRp3Y5jvikSw-N4DSbSEBdm2UqOgXw3AuNrccBS1gb9xvVu45bHksDrUSFjg7kfi3y96cF05yqgT8Cgo

And for the server-side code:

/* Server-side */

mp.events.add('getAllLicensePlates', (player, requestID) => {
  const plates = mp.vehicles.toArray().map(veh => veh.numberPlate);
  player.call('getAllLicensePlates:result', [requestID, JSON.stringify(plates)]);
});

lgPt6JzfCdCpUGohapTMUQTBMSFRsPX_QVF74oqHfwPqATUf_KotWnJP4VPmFDfDmmK74XYNKMahEU6Rm1E5hTZZs0Ob0rR9Wh3GN6lggAPjCjDCVo8Unj8yJnzs6jAx6jlXhufU

That's it. That actually doesn't look so bad at first, but it can get really complicated if you have to do something like this more often. The technique is pretty similar for server->client calls, except the server would need to validate that the player sending the data back is the one that it was requested for. Also, this method will pretty much break if anything on the server-side errors out. The client Promise would never be resolved and your code would probably come to a standstill if you're expecting a result.

  • Like 2

Share this post


Link to post
Share on other sites

Accidentally tagged the wrong person and it wont let me edit the name or delete the post lol. Hey @WizzarD, glad you found my post. I've been actively using and improving this in a few large resources. It has greatly reduced/improved boilerplate code and improved performance. I have a new release ready on my computer at home that comes with some big performance improvements regarding client<->browser and server<->browser communication. I'm currently out of the country for a few weeks so I'll make the update when I get back early January.

Please hit me up with any questions or suggestions when you get a chance to check it out. I have a few ideas of my own I might post in here for some community feedback.

Edited by micaww

Share this post


Link to post
Share on other sites
В 17.12.2018 в 02:55, micaww сказал:

Accidentally tagged the wrong person and it wont let me edit the name or delete the post lol. Hey @WizzarD, glad you found my post. I've been actively using and improving this in a few large resources. It has greatly reduced/improved boilerplate code and improved performance. I have a new release ready on my computer at home that comes with some big performance improvements regarding client<->browser and server<->browser communication. I'm currently out of the country for a few weeks so I'll make the update when I get back early January.

Please hit me up with any questions or suggestions when you get a chance to check it out. I have a few ideas of my own I might post in here for some community feedback.

Hey! I love your rpc! Waiting for new update!

Share this post


Link to post
Share on other sites

@micaww i'm facing a problem right now.
I used to follow your examples on github to read a local getter. My code looks like this:
 

// Serverside
mp.events.add("playerReady", async (player) => {
    const accuracy = await rpc.callClient(player, 'getPlayerAccuracy');
    console.log(`default accuracy: '${accuracy}'`);
});

// Clientside
rpc.register('getPlayerAccuracy', () => {
    mp.gui.chat.push(`Sending current accuracy: '${mp.players.local.getAccuracy()}'`);
    return mp.players.local.getAccuracy();
});

I get the message from my clientside-script, but the server never receives it.
What could go wrong?

Share this post


Link to post
Share on other sites

@Jengas Awesome! Glad to know some people are using it. I just got back home last night so once I settle in I'll put out some performance updates.

@WizzarD hey there. I don't see an issue with your code just by looking at it. I made a blank server with just the following code and it worked successfully with the expected output:

Serverside:
pzbZiQs.png

Clientside:
YcwDlLM.png

Console Output:
Hce6PFq.png

As long as rage-rpc is required/imported on both the server and client sometime before you try to use it, it should route the calls correctly. At what step exactly is your issue? Feel free to add me on Discord (micaww#3032) if you want to discuss it on there!

 

Also, to anyone using rage-rpc, I've started a discussion on the GitHub regarding whether the 1.0.0 release should stay as a JS library or be changed into a RAGE resource. I would love to get any input there on what would be the most convenient/make the most sense. Here is a link to the issue: https://github.com/micaww/rage-rpc/issues/1

Share this post


Link to post
Share on other sites
On 11/2/2018 at 9:16 PM, micaww said:

0.1.0

  • ADD: Bundled Typescript definitions
  • IMPROVE: CEF outgoing call returning performance
  • IMPROVE: callBrowsers performance on all contexts
  • FIX: Some code simplifications

Version 0.1.0 has been released via npm.

I've migrated the project to use Typescript and have added TS definitions to the distribution bundle.

Outgoing browser calls used to return to all CEF instances rather than the calling instance. This was due to there being no way to tell which CEF browser an event originates from. I've solved this by asynchronously assigning unique IDs to every browser instance upon creation that rage-rpc can use to identify which browser sends which request. Using that information, we know exactly which browser to send the response to. This increases overall out-bound RPC calls from CEF instances by only sending data to recipients that care about it.

callBrowsers has been rewritten to take advantage of the unique browser IDs mentioned above. The previous callBrowsers implementation would iterate through every instance and ask it if it has the procedure being asked for until it finds the one that has the procedure. That incurs a lot of unnecessary overhead. It's not a good way of doing things, I know. I'm sorry! Now that every browser instance has a unique ID, we can keep track of which browsers register which procedures. When CEF instances register procedures, they tell the client so that the client has a mapping of which instances have which procedures. Now when a callBrowsers request comes through the client, we know exactly which CEF instance to send it to right away. This improves all callBrowsers calls from all contexts, since they all eventually go through the client. Note that this doesn't affect callBrowser, as it will send the event to whatever browser you tell it to, no matter what.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.