JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Remote Procedure Calls: Making the Network Invisible

Guide 1 showed why a distributed system is so hard: no shared memory, no shared clock, messages that vanish. This guide is about the most influential idea for hiding that mess — let one machine call a function on another machine as if it were a normal local call. We will see exactly how stubs and marshalling pull off the illusion, and, just as importantly, where the illusion leaks.

From sending messages to calling functions

After guide 1 you know the raw tool for talking across machines: message passing over the network — pack some bytes, send them, hope they arrive, wait for a reply. That works, but it is miserable to program with. You spend your day deciding byte layouts, matching every request to its reply, handling the case where nothing comes back, and retrying. None of that is your actual problem; it is plumbing that drowns the one line of logic you cared about, namely 'ask the other machine to do X and give me the answer.'

So someone asked a wonderful question: what is the most familiar way a programmer already asks for something to be done? A function call. You write result = add(3, 4), control jumps somewhere, the work happens, an answer comes back, and you continue. What if calling a function on a remote machine could look exactly the same? That is the remote procedure call, or RPC: you call what looks like an ordinary local function, but the body actually runs on another computer, and the network round trip is hidden inside the call.

This is why RPC sits at the heart of so many systems. The Network File System you will meet in guide 3 is built on it; so is much of what runs inside a network operating system and the client-server model generally. The promise is intoxicating: write distributed code that reads like ordinary single-machine code. Most of this guide is how that promise is kept — and the last part is the honest fine print about how it is only ever half-kept.

The two stubs: where the magic actually lives

The illusion is carried by two small pieces of generated code called stubs — one on each side. Think of them as two perfectly synchronized translators sitting in two embassies. When you call the remote function, you are not really calling it; you are calling the client stub, a local function with the exact same name and shape. To you it is indistinguishable from the real thing. Its entire job is to take your arguments, turn them into a message, hand that to the network, wait, and turn the reply back into a return value.

On the far machine sits the matching half, the server stub (sometimes called the skeleton). It is the mirror image: it receives the message off the network, unpacks the arguments back into ordinary values, and then makes a perfectly normal local call to the real function — the one that actually adds the numbers, reads the file, whatever. When that function returns, the server stub packs up the result and ships it home. The real function never knows it was called from across the world; the calling code never knows its function lived across the world. The two stubs absorb the entire difference.

Notice what the stubs really are: they are a thin, private use of message passing dressed up to look like a local call. Everything from guide 1 is still underneath — bytes really do cross a wire, the reply really can be late or lost — but all of that is now sealed inside two functions you never have to read. That is the whole trick. The complexity did not disappear; it got moved into generated code so your business logic could stay clean.

Marshalling: flattening data for the wire

Now zoom in on the hardest part of the stub's job. Inside one machine, an argument like a number or a list lives at some memory address, in that machine's own byte order and word size. You cannot just copy those raw bytes to another machine — the other machine may order its bytes differently, size its integers differently, and certainly has no memory address that means the same thing. So the client stub must marshal the arguments: convert them into a flat, self-describing stream of bytes that any machine can read. Marshalling is the packing; un-marshalling on the other side is the unpacking.

A vivid way to feel marshalling: imagine mailing a piece of furniture. You cannot post an assembled bookshelf, so you flat-pack it into a box with a parts list and instructions; the person at the other end rebuilds it from the box alone, with no reference to your room. Marshalling flat-packs your data; un-marshalling rebuilds it. The agreed format for the box — the byte layout both sides promise to use — is the wire format, and it is what lets a machine running one language and CPU talk to a machine running a totally different one.

Tracing one RPC end to end

Let us walk a single remote call, result = add(3, 4), all the way across and back, where add really runs on a server. Keep your eye on one thing as we go: from the caller's point of view this is just one line, but underneath there are ten distinct hops, and any one of them can be slow or fail.

  1. The caller executes add(3, 4). Control jumps into the client stub, which looks exactly like add but is not.
  2. The client stub marshals the arguments: it flat-packs the values 3 and 4, plus a tag saying which function to run, into one self-describing message.
  3. The client stub hands the message to the OS, which sends it across the network to the server. (This is the part that can be slow, or arrive twice, or never arrive.)
  4. The caller's thread now blocks, waiting for a reply — exactly as a normal function call waits for its body to finish, except the wait may be very long.
  5. On the server, the OS receives the message and wakes the server stub.
  6. The server stub un-marshals the message back into ordinary values 3 and 4, and reads the tag to learn which function is wanted.
  7. The server stub makes a perfectly normal local call: add(3, 4). The real add runs and returns 7. It has no idea a network was involved.
  8. The server stub marshals the result 7 into a reply message and hands it to the server's OS to send back.
  9. The reply crosses the network and reaches the client's OS, which wakes the blocked caller's thread.
  10. The client stub un-marshals the reply into the value 7 and returns it. Back in the caller, the line result = add(3, 4) finally completes with result holding 7 — looking, for all the world, like a single local call.
  CLIENT machine                    SERVER machine
  --------------                    --------------
  caller code                       real add()
     |  add(3,4)                       ^  returns 7
     v                                 |  add(3,4)
  [client stub] --marshal--+        [server stub]
     ^                      |           ^   |
     |  return 7            |  message  |   | reply
     |                      v   3,4      |   v   7
  [  OS  ] ----------- network ----- [  OS  ]

  One line of caller code  =  10 hops underneath
The round trip of add(3, 4). The two stubs and the network sit between the caller and the real function; the caller sees only one line.

Where the illusion leaks (be honest)

Here is the crucial honesty this rung demands: a remote call can never be truly identical to a local one, and pretending otherwise is how distributed systems get hurt. There is a famous list of exactly these traps, the fallacies of distributed computing: the network is reliable, latency is zero, bandwidth is infinite, the network is secure, and more. Every one is false, and a local call hides all of them. RPC makes a remote call look local, which can quietly tempt you to assume those falsehoods.

Two leaks matter most. First, latency: a local call costs nanoseconds; a remote call costs milliseconds, perhaps a million times more. Code that loops calling a 'function' a thousand times is fine locally and a disaster remotely. Second, and worst, partial failure — the very thing guide 1 warned about. A local call either runs or your whole program crashes; there is no in-between. But a remote call can leave you with no reply and no way to know what happened: did the request never arrive, or did it run and only the reply get lost? With add that hardly matters. With charge_credit_card it matters enormously.

So hold both truths at once. RPC is a triumph: it lets you build a client-server system in language that reads like ordinary code, and it underpins the distributed file systems of guide 3. But the network it hides is still really there, and the leaks — latency and partial failure — are exactly the dragons guide 1 introduced. The mature view is not 'RPC makes the network go away'; it is 'RPC makes the network easy to use, as long as you remember it never truly went away.'