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
|
# Getting Started
|
||||||
Welcome to nim-libp2p! This guide will walk you through a peer to peer chat example. <br>
|
Welcome to nim-libp2p!
|
||||||
The full code can be found in [directchat.nim](examples/directchat.nim) under the examples folder.
|
|
||||||
|
|
||||||
|
|
||||||
### Direct Chat Example
|
To get started, please look at the [tutorials](../examples/tutorial_1_connect.md)
|
||||||
To run nim-libp2p, add it to your project's nimble file and spawn a node as follows:
|
|
||||||
|
|
||||||
```nim
|
For more concrete examples, you can look at the [hello world example](../examples/helloworld.nim) or the [direct chat](../examples/directchat.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())
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,54 +1,88 @@
|
||||||
when not(compileOption("threads")):
|
when not(compileOption("threads")):
|
||||||
{.fatal: "Please, compile this program with the --threads:on option!".}
|
{.fatal: "Please, compile this program with the --threads:on option!".}
|
||||||
|
|
||||||
import tables, strformat, strutils, bearssl
|
import
|
||||||
import chronos # an efficient library for async
|
strformat, strutils, bearssl,
|
||||||
import ../libp2p/[switch, # manage transports, a single entry point for dialing and listening
|
stew/byteutils,
|
||||||
builders, # helper to build the switch object
|
chronos,
|
||||||
multistream, # tag stream with short header to identify it
|
../libp2p
|
||||||
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
|
|
||||||
|
|
||||||
const ChatCodec = "/nim-libp2p/chat/1.0.0"
|
const DefaultAddr = "/ip4/127.0.0.1/tcp/0"
|
||||||
const DefaultAddr = "/ip4/127.0.0.1/tcp/55505"
|
|
||||||
|
|
||||||
const Help = """
|
const Help = """
|
||||||
Commands: /[?|hep|connect|disconnect|exit]
|
Commands: /[?|help|connect|disconnect|exit]
|
||||||
help: Prints this help
|
help: Prints this help
|
||||||
connect: dials a remote peer
|
connect: dials a remote peer
|
||||||
disconnect: ends current session
|
disconnect: ends current session
|
||||||
exit: closes the chat
|
exit: closes the chat
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type ChatProto = ref object of LPProtocol
|
type
|
||||||
switch: Switch # a single entry point for dialing and listening to peer
|
Chat = ref object
|
||||||
transp: StreamTransport # transport streams between read & write file descriptor
|
switch: Switch # a single entry point for dialing and listening to peer
|
||||||
conn: Connection # create and close read & write stream
|
stdinReader: StreamTransport # transport streams between read & write file descriptor
|
||||||
connected: bool # if the node is connected to another peer
|
conn: Connection # connection to the other peer
|
||||||
started: bool # if the node has started
|
connected: bool # if the node is connected to another peer
|
||||||
|
|
||||||
proc readAndPrint(p: ChatProto) {.async.} =
|
##
|
||||||
while true:
|
# Stdout helpers, to write the prompt
|
||||||
var strData = await p.conn.readLp(1024)
|
##
|
||||||
strData &= '\0'.uint8
|
proc writePrompt(c: Chat) =
|
||||||
var str = cast[cstring](addr strdata[0])
|
if c.connected:
|
||||||
echo $p.switch.peerInfo.peerId & ": " & $str
|
stdout.write '\r' & $c.switch.peerInfo.peerId & ": "
|
||||||
await sleepAsync(100.millis)
|
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
|
let
|
||||||
multiAddr = MultiAddress.init(address).tryGet()
|
multiAddr = MultiAddress.init(address).tryGet()
|
||||||
# split the peerId part /p2p/...
|
# split the peerId part /p2p/...
|
||||||
|
@ -63,86 +97,53 @@ proc dialPeer(p: ChatProto, address: string) {.async.} =
|
||||||
wireAddr = ip4Addr & tcpAddr
|
wireAddr = ip4Addr & tcpAddr
|
||||||
|
|
||||||
echo &"dialing peer: {multiAddr}"
|
echo &"dialing peer: {multiAddr}"
|
||||||
p.conn = await p.switch.dial(remotePeer, @[wireAddr], ChatCodec)
|
asyncSpawn c.handlePeer(await c.switch.dial(remotePeer, @[wireAddr], ChatCodec))
|
||||||
p.connected = true
|
|
||||||
asyncSpawn p.readAndPrint()
|
|
||||||
|
|
||||||
proc writeAndPrint(p: ChatProto) {.async.} =
|
proc readLoop(c: Chat) {.async.} =
|
||||||
while true:
|
while true:
|
||||||
if not p.connected:
|
if not c.connected:
|
||||||
echo "type an address or wait for a connection:"
|
echo "type an address or wait for a connection:"
|
||||||
echo "type /[help|?] for help"
|
echo "type /[help|?] for help"
|
||||||
|
|
||||||
let line = await p.transp.readLine()
|
c.writePrompt()
|
||||||
if line.startsWith("/help") or line.startsWith("/?") or not p.started:
|
|
||||||
|
let line = await c.stdinReader.readLine()
|
||||||
|
if line.startsWith("/help") or line.startsWith("/?"):
|
||||||
echo Help
|
echo Help
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startsWith("/disconnect"):
|
if line.startsWith("/disconnect"):
|
||||||
echo "Ending current session"
|
c.writeStdout "Ending current session"
|
||||||
if p.connected and p.conn.closed.not:
|
if c.connected and c.conn.closed.not:
|
||||||
await p.conn.close()
|
await c.conn.close()
|
||||||
p.connected = false
|
c.connected = false
|
||||||
elif line.startsWith("/connect"):
|
elif line.startsWith("/connect"):
|
||||||
if p.connected:
|
c.writeStdout "enter address of remote peer"
|
||||||
var yesno = "N"
|
let address = await c.stdinReader.readLine()
|
||||||
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()
|
|
||||||
if address.len > 0:
|
if address.len > 0:
|
||||||
await p.dialPeer(address)
|
await c.dialPeer(address)
|
||||||
|
|
||||||
elif line.startsWith("/exit"):
|
elif line.startsWith("/exit"):
|
||||||
if p.connected and p.conn.closed.not:
|
if c.connected and c.conn.closed.not:
|
||||||
await p.conn.close()
|
await c.conn.close()
|
||||||
p.connected = false
|
c.connected = false
|
||||||
|
|
||||||
await p.switch.stop()
|
await c.switch.stop()
|
||||||
echo "quitting..."
|
c.writeStdout "quitting..."
|
||||||
quit(0)
|
quit(0)
|
||||||
else:
|
else:
|
||||||
if p.connected:
|
if c.connected:
|
||||||
await p.conn.writeLp(line)
|
await c.conn.writeLp(line)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if line.startsWith("/") and "p2p" in line:
|
if line.startsWith("/") and "p2p" in line:
|
||||||
await p.dialPeer(line)
|
await c.dialPeer(line)
|
||||||
except:
|
except CatchableError as exc:
|
||||||
echo &"unable to dial remote peer {line}"
|
echo &"unable to dial remote peer {line}"
|
||||||
echo getCurrentExceptionMsg()
|
echo exc.msg
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
proc readInput(wfd: AsyncFD) {.thread.} =
|
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.
|
## pipe to main thread.
|
||||||
let transp = fromPipe(wfd)
|
let transp = fromPipe(wfd)
|
||||||
|
|
||||||
|
@ -150,36 +151,35 @@ proc readInput(wfd: AsyncFD) {.thread.} =
|
||||||
let line = stdin.readLine()
|
let line = stdin.readLine()
|
||||||
discard waitFor transp.write(line & "\r\n")
|
discard waitFor transp.write(line & "\r\n")
|
||||||
|
|
||||||
proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
|
proc main() {.async.} =
|
||||||
let transp = fromPipe(rfd)
|
let
|
||||||
|
rng = newRng() # Single random number source for the whole application
|
||||||
|
|
||||||
let seckey = PrivateKey.random(RSA, rng[]).get()
|
# Pipe to read stdin from main thread
|
||||||
var localAddress = DefaultAddr
|
(rfd, wfd) = createAsyncPipe()
|
||||||
while true:
|
stdinReader = fromPipe(rfd)
|
||||||
echo &"Type an address to bind to or Enter to use the default {DefaultAddr}"
|
|
||||||
let a = await transp.readLine()
|
var thread: Thread[AsyncFD]
|
||||||
try:
|
thread.createThread(readInput, wfd)
|
||||||
if a.len > 0:
|
|
||||||
localAddress = a
|
var localAddress = MultiAddress.init(DefaultAddr).tryGet()
|
||||||
break
|
|
||||||
# uise default
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
echo "invalid address"
|
|
||||||
localAddress = DefaultAddr
|
|
||||||
continue
|
|
||||||
|
|
||||||
var switch = SwitchBuilder
|
var switch = SwitchBuilder
|
||||||
.init()
|
.new()
|
||||||
.withRng(rng)
|
.withRng(rng) # Give the application RNG
|
||||||
.withPrivateKey(seckey)
|
.withAddress(localAddress)
|
||||||
.withAddress(MultiAddress.init(localAddress).tryGet())
|
.withTcpTransport() # Use TCP as transport
|
||||||
|
.withMplex() # Use Mplex as muxer
|
||||||
|
.withNoise() # Use Noise as secure manager
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
let chatProto = newChatProto(switch, transp)
|
let chat = Chat(
|
||||||
switch.mount(chatProto)
|
switch: switch,
|
||||||
|
stdinReader: stdinReader)
|
||||||
|
|
||||||
|
switch.mount(ChatProto.new(chat))
|
||||||
|
|
||||||
let libp2pFuts = await switch.start()
|
let libp2pFuts = await switch.start()
|
||||||
chatProto.started = true
|
|
||||||
|
|
||||||
let id = $switch.peerInfo.peerId
|
let id = $switch.peerInfo.peerId
|
||||||
echo "PeerID: " & id
|
echo "PeerID: " & id
|
||||||
|
@ -187,19 +187,7 @@ proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
|
||||||
for a in switch.peerInfo.addrs:
|
for a in switch.peerInfo.addrs:
|
||||||
echo &"{a}/p2p/{id}"
|
echo &"{a}/p2p/{id}"
|
||||||
|
|
||||||
await chatProto.readWriteLoop()
|
await chat.readLoop()
|
||||||
await allFuturesThrowing(libp2pFuts)
|
await allFuturesThrowing(libp2pFuts)
|
||||||
|
|
||||||
proc main() {.async.} =
|
waitFor(main())
|
||||||
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())
|
|
||||||
|
|
|
@ -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
|
exec excstr & " -d:chronicles_log_level=INFO -r" & " tests/" & filename
|
||||||
rmFile "tests/" & filename.toExe
|
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"
|
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(" --warning[CaseTransition]:off --warning[ObservableStores]:off --warning[LockLevel]:off")
|
||||||
excstr.add(" examples/" & filename)
|
excstr.add(" examples/" & filename)
|
||||||
exec excstr
|
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":
|
task testnative, "Runs libp2p native tests":
|
||||||
runTest("testnative")
|
runTest("testnative")
|
||||||
|
@ -75,6 +81,7 @@ task test, "Runs the test suite":
|
||||||
exec "nimble testdaemon"
|
exec "nimble testdaemon"
|
||||||
exec "nimble testinterop"
|
exec "nimble testinterop"
|
||||||
exec "nimble testfilter"
|
exec "nimble testfilter"
|
||||||
|
exec "nimble examples_build"
|
||||||
|
|
||||||
task test_slim, "Runs the test suite":
|
task test_slim, "Runs the test suite":
|
||||||
exec "nimble testnative"
|
exec "nimble testnative"
|
||||||
|
@ -84,3 +91,6 @@ task test_slim, "Runs the test suite":
|
||||||
|
|
||||||
task examples_build, "Build the samples":
|
task examples_build, "Build the samples":
|
||||||
buildSample("directchat")
|
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