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. 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.
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:
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.
Let's first create a function (called procedure in Nim) that prints a string "hi"!
start.nim
under the tutorial
directory.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
In this example, we hope to process our procedures while listening to the user input. To achieve this, we will use multi-threading.
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
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!