Event loop for HTTP client?

So, I’m trying to understand event loops better. I figured I’d try writing something myself. I’m thinking about rewriting one of my projects. It’s a bot that reacts to notifications. Imagine HTTPS client receiving SSE, basically a long lived HTTP connection that can only receive, and for certain messages it should do some more HTTP calls (preferably in parallel).

Is that a good use case for event loops?

Doesn’t seem like libxev (or libuv) has TLS/HTTP support, so this isn’t super common, is it?

1 Like

Do you want to build an event loop as a part of your web server? That is technically doable, but something that doesn’t make a lot sense from a practical point of view, imo. Libraries like libxev aim to provide general purpose interfaces for using async IO across different platforms and abstract away syscall level stuff.

Check out Karl Seguin’s http.zig which supports concurrent connections and even websockets and SSE.

If you want to learn more about event loops and async IO in general, looking up syscalls like poll and epoll and how sockets work would be a good place to start. I am sure you can easily find videos on YouTube that explain this stuff.

2 Likes

Why wouldn’t it make sense? (I haven’t really explored this area with zig / lowlevel programming yet)

From how I read the question it seemed to me like these notifications might be very rare, from looking at the project you linked, it says that it starts a thread for handling SSE:

Server Side Events can be enabled by calling res.startEventStream(). This method takes an arbitrary context and a function pointer. The provided function will be executed in a new thread, receiving the provided context and an std.net.Stream.

It also says that it is an HTTP/1.1 server, looking at this Using server-sent events - Web APIs | MDN it comes with a warning (which I don’t know whether it would matter in this case):

Warning: When not used over HTTP/2, SSE suffers from a limitation to the maximum number of open connections, which can be especially painful when opening multiple tabs, as the limit is per browser and is set to a very low number (6). The issue has been marked as “Won’t fix” in Chrome and Firefox. This limit is per browser + domain, which means that you can open 6 SSE connections across all of the tabs to www.example1.com and another 6 SSE connections to www.example2.com (per Stack Overflow). When using HTTP/2, the maximum number of simultaneous HTTP streams is negotiated between the server and the client (defaults to 100).


I haven’t worked with SSE so I am not sure about it and the details.

But if I was trying to use it and had the case where I rarely (like every few minutes) want to send a notification to a client, then I would want some kind of lightweight connection, where a single thread can manage multiple connections, because the majority of time those connections aren’t used and it would be a waste to have a thread for every single connection. The part I don’t know is whether that can be done easily.

If I tried I would probably try to use libxev.

libxev has:

Timers, TCP, UDP, Files, Processes. High-level platform-agnostic APIs for interacting with timers, TCP/UDP sockets, files, processes, and more.

So I would think that TCP/UDP support would be enough to use TLS/HTTP on top of that, I think somebody could create a library that pairs well with libxev to use its TCP/UDP support and then implements support for sending HTTP things over that, without the user having to worry about the details, but I don’t know whether somebody has already created that.


I picked up the sentiment of not running Zig servers directly exposed to the internet but instead running them behind something like nginx, but I don’t know where exactly that came from and I am not sure whether this was mostly connected to older posts / advice to not run std.http.Server (whether there have been newer projects/developments that aim at directly creating a production ready server written in Zig).

While http.zig looks good (without trying it), the fact that browsers limit numbers of SSE connections when not using a HTTP/2 connection and that it sounds as if it creates a thread for every SSE connection sounds bad (but maybe the description is just bad and it actually uses an event-loop/thread pool?).

To me the most practical approach would seem to be to give http.zig a chance and I would probably try to use websockets instead of SSE to avoid the issue of having to deal with the limited number of connections.

All of this is from somebody who hasn’t done a lot of web programming the last years (but eventually I will try to use Zig for this when I have a project that goes beyond static sites).

2 Likes

I’m thinking in terms of Rust/Go async style. You linked http.zig, but does it work like an HTTP client?

This is how it is currently implemented in a higher-level language. It connects to an HTTPS server, receives rare events. Each event may contain several work items, which are actually just I/O (several HTTP calls). The app does not serve HTTP.

Let thread=coroutine. The main connection is in a thread reading HTTP, when work comes in, it splits the received work, and sends to multiple threads/ workers.

I read Karl Seguin’s excellent article series about the epoll syscall, so I think I have some understanding of event loops. But I wanted to create something practical.

I’m currently working on a network library with support for plain tcp, udp, http client/server & websocket, dns resolution (using c-ares), all with tls support.
The event loop part is based on a C event loop library I wrote 15 years ago that supports epoll/kqueue/…

The HTTP parser is using llhttp (internally used by nodejs)

Still heavily work in progress though :slight_smile:

4 Likes

Interesting project! Couple of questions:

  1. Do you use nghttp2, or are you implementing your own HTTP2 support?
  2. How are you supporting TLS? Are you using an external library? Which one?

Cheers!

Hey,

The scope of the project is still unclear, but for now I’m focusing on supporting HTTP 1 and websocket (server and client).

TLS is supported through my C library (libapenetwork) using openssl. Ideally I would gradually rewrite part of the C lib in zig to eventually get rid of it.

2 Likes

I mentioned sockets in my original comment and libxev, like any non-blocking IO library would, provides a higher level (relative to directly using syscalls) API for using sockets. However, to use an async IO library directly in a web server, you will still need to implement HTTP on top of it. In addition, you will probably want a scheduler and figure out a way to batch tasks, if the library you are using doesn’t provide these APIs.

I think it would be easier to use an existing framework unless your use case is unique enough to require a lower level solution. Of course, writing some of this lower level stuff is a great way to learn, so if that’s your motivation, go right ahead.

If someone’s looking for a mature web framework with plenty of documentation and a large, active community which just works, I would also suggest checking out frameworks in other languages like JS, Python, and definitely Go. This is not to say to say that Zig frameworks are not ready for this yet, but the ride might be somewhat bumpier (but, imo, that’s where the fun is!).

There are other higher level frameworks that have built upon http.zig. They might be worth checking out.

Also, http.zig definitely uses a thread pool. Karl’s series on this topic is definitely worth a read.

As far as using nginx or any load balancer or reverse proxy, I only know enough to cover my needs. I can only talk about my approach which am not sure would be particularly useful. Hopefully, somewhere with a better understanding of server security can help you out there.

3 Likes

It connects to an HTTPS server, receives rare events.

If the events are rare enough, you might not need an event loop at all. That’s not a reason to not implement something to learn new things, but definitely worth a thought.

Leaving aside all the talk about HTTP and throughput, libxev does provide APIs you can use to send tasks and then wait for them to complete. Honestly, depending on the scope of your project, you could implement a simple non-blocking IO loop yourself. I would refer you to once again to Karl’s blog (I believe he published 8 or 9 articles on this topic) and this TigerBeetle article by a couple of very knowledgeable guys.

Let thread=coroutine. The main connection is in a thread reading HTTP, when work comes in, it splits the received work, and sends to multiple threads/ workers.

I believe you can do this using libxev. I have only glanced at libxev code, but it does provide timer APIs and thread pools. You will need some sort of a scheduler for more efficient thread use but even without that, you can do what I think you are looking to do.

You probably want to check out this video about the internals of Go’s work-stealing scheduler and this blog post about Tokio devs doing something similar.

Imagine HTTPS client receiving SSE, basically a long lived HTTP connection that can only receive, and for certain messages it should do some more HTTP calls (preferably in parallel).

If this service directly interact with another service, why do you want to use SSE? Or even TLS, especially if it’s a personal project and seemingly not exposed to any external services?

2 Likes

Thanks for the links! I’ll read/watch them, but I don’t feel like implementing all this is justified in my case. In any case, thanks for the help.

If this service directly interact with another service, why do you want to use SSE?

Because it’s Mastodon API, they provide streaming notification either in SSE or WebSocket form. And TLS is needed to connect to public endpoints.

You might want to check out std.http.Client.Connection which uses std.http.crypto.tls.Client for pointers regarding TLS. I would encourage you to try putting together bits from http.zig (also JetZig and other frameworks) and stdlib and see where that takes you. However, if this is a critical service and/or you are not comfortable dealing with crypto directly, it’s understandable if you don’t want to go down this route.

On a separate note, I would pick websockets over SSE for your use case (from what I understand about your project), but that might just be me.

Glad I could help.

2 Likes

Absolutely. For example, libevent, the (grand)mother of event loops, provides support for the HTTP (v1) protocol, so that you can implement an HTTP1 server on top of it without a lot of effort. OTOH, HTTP1 is a pretty simple protocol; support for HTTP2 would be a more ambitious proposition.

2 Likes

zzz seems like an interesting project. Documentation is a bit scarce. I don’t know what the library is actually capable of. It does use an async library that supports io_uring among other methods.

3 Likes