Nim-libp2p Tutorial: A Peer-to-Peer Chat Example (3)

Hi all, welcome to the last 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.

In this part, we will walk you through how to configure and establish a libp2p node. The full code can be found in directchat.nim under our main repository.

Hope you'll find it helpful. Happy learning! ;)

* Note: This tutorial is divided into three parts as below:

  • Part I: Set up the main function and use multi-thread for processing IO.
  • Part II: Dial remote peer and allow customized user input commands.
  • Part III(now): Configure and establish a libp2p node.

Before you start

Let's try to run this example on the same computer with peers running on two different ports!

1. Open a new terminal window and type the following:

nim c -r --threads:on examples/directchat.nim

2. Press enter to use the default address for our first peer.

Terminal output after running directchat.nim and pressed enter.

3. Open another terminal window and start our second peer.

nim c -r --threads:on examples/directchat.nim

4. Type an address with a new port (55504) to serve our second peer. Input /ip4/127.0.0.1/tcp/55504 and press enter.

Terminal output after running directchat.nim and entered /ip4/127.0.0.1/tcp/55504.

5. Copy the address of our first peer from the first terminal and connect to the first peer by entering its address in the second terminal.

Terminal output after running directchat.nim and entered another peer's address to connect.

6. That's all! The two peers are now connected. Anything you type is one termainl will appear on the other terminal. :)

Start coding!

  1. First, to help us distinguish from the first code file, rename our second.nim file to final.nim. This step is optional becasue our final code will be the same as in directchat.nim.
  2. Create a procedure for initializing libp2p node.
proc newChatProto(switch: Switch, transp: StreamTransport): ChatProto =
  var chatproto = ChatProto(switch: switch, transp: transp, codec: 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

  # assign the new handler
  chatproto.handler = handle
  return chatproto

In the above code, we instantiate a new ChatProto instance named chatproto with the values passed in, create a handler for incoming connections, and assign it to our new ChatProto instance.

Note that we use var instead of let for chatproto here because we will change the value assigned to chatproto after its initial declaration.

3. Modify the processInput procedure to initialize node address by user input as follows:

proc processInput(rfd: AsyncFD) {.async.} =
  let transp = fromPipe(rfd)

  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

First, we generate a random private key using the RSA encryption scheme and use this key to initialize our new peer.

Then, we assign the default address to localAddress and replace it if the user enters a valid address (with the multiaddress format). If an invalid address is entered, we set the address back to default and continue listening to user input.

4. Build the switch we are going to use for our libp2p node (add the following to the processInput procedure).

  var switch = SwitchBuilder
    .init()
    .withRng(rng)
    .withPrivateKey(seckey)
    .withAddress(MultiAddress.init(localAddress).tryGet())
    .build()

  let chatProto = newChatProto(switch, transp)
  switch.mount(chatProto)
  let libp2pFuts = await switch.start()
  chatProto.started = true

In the final part, transp was used when instantiating with newChatProto to let us process the user input messages using pipes. The libp2pFuts variable stores a sequence of Futures that were returned after the switch has started.

Don'f forget to set chatProto.started to true so that we won't print the Help message specified in the writeAndPrint procedure before.

5. Print the current peer ID and the listening address afte r the node has started (add the following to the processInput procedure).

let id = peerInfo.peerId.pretty
echo "PeerID: " & id
echo "listening on: "
for a in peerInfo.addrs:
  echo &"{a}/p2p/{id}"

6. Start to read and write messages and commands simultaneously and wait for everything to finish after calling switch.start() (add the following to the processInput procedure).

await chatProto.readWriteLoop()
await allFuturesThrowing(libp2pFuts)

7. Now your code should be the same as directchat.nim. Run it and start experimenting with your first libp2p chatting application.

nim c -r --threads:on examples/directchat.nim
Terminal output after running directchat.nim and press enter to start a peer that listens on the default address.

Conclusion

Congratulations on finishing all the steps of this peer to peer chat example! Now you have learned how to define and establish a customized libp2p node.

Feel free to leave comments below for feedback and suggestions. Thank you for your time reading. See you next time :)