Add examples to CI (#599)
* add examples to CI * add markdown runner * two tutorials
This commit is contained in:
parent
e1d96a0f4d
commit
e787fc35a6
|
@ -1,98 +1,7 @@
|
|||
# Getting Started
|
||||
Welcome to nim-libp2p! This guide will walk you through a peer to peer chat example. <br>
|
||||
The full code can be found in [directchat.nim](examples/directchat.nim) under the examples folder.
|
||||
Welcome to nim-libp2p!
|
||||
|
||||
|
||||
### Direct Chat Example
|
||||
To run nim-libp2p, add it to your project's nimble file and spawn a node as follows:
|
||||
To get started, please look at the [tutorials](../examples/tutorial_1_connect.md)
|
||||
|
||||
```nim
|
||||
import tables
|
||||
import chronos
|
||||
import ../libp2p/[switch,
|
||||
multistream,
|
||||
protocols/identify,
|
||||
connection,
|
||||
transports/transport,
|
||||
transports/tcptransport,
|
||||
multiaddress,
|
||||
peerinfo,
|
||||
crypto/crypto,
|
||||
peerid,
|
||||
protocols/protocol,
|
||||
muxers/muxer,
|
||||
muxers/mplex/mplex,
|
||||
protocols/secure/secio,
|
||||
protocols/secure/secure]
|
||||
|
||||
const TestCodec = "/test/proto/1.0.0" # custom protocol string
|
||||
|
||||
type
|
||||
TestProto = ref object of LPProtocol # declare a custom protocol
|
||||
|
||||
method init(p: TestProto) {.gcsafe.} =
|
||||
# handle incoming connections in closure
|
||||
proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
|
||||
echo "Got from remote - ", cast[string](await conn.readLp(1024))
|
||||
await conn.writeLp("Hello!")
|
||||
await conn.close()
|
||||
|
||||
p.codec = TestCodec # init proto with the correct string id
|
||||
p.handler = handle # set proto handler
|
||||
|
||||
proc createSwitch(ma: MultiAddress): (Switch, PeerInfo) =
|
||||
## Helper to create a swith
|
||||
|
||||
let seckey = PrivateKey.random(RSA) # use a random key for peer id
|
||||
var peerInfo = PeerInfo.init(seckey) # create a peer id and assign
|
||||
peerInfo.addrs.add(ma) # set this peer's multiaddresses (can be any number)
|
||||
|
||||
let identify = newIdentify(peerInfo) # create the identify proto
|
||||
|
||||
proc createMplex(conn: Connection): Muxer =
|
||||
# helper proc to create multiplexers,
|
||||
# use this to perform any custom setup up,
|
||||
# such as adjusting timeout or anything else
|
||||
# that the muxer requires
|
||||
result = newMplex(conn)
|
||||
|
||||
let mplexProvider = newMuxerProvider(createMplex, MplexCodec) # create multiplexer
|
||||
let transports = @[Transport(newTransport(TcpTransport))] # add all transports (tcp only for now, but can be anything in the future)
|
||||
let muxers = {MplexCodec: mplexProvider}.toTable() # add all muxers
|
||||
let secureManagers = {SecioCodec: Secure(Secio.new(seckey))}.toTable() # setup the secio and any other secure provider
|
||||
|
||||
# create the switch
|
||||
let switch = newSwitch(peerInfo,
|
||||
transports,
|
||||
identify,
|
||||
muxers,
|
||||
secureManagers)
|
||||
result = (switch, peerInfo)
|
||||
|
||||
proc main() {.async, gcsafe.} =
|
||||
let ma1: MultiAddress = Multiaddress.init("/ip4/0.0.0.0/tcp/0")
|
||||
let ma2: MultiAddress = Multiaddress.init("/ip4/0.0.0.0/tcp/0")
|
||||
|
||||
var peerInfo1, peerInfo2: PeerInfo
|
||||
var switch1, switch2: Switch
|
||||
(switch1, peerInfo1) = createSwitch(ma1) # create node 1
|
||||
|
||||
# setup the custom proto
|
||||
let testProto = new TestProto
|
||||
testProto.init() # run it's init method to perform any required initialization
|
||||
switch1.mount(testProto) # mount the proto
|
||||
var switch1Fut = await switch1.start() # start the node
|
||||
|
||||
(switch2, peerInfo2) = createSwitch(ma2) # create node 2
|
||||
var switch2Fut = await switch2.start() # start second node
|
||||
let conn = await switch2.dial(switch1.peerInfo, TestCodec) # dial the first node
|
||||
|
||||
await conn.writeLp("Hello!") # writeLp send a length prefixed buffer over the wire
|
||||
# readLp reads length prefixed bytes and returns a buffer without the prefix
|
||||
echo "Remote responded with - ", cast[string](await conn.readLp(1024))
|
||||
|
||||
await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
|
||||
await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown
|
||||
|
||||
waitFor(main())
|
||||
```
|
||||
For more concrete examples, you can look at the [hello world example](../examples/helloworld.nim) or the [direct chat](../examples/directchat.nim)
|
||||
|
|
|
@ -1,54 +1,88 @@
|
|||
when not(compileOption("threads")):
|
||||
{.fatal: "Please, compile this program with the --threads:on option!".}
|
||||
|
||||
import tables, strformat, strutils, bearssl
|
||||
import chronos # an efficient library for async
|
||||
import ../libp2p/[switch, # manage transports, a single entry point for dialing and listening
|
||||
builders, # helper to build the switch object
|
||||
multistream, # tag stream with short header to identify it
|
||||
multicodec, # multicodec utilities
|
||||
crypto/crypto, # cryptographic functions
|
||||
errors, # error handling utilities
|
||||
protocols/identify, # identify the peer info of a peer
|
||||
stream/connection, # create and close stream read / write connections
|
||||
transports/transport, # listen and dial to other peers using p2p protocol
|
||||
transports/tcptransport, # listen and dial to other peers using client-server protocol
|
||||
multiaddress, # encode different addressing schemes. For example, /ip4/7.7.7.7/tcp/6543 means it is using IPv4 protocol and TCP
|
||||
peerinfo, # manage the information of a peer, such as peer ID and public / private key
|
||||
peerid, # Implement how peers interact
|
||||
protocols/protocol, # define the protocol base type
|
||||
protocols/secure/secure, # define the protocol of secure connection
|
||||
protocols/secure/secio, # define the protocol of secure input / output, allows encrypted communication that uses public keys to validate signed messages instead of a certificate authority like in TLS
|
||||
muxers/muxer, # define an interface for stream multiplexing, allowing peers to offer many protocols over a single connection
|
||||
muxers/mplex/mplex] # define some contants and message types for stream multiplexing
|
||||
import
|
||||
strformat, strutils, bearssl,
|
||||
stew/byteutils,
|
||||
chronos,
|
||||
../libp2p
|
||||
|
||||
const ChatCodec = "/nim-libp2p/chat/1.0.0"
|
||||
const DefaultAddr = "/ip4/127.0.0.1/tcp/55505"
|
||||
const DefaultAddr = "/ip4/127.0.0.1/tcp/0"
|
||||
|
||||
const Help = """
|
||||
Commands: /[?|hep|connect|disconnect|exit]
|
||||
Commands: /[?|help|connect|disconnect|exit]
|
||||
help: Prints this help
|
||||
connect: dials a remote peer
|
||||
disconnect: ends current session
|
||||
exit: closes the chat
|
||||
"""
|
||||
|
||||
type ChatProto = ref object of LPProtocol
|
||||
switch: Switch # a single entry point for dialing and listening to peer
|
||||
transp: StreamTransport # transport streams between read & write file descriptor
|
||||
conn: Connection # create and close read & write stream
|
||||
connected: bool # if the node is connected to another peer
|
||||
started: bool # if the node has started
|
||||
type
|
||||
Chat = ref object
|
||||
switch: Switch # a single entry point for dialing and listening to peer
|
||||
stdinReader: StreamTransport # transport streams between read & write file descriptor
|
||||
conn: Connection # connection to the other peer
|
||||
connected: bool # if the node is connected to another peer
|
||||
|
||||
proc readAndPrint(p: ChatProto) {.async.} =
|
||||
while true:
|
||||
var strData = await p.conn.readLp(1024)
|
||||
strData &= '\0'.uint8
|
||||
var str = cast[cstring](addr strdata[0])
|
||||
echo $p.switch.peerInfo.peerId & ": " & $str
|
||||
await sleepAsync(100.millis)
|
||||
##
|
||||
# Stdout helpers, to write the prompt
|
||||
##
|
||||
proc writePrompt(c: Chat) =
|
||||
if c.connected:
|
||||
stdout.write '\r' & $c.switch.peerInfo.peerId & ": "
|
||||
stdout.flushFile()
|
||||
|
||||
proc dialPeer(p: ChatProto, address: string) {.async.} =
|
||||
proc writeStdout(c: Chat, str: string) =
|
||||
echo '\r' & str
|
||||
c.writePrompt()
|
||||
|
||||
##
|
||||
# Chat Protocol
|
||||
##
|
||||
const ChatCodec = "/nim-libp2p/chat/1.0.0"
|
||||
|
||||
type
|
||||
ChatProto = ref object of LPProtocol
|
||||
|
||||
proc new(T: typedesc[ChatProto], c: Chat): T =
|
||||
let chatproto = T()
|
||||
|
||||
# create handler for incoming connection
|
||||
proc handle(stream: Connection, proto: string) {.async.} =
|
||||
if c.connected and not c.conn.closed:
|
||||
c.writeStdout "a chat session is already in progress - refusing incoming peer!"
|
||||
await stream.close()
|
||||
else:
|
||||
await c.handlePeer(stream)
|
||||
await stream.close()
|
||||
|
||||
# assign the new handler
|
||||
chatproto.handler = handle
|
||||
chatproto.codec = ChatCodec
|
||||
return chatproto
|
||||
|
||||
##
|
||||
# Chat application
|
||||
##
|
||||
proc handlePeer(c: Chat, conn: Connection) {.async.} =
|
||||
# Handle a peer (incoming or outgoing)
|
||||
try:
|
||||
c.conn = conn
|
||||
c.connected = true
|
||||
c.writeStdout $conn.peerId & " connected"
|
||||
|
||||
# Read loop
|
||||
while true:
|
||||
let
|
||||
strData = await conn.readLp(1024)
|
||||
str = string.fromBytes(strData)
|
||||
c.writeStdout $conn.peerId & ": " & $str
|
||||
|
||||
except LPStreamEOFError:
|
||||
c.writeStdout $conn.peerId & " disconnected"
|
||||
|
||||
proc dialPeer(c: Chat, address: string) {.async.} =
|
||||
# Parse and dial address
|
||||
let
|
||||
multiAddr = MultiAddress.init(address).tryGet()
|
||||
# split the peerId part /p2p/...
|
||||
|
@ -63,86 +97,53 @@ proc dialPeer(p: ChatProto, address: string) {.async.} =
|
|||
wireAddr = ip4Addr & tcpAddr
|
||||
|
||||
echo &"dialing peer: {multiAddr}"
|
||||
p.conn = await p.switch.dial(remotePeer, @[wireAddr], ChatCodec)
|
||||
p.connected = true
|
||||
asyncSpawn p.readAndPrint()
|
||||
asyncSpawn c.handlePeer(await c.switch.dial(remotePeer, @[wireAddr], ChatCodec))
|
||||
|
||||
proc writeAndPrint(p: ChatProto) {.async.} =
|
||||
proc readLoop(c: Chat) {.async.} =
|
||||
while true:
|
||||
if not p.connected:
|
||||
if not c.connected:
|
||||
echo "type an address or wait for a connection:"
|
||||
echo "type /[help|?] for help"
|
||||
|
||||
let line = await p.transp.readLine()
|
||||
if line.startsWith("/help") or line.startsWith("/?") or not p.started:
|
||||
c.writePrompt()
|
||||
|
||||
let line = await c.stdinReader.readLine()
|
||||
if line.startsWith("/help") or line.startsWith("/?"):
|
||||
echo Help
|
||||
continue
|
||||
|
||||
if line.startsWith("/disconnect"):
|
||||
echo "Ending current session"
|
||||
if p.connected and p.conn.closed.not:
|
||||
await p.conn.close()
|
||||
p.connected = false
|
||||
c.writeStdout "Ending current session"
|
||||
if c.connected and c.conn.closed.not:
|
||||
await c.conn.close()
|
||||
c.connected = false
|
||||
elif line.startsWith("/connect"):
|
||||
if p.connected:
|
||||
var yesno = "N"
|
||||
echo "a session is already in progress, do you want end it [y/N]?"
|
||||
yesno = await p.transp.readLine()
|
||||
if yesno.cmpIgnoreCase("y") == 0:
|
||||
await p.conn.close()
|
||||
p.connected = false
|
||||
elif yesno.cmpIgnoreCase("n") == 0:
|
||||
continue
|
||||
else:
|
||||
echo "unrecognized response"
|
||||
continue
|
||||
|
||||
echo "enter address of remote peer"
|
||||
let address = await p.transp.readLine()
|
||||
c.writeStdout "enter address of remote peer"
|
||||
let address = await c.stdinReader.readLine()
|
||||
if address.len > 0:
|
||||
await p.dialPeer(address)
|
||||
await c.dialPeer(address)
|
||||
|
||||
elif line.startsWith("/exit"):
|
||||
if p.connected and p.conn.closed.not:
|
||||
await p.conn.close()
|
||||
p.connected = false
|
||||
if c.connected and c.conn.closed.not:
|
||||
await c.conn.close()
|
||||
c.connected = false
|
||||
|
||||
await p.switch.stop()
|
||||
echo "quitting..."
|
||||
await c.switch.stop()
|
||||
c.writeStdout "quitting..."
|
||||
quit(0)
|
||||
else:
|
||||
if p.connected:
|
||||
await p.conn.writeLp(line)
|
||||
if c.connected:
|
||||
await c.conn.writeLp(line)
|
||||
else:
|
||||
try:
|
||||
if line.startsWith("/") and "p2p" in line:
|
||||
await p.dialPeer(line)
|
||||
except:
|
||||
await c.dialPeer(line)
|
||||
except CatchableError as exc:
|
||||
echo &"unable to dial remote peer {line}"
|
||||
echo getCurrentExceptionMsg()
|
||||
|
||||
proc readWriteLoop(p: ChatProto) {.async.} =
|
||||
await p.writeAndPrint()
|
||||
|
||||
proc newChatProto(switch: Switch, transp: StreamTransport): ChatProto =
|
||||
var chatproto = ChatProto(switch: switch, transp: transp, codecs: @[ChatCodec])
|
||||
|
||||
# create handler for incoming connection
|
||||
proc handle(stream: Connection, proto: string) {.async.} =
|
||||
if chatproto.connected and not chatproto.conn.closed:
|
||||
echo "a chat session is already in progress - disconnecting!"
|
||||
await stream.close()
|
||||
else:
|
||||
chatproto.conn = stream
|
||||
chatproto.connected = true
|
||||
await chatproto.readAndPrint()
|
||||
|
||||
# assign the new handler
|
||||
chatproto.handler = handle
|
||||
return chatproto
|
||||
echo exc.msg
|
||||
|
||||
proc readInput(wfd: AsyncFD) {.thread.} =
|
||||
## This procedure performs reading from `stdin` and sends data over
|
||||
## This thread performs reading from `stdin` and sends data over
|
||||
## pipe to main thread.
|
||||
let transp = fromPipe(wfd)
|
||||
|
||||
|
@ -150,36 +151,35 @@ proc readInput(wfd: AsyncFD) {.thread.} =
|
|||
let line = stdin.readLine()
|
||||
discard waitFor transp.write(line & "\r\n")
|
||||
|
||||
proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
|
||||
let transp = fromPipe(rfd)
|
||||
proc main() {.async.} =
|
||||
let
|
||||
rng = newRng() # Single random number source for the whole application
|
||||
|
||||
let seckey = PrivateKey.random(RSA, rng[]).get()
|
||||
var localAddress = DefaultAddr
|
||||
while true:
|
||||
echo &"Type an address to bind to or Enter to use the default {DefaultAddr}"
|
||||
let a = await transp.readLine()
|
||||
try:
|
||||
if a.len > 0:
|
||||
localAddress = a
|
||||
break
|
||||
# uise default
|
||||
break
|
||||
except:
|
||||
echo "invalid address"
|
||||
localAddress = DefaultAddr
|
||||
continue
|
||||
# Pipe to read stdin from main thread
|
||||
(rfd, wfd) = createAsyncPipe()
|
||||
stdinReader = fromPipe(rfd)
|
||||
|
||||
var thread: Thread[AsyncFD]
|
||||
thread.createThread(readInput, wfd)
|
||||
|
||||
var localAddress = MultiAddress.init(DefaultAddr).tryGet()
|
||||
|
||||
var switch = SwitchBuilder
|
||||
.init()
|
||||
.withRng(rng)
|
||||
.withPrivateKey(seckey)
|
||||
.withAddress(MultiAddress.init(localAddress).tryGet())
|
||||
.new()
|
||||
.withRng(rng) # Give the application RNG
|
||||
.withAddress(localAddress)
|
||||
.withTcpTransport() # Use TCP as transport
|
||||
.withMplex() # Use Mplex as muxer
|
||||
.withNoise() # Use Noise as secure manager
|
||||
.build()
|
||||
|
||||
let chatProto = newChatProto(switch, transp)
|
||||
switch.mount(chatProto)
|
||||
let chat = Chat(
|
||||
switch: switch,
|
||||
stdinReader: stdinReader)
|
||||
|
||||
switch.mount(ChatProto.new(chat))
|
||||
|
||||
let libp2pFuts = await switch.start()
|
||||
chatProto.started = true
|
||||
|
||||
let id = $switch.peerInfo.peerId
|
||||
echo "PeerID: " & id
|
||||
|
@ -187,19 +187,7 @@ proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
|
|||
for a in switch.peerInfo.addrs:
|
||||
echo &"{a}/p2p/{id}"
|
||||
|
||||
await chatProto.readWriteLoop()
|
||||
await chat.readLoop()
|
||||
await allFuturesThrowing(libp2pFuts)
|
||||
|
||||
proc main() {.async.} =
|
||||
let rng = newRng() # Singe random number source for the whole application
|
||||
let (rfd, wfd) = createAsyncPipe()
|
||||
if rfd == asyncInvalidPipe or wfd == asyncInvalidPipe:
|
||||
raise newException(ValueError, "Could not initialize pipe!")
|
||||
|
||||
var thread: Thread[AsyncFD]
|
||||
thread.createThread(readInput, wfd)
|
||||
|
||||
await processInput(rfd, rng)
|
||||
|
||||
when isMainModule: # isMainModule = true when the module is compiled as the main file
|
||||
waitFor(main())
|
||||
waitFor(main())
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import bearssl
|
||||
import chronos # an efficient library for async
|
||||
import stew/byteutils # various utils
|
||||
import ../libp2p # when installed through nimble, just use `import libp2p`
|
||||
|
||||
##
|
||||
# Create our custom protocol
|
||||
##
|
||||
const TestCodec = "/test/proto/1.0.0" # custom protocol string identifier
|
||||
|
||||
type
|
||||
TestProto = ref object of LPProtocol # declare a custom protocol
|
||||
|
||||
proc new(T: typedesc[TestProto]): T =
|
||||
|
||||
# every incoming connections will be in handled in this closure
|
||||
proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
|
||||
echo "Got from remote - ", string.fromBytes(await conn.readLp(1024))
|
||||
await conn.writeLp("Roger p2p!")
|
||||
|
||||
# We must close the connections ourselves when we're done with it
|
||||
await conn.close()
|
||||
|
||||
return T(codecs: @[TestCodec], handler: handle)
|
||||
|
||||
##
|
||||
# Helper to create a switch/node
|
||||
##
|
||||
proc createSwitch(ma: MultiAddress, rng: ref BrHmacDrbgContext): Switch =
|
||||
var switch = SwitchBuilder
|
||||
.new()
|
||||
.withRng(rng) # Give the application RNG
|
||||
.withAddress(ma) # Our local address(es)
|
||||
.withTcpTransport() # Use TCP as transport
|
||||
.withMplex() # Use Mplex as muxer
|
||||
.withNoise() # Use Noise as secure manager
|
||||
.build()
|
||||
|
||||
result = switch
|
||||
|
||||
##
|
||||
# The actual application
|
||||
##
|
||||
proc main() {.async, gcsafe.} =
|
||||
let
|
||||
rng = newRng() # Single random number source for the whole application
|
||||
# port 0 will take a random available port
|
||||
# `tryGet` will throw an exception if the Multiaddress failed
|
||||
# (for instance, if the address is not well formatted)
|
||||
ma1 = Multiaddress.init("/ip4/0.0.0.0/tcp/0").tryGet()
|
||||
ma2 = Multiaddress.init("/ip4/0.0.0.0/tcp/0").tryGet()
|
||||
|
||||
# setup the custom proto
|
||||
let testProto = TestProto.new()
|
||||
|
||||
# setup the two nodes
|
||||
let
|
||||
switch1 = createSwitch(ma1, rng) #Create the two switches
|
||||
switch2 = createSwitch(ma2, rng)
|
||||
|
||||
# mount the proto on switch1
|
||||
# the node will now listen for this proto
|
||||
# and call the handler everytime a client request it
|
||||
switch1.mount(testProto)
|
||||
|
||||
# Start the nodes. This will start the transports
|
||||
# and listen on each local addresses
|
||||
let
|
||||
switch1Fut = await switch1.start()
|
||||
switch2Fut = await switch2.start()
|
||||
|
||||
# the node addrs is populated with it's
|
||||
# actual port during the start
|
||||
|
||||
# use the second node to dial the first node
|
||||
# using the first node peerid and address
|
||||
# and specify our custom protocol codec
|
||||
let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec)
|
||||
|
||||
# conn is now a fully setup connection, we talk directly to the node1 custom protocol handler
|
||||
await conn.writeLp("Hello p2p!") # writeLp send a length prefixed buffer over the wire
|
||||
|
||||
# readLp reads length prefixed bytes and returns a buffer without the prefix
|
||||
echo "Remote responded with - ", string.fromBytes(await conn.readLp(1024))
|
||||
|
||||
# We must close the connection ourselves when we're done with it
|
||||
await conn.close()
|
||||
|
||||
await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
|
||||
await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown
|
||||
|
||||
waitFor(main())
|
|
@ -0,0 +1,108 @@
|
|||
Hi all, welcome to the first article of the nim-libp2p's tutorial series!
|
||||
|
||||
_This tutorial is for everyone who is interested in building peer-to-peer chatting applications. No Nim programming experience is needed._
|
||||
|
||||
To give you a quick overview, **Nim** is the programming language we are using and **nim-libp2p** is the Nim implementation of [libp2p](https://libp2p.io/), a modular library that enables the development of peer-to-peer network applications.
|
||||
|
||||
Hope you'll find it helpful in your journey of learning. Happy coding! ;)
|
||||
|
||||
# Before you start
|
||||
The only prerequisite here is [Nim](https://nim-lang.org/), the programming language with a Python-like syntax and a performance similar to C. Detailed information can be found [here](https://nim-lang.org/docs/tut1.html).
|
||||
|
||||
Install Nim via their official website: [https://nim-lang.org/install.html](https://nim-lang.org/install.html)
|
||||
Check Nim's installation via `nim --version` and its package manager Nimble via `nimble --version`.
|
||||
|
||||
You can now install the latest version of `nim-libp2p`:
|
||||
```bash
|
||||
nimble install libp2p@#master
|
||||
```
|
||||
|
||||
# A simple ping application
|
||||
We'll start by creating a simple application, which is starting two libp2p [switch](https://docs.libp2p.io/concepts/stream-multiplexing/#switch-swarm), and pinging each other using the [Ping](https://docs.libp2p.io/concepts/protocols/#ping) protocol.
|
||||
|
||||
_TIP: You can extract the code from this tutorial by running `nim c -r tools/markdown_runner.nim examples/tutorial_1_connect.md` in the libp2p folder!_
|
||||
|
||||
Let's create a `part1.nim`, and import our dependencies:
|
||||
```nim
|
||||
import bearssl
|
||||
import chronos
|
||||
|
||||
import libp2p
|
||||
import libp2p/protocols/ping
|
||||
```
|
||||
[bearssl](https://github.com/status-im/nim-bearssl) is used as a [cryptographic pseudorandom number generator](https://en.wikipedia.org/wiki/Cryptographically-secure_pseudorandom_number_generator)
|
||||
[chronos](https://github.com/status-im/nim-chronos) the asynchronous framework used by `nim-libp2p`
|
||||
|
||||
Next, we'll create an helper procedure to create our switches. A switch needs a bit of configuration, and it will be easier to do this configuration only once:
|
||||
```nim
|
||||
proc createSwitch(ma: MultiAddress, rng: ref BrHmacDrbgContext): Switch =
|
||||
var switch = SwitchBuilder
|
||||
.new()
|
||||
.withRng(rng) # Give the application RNG
|
||||
.withAddress(ma) # Our local address(es)
|
||||
.withTcpTransport() # Use TCP as transport
|
||||
.withMplex() # Use Mplex as muxer
|
||||
.withNoise() # Use Noise as secure manager
|
||||
.build()
|
||||
|
||||
return switch
|
||||
```
|
||||
This will create a switch using [Mplex](https://docs.libp2p.io/concepts/stream-multiplexing/) as a multiplexer, Noise to secure the communication, and TCP as an underlying transport.
|
||||
|
||||
You can of course tweak this, to use a different or multiple transport, or tweak the configuration of Mplex and Noise, but this is some sane defaults that we'll use going forward.
|
||||
|
||||
|
||||
Let's now start to create our main procedure:
|
||||
```nim
|
||||
proc main() {.async, gcsafe.} =
|
||||
let
|
||||
rng = newRng()
|
||||
localAddress = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()
|
||||
pingProtocol = Ping.new(rng=rng)
|
||||
```
|
||||
We created some variables that we'll need for the rest of the application: the global `rng` instance, our `localAddress`, and an instance of the `Ping` protocol.
|
||||
The address is in the [MultiAddress](https://github.com/multiformats/multiaddr) format. The port `0` means "take any port available".
|
||||
|
||||
`tryGet` is procedure which is part of the [nim-result](https://github.com/arnetheduck/nim-result/), that will throw an exception if the supplied MultiAddress is not valid.
|
||||
|
||||
We can now create our two switches:
|
||||
```nim
|
||||
let
|
||||
switch1 = createSwitch(localAddress, rng)
|
||||
switch2 = createSwitch(localAddress, rng)
|
||||
|
||||
switch1.mount(pingProtocol)
|
||||
|
||||
let
|
||||
switch1Fut = await switch1.start()
|
||||
switch2Fut = await switch2.start()
|
||||
```
|
||||
We've **mounted** the `pingProtocol` on our first switch. This means that the first switch will actually listen for any ping requests coming in, and handle them accordingly.
|
||||
|
||||
Now that we've started the nodes, they are listening for incoming peers.
|
||||
We can find out which port was attributed, and the resulting local addresses, by using `switch1.peerInfo.addrs`.
|
||||
|
||||
We'll **dial** the first switch from the second one, by specifying it's **Peer ID**, it's **MultiAddress** and the **`Ping` protocol codec**:
|
||||
```nim
|
||||
let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, PingCodec)
|
||||
```
|
||||
We now have a `Ping` connection setup between the second and the first switch, we can use it to actually ping the node:
|
||||
```nim
|
||||
# ping the other node and echo the ping duration
|
||||
echo "ping: ", await pingProtocol.ping(conn)
|
||||
|
||||
# We must close the connection ourselves when we're done with it
|
||||
await conn.close()
|
||||
```
|
||||
|
||||
And that's it! Just a little bit of cleanup: shutting down the switches, waiting for them to stop, and we'll call our `main` procedure:
|
||||
```nim
|
||||
await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
|
||||
await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown
|
||||
|
||||
waitFor(main())
|
||||
```
|
||||
|
||||
You can now run this program using `nim c -r part1.nim`, and you should see the dialing sequence, ending with a ping output.
|
||||
|
||||
In the [next tutorial](tutorial_2_customproto.md), we'll look at how to create our own custom protocol.
|
|
@ -0,0 +1,82 @@
|
|||
In the [previous tutorial](tutorial_1_connect.md), we've looked at how to create a simple ping program using the `nim-libp2p`.
|
||||
|
||||
We'll now look at how to create a custom protocol inside the libp2p
|
||||
|
||||
# Custom protocol in libp2p
|
||||
Let's create a `part2.nim`, and import our dependencies:
|
||||
```nim
|
||||
import bearssl
|
||||
import chronos
|
||||
import stew/byteutils
|
||||
|
||||
import libp2p
|
||||
```
|
||||
This is similar to the first tutorial, except we don't need to import the `Ping` protocol.
|
||||
|
||||
Next, we'll declare our custom protocol
|
||||
```nim
|
||||
const TestCodec = "/test/proto/1.0.0"
|
||||
|
||||
type TestProto = ref object of LPProtocol
|
||||
```
|
||||
|
||||
We've set a [protocol ID](https://docs.libp2p.io/concepts/protocols/#protocol-ids), and created a custom `LPProtocol`. In a more complex protocol, we could use this structure to store interesting variables.
|
||||
|
||||
A protocol generally has two part: and handling/server part, and a dialing/client part.
|
||||
Theses two parts can be identical, but in our trivial protocol, the server will wait for a message from the client, and the client will send a message, so we have to handle the two cases separately.
|
||||
|
||||
Let's start with the server part:
|
||||
```nim
|
||||
proc new(T: typedesc[TestProto]): T =
|
||||
# every incoming connections will in be handled in this closure
|
||||
proc handle(conn: Connection, proto: string) {.async, gcsafe.} =
|
||||
echo "Got from remote - ", string.fromBytes(await conn.readLp(1024))
|
||||
# We must close the connections ourselves when we're done with it
|
||||
await conn.close()
|
||||
|
||||
return T(codecs: @[TestCodec], handler: handle)
|
||||
```
|
||||
This is a constructor for our `TestProto`, that will specify our `codecs` and a `handler`, which will be called for each incoming peer asking for this protocol.
|
||||
In our handle, we simply read a message from the connection and `echo` it.
|
||||
|
||||
We can now create our client part:
|
||||
```nim
|
||||
proc hello(p: TestProto, conn: Connection) {.async.} =
|
||||
await conn.writeLp("Hello p2p!")
|
||||
```
|
||||
Again, pretty straight-forward, we just send a message on the connection.
|
||||
|
||||
We can now create our main procedure:
|
||||
```nim
|
||||
proc main() {.async, gcsafe.} =
|
||||
let
|
||||
rng = newRng()
|
||||
testProto = TestProto.new()
|
||||
switch1 = newStandardSwitch(rng=rng)
|
||||
switch2 = newStandardSwitch(rng=rng)
|
||||
|
||||
switch1.mount(testProto)
|
||||
|
||||
let
|
||||
switch1Fut = await switch1.start()
|
||||
switch2Fut = await switch2.start()
|
||||
|
||||
conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec)
|
||||
|
||||
await testProto.hello(conn)
|
||||
|
||||
# We must close the connection ourselves when we're done with it
|
||||
await conn.close()
|
||||
|
||||
await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports
|
||||
await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown
|
||||
```
|
||||
|
||||
This is very similar to the first tutorial's `main`, the only noteworthy difference is that we use `newStandardSwitch`, which is similar to `createSwitch` but is bundled directly in libp2p
|
||||
|
||||
We can now wrap our program by calling our main proc:
|
||||
```nim
|
||||
waitFor(main())
|
||||
```
|
||||
|
||||
And that's it!
|
|
@ -33,12 +33,18 @@ proc runTest(filename: string, verify: bool = true, sign: bool = true,
|
|||
exec excstr & " -d:chronicles_log_level=INFO -r" & " tests/" & filename
|
||||
rmFile "tests/" & filename.toExe
|
||||
|
||||
proc buildSample(filename: string) =
|
||||
proc buildSample(filename: string, run = false) =
|
||||
var excstr = "nim c --opt:speed --threads:on -d:debug --verbosity:0 --hints:off"
|
||||
excstr.add(" --warning[CaseTransition]:off --warning[ObservableStores]:off --warning[LockLevel]:off")
|
||||
excstr.add(" examples/" & filename)
|
||||
exec excstr
|
||||
rmFile "examples" & filename.toExe
|
||||
if run:
|
||||
exec "./examples/" & filename.toExe
|
||||
rmFile "examples/" & filename.toExe
|
||||
|
||||
proc buildTutorial(filename: string) =
|
||||
discard gorge "cat " & filename & " | nim c -r --hints:off tools/markdown_runner.nim | " &
|
||||
" nim --warning[CaseTransition]:off --warning[ObservableStores]:off --warning[LockLevel]:off c -"
|
||||
|
||||
task testnative, "Runs libp2p native tests":
|
||||
runTest("testnative")
|
||||
|
@ -75,6 +81,7 @@ task test, "Runs the test suite":
|
|||
exec "nimble testdaemon"
|
||||
exec "nimble testinterop"
|
||||
exec "nimble testfilter"
|
||||
exec "nimble examples_build"
|
||||
|
||||
task test_slim, "Runs the test suite":
|
||||
exec "nimble testnative"
|
||||
|
@ -84,3 +91,6 @@ task test_slim, "Runs the test suite":
|
|||
|
||||
task examples_build, "Build the samples":
|
||||
buildSample("directchat")
|
||||
buildSample("helloworld", true)
|
||||
buildTutorial("examples/tutorial_1_connect.md")
|
||||
buildTutorial("examples/tutorial_2_customproto.md")
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import os, osproc, streams, strutils
|
||||
import parseutils
|
||||
|
||||
let contents =
|
||||
if paramCount() > 0:
|
||||
readFile(paramStr(1))
|
||||
else:
|
||||
stdin.readAll()
|
||||
var index = 0
|
||||
|
||||
const startDelim = "```nim\n"
|
||||
const endDelim = "\n```"
|
||||
while true:
|
||||
let startOfBlock = contents.find(startDelim, start = index)
|
||||
if startOfBlock == -1: break
|
||||
|
||||
let endOfBlock = contents.find(endDelim, start = startOfBlock + startDelim.len)
|
||||
if endOfBlock == -1:
|
||||
quit "Unfinished block!"
|
||||
|
||||
let code = contents[startOfBlock + startDelim.len .. endOfBlock]
|
||||
|
||||
echo code
|
||||
|
||||
index = endOfBlock + endDelim.len
|
Loading…
Reference in New Issue