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

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

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, a modular library that enables the development of peer-to-peer network applications.

Today we're going to walk you through a peer to peer chat example. The full code can be found in directchat.nim under our main repository. The code for this part is in start.nim.

Hope you'll find it helpful in your journey of learning. Happy coding! ;)

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

  • Part I (now): 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: Configure and establish a libp2p node.

Before you start

The only prerequisite here is Nim, the programming language with a Python-like syntax and a performance similar to C. Detailed information can be found here.

Running the Example

  1. First, open your terminal and clone the git repository which contains our example code.
git clone https://github.com/status-im/nim-libp2p.git

2. Then, install the dependencies.

cd nim-libp2p
nimble install

3. Navigate into the examples folder.

cd examples

4. Try compiling and running the code to make sure everything is working.

nim c -r --threads:on directchat.nim
# This is equivalent to: nim compile --run --threads:on directchat.nim
# --threads:on means to turn on support for multi-threading

You're good to go if you see the following output:

Terminal output after running directchat.nim

Start coding!

In this section we will go through the code line by line, starting from the very basics. Feel free to skip it if you are already very experienced in Nim.

Set up the main procedure

Let's first create a function (called procedure in Nim) that prints a string "hi"!

  1. Create a new file called start.nim under the tutorial directory.
  2. Copy and paste the following code in our new file.
import chronos

proc main() {.async.} =
  echo "hi"
  
when isMainModule:
  waitFor(main())

The first line imports the module "chronos", which is an efficient library for asynchronous programming. Developed by Status, chronos started as a fork of the Nim standard library async but has since diverged significantly. You can find more documentation, notes, and examples in its Wiki.

Then, the proc keyword defines our main procedure with the name main. The {. .} syntax is the called pragma in Nim. The async keyword in it tells the compiler to enable the async capabilities to our procedure.

The final part of this code waits for our async main procedure to execute by the waitFor keyword. In the second-last line there is a special constant isMainModule, which will be true when this module is compiled as the main file.

3. Try running the above snippet by typing this in the terminal. You should see "hi" printing out.

cd ../tutorial
nim c -r start.nim
Terminal output after running start.nim

Read and process the console input

In this example, we hope to process our procedures while listening to the user input. To achieve this, we will use multi-threading.

  1. First, create a procedure to listen to user input.
proc readInput(wfd: AsyncFD) {.thread.} =
  ## This procedure performs reading from `stdin` and sends data over
  ## pipe to main thread.
  let transp = fromPipe(wfd)

  while true:
    let line = stdin.readLine()
    discard waitFor transp.write(line & "\r\n")

In this procedure we use while true to continuously listen to user input from stdin, and write the result to our write file descriptor wfd. The input will then be forwarded to rfd to be processed later.

2. Create another procedure to print the input.

proc processInput(rfd: AsyncFD) {.async.} =
  echo "Type something below to see if the multithread IO works:\nType 'exit' to exit."

  let transp = fromPipe(rfd)
  while true:
    let a = await transp.readLine()

    if a == "exit":
      quit(0);
      
    echo "You just entered: " & a

Read more about the what pipe, stdin, stdout, and file descriptor is in this blog. In short, pipe is for connecting standard input and output here.

Here you can see that when the user enters "exit", the program exits with quit(0) where 0 means exiting without errors.

3. Then, edit our main procedure to create a pipeline to send data between different threads and assign its handler to the new instance based on our object.

proc main() {.async.} =
  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)

In the first part of the code, rfd stands for read file descriptor and wfd stands for write file descriptor. createAsyncPipe sets up and returns an asynchronous pipeline that forwards the data written in wfd to be read in rfd.  Exceptions are raised if the return value is invalid.

The remaining part is self-explanatory. We create a thread to continuously listen to user input and then process our input data.

Another thing to note is that we use var instead of let keyword when declaring thread because let requires initialization and the value cannot be changed once assigned. We only specify the type of thread but not giving it the initial value here.

4. Lastly, remind our user to add the threads:on option for multi-threading when executing our script. Add this line before our import statement.

when not(compileOption("threads")):
  {.fatal: "Please, compile this program with the --threads:on option!".}

5. Run start.nim and see if your input / output works fine. Remember to add threads:on here since we are using multi-thread.

nim c -r --threads:on start.nim 
Terminal output after running start.nim.

Conclusion

Now you have learned the basic syntax of Nim and have a fully functional script to easily use multi-threading to process input and output.

In the next tutorial, we will go through how to dial a remote peer and let the user input customized commands. Stay tuned!