HTTP

In this tutorial we will create a simple HTTP chat server.

Prerequisites

We assume that you have set up your development environment as described in the IDE tutorial.

We also assume that you have flashed your device with Jaguar and that you are familiar with running Toit programs on it. If not, have a look at the Hello world tutorial.

Note that you can do this tutorial without a device. In that case, you need to use the -d host option whenever you invoke jag run. The program will then run on your computer instead of on a device.

Packages

The HTTP functionality is not part of the core libraries and must be imported as a package. See the packages tutorial for details.

We are using the http package. To install it, run the following command:

jag pkg install github.com/toitlang/pkg-http@v2

You can probably just write jag pkg install http, but the full ID together with the version is more explicit, and will make sure you get the right package.

Feel free to use a newer version of the package if one is available. You might need to update the code samples below if you do.

Architecture

The server will be a simple chat server. It will listen for incoming connections and accept messages from clients. It will then broadcast the messages to all connected clients.

We will implement two endpoints:

  • / will serve a simple HTML page with a chat window.
  • /ws will be the WebSocket endpoint for the chat.

For simplicity we will use chat.openai.com to create web page for us. The HTML/JavaScript below was created by asking chat.openai.com with the following prompt.

Create a simple, but stylish web-page for a chat-server with the following end-points:
- `/` serves the page you provide.
- `/ws`is the Websocket endpoint.

Each message sent to through the Websocket should be a simple text message.
Each message received from the Websocket is also text and should be displayed.
The server broadcasts all messages to all connected clients (including the
sender).

Messages should be sent when the user presses Enter.

The following HTML and JavaScript was generated by chat.openai.com. It is not vetted by us. Use at your own risk.

<!DOCTYPE html>
<html>
<head>
    <title>Chat Server</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #F5F5F5;
            color: #333;
            padding: 10px;
        }
        #messages {
            height: 70vh;
            border: 1px solid #ddd;
            padding: 10px;
            border-radius: 5px;
            overflow-y: auto;
            margin-bottom: 10px;
            background-color: #fff;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        #input {
            width: 100%;
            height: 30px;
            padding: 5px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>Chat Server</h1>
    <div id="messages"></div>
    <input id="input" type="text" placeholder="Type your message and hit Enter...">
<script>
    var ws = new WebSocket('ws://' + window.location.host + '/ws');
    var messages = document.getElementById('messages');
    var input = document.getElementById('input');

    ws.onmessage = function(event) {
        var message = document.createElement('p');
        message.textContent = event.data;
        messages.appendChild(message);
        messages.scrollTop = messages.scrollHeight;
    };

    input.addEventListener('keydown', function(event) {
        if (event.key === 'Enter') {
            ws.send(input.value);
            input.value = '';
        }
    });
</script>
</body>
</html>

Store the content of this page in index.toit as a string constant:

INDEX-HTML ::= """
<!DOCTYPE html>
...
</html>
"""

If you change the HTML make sure to escape the $ character in the string which would be interpreted as a variable otherwise. See Toit's string interpolation.

Code

We can now implement the actual server.

Create a new file called server.toit and watch it with Jaguar.

Insert the following code:

import http
import net
import .index

main:
  network := net.open
  server-socket := network.tcp-listen 0
  port := server-socket.local-address.port
  print "Listening on http://$network.address:$port/"

  clients := []
  server := http.Server --max-tasks=5
  server.listen server-socket:: | request/http.RequestIncoming response-writer/http.ResponseWriter |
    if request.path == "/" or request.path == "/index.html":
      response-writer.headers.add "Content-Type" "text/html"
      response-writer.out.write INDEX-HTML
    else if request.path == "/ws":
      web-socket := server.web-socket request response-writer
      clients.add web-socket
      while data := web-socket.receive:
        clients.do: it.send data
      clients.remove web-socket
    else:
      response-writer.write-headers http.STATUS-NOT-FOUND --message="Not Found"

We start by creating a new server on an ephemeral port. We then print the port number so that we can connect to it.

After initializing the client list, we create a new http.Server instance. We set the max-tasks to 5. This means that the server will handle at most 5 requests concurrently. If more requests arrive, they will be queued until a task becomes available. On a normal desktop computer, this number can be much higher, but on a microcontroller, we need to be more careful.

The listen callback simply dispatches on the path:

  • / or /index.html for the index page that we created above.
  • /ws for the WebSocket endpoint.

The websocket branch takes the existing request and upgrades it to a WebSocket. After that, we can use the WebSocket to send and receive messages.

Conclusion

We have now created a simple chat server that can be accessed from a web browser. We also demonstrated how to upgrade a regular HTTP connection to a WebSocket connection, and showed how to send and receive messages.