refactor: examples transports (#503)

* refactor: examples-transports

* chore: apply suggestions from code review

Co-Authored-By: Jacob Heun <jacobheun@gmail.com>

* chore: address review

* chore: address review

Co-authored-by: Jacob Heun <jacobheun@gmail.com>
This commit is contained in:
Vasco Santos 2020-01-07 14:56:05 +01:00 committed by Jacob Heun
parent 721151b9fc
commit afb552c063
4 changed files with 240 additions and 305 deletions

View File

@ -1,39 +1,33 @@
/* eslint-disable no-console */
'use strict'
const libp2p = require('../../')
const Libp2p = require('../..')
const TCP = require('libp2p-tcp')
const SECIO = require('libp2p-secio')
const PeerInfo = require('peer-info')
const waterfall = require('async/waterfall')
const defaultsDeep = require('@nodeutils/defaults-deep')
class MyBundle extends libp2p {
constructor (_options) {
const defaults = {
modules: {
transport: [
TCP
]
}
const createNode = async (peerInfo) => {
// To signall the addresses we want to be available, we use
// the multiaddr format, a self describable address
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
const node = await Libp2p.create({
peerInfo,
modules: {
transport: [TCP],
connEncryption: [SECIO]
}
})
super(defaultsDeep(_options, defaults))
}
await node.start()
return node
}
let node
waterfall([
(cb) => PeerInfo.create(cb),
(peerInfo, cb) => {
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
node = new MyBundle({ peerInfo: peerInfo })
node.start(cb)
}
], (err) => {
if (err) { throw err }
;(async () => {
const peerInfo = await PeerInfo.create()
const node = await createNode(peerInfo)
console.log('node has started (true/false):', node.isStarted())
console.log('listening on:')
node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString()))
})
})();

View File

@ -1,39 +1,31 @@
/* eslint-disable no-console */
'use strict'
const libp2p = require('../../')
const Libp2p = require('../..')
const TCP = require('libp2p-tcp')
const SECIO = require('libp2p-secio')
const MPLEX = require('libp2p-mplex')
const PeerInfo = require('peer-info')
const waterfall = require('async/waterfall')
const defaultsDeep = require('@nodeutils/defaults-deep')
const parallel = require('async/parallel')
const pull = require('pull-stream')
class MyBundle extends libp2p {
constructor (_options) {
const defaults = {
modules: {
transport: [
TCP
]
}
const pipe = require('it-pipe')
const concat = require('it-concat')
const createNode = async (peerInfo) => {
// To signall the addresses we want to be available, we use
// the multiaddr format, a self describable address
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
const node = await Libp2p.create({
peerInfo,
modules: {
transport: [TCP],
connEncryption: [SECIO],
streamMuxer: [MPLEX]
}
})
super(defaultsDeep(_options, defaults))
}
}
function createNode (callback) {
let node
waterfall([
(cb) => PeerInfo.create(cb),
(peerInfo, cb) => {
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
node = new MyBundle({ peerInfo: peerInfo })
node.start(cb)
}
], (err) => callback(err, node))
await node.start()
return node
}
function printAddrs (node, number) {
@ -41,29 +33,31 @@ function printAddrs (node, number) {
node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString()))
}
parallel([
(cb) => createNode(cb),
(cb) => createNode(cb)
], (err, nodes) => {
if (err) { throw err }
const node1 = nodes[0]
const node2 = nodes[1]
;(async () => {
const [peerInfo1, peerInfo2] = await Promise.all([
PeerInfo.create(),
PeerInfo.create()
])
const [node1, node2] = await Promise.all([
createNode(peerInfo1),
createNode(peerInfo2)
])
printAddrs(node1, '1')
printAddrs(node2, '2')
node2.handle('/print', (protocol, conn) => {
pull(
conn,
pull.map((v) => v.toString()),
pull.log()
node2.handle('/print', async ({ stream }) => {
const result = await pipe(
stream,
concat
)
console.log(result.toString())
})
node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => {
if (err) { throw err }
const { stream } = await node1.dialProtocol(node2.peerInfo, '/print')
pull(pull.values(['Hello', ' ', 'p2p', ' ', 'world', '!']), conn)
})
})
await pipe(
['Hello', ' ', 'p2p', ' ', 'world', '!'],
stream
)
})();

View File

@ -1,70 +1,62 @@
/* eslint-disable no-console */
'use strict'
const libp2p = require('../../')
const Libp2p = require('../..')
const TCP = require('libp2p-tcp')
const WebSockets = require('libp2p-websockets')
const SECIO = require('libp2p-secio')
const MPLEX = require('libp2p-mplex')
const PeerInfo = require('peer-info')
const waterfall = require('async/waterfall')
const defaultsDeep = require('@nodeutils/defaults-deep')
const parallel = require('async/parallel')
const pull = require('pull-stream')
class MyBundle extends libp2p {
constructor (_options) {
const defaults = {
modules: {
transport: [
TCP,
WebSockets
]
}
}
const pipe = require('it-pipe')
super(defaultsDeep(_options, defaults))
}
}
function createNode (addrs, callback) {
if (!Array.isArray(addrs)) {
addrs = [addrs]
const createNode = async (peerInfo, transports, multiaddrs = []) => {
if (!Array.isArray(multiaddrs)) {
multiaddrs = [multiaddrs]
}
let node
multiaddrs.forEach((addr) => peerInfo.multiaddrs.add(addr))
waterfall([
(cb) => PeerInfo.create(cb),
(peerInfo, cb) => {
addrs.forEach((addr) => peerInfo.multiaddrs.add(addr))
node = new MyBundle({ peerInfo: peerInfo })
node.start(cb)
const node = await Libp2p.create({
peerInfo,
modules: {
transport: transports,
connEncryption: [SECIO],
streamMuxer: [MPLEX]
}
], (err) => callback(err, node))
})
await node.start()
return node
}
function printAddrs (node, number) {
function printAddrs(node, number) {
console.log('node %s is listening on:', number)
node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString()))
}
function print (protocol, conn) {
pull(
conn,
pull.map((v) => v.toString()),
pull.log()
function print ({ stream }) {
pipe(
stream,
async function (source) {
for await (const msg of source) {
console.log(msg.toString())
}
}
)
}
parallel([
(cb) => createNode('/ip4/0.0.0.0/tcp/0', cb),
(cb) => createNode(['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws'], cb),
(cb) => createNode('/ip4/127.0.0.1/tcp/20000/ws', cb)
], (err, nodes) => {
if (err) { throw err }
const node1 = nodes[0]
const node2 = nodes[1]
const node3 = nodes[2]
;(async () => {
const [peerInfo1, peerInfo2, peerInfo3] = await Promise.all([
PeerInfo.create(),
PeerInfo.create(),
PeerInfo.create()
])
const [node1, node2, node3] = await Promise.all([
createNode(peerInfo1, [TCP], '/ip4/0.0.0.0/tcp/0'),
createNode(peerInfo2, [TCP, WebSockets], ['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws']),
createNode(peerInfo3, [WebSockets], '/ip4/127.0.0.1/tcp/20000/ws')
])
printAddrs(node1, '1')
printAddrs(node2, '2')
@ -74,21 +66,24 @@ parallel([
node2.handle('/print', print)
node3.handle('/print', print)
node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => {
if (err) { throw err }
// node 1 (TCP) dials to node 2 (TCP+WebSockets)
const { stream } = await node1.dialProtocol(node2.peerInfo, '/print')
await pipe(
['node 1 dialed to node 2 successfully'],
stream
)
pull(pull.values(['node 1 dialed to node 2 successfully']), conn)
})
// node 2 (TCP+WebSockets) dials to node 2 (WebSockets)
const { stream: stream2 } = await node2.dialProtocol(node3.peerInfo, '/print')
await pipe(
['node 2 dialed to node 3 successfully'],
stream2
)
node2.dialProtocol(node3.peerInfo, '/print', (err, conn) => {
if (err) { throw err }
pull(pull.values(['node 2 dialed to node 3 successfully']), conn)
})
node3.dialProtocol(node1.peerInfo, '/print', (err, conn) => {
if (err) {
console.log('node 3 failed to dial to node 1 with:', err.message)
}
})
})
// node 3 (listening WebSockets) can dial node 1 (TCP)
try {
await node3.dialProtocol(node1.peerInfo, '/print')
} catch (err) {
console.log('node 3 failed to dial to node 1 with:', err.message)
}
})();

View File

@ -1,85 +1,65 @@
# [Transports](http://libp2p.io/implementations/#transports)
libp2p doesn't make assumptions for you, instead, it enables you as the developer of the application to pick the modules you need to run your application, which can vary depending on the runtime you are executing. A libp2p node can use one or more Transports to dial and listen for Connections. These transports are modules that offer a clean interface for dialing and listening, defined by the [interface-transport](https://github.com/libp2p/interface-transport) specification. Some examples of possible transports are: TCP, UTP, WebRTC, QUIC, HTTP, Pigeon and so on.
libp2p doesn't make assumptions for you, instead, it enables you as the developer of the application to pick the modules you need to run your application, which can vary depending on the runtime you are executing. A libp2p node can use one or more Transports to dial and listen for Connections. These transports are modules that offer a clean interface for dialing and listening, defined by the [interface-transport](https://github.com/libp2p/js-interfaces/tree/master/src/transport) specification. Some examples of possible transports are: TCP, UTP, WebRTC, QUIC, HTTP, Pigeon and so on.
A more complete definition of what is a transport can be found on the [interface-transport](https://github.com/libp2p/interface-transport) specification. A way to recognize a candidate transport is through the badge:
A more complete definition of what is a transport can be found on the [interface-transport](https://github.com/libp2p/js-interfaces/tree/master/src/transport) specification. A way to recognize a candidate transport is through the badge:
[![](https://raw.githubusercontent.com/diasdavid/interface-transport/master/img/badge.png)](https://raw.githubusercontent.com/diasdavid/interface-transport/master/img/badge.png)
## 1. Creating a libp2p Bundle with TCP
## 1. Creating a libp2p node with TCP
When using libp2p, you always want to create your own libp2p Bundle, that is, pick your set of modules and create your network stack with the properties you need. In this example, we will create a bundle with TCP. You can find the complete solution on the file [1.js](./1.js).
When using libp2p, you need properly configure it, that is, pick your set of modules and create your network stack with the properties you need. In this example, we will create a libp2p node TCP. You can find the complete solution on the file [1.js](./1.js).
You will need 5 deps total, so go ahead and install all of them with:
You will need 4 dependencies total, so go ahead and install all of them with:
```bash
> npm install libp2p libp2p-tcp peer-info async @nodeutils/defaults-deep
> npm install libp2p libp2p-tcp libp2p-secio peer-info
```
Then, on your favorite text editor create a file with the `.js` extension. I've called mine `1.js`.
First thing is to create our own bundle! Insert:
First thing is to create our own libp2p node! Insert:
```JavaScript
'use strict'
const libp2p = require('libp2p')
const Libp2p = require('libp2p')
const TCP = require('libp2p-tcp')
const PeerInfo = require('peer-info')
const waterfall = require('async/waterfall')
const defaultsDeep = require('@nodeutils/defaults-deep')
const SECIO = require('libp2p-secio')
// This MyBundle class is your libp2p bundle packed with TCP
class MyBundle extends libp2p {
constructor (_options) {
const defaults = {
// modules is a JS object that will describe the components
// we want for our libp2p bundle
modules: {
transport: [
TCP
]
}
const createNode = async (peerInfo) => {
// To signall the addresses we want to be available, we use
// the multiaddr format, a self describable address
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
const node = await Libp2p.create({
peerInfo,
modules: {
transport: [ TCP ],
connEncryption: [ SECIO ]
}
})
super(defaultsDeep(_options, defaults))
}
await node.start()
return node
}
```
Now that we have our own MyBundle class that extends libp2p, let's create a node with it. We will use `async/waterfall` just for code structure, but you don't need to. Append to the same file:
Now that we have a function to create our own libp2p node, let's create a node with it.
```JavaScript
let node
const peerInfo = await PeerInfo.create()
const node = await createNode(peerInfo)
waterfall([
// First we create a PeerInfo object, which will pack the
// info about our peer. Creating a PeerInfo is an async
// operation because we use the WebCrypto API
// (yeei Universal JS)
(cb) => PeerInfo.create(cb),
(peerInfo, cb) => {
// To signall the addresses we want to be available, we use
// the multiaddr format, a self describable address
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
// Now we can create a node with that PeerInfo object
node = new MyBundle({ peerInfo: peerInfo })
// Last, we start the node!
node.start(cb)
}
], (err) => {
if (err) { throw err }
// At this point the node has started
console.log('node has started (true/false):', node.isStarted())
// And we can print the now listening addresses.
// If you are familiar with TCP, you might have noticed
// that we specified the node to listen in 0.0.0.0 and port
// 0, which means "listen in any network interface and pick
// a port for me
console.log('listening on:')
node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString()))
})
// At this point the node has started
console.log('node has started (true/false):', node.isStarted())
// And we can print the now listening addresses.
// If you are familiar with TCP, you might have noticed
// that we specified the node to listen in 0.0.0.0 and port
// 0, which means "listen in any network interface and pick
// a port for me
console.log('listening on:')
node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString()))
```
Running this should result in something like:
@ -96,78 +76,64 @@ That `QmW2cKTakTYqbQkUzBTEGXgWYFj1YEPeUndE1YWs6CBzDQ` is the PeerId that was cre
## 2. Dialing from one node to another node
Now that we have our bundle, let's create two nodes and make them dial to each other! You can find the complete solution at [2.js](./2.js).
Now that we have our `createNode` function, let's create two nodes and make them dial to each other! You can find the complete solution at [2.js](./2.js).
For this step, we will need one more dependency.
```bash
> npm install pull-stream
> npm install it-pipe it-buffer
```
And we also need to import the module on our .js file:
```js
const pull = require('pull-stream')
const pipe = require('it-pipe')
const { toBuffer } = require('it-buffer')
```
We are going to reuse the MyBundle class from step 1, but this time to make things simpler, we will create two functions, one to create nodes and another to print the addrs to avoid duplicating code.
We are going to reuse the `createNode` function from step 1, but this time to make things simpler, we will create another function to print the addrs to avoid duplicating code.
```JavaScript
function createNode (callback) {
let node
waterfall([
(cb) => PeerInfo.create(cb),
(peerInfo, cb) => {
peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
node = new MyBundle({ peerInfo: peerInfo })
node.start(cb)
}
], (err) => callback(err, node))
}
function printAddrs (node, number) {
console.log('node %s is listening on:', number)
node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString()))
}
```
Now we are going to use `async/parallel` to create two nodes, print their addresses and dial from one node to the other. We already added `async` as a dependency, but still need to import `async/parallel`:
```js
const parallel = require('async/parallel')
```
Then,
```js
parallel([
(cb) => createNode(cb),
(cb) => createNode(cb)
], (err, nodes) => {
if (err) { throw err }
const node1 = nodes[0]
const node2 = nodes[1]
;(async () => {
const [peerInfo1, peerInfo2] = await Promise.all([
PeerInfo.create(),
PeerInfo.create()
])
const [node1, node2] = await Promise.all([
createNode(),
createNode()
])
printAddrs(node1, '1')
printAddrs(node2, '2')
node2.handle('/print', (protocol, conn) => {
pull(
conn,
pull.map((v) => v.toString()),
pull.log()
node2.handle('/print', ({ stream }) => {
pipe(
stream,
async function (source) {
for await (const msg of source) {
console.log(msg.toString())
}
}
)
})
node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => {
if (err) { throw err }
const { stream } = await node1.dialProtocol(node2.peerInfo, '/print')
pull(pull.values(['Hello', ' ', 'p2p', ' ', 'world', '!']), conn)
})
})
await pipe(
['Hello', ' ', 'p2p', ' ', 'world', '!'],
stream
)
})();
```
The result should be look like:
@ -195,46 +161,28 @@ In this example, we will need to also install `libp2p-websockets`, go ahead and
> npm install libp2p-websockets
```
We want to create 3 nodes, one with TCP, one with TCP+WebSockets and one with just WebSockets. We need to update our `MyBundle` class to contemplate WebSockets as well:
We want to create 3 nodes, one with TCP, one with TCP+WebSockets and one with just WebSockets. We need to update our `createNode` function to contemplate WebSockets as well. Moreover, let's upgrade our function to enable us to pick the addrs in which a node will start a listener:
```JavaScript
const WebSockets = require('libp2p-websockets')
// ...
class MyBundle extends libp2p {
constructor (_options) {
const defaults = {
modules: {
transport: [
TCP,
WebSockets
]
}
}
super(defaultsDeep(_options, defaults))
}
}
```
Now that we have our bundle ready, let's upgrade our createNode function to enable us to pick the addrs in which a node will start a listener.
```JavaScript
function createNode (addrs, callback) {
if (!Array.isArray(addrs)) {
addrs = [addrs]
const createNode = async (peerInfo, transports, multiaddrs = []) => {
if (!Array.isArray(multiaddrs)) {
multiaddrs = [multiaddrs]
}
let node
multiaddrs.forEach((addr) => peerInfo.multiaddrs.add(addr))
waterfall([
(cb) => PeerInfo.create(cb),
(peerInfo, cb) => {
addrs.forEach((addr) => peerInfo.multiaddrs.add(addr))
node = new MyBundle({ peerInfo: peerInfo })
node.start(cb)
const node = await Libp2p.create({
peerInfo,
modules: {
transport: transports,
connEncryption: [ SECIO ]
}
], (err) => callback(err, node))
})
await node.start()
return node
}
```
@ -243,58 +191,61 @@ As a rule, a libp2p node will only be capable of using a transport if: a) it has
Let's update our flow to create nodes and see how they behave when dialing to each other:
```JavaScript
parallel([
(cb) => createNode('/ip4/0.0.0.0/tcp/0', cb),
// Here we add an extra multiaddr that has a /ws at the end, this means that we want
// to create a TCP socket, but mount it as WebSockets instead.
(cb) => createNode(['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws'], cb),
(cb) => createNode('/ip4/127.0.0.1/tcp/20000/ws', cb)
], (err, nodes) => {
if (err) { throw err }
const WebSockets = require('libp2p-websockets')
const TCP = require('libp2p-tcp')
const node1 = nodes[0]
const node2 = nodes[1]
const node3 = nodes[2]
const [peerInfo1, peerInfo2, peerInfo3] = await Promise.all([
PeerInfo.create(),
PeerInfo.create(),
PeerInfo.create()
])
const [node1, node2, node3] = await Promise.all([
createNode(peerInfo1, [TCP], '/ip4/0.0.0.0/tcp/0'),
createNode(peerInfo2, [TCP, WebSockets], ['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws']),
createNode(peerInfo3, [WebSockets], '/ip4/127.0.0.1/tcp/20000/ws')
])
printAddrs(node1, '1')
printAddrs(node2, '2')
printAddrs(node3, '3')
printAddrs(node1, '1')
printAddrs(node2, '2')
printAddrs(node3, '3')
node1.handle('/print', print)
node2.handle('/print', print)
node3.handle('/print', print)
node1.handle('/print', print)
node2.handle('/print', print)
node3.handle('/print', print)
// node 1 (TCP) dials to node 2 (TCP+WebSockets)
node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => {
if (err) { throw err }
// node 1 (TCP) dials to node 2 (TCP+WebSockets)
const { stream } = await node1.dialProtocol(node2.peerInfo, '/print')
await pipe(
['node 1 dialed to node 2 successfully'],
stream
)
pull(pull.values(['node 1 dialed to node 2 successfully']), conn)
})
// node 2 (TCP+WebSockets) dials to node 2 (WebSockets)
const { stream: stream2 } = await node2.dialProtocol(node3.peerInfo, '/print')
await pipe(
['node 2 dialed to node 3 successfully'],
stream2
)
// node 2 (TCP+WebSockets) dials to node 2 (WebSockets)
node2.dialProtocol(node3.peerInfo, '/print', (err, conn) => {
if (err) { throw err }
pull(pull.values(['node 2 dialed to node 3 successfully']), conn)
})
// node 3 (WebSockets) attempts to dial to node 1 (TCP)
node3.dialProtocol(node1.peerInfo, '/print', (err, conn) => {
if (err) {
console.log('node 3 failed to dial to node 1 with:', err.message)
}
})
})
// node 3 (WebSockets) attempts to dial to node 1 (TCP)
try {
await node3.dialProtocol(node1.peerInfo, '/print')
} catch (err) {
console.log('node 3 failed to dial to node 1 with:', err.message)
}
```
`print` is a function created using the code from 2.js, but factored into its own function to save lines, here it is:
```JavaScript
function print (protocol, conn) {
pull(
conn,
pull.map((v) => v.toString()),
pull.log()
function print ({ stream }) {
pipe(
stream,
async function (source) {
for await (const msg of source) {
console.log(msg.toString())
}
}
)
}
```
@ -312,18 +263,19 @@ node 2 is listening on:
/ip4/192.168.2.156/tcp/62619/p2p/QmWAQtWdzWXibgfyc7WRHhhv6MdqVKzXvyfSTnN2aAvixX
node 3 is listening on:
/ip4/127.0.0.1/tcp/20000/ws/p2p/QmVq1PWh3VSDYdFqYMtqp4YQyXcrH27N7968tGdM1VQPj1
node 3 failed to dial to node 1 with: No available transport to dial to
node 1 dialed to node 2 successfully
node 2 dialed to node 3 successfully
node 3 failed to dial to node 1 with:
Error: No transport available for address /ip4/127.0.0.1/tcp/51482
```
As expected, we created 3 nodes, node 1 with TCP, node 2 with TCP+WebSockets and node 3 with just WebSockets. node 1 -> node 2 and node 2 -> node 3 managed to dial correctly because they shared a common transport, however, node 3 -> node 1 failed because they didn't share any.
## 4. How to create a new libp2p transport
Today there are already 3 transports available, one in the works and plenty to come, you can find these at [interface-transport implementations](https://github.com/libp2p/interface-transport#modules-that-implement-the-interface) list.
Today there are already several transports available and plenty to come, you can find these at [interface-transport implementations](https://github.com/libp2p/js-interfaces/tree/master/src/transport#modules-that-implement-the-interface) list.
Adding more transports is done through the same way as you added TCP and WebSockets. Some transports might offer extra functionalities, but as far as libp2p is concerned, if it follows the interface defined at the [spec](https://github.com/libp2p/interface-transport#api) it will be able to use it.
Adding more transports is done through the same way as you added TCP and WebSockets. Some transports might offer extra functionalities, but as far as libp2p is concerned, if it follows the interface defined at the [spec](https://github.com/libp2p/js-interfaces/tree/master/src/transport#api) it will be able to use it.
If you decide to implement a transport yourself, please consider adding to the list so that others can use it as well.