This article shows how to get started with NDNts, Named Data Networking (NDN) libraries for the modern web. In particular, it demonstrates how to write a producer and a consumer application in Node.js using JavaScript programming language, and transmit a few Interest and Data packets via NFD forwarder on the local machine.
Code samples in this article were last updated on 2024-03-06 to reflect latest changes.
Prepare the System
This guide is written for Ubuntu 22.04 operating system. If you have a Windows PC, you can enable Windows Subsystem for Linux and install Ubuntu 22.04 from the Microsoft Store. If you have a Macbook or a Linux machine other than Ubuntu 22.04, you can install Vagrant, and create a virtual machine from bento/ubuntu-22.04 template. All steps below should be executed inside Ubuntu 22.04 environment.
To use NDNts, you must have Node.js. As of this writing, NDNts works best with Node.js 20.x, and you should install that version. The easiest way to install Node.js is through Node Version Manager (nvm). To install nvm and then install Node.js, type the following commands in Ubuntu 22.04 terminal:
$ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
$ nvm install 20 --latest-npm
Now using node v20.11.1 (npm v10.2.4)
You also need the NDN Forwarding Daemon (NFD) and ndnpeek
program.
They can be installed with the following commands:
$ echo "deb [arch=amd64 trusted=yes] https://nfd-nightly-apt.ndn.today/ubuntu jammy main" \
| sudo tee /etc/apt/sources.list.d/nfd-nightly.list
$ sudo apt install nfd ndnpeek
Now create a directory for our new project, and then open a code editor such as Visual Studio Code at this directory.
If you have installed Ubuntu 22.04 from Microsoft Store, you can type explorer.exe .
at Ubuntu 22.04 terminal to open Windows Explorer, and then right click to select Open with Code
.
If you have installed Ubuntu 22.04 as a Vagrant virtual machine, you can create the project directory under /vagrant
, and the directory should show up alongside where you placed the Vagrantfile
.
Install NDNts
NDNts is a group of Node.js packages. Although they have been published to NPM registry, NPM releases happen rather infrequently. Thus, it's best to install nightly builds to obtain the latest features.
To install NDNts nightly builds, create a file package.json in the project directory, and then paste the following content:
{
"private": true,
"packageManager": "pnpm@8.15.4",
"dependencies": {
"@ndn/cli-common": "https://ndnts-nightly.ndn.today/cli-common.tgz",
"@ndn/endpoint": "https://ndnts-nightly.ndn.today/endpoint.tgz",
"@ndn/packet": "https://ndnts-nightly.ndn.today/packet.tgz"
}
}
Then, type corepack pnpm install
at the terminal.
A minute later, all packages and their dependencies are installed automatically.
The Producer
Create a file producer.mjs and paste the following content:
import { openUplinks } from "@ndn/cli-common";
import { produce } from "@ndn/endpoint";
import { Data } from "@ndn/packet";
// openUplinks() creates a connection to the "uplink", in this case the local NFD forwarder.
// It returns a Promise, so remember to await it.
await openUplinks();
// produce() creates a producer.
// The first argument is the name prefix.
// The second argument is a callback function that is invoked for each incoming Interest;
// this must be an async function that returns a Promise.
produce("/add", async (interest) => {
console.log(`Got Interest ${interest.name}`);
// This producer is a calculator. It expects Interest name to have three
// components: "add", x, and y. If it's not, reject the Interest.
if (interest.name.length !== 3) {
console.log("Wrong name length.");
return;
}
// Extract x and y numbers, then compute the sum.
const x = Number.parseInt(interest.name.at(1).text, 10);
const y = Number.parseInt(interest.name.at(2).text, 10);
const sum = x + y;
console.log(`${x} + ${y} = ${sum}`);
// Make a Data packet that has the same name as the Interest.
const data = new Data(interest.name);
data.freshnessPeriod = 1000;
data.content = new TextEncoder().encode(`${sum}\n`);
// Sending the Data is as simple as returning it from the function.
return data;
},
// options
);
console.log("Producer running, press CTRL+C to stop");
Run this script:
$ node ./producer.mjs
Producer running, press CTRL+C to stop
Open another terminal window, and run ndnpeek
(a simple consumer):
$ ndnpeek -fp /add/12345678/87654321
99999999
It prints the Data payload, which is the sum of the two numbers passed into the name.
The Consumer
Create a file consumer.mjs and paste the following content:
import { closeUplinks, openUplinks } from "@ndn/cli-common";
import { consume } from "@ndn/endpoint";
import { Interest } from "@ndn/packet";
// Connect to NFD.
await openUplinks();
// Parse x and y from command line arguments.
const [x, y] = process.argv.slice(2);
// Make an Interest packet, asking the producer to compute x+y.
const interest = new Interest(`/add/${x}/${y}`);
interest.mustBeFresh = true;
try {
// Send the Interest, and wait for Data to come back.
const data = await consume(interest);
// Print the Data payload.
process.stdout.write(data.content);
} catch (err) {
// In case of Data retrieval failure, show what went wrong.
console.warn(err);
}
// Disconnect from NFD, so that Node.js can exit normally.
closeUplinks();
Keep the producer running in the first terminal window. In the second terminal, run this consumer script, sending an Interest for 1 + 1:
$ node ./consumer.mjs 1 1
2
This is how you compute 1 + 1 using NDN.
The above two scripts use the @ndn/endpoint package, to create simple producer and consumer. The @ndn/endpoint package is a centerpiece of NDNts. It is similar to, but more powerful than, "face" in other NDN libraries. You'll soon see some of its powers.
Consumer Retransmissions
Now let me show you one of the powers in @ndn/endpoint: consumer retransmissions. As you may already know, NDN's communication model is receiver-driver: the network provides a best-effort service, while the consumer is ultimately responsible for retransmitting its Interests if the Data does not arrive. In other libraries, your application may have to deal with retransmissions yourself, while in NDNts, retransmissions can be enabled with a single flag.
Go to the first terminal window, press CTRL+C to stop the producer. Then, run the consumer again, asking for 1 + 2:
$ node ./consumer.mjs 1 2
Error: Interest rejected: expire @consume(/8=add/8=1/8=2)
You got an error: the Interest has been rejected, because the producer isn't running.
To enable retransmissions, change consume(interest)
line of the consumer script as:
const data = await consume(interest, { retx: 100 });
This allows NDNts to retransmit the Interest for up to 100 times, if the initial Interest does not receive a reply.
After this modification, run the consumer.
This time, add NDNTS_PKTTRACE=1
environment variable, so that you can see the packet exchanges.
$ NDNTS_PKTTRACE=1 node ./consumer.mjs 1 2
consume(/8=add/8=1/8=2) >I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) <I /8=add/8=1/8=2[F]
consume(/8=add/8=1/8=2) >I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) <I /8=add/8=1/8=2[F]
consume(/8=add/8=1/8=2) >I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) <I /8=add/8=1/8=2[F]
consume(/8=add/8=1/8=2) >I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) <I /8=add/8=1/8=2[F]
As you can see from the log messages, the consumer does not fail right away, but keeps retransmitting the Interest. While the consumer is still running, go to the first terminal window and start the producer again. After that, the consumer should receive the Data and exit.
$ NDNTS_PKTTRACE=1 node ./consumer.mjs 1 2
+Face Unix(/run/nfd.sock)
Unix(/run/nfd.sock) +Prefix /
+Face consume(/8=add/8=1/8=2)
consume(/8=add/8=1/8=2) >I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) <I /8=add/8=1/8=2[F]
consume(/8=add/8=1/8=2) >I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) <I /8=add/8=1/8=2[F]
Unix(/run/nfd.sock) >D /8=add/8=1/8=2
consume(/8=add/8=1/8=2) <D /8=add/8=1/8=2
3
-Face Unix(/run/nfd.sock)
-Face consume(/8=add/8=1/8=2)
A word on /8=add/8=1/8=2
: this is the same name as /add/1/2
, written in canonical format.
NDN Packet Format v0.3 introduces typed name component, which allows a name component to have a type number between 1 and 65535.
When a name is written in its canonical format, the type number appears in each component.
NDNts by default prints names in the canonical format, although the AltUriFormat
class can be used to print in the alternate format such as /add/1/2
.
The Name
constructor, however, only accepts canonical format, with a special case that allows omitting type number "8".
Producer Parallelism
Another powerful feature of @ndn/endpoint, on the producer side, is controlling parallelism of the producer callback function.
To demonstrate this effect, we'd make the "x + y" function a bit slower.
In the producer script, insert this line just above return data;
:
// Simulate 100 milliseconds processing delay.
await new Promise((r) => setTimeout(r, 100));
We also need a consumer that sends many requests at once, bulk.js:
import { closeUplinks, openUplinks } from "@ndn/cli-common";
import { consume } from "@ndn/endpoint";
import { Interest } from "@ndn/packet";
await openUplinks();
const interests = [];
for (let i = 0; i < 100; ++i) {
const x = Math.trunc(Math.random() * 1000000);
const y = Math.trunc(Math.random() * 1000000);
const interest = new Interest(`/add/${x}/${y}`);
interest.mustBeFresh = true;
interests.push(interest);
}
const t0 = Date.now();
const settled = await Promise.allSettled(
interests.map((interest) => consume(interest, { retx: 5 })),
);
const t1 = Date.now();
const nFulfilled = settled.filter(({ status }) => status === "fulfilled").length;
console.log(`${nFulfilled} fulfilled in ${t1 - t0}ms`);
closeUplinks();
Start the producer in the first terminal, and run node ./bulk.js
in the second terminal.
You would see something like:
$ node ./bulk.mjs
100 fulfilled in 10524ms
All 100 requests were successful, and they took a total of 10.5 seconds.
Now we modify the producer script, replace // options
line near the end with:
{ concurrency: 10 }
This tells endpoint to invoke at most 10 instances of the producer callback function in parallel.
Run the bulk consumer again:
$ node ./bulk.mjs
100 fulfilled in 1098ms
Much faster: 100 requests took a total of 1.1 seconds.
As of this writing, this feature is unique to NDNts. Other NDN libraries either let the application process one Interest at a time, or pass all requests to the application callback and you have to handle parallelism yourself.
Conclusion
This article is a getting started guide of NDNts libraries in Node.js environment. If you followed along, you have installed NDNts in Node.js 20.x, and learned how to write a producer and consumer using the @ndn/endpoint package. You also witnessed two powerful features of this package.
Source code in this article can be downloaded from NDNts-starter repository.