In the last post, we saw how to use the
task function to repeat tasks periodically or listen for asynchronous events. Now let's demonstrate it using a little example - a simple chat demo app.
(This example was also included in the pil64-version of PicoLisp - if you have it installed, you can find the source code in the
misc/ folder of your installation.)
How it works
The requirements to our app are very simple: We want to open a TCP port which is dedicated to communication. Any client can connect to this port, for example with telnet, and send and receive messages.
The little GIF below illustrates how the final app should work:
- All "chat users" are connecting to port 4004 in localhost.
- If a new user joins the "room", the other users receive a notification.
- If a user sends a message, the others receive it together with the name prompt.
- If a user leaves the room, the other users are notified too.
File Descriptors and Forks in PicoLisp
All users are connecting to the same port (localhost:4004), where each one is creating an individual socket to communicate with each other. So let's take a look at socket handling in PicoLisp now.
We want the port 4004 to stay open constantly for any client to connect. At the same time, everyone connecting to the port receives their own socket for communication. First, we create a new TCP port with the
port function, which returns a file descriptor (read here for more on Unix file descriptors: Everything is a file).
(setq *Port (port 4004))
Now that the port is open, the client can connect. However, we want to be able to handle multiple clients. How can we do this? We use another important Unix feature, which is forking).
A fork creates a copy of a process - a so-called child-process. Now we establish the following division of tasks: The parent process takes care of keeping the port open for clients to connect, while each child process inherits a socket for the client.
We realize this with a
loop function. The loop from the parent's process view looks like this:
- Waits until a client connects with
(listen *Port). If a client connects, a new file descriptor is created which is stored in the variable
- Then the process is forked and thus a child process is created.
- The parent process closes the socked (
(close *Sock)) as it is now handled by the child process.
On the other hand, the child process as a copy of the parent is also stepping inside the loop, but instantly leaves the loop to take care of the socket handling. In PicoLisp, we can create a child process with
NIL in the child and the child's process ID in the parent.
These four lines take care of the parents/child process handling:
(loop (setq *Sock (listen *Port)) (NIL (fork) (close *Port)) (close *Sock) )
(NIL (fork) (close *Port)) corresponds to the exit condition of the
NIL (i. e., is a child), then the loop is left. If it is non-
NIL, it means that we are in the parent process, so we will simply close the
*Sock socket and start listening on the port again.
Registering the user name
Now our child process has left the
loop and is ready to interact with the user. First of all, we want to register the name. In order to print to the socket's output stream (like
telnet), we use the
out <FD> function, where is the file descriptor of the socket.
(out *Sock (prin "Please enter your name: ") (flush) )
(flush) empties the buffered data by sending all to the output stream (= flushing). Alternatively, this can be triggered by a new line, i. e. by using
prinl instead of
Next, we read the username from the terminal with
(in <FD>). It takes the whole line as input.
(in *Sock (setq *Name (line T)))
Note: In a "real" app, you might want to add some input validation for the name variable.
Spreading the word with
Now let's announce to all connected clients that the new user has arrived. This kind of inter-process communication can be handled by
tell is a function that "knows" all process family members and can send them messages (family members are all children of the current process, as well as all other children of the parent process). It takes a symbol as argument specifying the exact message to be sent.
In our case, we want the message to print a text to the current output of the socket. In order to keep it flexible, let's maintain the text itself as a list.
(de chat Lst (out *Sock (mapc prin Lst) (prinl) ) )
chat takes a list as argument, prints each list item and then a line feed. This can now be used within
(tell 'chat "+++ " *Name " arrived +++")
Everytime a new client connects to a port, all other connected clients will be informed with this text.
Handling the actual chat messages
Now finally, we come to use the
task function that we've been talking about in the beginning. We create a task, i. e. waiting at the socket for user input. If there is input to this socket, we read it and directly hand it over to
tell. Then we wait again for user input.
(task *Sock (in @ (tell 'chat *Name "> " (line T))
However, there is one exception: If the client ends the connection (for example with
Ctrl-] in telnet), we spread this information with
tell and then close the task. So, the complete
task looks like this:
(task *Sock (in @ (ifn (eof) (tell 'chat *Name "> " (line T)) (tell 'chat "--- " *Name " left ---") (bye) ) ) )
With this, we have already everything we need for our little chat app. As a last (rather cosmetic) action, we call
(wait) in order to surpress the PicoLisp prompt when the chat function gets called.
You can download the full source code of this example here.