123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- <!doctype html><!-- This is a valid HTML5 document. -->
- <!-- Screen readers, SEO, extensions and so on. -->
- <html lang="fr">
- <!-- Has to be within the first 1024 bytes, hence before the `title` element
- See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
- <meta charset="utf-8">
- <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
- <!-- The viewport meta is quite crowded and we are responsible for that.
- See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <!-- Required to make a valid HTML5 document. -->
- <title>Server-Sent Events: the alternative to WebSockets you should be using (archive) — David Larlet</title>
- <meta name="description" content="Publication mise en cache pour en conserver une trace.">
- <!-- That good ol' feed, subscribe :). -->
- <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
- <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
- <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
- <link rel="manifest" href="/static/david/icons2/site.webmanifest">
- <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
- <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
- <meta name="msapplication-TileColor" content="#f7f7f7">
- <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
- <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
- <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
- <!-- Documented, feel free to shoot an email. -->
- <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
- <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <script>
- function toggleTheme(themeName) {
- document.documentElement.classList.toggle(
- 'forced-dark',
- themeName === 'dark'
- )
- document.documentElement.classList.toggle(
- 'forced-light',
- themeName === 'light'
- )
- }
- const selectedTheme = localStorage.getItem('theme')
- if (selectedTheme !== 'undefined') {
- toggleTheme(selectedTheme)
- }
- </script>
-
- <meta name="robots" content="noindex, nofollow">
- <meta content="origin-when-cross-origin" name="referrer">
- <!-- Canonical URL for SEO purposes -->
- <link rel="canonical" href="https://germano.dev/sse-websockets/#comments">
-
- <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
-
-
- <article>
- <header>
- <h1>Server-Sent Events: the alternative to WebSockets you should be using</h1>
- </header>
- <nav>
- <p class="center">
- <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
- </svg> Accueil</a> •
- <a href="https://germano.dev/sse-websockets/#comments" title="Lien vers le contenu original">Source originale</a>
- </p>
- </nav>
- <hr>
- <p>When developing real-time web applications, WebSockets might be the first thing
- that come to your mind. However, Server Sent Events (SSE) are a simpler
- alternative that is often superior.</p>
-
- <h2 id="prologue">Prologue<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#prologue"><span class="icon icon-link"></span></a></h2>
- <p>Recently I have been curious about the best way to implement a
- <em>real-time web application</em>. That is, an application containing one ore more components
- which automatically update, in real-time, reacting to some external event.
- The most common example of such an application, would be a messaging service, where we
- want every message to be immediately broadcasted to everyone that is connected,
- without requiring any user interaction.</p>
- <p>After some research I stumbled upon an <a href="https://www.youtube.com/watch?v=n9mRjkQg3VE" target="_blank" rel="nofollow noopener noreferrer">amazing talk by Martin Chaov</a>, which compares Server Sent
- Events, WebSockets and Long Polling. The talk, which is also <a href="https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/#comments-sse-websockets-data-flow-http2" target="_blank" rel="nofollow noopener noreferrer">available as a blog post</a>,
- is entertaining and very informative. I really recommend it.
- However, it is from 2018 and some small things have changed, so I decided to write this article.</p>
- <h2 id="websockets">WebSockets?<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#websockets"><span class="icon icon-link"></span></a></h2>
- <p><a href="https://tools.ietf.org/html/rfc6455" target="_blank" rel="nofollow noopener noreferrer">WebSockets</a> enable the creation of <strong>two-way</strong> <strong>low-latency</strong> communication
- channels between the browser and a server.</p>
-
- <p>This makes them ideal in certain scenarios, like multiplayer games, where the
- communication is <strong>two-way</strong>, in the sense that both the browser and server
- send messages on the channel <strong>all the time</strong>, and it is required that these messages
- be delivered with <strong>low latency</strong>.</p>
- <p>In a First-Person Shooter,
- the browser could be continuously streaming
- the player’s position, while simoultaneously receiving updates on the location of all
- the other players from the server. Moreover, we definitely want
- these messages to be delivered with as little overhead as possible, to avoid
- the game feeling sluggish.</p>
- <p>This is the opposite of the traditional <a href="https://en.wikipedia.org/wiki/Request%E2%80%93response" target="_blank" rel="nofollow noopener noreferrer">request-response model</a> of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP" target="_blank" rel="nofollow noopener noreferrer">HTTP</a>, where
- the browser is always the one initiating the communication, and each message
- has a significant overhead, due to establishing <a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol" target="_blank" rel="nofollow noopener noreferrer">TCP connections</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" target="_blank" rel="nofollow noopener noreferrer">HTTP headers</a>.</p>
- <p>However, many applications do not have requirements this strict.
- Even among real-time applications, <strong>the data flow is usually asymmetric</strong>:
- the server sends the majority of the messages while the client mostly just listens and
- only once in a while sends some updates. For example, in a chat application
- an user may be connected to many rooms each with tens or hundreds of participants.
- Thus, the volume of messages received far exceeds the one of messages sent.</p>
- <h2 id="what-is-wrong-with-websockets">What is wrong with WebSockets<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#what-is-wrong-with-websockets"><span class="icon icon-link"></span></a></h2>
- <p>Two-way channels and low latency are extremely good features. Why bother looking
- further? </p>
- <p>WebSockets have one major drawback: <strong>they do not work on top of HTTP</strong>, at least
- not fully. They require their own TCP connection. They use HTTP only to establish
- the connection, but then upgrade it to a standalone TCP connection on top of
- which the WebSocket protocol can be used.</p>
- <p>This may not seem a big deal, however it means that <strong>WebSockets cannot benefit
- from any HTTP feature</strong>. That is:</p>
- <ul>
- <li>No support for compression</li>
- <li>No support for HTTP/2 multiplexing</li>
- <li>Potential issues with proxies</li>
- <li>No protection from Cross-Site Hijacking</li>
- </ul>
-
- <p>At least, this was the situation when the WebSocket protocol was first released.
- Nowadays, there are some complementary standards that try to improve upon this
- situation. Let’s take a closer look to the current situation.</p>
- <p><strong>Note</strong>: If you do not care about the details, feel free to skip the rest of
- this section and jump directly to <a href="#sse">Server-Sent Events</a> or the <a href="#code">demo</a>.</p>
- <h3 id="compression">Compression<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#compression"><span class="icon icon-link"></span></a></h3>
-
- <p>On standard connections,
- <a href="https://en.wikipedia.org/wiki/HTTP_compression" target="_blank" rel="nofollow noopener noreferrer">HTTP compression</a> is supported by every browser, and is super easy to enable
- server-side. Just flip a switch in your reverse-proxy of choice. With WebSockets
- the question is more complex, because there are no requests and responses, but
- one needs to compress the individual WebSocket frames.</p>
- <p><a href="https://tools.ietf.org/html/rfc7692" target="_blank" rel="nofollow noopener noreferrer">RFC 7692</a>, released on December 2015, tries to improve the situation by
- definining <em>“Compression Extensions for WebSocket”</em>. However, to the best of my knowledge,
- no popular reverse-proxy (e.g. nginx, caddy) implements this, making it impossible
- to have compression enabled transparently.</p>
- <p>This means that if you want compression, it has to be implemented directly in your backend.
- Luckily, I was able to find some libraries supporting RFC 7692. For example,
- the <a href="https://websockets.readthedocs.io/en/stable/extensions.html" target="_blank" rel="nofollow noopener noreferrer">websockets</a> and <a href="https://github.com/python-hyper/wsproto/" target="_blank" rel="nofollow noopener noreferrer">wsproto</a> Python libraries, and
- the <a href="https://github.com/websockets/ws" target="_blank" rel="nofollow noopener noreferrer">ws</a> library for nodejs.</p>
- <p>However, the latter suggests not to use the feature:</p>
- <blockquote>
- <p>The extension is disabled by default on the server and enabled by default on
- the client. It adds a significant overhead in terms of performance and memory
- consumption so we suggest to enable it only if it is really needed.</p>
- <p>Note that Node.js has a variety of issues with high-performance compression,
- where increased concurrency, especially on Linux, can lead to catastrophic
- memory fragmentation and slow performance.</p>
- </blockquote>
- <p>On the browsers side, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/37#networking" target="_blank" rel="nofollow noopener noreferrer">Firefox supports WebSocket compression since version 37</a>.
- <a href="https://chromestatus.com/feature/6555138000945152" target="_blank" rel="nofollow noopener noreferrer">Chrome supports it as well</a>. However, apparently Safari and Edge do not.</p>
- <p>I did not take the time to verify what is the situation on the mobile landscape.</p>
-
- <h3 id="multiplexing">Multiplexing<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#multiplexing"><span class="icon icon-link"></span></a></h3>
- <p><a href="https://tools.ietf.org/html/rfc7540" target="_blank" rel="nofollow noopener noreferrer">HTTP/2</a> introduced support for multiplexing, meaning that multiple request/response
- pairs to the same host no longer require separate TCP connections. Instead, they all share
- the same TCP connection, each operating on its own independent <a href="https://tools.ietf.org/html/rfc7540#section-5" target="_blank" rel="nofollow noopener noreferrer">HTTP/2 stream</a>.</p>
- <p>This is, again, <a href="https://caniuse.com/http2" target="_blank" rel="nofollow noopener noreferrer">supported by every browser</a> and is very easy to
- transparently enable on most reverse-proxies.</p>
- <p>On the contrary, the WebSocket protocol has no support, by default, for multiplexing.
- Multiple WebSockets to the same host will each open their own separate TCP
- connection. If you want to have two separate WebSocket endpoints share their
- underlying connection you must add multiplexing in your application’s code.</p>
- <p><a href="https://tools.ietf.org/html/rfc8441" target="_blank" rel="nofollow noopener noreferrer">RFC 8441</a>, released on September 2018, tries to fix this limitation by
- adding support for <em>“Bootstrapping WebSockets with HTTP/2”</em>. It has been
- <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1434137" target="_blank" rel="nofollow noopener noreferrer">implemented in Firefox</a> <a href="https://chromestatus.com/feature/6251293127475200" target="_blank" rel="nofollow noopener noreferrer">and Chrome</a>. However, as far as I know, no major
- reverse-proxy implements it. Unfortunately, I could not find any implementation in
- Python or Javascript either.</p>
- <h3 id="proxies">Issues with proxies<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#proxies"><span class="icon icon-link"></span></a></h3>
- <p>HTTP proxies without explicit support for WebSockets can prevent unencrypted
- WebSocket connections to work. This is because the proxy will not be able to
- parse the WebSocket frames and close the connection.</p>
- <p>However, WebSocket connections happening over HTTPS should be unaffected by
- this problem, since the frames will be encrypted and the proxy should just
- forward everything without closing the connection.</p>
- <p>To learn more, see <a href="https://www.infoq.com/articles/Web-Sockets-Proxy-Servers/" target="_blank" rel="nofollow noopener noreferrer">“How HTML5 Web Sockets Interact With Proxy Servers”</a>
- by Peter Lubbers.</p>
- <h3 id="hijacking">Cross-Site WebSocket Hijacking<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#hijacking"><span class="icon icon-link"></span></a></h3>
-
- <p>WebSocket connections are not protected by the same-origin policy. This makes
- them vulnerable to Cross-Site WebSocket Hijacking.</p>
- <p>Therefore, <strong>WebSocket backends must check the correctness of the <code>Origin</code> header</strong>,
- if they use any kind of client-cached authentication, such as <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies" target="_blank" rel="nofollow noopener noreferrer">cookies</a> or
- <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication" target="_blank" rel="nofollow noopener noreferrer">HTTP authentication</a>.</p>
- <p>I will not go into the details here, but consider this short example. Assume
- a Bitcoin Exchange uses WebSockets to provide its trading service. When you
- log in, the Exchange might set a cookie to keep your session active
- for a given period of time. Now, all an attacker has to do to steal your precious
- Bitcoins is make you visit a site under her control, and simply open a WebSocket
- connection to the Exchange. The malicious connection is going to be automatically
- authenticated. That is, unless the Exchange checks the <code>Origin</code> header and blocks
- the connections coming from unauthorized domains.</p>
- <p>I encourage you to check out the great article about <a href="https://christian-schneider.net/CrossSiteWebSocketHijacking.html#main" target="_blank" rel="nofollow noopener noreferrer">Cross-Site WebSocket Hijacking</a> by
- Christian Schneider, to learn more.</p>
- <h2 id="sse">Server-Sent Events<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#sse"><span class="icon icon-link"></span></a></h2>
- <p>Now that we know a bit more about WebSockets, including their advantages and shortcomings,
- let us learn about Server-Sent Events and find out if they are a valid alternative.</p>
-
- <p><a href="https://html.spec.whatwg.org/#server-sent-events" target="_blank" rel="nofollow noopener noreferrer">Server-Sent Events</a> enable the server to send low-latency push
- events to the client, at any time.
- They use a very simple protocol that is <a href="https://html.spec.whatwg.org/#server-sent-events" target="_blank" rel="nofollow noopener noreferrer">part of the HTML Standard</a> and <a href="https://caniuse.com/eventsource" target="_blank" rel="nofollow noopener noreferrer">supported by every
- browser</a>.</p>
- <p>Unlike WebSockets, <strong>Server-sent Events flow only one way</strong>: from the
- server to the client. This makes them unsuitable for a very specific set of
- applications, that is, those that require a communication channel that is <strong>both
- two-way and low latency</strong>, like real-time games.
- However, this trade-off is also their major advantage
- over WebSockets, because being <em>one-way</em>, <strong>Server-Sent Events work seamlessly on top of HTTP,
- without requiring a custom protocol</strong>. This gives them automatic access to
- all of HTTP’s features, such as compression or HTTP/2 multiplexing, making them a very convenient choice
- for the majority of real-time applications, where the bulk of the data is sent from the server, and
- where a little overhead in requests, due to HTTP headers, is acceptable.</p>
-
- <p>The protocol is very simple. It uses the <code>text/event-stream</code> Content-Type and
- messages of the form:</p>
- <pre class="language-text"><code class="language-text">data: First message
-
- event: join
- data: Second message. It has two
- data: lines, a custom event type and an id.
- id: 5
-
- : comment. Can be used as keep-alive
-
- data: Third message. I do not have more data.
- data: Please retry later.
- retry: 10</code></pre>
- <p>Each event is separated by two empty lines (<code>\n</code>) and consists of various optional
- fields.</p>
- <p>The <code>data</code> field, which can be repeted to denote multiple lines in the message,
- is unsurprisingly used for the content of the event.</p>
- <p>The <code>event</code> field allows to specify custom event types, which as we will show
- in the next section, can be used to fire different event handlers on the client.</p>
-
- <p>The other two fields, <code>id</code> and <code>retry</code>, are used to configure the behaviour of the <em>automatic
- reconnection mechanism</em>. This is one of the most interesting features of Server-Sent
- Events. It ensures that
- <strong>when the connection is dropped or closed by the server, the client will
- automatically try to reconnect</strong>, without any user intervention.</p>
- <p>The <code>retry</code> field is used to specify the minimum amount of time, in seconds,
- to wait before trying to reconnect.
- It can also be sent by a server, immediately before closing the client’s connection,
- to reduce its load when too many clients are connected.</p>
- <p>The <code>id</code> field associates an identifier with the current event. When reconnecting
- the client will transmit to the server the last seen id, using the <code>Last-Event-ID</code> HTTP header.
- This allows the stream to be resumed from the correct point.</p>
- <p>Finally, the server can stop the automatic reconnection mechanism altogether
- by returning an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204" target="_blank" rel="nofollow noopener noreferrer">HTTP 204 No Content</a> response.</p>
-
- <h2 id="code">Let’s write some code!<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#code"><span class="icon icon-link"></span></a></h2>
-
- <p>Let us now put into practice what we learned.
- In this section we will implement a simple service
- both with Server-Sent Events and WebSockets.
- This should enable us to compare the two technologies. We will find out how easy
- it is to get started with each one, and verify by hand the features discussed
- in the previous sections.</p>
- <p>We are going to use Python
- for the backend, Caddy as a reverse-proxy and of course a couple of lines of
- JavaScript for the frontend.</p>
- <p>To make our example as simple as possible, our backend is just going to consist of
- two endpoints, each streaming a unique sequence of random numbers. They are going to be reachable from
- <code>/sse1</code> and <code>/sse2</code> for Server-Sent Events, and from <code>/ws1</code> and <code>/ws2</code> for
- WebSockets. While our frontend is going to consist of a single <code>index.html</code>
- file, with some JavaScript which will let us start and stop WebSockets and
- Server-Sent Events connections.</p>
- <p><a href="https://github.com/tyrion/sse-websockets-demo" target="_blank" rel="nofollow noopener noreferrer">The code of this example is available on GitHub</a>.</p>
- <h3 id="reverse-proxy">The Reverse-Proxy<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#reverse-proxy"><span class="icon icon-link"></span></a></h3>
-
- <p>Using a reverse-proxy, such as Caddy or nginx, is very useful, even in a small
- example such as this one.
- It gives us very easy access to many features that our backend
- of choice may lack.</p>
- <p>More specifically, it allows us to easily serve static files and automatically
- compress HTTP responses; to provide support for HTTP/2, letting us benefit
- from multiplexing, even if our
- backend only supports HTTP/1; and finally to do load balancing.</p>
- <p>I chose Caddy because it automatically manages for us HTTPS certificates,
- letting us skip a very boring task, especially for a quick experiment.</p>
- <p>The basic configuration, which resides in a <code>Caddyfile</code> at the root of our
- project, looks something like this:</p>
- <pre class="language-text"><code class="language-text">localhost
-
- bind 127.0.0.1 ::1
-
- root ./static
- file_server browse
-
- encode zstd gzip</code></pre>
- <p>This instructs Caddy to listen on the local interface on ports 80 and 443,
- enabling support for HTTPS and generating a self-signed certificate. It also
- enables compression and serving static files from the <code>static</code> directory.</p>
- <p>As the last step we need to ask Caddy to proxy our backend services. Server-Sent
- Events is just regular HTTP, so nothing special here:</p>
- <pre class="language-text"><code class="language-text">reverse_proxy /sse1 127.0.1.1:6001
- reverse_proxy /sse2 127.0.1.1:6002</code></pre>
- <p>To proxy WebSockets our reverse-proxy needs to have explicit support for it.
- Luckily, Caddy can handle this without problems, even though the configuration
- is slighly more verbose:</p>
- <pre class="language-text"><code class="language-text">@websockets {
- header Connection *Upgrade*
- header Upgrade websocket
- }
-
- handle /ws1 {
- reverse_proxy @websockets 127.0.1.1:6001
- }
-
- handle /ws2 {
- reverse_proxy @websockets 127.0.1.1:6002
- }</code></pre>
- <p>Finally you should start Caddy with</p>
- <pre class="language-bash"><code class="language-bash">$ <span class="token function">sudo</span> caddy start</code></pre>
- <h3 id="frontend">The Frontend<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#frontend"><span class="icon icon-link"></span></a></h3>
- <p>Let us start with the frontend, by comparing the JavaScript APIs of WebSockets
- and Server-Sent Events.</p>
- <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Websockets_API" target="_blank" rel="nofollow noopener noreferrer">WebSocket JavaScript API</a> is very simple to use.
- First, we need to create a new
- <code>WebSocket</code> object passing the URL of the server. Here <code>wss</code> indicates that
- the connection is to happen over HTTPS. As mentioned above it is really recommended
- to use HTTPS to avoid issues with proxies.</p>
- <p>Then, we should listen to some of the possible events (i.e. <code>open</code>,
- <code>message</code>, <code>close</code>, <code>error</code>), by either setting the <code>on$event</code> property or
- by using <code>addEventListener()</code>.</p>
- <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> ws <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WebSocket</span><span class="token punctuation">(</span><span class="token string">"wss://localhost/ws"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
-
- ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"WebSocket open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
-
- ws<span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span>
- <span class="token string">"message"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
- <p>The JavaScript API for Server-Sent Events is very similar. It requires us to
- create a new <code>EventSource</code> object passing the URL of the server, and then
- allows us to subscribe to the events in the same way as before.</p>
- <p>The main difference is that we can also subscribe to custom events.</p>
- <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> es <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">EventSource</span><span class="token punctuation">(</span><span class="token string">"https://localhost/sse"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
-
- es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"EventSource open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
-
- es<span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span>
- <span class="token string">"message"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
-
-
- es<span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span>
- <span class="token string">"join"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> joined</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
- <p>We can now use all this freshly aquired knowledge about JS APIs to build our
- actual frontend.</p>
- <p>To keep things as simple as possible, it is going to consist of only one <code>index.html</code>
- file, with a bunch of buttons that will let us start and stop our WebSockets
- and EventSources. Like so</p>
-
- <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>startWS(1)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Start WS1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
- <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>closeWS(1)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Close WS1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
- <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>br</span><span class="token punctuation">></span></span>
- <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>startWS(2)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Start WS2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
- <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>closeWS(2)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Close WS2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span></code></pre>
- <p>We want more than one WebSocket/EventSource so we can test if HTTP/2 multiplexing
- works and how many connections are open.</p>
- <p>Now let us implement the two functions needed by those buttons to work:</p>
- <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> wss <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
-
- <span class="token keyword">function</span> <span class="token function">startWS</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- <span class="token keyword">if</span> <span class="token punctuation">(</span>wss<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span>
-
- <span class="token keyword">const</span> ws <span class="token operator">=</span> wss<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WebSocket</span><span class="token punctuation">(</span><span class="token string">"wss://localhost/ws"</span><span class="token operator">+</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
- ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"WS open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onmessage</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onclose</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token function">closeWS</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span>
-
- <span class="token keyword">function</span> <span class="token function">closeWS</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- <span class="token keyword">if</span> <span class="token punctuation">(</span>wss<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"Closing websocket"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- websockets<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token method function property-access">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token keyword">delete</span> websockets<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span></code></pre>
- <p>The frontend code for Server-Sent Events is almost identical. The only difference
- is the <code>onerror</code> event handler, which is there because in case of error a message
- is logged and the browser will attempt to reconnect.</p>
- <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> ess <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
-
- <span class="token keyword">function</span> <span class="token function">startES</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- <span class="token keyword">if</span> <span class="token punctuation">(</span>ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span>
-
- <span class="token keyword">const</span> es <span class="token operator">=</span> ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">EventSource</span><span class="token punctuation">(</span><span class="token string">"https://localhost/sse"</span><span class="token operator">+</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
- es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"ES open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onerror</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"ES error"</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span>
- es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onmessage</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=></span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span>
-
- <span class="token keyword">function</span> <span class="token function">closeES</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- <span class="token keyword">if</span> <span class="token punctuation">(</span>ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"Closing EventSource"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token method function property-access">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
- <span class="token keyword">delete</span> ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span></code></pre>
- <h3 id="backend">The Backend<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#backend"><span class="icon icon-link"></span></a></h3>
-
- <p>To write our backend, we are going to use <a href="https://www.starlette.io/" target="_blank" rel="nofollow noopener noreferrer">Starlette</a>, a simple async web framework
- for Python, and <a href="https://www.uvicorn.org/" target="_blank" rel="nofollow noopener noreferrer">Uvicorn</a> as the server.
- Moreover, to make things modular, we are going to separate the <em>data-generating process</em>,
- from the implementation of the endpoints.</p>
- <p>We want each of the two endpoints to generate an <em>unique</em> random sequence
- of numbers. To accomplish this we will use the stream id (i.e. <code>1</code> or <code>2</code>) as
- part of the <a href="https://en.wikipedia.org/wiki/Random_seed" target="_blank" rel="nofollow noopener noreferrer">random seed</a>.</p>
- <p>Ideally, we would also like our streams to be <em>resumable</em>. That is, a client
- should be able to resume the stream from the last message it received, in case
- the connection is dropped, instead or re-reading the whole sequence.
- To make this possible we will assign an ID to each message/event, and use it
- to initialize the random seed, together with the stream id, before each message
- is generated.
- In our case, the ID is just going to be a counter starting from <code>0</code>.</p>
-
- <p>With all that said, we are ready to write the <code>get_data</code> function which is
- responsible to generate our random numbers:</p>
- <pre class="language-python"><code class="language-python"><span class="token keyword">import</span> random
-
- <span class="token keyword">def</span> <span class="token function">get_data</span><span class="token punctuation">(</span>stream_id<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> event_id<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">int</span><span class="token punctuation">:</span>
- rnd <span class="token operator">=</span> random<span class="token punctuation">.</span>Random<span class="token punctuation">(</span><span class="token punctuation">)</span>
- rnd<span class="token punctuation">.</span>seed<span class="token punctuation">(</span>stream_id <span class="token operator">*</span> event_id<span class="token punctuation">)</span>
- <span class="token keyword">return</span> rnd<span class="token punctuation">.</span>randrange<span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span></code></pre>
- <p>Let’s now write the actual endpoints.</p>
- <p>Getting started with Starlette is very simple. We just need to initialize
- an <code>app</code> and then register some routes:</p>
- <pre class="language-python"><code class="language-python"><span class="token keyword">from</span> starlette<span class="token punctuation">.</span>applications <span class="token keyword">import</span> Starlette
-
- app <span class="token operator">=</span> Starlette<span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
- <p>To write a WebSocket service both our web server and framework of choice must
- have explicit support. Luckily Uvicorn and Starlette are up to the task,
- and writing a WebSocket endpoint is as convenient as writing a normal route.</p>
- <p>This all the code that we need:</p>
- <pre class="language-python"><code class="language-python"><span class="token keyword">from</span> websockets<span class="token punctuation">.</span>exceptions <span class="token keyword">import</span> WebSocketException
-
- <span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>websocket_route</span><span class="token punctuation">(</span><span class="token string">"/ws{id:int}"</span><span class="token punctuation">)</span>
- <span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">websocket_endpoint</span><span class="token punctuation">(</span>ws<span class="token punctuation">)</span><span class="token punctuation">:</span>
- <span class="token builtin">id</span> <span class="token operator">=</span> ws<span class="token punctuation">.</span>path_params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span>
- <span class="token keyword">try</span><span class="token punctuation">:</span>
- <span class="token keyword">await</span> ws<span class="token punctuation">.</span>accept<span class="token punctuation">(</span><span class="token punctuation">)</span>
-
- <span class="token keyword">for</span> i <span class="token keyword">in</span> itertools<span class="token punctuation">.</span>count<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
- data <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">"id"</span><span class="token punctuation">:</span> i<span class="token punctuation">,</span> <span class="token string">"msg"</span><span class="token punctuation">:</span> get_data<span class="token punctuation">(</span><span class="token builtin">id</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">}</span>
- <span class="token keyword">await</span> ws<span class="token punctuation">.</span>send_json<span class="token punctuation">(</span>data<span class="token punctuation">)</span>
- <span class="token keyword">await</span> asyncio<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span>
- <span class="token keyword">except</span> WebSocketException<span class="token punctuation">:</span>
- <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"client disconnected"</span><span class="token punctuation">)</span></code></pre>
- <p>The code above will make sure our <code>websocket_endpoint</code> function is called every time
- a browser requests a path starting with <code>/ws</code> and followed by a number (e.g. <code>/ws1</code>, <code>/ws2</code>).</p>
- <p>Then, for every matching request, it will wait for a WebSocket connection to be
- established and subsequently start an infinite loop sending random numbers,
- encoded as a JSON payload, every second.</p>
-
- <p>For Server-Sent Events the code is very similar, except that no special
- framework support is needed.
- In this case, we register a route matching URLs starting with <code>/sse</code> and ending
- with a number (e.g. <code>/sse1</code>, <code>/sse2</code>).
- However, this time our endpoint just sets the appropriate headers and returns a <code>StreamingResponse</code>:</p>
- <pre class="language-python"><code class="language-python"><span class="token keyword">from</span> starlette<span class="token punctuation">.</span>responses <span class="token keyword">import</span> StreamingResponse
-
- <span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>route</span><span class="token punctuation">(</span><span class="token string">"/sse{id:int}"</span><span class="token punctuation">)</span>
- <span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">sse_endpoint</span><span class="token punctuation">(</span>req<span class="token punctuation">)</span><span class="token punctuation">:</span>
- <span class="token keyword">return</span> StreamingResponse<span class="token punctuation">(</span>
- sse_generator<span class="token punctuation">(</span>req<span class="token punctuation">)</span><span class="token punctuation">,</span>
- headers<span class="token operator">=</span><span class="token punctuation">{</span>
- <span class="token string">"Content-type"</span><span class="token punctuation">:</span> <span class="token string">"text/event-stream"</span><span class="token punctuation">,</span>
- <span class="token string">"Cache-Control"</span><span class="token punctuation">:</span> <span class="token string">"no-cache"</span><span class="token punctuation">,</span>
- <span class="token string">"Connection"</span><span class="token punctuation">:</span> <span class="token string">"keep-alive"</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
- <span class="token punctuation">)</span></code></pre>
- <p><code>StreamingResponse</code> is an utility class, provided by Starlette, which takes a generator and streams
- its output to the client, keeping the connection open.</p>
- <p>The code of <code>sse_generator</code> is shown below, and is almost identical to the WebSocket
- endpoint, except that messages are encoded according to the Server-Sent Events
- protocol:</p>
- <pre class="language-python"><code class="language-python"><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">sse_generator</span><span class="token punctuation">(</span>req<span class="token punctuation">)</span><span class="token punctuation">:</span>
- <span class="token builtin">id</span> <span class="token operator">=</span> req<span class="token punctuation">.</span>path_params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span>
- <span class="token keyword">for</span> i <span class="token keyword">in</span> itertools<span class="token punctuation">.</span>count<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
- data <span class="token operator">=</span> get_data<span class="token punctuation">(</span><span class="token builtin">id</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span>
- data <span class="token operator">=</span> <span class="token string">b"id: %d\ndata: %d\n\n"</span> <span class="token operator">%</span> <span class="token punctuation">(</span>i<span class="token punctuation">,</span> data<span class="token punctuation">)</span>
- <span class="token keyword">yield</span> data
- <span class="token keyword">await</span> asyncio<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span></code></pre>
- <p>We are done!</p>
- <p>Finally, assuming we put all our code in a file named <code>server.py</code>, we can start
- our backend endpoints using Uvicorn, like so:</p>
- <pre class="language-text"><code class="language-text">$ uvicorn --host 127.0.1.1 --port 6001 server:app &
- $ uvicorn --host 127.0.1.1 --port 6002 server:app &</code></pre>
- <h2 id="bonus">Bonus: Cool SSE features<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#bonus"><span class="icon icon-link"></span></a></h2>
- <p>Ok, let us now conclude by showing how easy it is to implement all those nice features we bragged about earlier.</p>
-
- <p><strong>Compression</strong> can be enabled by changing just a few lines in our endpoint:</p>
- <pre class="language-diff"><code class="language-diff">@@ -32,10 +33,12 @@ async def websocket_endpoint(ws):
- <span class="token unchanged">
- async def sse_generator(req):
- id = req.path_params["id"]
- </span><span class="token inserted-sign inserted">+ stream = zlib.compressobj()
- </span><span class="token unchanged"> for i in itertools.count():
- data = get_data(id, i)
- data = b"id: %d\ndata: %d\n\n" % (i, data)
- </span><span class="token deleted-sign deleted">- yield data
- </span><span class="token inserted-sign inserted">+ yield stream.compress(data)
- + yield stream.flush(zlib.Z_SYNC_FLUSH)
- </span><span class="token unchanged"> await asyncio.sleep(1)
-
-
- </span>@@ -47,5 +50,6 @@ async def sse_endpoint(req):
- <span class="token unchanged"> "Content-type": "text/event-stream",
- "Cache-Control": "no-cache",
- "Connection": "keep-alive",
- </span><span class="token inserted-sign inserted">+ "Content-Encoding": "deflate",
- </span><span class="token unchanged"> },
- )</span></code></pre>
- <p>We can then verify that everything is working as expected by checking the DevTools:</p>
- <p><img class="g-image g-image--lazy g-image--loading" src="data:image/svg+xml,%3csvg%20fill='none'%20viewBox='0%200%20646%20288'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3cdefs%3e%3cfilter%20id='__svg-blur-aa886b223fb216a8551fa2b5df639959'%3e%3cfeGaussianBlur%20in='SourceGraphic'%20stdDeviation='40'/%3e%3c/filter%3e%3c/defs%3e%3cimage%20x='0'%20y='0'%20filter='url(%23__svg-blur-aa886b223fb216a8551fa2b5df639959)'%20width='646'%20height='288'%20xlink:href='data:image/png%3bbase64%2ciVBORw0KGgoAAAANSUhEUgAAAEAAAAAdCAYAAAAaeWr3AAAACXBIWXMAAAsSAAALEgHS3X78AAALF0lEQVRYw7VYaVMiWxLl/3%2bazxMxETOv%2b6ntDooCKrYrrig2IIugIiCyyCKrgDnnZFUh0rTR2jNEXKrqLpl582aezLy2p0ZTavWGVGpPUn2qa9N3tEqt/qZ/uFn9uUIR65vSaLaUDvve0jOepXJFCqVHaXWe5anxOm48n/Q5jhf7SC9fLMljpSrNducN/dF3PrmnOuSpmHuw6LCPY9UhPrbM/YMUIRwHytWadtabzTfCv6eAQqksqXRGkqm00qlhTa5QMhgNzaMCio9luYzG5A48Gy1DwIYKWn%2bXT61eVwVUIF88ca30rQ2y39p4qVxWJWVzOblOZfRguNZQXFtu7zKSfcgP5nO97d8TMzLrcIrT5ZHZxWWxr7hlctYukcStCll%2bxwLYSPzs/Ey%2bTC%2bKd3tX5hbtsuT2yt7RidiXVyUBQbhJnj7neze/y8bOnrjXNmR1fVO%2bTM1J9B1elgIfq08SjUZl9%2bBY9o9OQXtFlt3rMudYlXn7srhAb2phWb7v%2bsTudMk6%2bDg967L2fVc2t3dUpr%2bnZmXObqxbda/JWSgmtnDsSrb3fLKxtSOB4KWc/wjifU/SufyrqY5plukWcKo3Nzfi3fGJD5te/74jJ%2bcXKsgmNnp1c6ebo6UUHyuSvc/J0empOKCc9e19zNmX9P2veQ0sDXxyDw/iD1yI79gve75DCV5GZe/wVOUPhiNy6A/Ij8sY9hGW/cNjjEdwEKdycHwiO75j2cWao9OA9i86HOI7C4qNpkS/qrfaalb6jffhUy5XX310uHHOA8yRrmP5V73ZHvhgfQgX6AJ5WAHHaA162hhrgXdliNfb9ooBDzB1rrEwqzEkr8WbfRwz%2bBKX2qr8GsZ0j%2bjnHLpDLl%2bE9dbEViwWJQ8gGzyhafpVAa0IQflN3yuYfRReAc38Tmfudd4j%2bqzG74eCQY%2bNa%2b7hexmcfgn0ynAbi5aOkxf63/Dgt44b31ybAw3S5rc%2bMTc/xMfgafWNG3t9L5n8bDe5hmyHW%2bJPtKTVeNINEcgaAI8aNF4sFOQyEpUnat3soyk%2b4dQaMNtHAE8OoJNIXstt6k6S19fKoAUgrWM%2bG%2bdVKhWpQpGPpaLcpbPSxilUq1WpY6zRgI9DoKa5plarQdCCzufaJlqJ1gO5OMZ1%2bXxeFWmtaZi8fqcNz7WFUk35h6sj80dt6XcbsrzskuVVj3g2d2R3b0%2b%2bARAnJqbEubYpSwAXJ8YWMcdSCK0jeRWXhaVVmZlbkInJb3J0HpZOp2Mw0Q02VOgylOVaxTy7U4HKC9yhX66teWR63gHabplZBCCvugBsDskVy6pIVSDWZ9JpWcGcNa8XoOfVNcnb1IBXw%2bT1kWaL3jXkn96WrPib8txpSDAYkkOAxvlFEEDjg4BHEovF5EcoLAcAlONTvxz7z3XjqgCcAsfPAkEJXPzQ8Rsgfwu%2bxxN7tYCqWksgEJCTswBonUgAgMvn6dm5xOJX4seTCM/3c8wjvjSbDaVB18o/5GRyelZ29g8g3w9VYi5fUCuweH202VoKCl2A0TOEbkq32zXbs7y8vIi89PW71%2btKv9/T1uv1dIM0Y/rS8/Oz9r9gLtfwm0JxjjWPbsVT5Nouxnug2cd7z%2bTHNXySH9/ZuI50%2bCzDhSxlWjR0Hk5/mNdHm41/2WwWG3mU0R9Nq9czNjXarB9jvCVwC4K2gba9fv8nWtb4KP3RH9cPfkN8muDTxcaN7mFZ5I9%2bNqIiAYbgR%2b1fBIOys3eg%2bcD%2b/qEUK09jmL62DlJbgiITj43NLfEdHMhtNq9r%2bv3%2bQFmWVVxcXMjRiV82kC8sOpbFh/ejkzO4zpmcwe1IYwdx3eneQPz2W5qAog3rfE%2bWzzQbQ0Emew8ETyvxEyQpaxtb8NMLicYSGj8tpsO/l6GTYSyPRCJyAmxIZ5gSV39aQ0XRbI8OD2V9a1e2kCjt7RnJ0ya%2bmdjsHgAP/GeqBCZjISQ6byyg25P/9c9GrdI3eTrDAlvvfFonOdysPkYDuomMqMca75vu0FFfbX1IOK613KmBZOb5uftLeT5tAQSnByQYj8AAKqEDP7XAhbH7vRDC%2bXQfvrdN1CemNM3QNTyPMZ1ARtpURtd0Cb5b7fm5Y9CCDARhPvlNcGaeQFlHaf9ps6Xu0prExOIJoOyTXCUSCHmXMP84zNEvMRQqnU5bE59xYYTh7eb2VraQ9%2b/CpPeBAUmEQSryaSgRYh5QLJXk6upKIgibDLMeFDAB8IqgQgwjP49fJWR7d18ugD%2bHwIZThMuWKqGuiq7WjMToM%2bHul2GQCkhnsigzkxC6JW6PR75Ozmh15XC6JQVwbKN/NNGwvsuMz8javm9vo8LzIn6fy9VtGhbUNpkYmRwVVYMSnE6nVptTMwsyt2DXpGt2wQFeq6hEPaCxLpPfZuQvVIk7AOGWaQWMUjVTAaMy/JEFlHAqyesbVQI7aMZsTFroGjw5nsDYlBLziwBRY03NENJcPzqP7kRl1c10ularqlmTDzNEvltzanAX472scxtmKkw34vqnIR7Wu9E%2bYQEElDZ9UpOd/lgwYuIxrnEtNzwu7o/O40kyLf7sj%2b7ESDIKkr8r66%2bajZlc8uZWbu%2bMMMgTZ5pbgzbz%2bQdJXKfeFYyJCwucwI%2bQ1uRnqNfvUWr%2bFAZN8CN9prVUhlFxlhCCU3pXwFNkgcMD4emXzfS5jgjAUMsNx%2bNxpX%2bFdJmhs4ySOYUirPj4qPcHn0iECnqKmWxOw9f6xib80SX/%2bmtS9pBzXwQj0nsxsrJxYYSh6eY6ISueTbEvOWV6dl4uIomfEiEqgIXN/PyiOD0bKJzmZWIWBZBrXRYWFrX4mgcmOFxesaPYcrs8eokxBTzwnV4oDVads7Nz8nV6Hs2OnOJAvs3b5evEtCw4lsS9uf%2bG72%2bFQfoNy9j73IMuZqjiqbDeZhii2b2XbdK0eVr0UeYTxAFeqPycCneVnoEJTfXXCkCNFlStGHcIRaVR08yScziXVtLRuqCjIZSYQ8tg/kGL4h0BrYSW3PhgnjFIhCwMIMGB9piEMPfWdLcztlEggtYodrAwMpT3Oo8mT0AjbRZWFjb0zPx%2bYGHo62vrmbL0tWiiYpgD6FT6/sC9Xgbre%2bYePtJs9/c5CUeiiP9J44IDJ/hohhxiwGU0rsL/CkWp/Ww2ozRiiONhpMS8aW4NlagaLgcIXx1UduTDKJRBBGJNYkUBSw6etiZAkKVoJlzJZFKvx1K3N%2bI7PMbJV%2bQexRwtRS9YPpMHFIADyetbCF2Xde%2bmrLjXZBJxmrmAFzk5QagxLuaij8nJVTymFyL0/78npuToPPTmkoI1vWaC2NSi3SHLrjX4%2b6JMzS8p3kzDrz0ojpaWnbKEImgFuOBadaFeOJC5%2bQXZPfQLy/Y7gOW3b9PAjkX5z98z4nG79Rb7y9cp0HGK27trWNsH8gNbBtoLIQuLAlV5aqHwpfjPA1qd8YIjFIlpJVb/xYUDTy2B7JERgFdnzOLukDwxfR29EKECeOESRdYZCoWRDYbk%2biYll%2bEwwDaMzPPcuN09PkXGmESGmtQLlmsUalR0IZ%2bXAKIMb4ZDkThA8R6Z5xHkDGr0icaTby5ifssCLGCyLh7ob/Q7MYuO4cuP4cYTYYpMF7AuRPqmXzPPt9a0TCwg6NEK6LPG5UpffZnYoz6vfF%2bUjvozLz26ZvGDd7qGVbBZuEC%2bFiYYmNUdK%2bt7zTaKiu9dfoz7Ea1/Cjtj5jEKWBciH6nW%2bn2DWrP1Wg4Py/URWcdGgT8pJfkjQFoZ2XvzGE5brfanLjSMm6f/z4XIfwERJGHCHQ2p7wAAAABJRU5ErkJggg=='%20/%3e%3c/svg%3e" alt="SSE Compression" data-srcset="https://d33wubrfki0l68.cloudfront.net/d9aa3cb7e8ec5b4cc500b1e2b4c9275c8f73bb7b/3df27/assets/static/sse-compression.0a1763c.a6a3649bb9d1351f6e26788e472622a9.png 646w" data-sizes="(max-width: 646px) 100vw, 646px" data-src="https://d33wubrfki0l68.cloudfront.net/d9aa3cb7e8ec5b4cc500b1e2b4c9275c8f73bb7b/3df27/assets/static/sse-compression.0a1763c.a6a3649bb9d1351f6e26788e472622a9.png"><noscript><img class="g-image g-image--lazy g-image--loaded" src="https://d33wubrfki0l68.cloudfront.net/d9aa3cb7e8ec5b4cc500b1e2b4c9275c8f73bb7b/3df27/assets/static/sse-compression.0a1763c.a6a3649bb9d1351f6e26788e472622a9.png" alt="SSE Compression"></noscript></p>
- <p><strong>Multiplexing</strong> is enabled by default since Caddy supports HTTP/2. We can confirm
- that the same connection is being used for all our SSE requests using the
- DevTools again:</p>
- <p><img class="g-image g-image--lazy g-image--loading" src="data:image/svg+xml,%3csvg%20fill='none'%20viewBox='0%200%20620%20270'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3cdefs%3e%3cfilter%20id='__svg-blur-0a52f18c0d2930b06db6e22b1898fd5c'%3e%3cfeGaussianBlur%20in='SourceGraphic'%20stdDeviation='40'/%3e%3c/filter%3e%3c/defs%3e%3cimage%20x='0'%20y='0'%20filter='url(%23__svg-blur-0a52f18c0d2930b06db6e22b1898fd5c)'%20width='620'%20height='270'%20xlink:href='data:image/png%3bbase64%2ciVBORw0KGgoAAAANSUhEUgAAAEAAAAAcCAYAAADRJblSAAAACXBIWXMAAAsSAAALEgHS3X78AAAMIklEQVRYw61Y%2bW9kxRGevyBkgQ2wC4mU5MdIIVESCIQIBSQkEoGEkKJIiQIhSgSbXfaAECmHsoBAJBG5CBKEcOwuu2vjY%2bzxMb6P8bUeH%2btr7PHMeDwznvO9ua83l79U9XvPHo8PdgFLn7tfd3VVdXdVddUY3N4NhCIyAuEIAqEI/KHwjjYYlg5EKCLB4wsI2pC0zUeHn9uwCqZjWWGmC0WuS041PD6/aJmPv1qO1up9pmG5uoyA0Ffe2i/PG3gDmVyekENOKaBQKkEpFqktI18oIJXJELL7Ip3NQYrGEEskESUwr3yhiGxeEVCKJWrzSKYzRBeHHIsjQvRMw/xZDss7SEYteH0ynUZYjgr%2bCvESLetNbaFKJuuj7ylDurKeUiyBLI3z3gxSNIrWtg509AxgfGoWQ5Yx9A2PYWR8Atari7TBPBLJFBKp9L6I0/z8wjwaTGaMTlhhnZ5GZ08/FuxOjF%2bZhG3VJRSPxpOwLS2iqb0bizY7LONWIYflpjK5A2Vsy0ojFo8TXyvGrbOYmr2KEZI5PDqBURobnZgU/Mat0%2bjq7oN1dp5kXIFlYgpO1xpaOswwdfZgPRCmA8nCEJQkjBGDxtYOvHPuEuqa2tFnGce5CxfROTCGTL5Ap5YSgvcEKRWJJuB2u3G5yYTWjm68/2EdCepBq7kXH9Y1otcygSRtMCzHEAyFhMIXPzLig4uNeJ/kGDv7kcoqB8vREEukhRVN0ib7hsbQbGpHe98Qmlo60NU7gPP1jWg0daF7cASmdjPa6WIv1jehq9%2bCJbqQ9u5emMx98AYlcSkGWZZQKRewWVZQKuZEv1JSRFsq5qEoWYGCBqUa%2bQyKBbq5RAzZTGqLpkjreFyfZ%2bSpH49HUaB%2bJkVuIIURjUpbcnbxPgDxeEzQZzNJanMok761YNkFTTbz5zHu87p8LiP2yjSGqWU/rKsyxm0RTCxLmLKrmF6VMONQMUuw0ve01p91bM9NO2RMrQSIPkKQBQ2vVdfLW30G06nrZMytxQS2ZDg1aP0t/qu7wTqznBmnLGisrPPqTkzXyq7m4dj%2bNjzwqgdfOBnEt14M45tnw7j9TBBHzwRw84kAPv8bP27S2i8%2bHyQ6tc84dNwvvm96NojvvujFV17w45ZTAdxyUsWtp3a2R08HcO9LXtz8rPp943GVD7cs43PH/LiBvm84poLnmJbX1%2bK%2bl1U%2brCO3rO%2bR03vTfhwM97/iwdf/LOPX5%2bKEBPUjuO%2bvMh79ZxQP/E3Gva9IeOgfUTx9Po6f/y%2bG%2b16T8dDrMh55I4qHqT10IkQH4MM36PCefDeOp96L4%2bkLCTz%2bnygJCOLImRBuI9zxvEr3NeLPsn5E/O//i4zvE79vvxTBY2/G8Mi/ovghjT9MePTfUTxGPG45FaINhgQfxtHnQrjrrBc/eD1K%2bsTxk7dieILkPkg633giKOaPnLl2iAO486yEPxmT%2bGNzEneSgve8JuH5ugROEX72dgx/MKZw7IK6uWcuJnCc%2br9tSOLxN2SygBDuPuvD916L4AlS6Kn34zhxKYEfvxnFbae3FfoSWdB3iO7Bv8v4fWNS8H6GDvWn78Rwinj%2bitb9kg7mZVMKv6A%2bfz/1bowOIIjbn1MPgcF9triH6bCO0foniYYP%2b6u/C%2bFWlncmdF0QB3D4ZBj3vCrj7ldkIeCmE3xzQRwm87rjuaDYCN/mzWTurBB/M26nuUPHQ2JjX34hINYwHbsCK8Pucfjk9rq7iI7dTadjFzp6RuXDbnAj4TCNsVzuHzm9vf5wFS92pUPHgypObOvG84efvT4YLLYYBu1FdM6lCGnqFzC8WhDtiLOEYUeJ2jIGVxQM2RXR6hgQY0RvS2CY5kZcFViIflRrmb4aluUk8S6qa6hlvowh6jO9xaGP6f2K6O/io8kbtm/rVEtzrTBU6DlAOYe4FBBAKQugQiihUqBnLBtDVAriwL8KPaNEKwW9CPrWqPXQUxfbRbZZytP/AqFMHwoysSACG17RJ0FiLpeUIYd9Qn4qGoCSS%2b0Wxzp/Rn8GzvTCUlQkBQmRaKSwurKC985fRnNHL65MzeDK9JIgLpUrKFc2t1Aqb4IaSjcVJFMZdPcPwkQJ0PjEBGZtTrGmWFLXFDnlLZSwbFuihOsyzl1qRLPRhPqWTjQ0m9DRO4i6hhZKbkZF4nKR%2bpfqG2BzelU%2bVbKT6ewOPT4NDJwf5ylnX/f64CHkFU4YFPg2AghHIuBUmecP%2bstSHVGuVJDNZuAPBKmluqJmzebmpuCdz2Wx4aeiiAukYAg%2b6vMaj29DfPsDIZIrCV1YPufutX%2bs82dmAbF4grVDuVymrKws2hLdlq60/lekYqJERUU1eIzpkynKAqkgqd1w9RruJ6lmYPpa3nq/eqxqcpdsrk2KxdIufT4JDBKVlHEqLhxOF2EN0VhMswKq1KjltJNvKUuFQ4YqsAxVUDrSBL71KFeDtC5CNxYMBul2w5BpjNektTXcSlS95chaisWCEJ5KJkQ5zRvMkWUUSGYykaA0WSL3KiFNB5vgCrNKJkOS5V1jnxSGGBUWbJJsbiGqm4OEVfsK2ju7YKbiYtgySn45IkrXjNhwdguZjNrGSUkuctrMXTBSUTUwMIArswuUbxfFxtWDyNDNJbFM1aCpsxstpg4YjS2oN7bDTAXKAMnp5HbQQjGhBe1dPbhc/xHmbA5xGdWyZTrIaj0%2bDQx8e6y8a80tysUIWYTP4xZKWqdn0UuBbW5xheJAfs8TZCbMg817hQ5unMpRl8sF/qGF16R1a6GDYNN1r7nQRrz76VAnqFTmMnZw2EIHMIb%2bwWFMUBlrGR1Hd9%2bgKKVXXe4dfFQLiH52FsAuwAJ4E2z%2bvCFFUUQ8UGic5/TTYvOtBo%2bJtbSOrYFNmd1GEcjvWKMfFAdUEWvIBfIaf0VzN9UVcqJlMB/V0jI7eEU199LBPGp1u1YYklTPU6QRN5iifrlc2TNacoDaC%2bIVoKjMypY4kG5WCJs7gh2jQq9EOpP9VBGbeTAvdqfqP132fjoeBAMrz4EmGFb9nw8kmYjD5V6HV3sap2bmhJ/vF705aHJAGxe/yFgxNDyMuSX7DhpWPq8UEIvKsNkdQt7KqkME3hXKO2bmbdjY8MO97oXX56dbjtK8i%2bKTH3aHE4lUZosP/%2bTlca/BOnMVV%2bcXhds4133a/Ob1PYP8hkcpEPIhcEBM0emurzlhbOtEEwUjIyU2VxdWxNxelsDWwwr5N3zoGxwRMaPV1Ibx6fkd9GXtiZ2ZtqKuuQ09ff0i4ens6qcYMIweSoDMZjNaOnvR0tomflIbGrVicnISF%2boa4fGHBT/mwe4xNzuD1o4uSr6GMDkzL377O8hS97UANktmykkIWwA/T%2byj7NM5zT9109vvj5Me9idew9bA39zWulCOlGRZBdoAf/OzJ%2bSJJCorEiqm4XnVXdQ1rEe1/NpEiHX8xImQLMeEEuxHhYIilOKkRk2ISlsKlkpFNWBtBTlFfDM9J1OKoiuhJTVs8lX03Gc6pucAyLKqExLeILf6DW8nWuoY0%2bv82BqFjtpakchpQbVav2uBgd9UiRKPZfsqPWOrIglJUQLCkTZBSUmC4gH7qIdSY/Ec1SRDWQIrFA4FMUf%2buGhbxvzCIvmtawc9t5wcMW9OZFL0zX7OcsL0DLs9PuGGPMatTideJxrjIJ3V%2bMRiCSGPn273ugdX5%2bbhWFsXG6rV79oSIQo%2bMgnhzXNCtLQwj8sNRtRRInKhrgkd5h4sO9wig6tNhhj8vnvW3WiiJKi%2b0YgPzp2HeWBU/Pae1ehVxbIYG7Xgvx9cQluHGW%2b9dx51HxnR3NKCBlMnWlpaUU/xob6hGV19A8TPjMGhIbxNdEt2FyVWBfFScU4wPDhABVU96ptaRbxwUfAsFvbW78BESFgAnTRHWoY4iHAI84s2LC7ZsEop8gL1I5zGam9yLeJkKV6vl6zIIZIptqQ1UihXYwFxcoENCpYc%2bdfolbHTK8Ctz%2bcTspeXl7G0bCeLs2NuYUkUSU6nk6xqRfwUrmeUbEmSFKHXYkNksR5KuvbT7WMtgJmxf2e0E9HjAAdC9v2yiAFlMbaXD/F4XIsBejHF9GUtrlTHAKbTfb68VXiVdozxWparxx19nPXSeXEsEfGhau31%2br6O/wODFRuSjruYwgAAAABJRU5ErkJggg=='%20/%3e%3c/svg%3e" alt="SSE Multiplexing" data-srcset="https://d33wubrfki0l68.cloudfront.net/a214dba7e635b9c1705b811e1f9e4f1ec063ff1d/35a94/assets/static/sse.41b9fa4.1efd0be9ccdca2dac72468578f5a26a7.png 620w" data-sizes="(max-width: 620px) 100vw, 620px" data-src="https://d33wubrfki0l68.cloudfront.net/a214dba7e635b9c1705b811e1f9e4f1ec063ff1d/35a94/assets/static/sse.41b9fa4.1efd0be9ccdca2dac72468578f5a26a7.png"><noscript><img class="g-image g-image--lazy g-image--loaded" src="https://d33wubrfki0l68.cloudfront.net/a214dba7e635b9c1705b811e1f9e4f1ec063ff1d/35a94/assets/static/sse.41b9fa4.1efd0be9ccdca2dac72468578f5a26a7.png" alt="SSE Multiplexing"></noscript></p>
- <p><strong>Automatic reconnection</strong> on unexpected connection errors is as simple as
- reading the <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#last-event-id" target="_blank" rel="nofollow noopener noreferrer"><code>Last-Event-ID</code></a> header in our backend code:</p>
- <pre class="language-diff"><code class="language-diff"><span class="token deleted-arrow deleted">< for i in itertools.count():
- </span><span class="token coord">---</span>
- <span class="token inserted-arrow inserted">> start = int(req.headers.get("last-event-id", 0))
- > for i in itertools.count(start):</span></code></pre>
- <p><em>Nothing has to be changed in the front-end code.</em></p>
- <p>We can test that it is working by starting the connection to one of the SSE
- endpoints and then killing uvicorn. The connection will drop, but the browser
- will automatically try to reconnect. Thus, if we re-start
- the server, we will see the stream resume from where it left off!</p>
- <p>Notice how the stream resumes from the message <code>243</code>. Feels like magic 🔥</p>
- <p><img class="g-image g-image--lazy g-image--loading" src="data:image/svg+xml,%3csvg%20fill='none'%20viewBox='0%200%20836%20728'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3cdefs%3e%3cfilter%20id='__svg-blur-56b8ef60bc5a52f2598b1e8ef1b1a2dc'%3e%3cfeGaussianBlur%20in='SourceGraphic'%20stdDeviation='40'/%3e%3c/filter%3e%3c/defs%3e%3cimage%20x='0'%20y='0'%20filter='url(%23__svg-blur-56b8ef60bc5a52f2598b1e8ef1b1a2dc)'%20width='836'%20height='728'%20xlink:href='data:image/gif%3bbase64%2ciVBORw0KGgoAAAANSUhEUgAAAEAAAAA4CAYAAABNGP5yAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAUoklEQVRo3t1aB3uVVbbOf7iKOqSdnpyck3PSQ8pJ7703IIUUQiAYpINebKhzda4z984zjlewjQURpINUIbRQI4QSkN57r4rvfdc%2bOSGgMwNzQeM9z7Oevb/97br2Wu9615e4nTx5EqdOncLZs2dZnoY8nzx56jHISTV/Z2cnTpw4gQsXLuDcuXM4f/78rypu4O/mzZs4cuQofvjhBzzunxz89u3b6C0/pYCLFy9h%2b/YOXLt2TTXeuXMHP/744yMVmVN%2bZ2hpN67f%2bMlGRPnff//9T9vZJu2Pej8ucXNVXL%2be9Uf167mGuJpYwJXLl3H16jVcv3YVFy5e7O4rz%2bvb2rCjYxs69%2b77yTyPeo9uPSd9HAvcP7fgjfzefvMNTJsxGwu%2bmob//O93ce3KJXyzshUH9u1FVU0NJk0ci6l/m4arly9gxsxZuPPj47kgt54b/CUUIGAov/966w1EREQi1hGNya%2b/iYa6GgQEheDlVyfj3ydNwptvTMb/TH0fo0Y%2bh9LSUvzpz39V41zu8EgV8Lh/P6eASRPG4rXf/wF/%2bdNbGN4yEqlJiSgsqcCHH36I/v37Y9zokRjWPByNQ5rwbHMzxk2c9P9DAafPnFHl5o0bsHffARzavxft23dg66Y2/P4/3sTxEycxf95crFixAouXLMP6Nasw6cWXce7CJSdAPw4X%2bCUVcPbsOVy/fv1fnueRg%2bAvr4Czinf8yLAooa%2bnSHSQcClm7gyL97Y/aGi7f81eqYDHvV6vt4CrV6%2bTil4iM7xESkxafP5i1/NlErMrlMvq%2bfLlK7hx46bqI%2b3n2e/s2QvOMZQLF%2b/tf%2bnSld5tAcL/r127jgMHjmDbtp3o3HMAR46exMGDR7H3u0PYsaOTrHQXtm7djg7W9%2b0/gkOHjmHPnv3YsmU79u07jGPHTuHo0VM4fPg4Ojp2Yzvnkf47duzB/gNHqeBbD2wJv4oCrpJyH%2bahFi78Gh9/9AnmzVuAzz%2bdhtXrNuGzTz/H1CkfYNZXszF37nzsO3gCx3ngNavXYVXrOkz79DPMY/tnn3%2bJla3rOe5zTHnvfcwkWZo7ZwGOHD/TTbV7rQIkChw6eAxt6zdj5TdrsHLlWqxdu4kWsRubN32LDRva%2bbwRmzdvU1ZxlBbSzhtev34LvlneitWr12PZ8tXYsrUDW9hnw4atWLNmA/tvx3fs70q2eikInqNP3%2bBGD2L37gN0hePYv/%2bYKjs7D2Dv3sN8d5jlIezatU/JkSPHVbl79366wFHV/%2bDB43SLQ3ShQ2w7osbs2rUfO3fsVXjQqy1Ass5D9Pkli5dhyZIV9ONdaGUesKV9J%2bbPXYAZX87EokVLeMursHPnXuXvO9ln8ZJvsH7dBrR/u4O33c6EaTdWLP8Gc2bPY74wF8uWLse2jk4Fkr0bA5gFHqNZy2G%2bnD4Ds2fP5SHmY2v7DnWIBQsWo7V1DWbxYAKSAnrbv%2b3AqlVrsXjRYsyZuwgfvv8Rk6fVWLd2PfFirlLWrK/moJ1u1KsV4AqDgvArV7Whjf6%2bes1GpsDtCgc2bqJPb/yWfr6RskGhv7hLR8de5eNb23ehnZYieLFtWyeR/ztl%2btK2h8qSiHHlytXe7wJtBK5585bQrFsJYJvR1raVKL%2bRtzifFvE1I8RyLF22RoXL/Qx9bW1bsHPXdzzgcfr/MSWbNm4lMG5WYNn%2b7a5u338YMvSrKODK1asK7T9hvv/Ht/%2bMTz%2bZhpZhzXjnr1Pw0fsf44MPPsWHlEWLVzE5Oo02usqKb1Zj1syvMG/%2bQnz08adU3iIsXLAAc%2bbMx0y6gESCa9duPHTO8Mu7QFcY3LipnTe9CNOnz6bPL0ND/WAMHToU8%2bcv4YEWYD7blixtJQCeINh1Yh1veh1Do7iJhM2NdJONPPRacofNWzqUC9yP/r06DHbSt7dt78QehjuR7/YdpCusx/aOPfTrvcrHt2/fTbZ3TIXH3bv3qRB5%2bPBJHKLs33%2bUPn9QibwXjBD/7/W5gIsJHuHBljLfX7hoKQ/bocLZZhKbebPnqLaFCxfT5%2bVQJ7Bv734sZ3RoJRtsJWBuaNvId50csxKffTYdK1asYkRY%2bxtSAMOgbHbt2jbMmD6TNFZo70KsI9CJIlpJeWfOmMVI4eT2ezq/Q%2buqNeqgX0yfhS8%2b/wJfL15Oi2nDcoa/RVTW0mWraB0nfjvZ4Jkz51Uyc%2brUORw/fhqnT59neUqxPmk/cuQEDrMun%2bxPnjqrnoUPnDx5hnIWJ06cURRZFHnsmLM8c%2bbcb0cBrnbXhw4p79y5/8PGvR837va70z3u51Lf35QCHuXHjQf9BtCrFPCo5aEt4HH9yenv/mnszFkVBh/Xn%2bAe%2bk9j%2bIV/8hfZ27e/R2/5ud2gOXbLrZ/Wb/K2frbtlrO8573r%2bVbPuW4okxe5ffuW%2btP42QtnVLt867vZQ27cuN7dJnOJXOshV7tKtU7XesIq5ROY%2bs5IKnzx8nXcuuV8J6W0ufpeu8421z5vOsUtPSMTaUkpSEtORVoiy7R0pKWmsZ6s2lKzs53vRJKc71OzstX71PQMpGZkON9Ju3rOdM4jfZPYzufElBTEJyWrMoX9mgrHIz%2brGAlpSYhPTVRlQmoS0tIzkZSWop6T09KQwvlyOX8OJZtSRMmjyDoJnC%2bZc2Vyf7HxCUhMzUBFaQ5GDs5HTCLPwH6xSekoKsjmmhnIyMxEbm4WElKc71K7xM3D4oenStLRJyseT1Zk4WlHGJ5OjEKf/tl8zsETVXnoU5CKPvnJ6JNHyU1Cn/JstqXgydIMtqWosc8E29GnKA1P1hSiD9ufqCnAU6XpeMLTAykZ2aipa0BxcSGe6uuB8sAWGDVmhJlSUGpvQrqlP1L8iliWI9Mq9RJEmRLxFMcm6DRI0HsjnpJBSaO4u3vwcKnIzctFQUk5KqsG4t%2be8UJhihmb/xKFQIsWOr0GjYVWTBhoQ1qUEXlJvphcb4fRwPFeXujr6RQ3vb8NT8fHQpvogFdaHLxtVngH2uHJuiY1HpqYcGj4TpPMPvFR0FBB2qhQaJKiVKmN5vv4COhMRmhjI9kvhv2joUlxcM4oeOt1iImNQzq17YgMh1FnQoJ/Bry07gj2jUCifzbirLwtWxrspn6I8EtGpCUJvkYr3HU6JPj5ItjoAxPH2U0%2bSLaYodUbEBkTi9CwMEQ44lBcWgJPvk%2bOteGNYf2Q6rByPz7ITrQhM96GZIc/EqP9VT063AID3%2bm7xM1uMyM/hpN662A2%2biIsPBL9wvvBZuYkXhoYg6KhN5qho7Z0BjN8HJkwBvSDwT8MBksgDNYg1kNhCKISNDrofe0wcYzBGgqfyDQYA8Ph7esPT86hDU%2bAT3QqDHa%2bi8%2bDIToJmggqNzAY7hpPhEfz9sKCYLLSOqKiEO2IRl%2bNHnFRVmQn%2bMNi8YGXwQid0QQvjRZeOj0VqeNNejvbdEY86a6Hh1b6GNHX26DqIp4Ud41B9dEaTDyLlEa4JThsmDouEu7eWlRUVSOvqARVtXVoGTkCRh8fWEqbYQp3QKfTQm8JgKX/aASOeRchr82GbchrsA//A/xyaxA46h0qwgZDWByslWPgX/cKzPn1LF%2bCObUYfplVCBo3VT0HsK%2blvIXjBsGS3wBLRgU8uH4RzXns%2bPFobhmBSS9Owugxo2DwMSMj3oqX6sMwaVAID2zkzZnU7cmhRQw%2bvupZblb27LphZ2lSYpD2rncyRtulSDdpNBoMSiPe1Ki7lzc0rPtarPCklj28NUqcdaemvalBbXA0tGberMEHHvQpDy21q9GoNr0tGBqaqdqchc%2bc35s35W0yOxdnqVWlL7RUqt4WBE/OLevLexkrIs9Sarv2ZvIxcB/ci8zFZ626RYPas/iz7NNpEV7KKmTPMt5Lq%2b16L/6v6T68sgC93QavdPotO8UTLZPTUpFDcMkrKCRipiEnNxdZlILCQmTl5PIgWgSFhMLf3599ilBbU420zCzWC1BVXYOg4CD4mC3IzS/gPHmorK5GINuiE5JRX1uN33l4ISs7F2aCb1hEJBITE%2bHOzYXQ7fwDAoj8mSgsLuH4PIXSVTW1sFotavyguno0NrdgcGM9KgYMgD0oGEY/K0rKK9SYfO4xMTkZeYVFxIVSFSHC%2bkUggVFK%2bhcUl6KkuIhW4tvDAmw2aAl4Xh4alJRWoqSoDCkMYbGJScjOL0J5//5KIQncaEZWLi1FDxt9NjA0HHHcVGVVFQpLSlExcCDyeOiAIPqwnz%2bfq1BcUox4zhNAhaVSSRUV5QjiuFwqLjY%2bXoXGjKwsePCmwiOjqIBAVA%2bq50EbMLCyEpHR0VRCFpVtRUi/aNQNqkElFTKA8/TnofuFhVPZVtQOYltlFbKyMhEe5UB/1mvY1xEbi6CwfsigwseOHYem4SPR3NSg3KNbAXoCi3e4FtoAgkQoTSnYGx4WDW/aCTBiRiKuupaILLdlpeLErJ9hSJKJfMx%2bdAVxHxMCgoO7%2buuUeQoii4vJ7btMWrkSzVjE6GvuXkfaBQ/6ejlNWMbb7IEwhwTjGUaAvryAvn5meATaoAkJJIDa4c7IZbMHqP4yn5i7jJM9yFqudh337nKvuy4QbIJ7jQZ9m3nwZzVwL/OGT6UFyfFpDC3ae8BGkFXv64fawY14dsRzqKH5j50wUYWhusFNGNEyHKPHP4/qmkoe2hfDR4xQ/ZqahtCN8jC0eTiebWlBfUO9woXs/GKMJdAVlVWgeXgzqmtrMXhIk%2brXNHQo60OoWF%2bkk%2bREFOfDIzsJuqwUaGqKoM1JhqYsF5oKumVxBi11AExUpBzKFeJ6gp2rru1Rd1qAHy0glLfiz5c%2bIgboA4n%2bVvtdTXWJE6CMsPJGrNR4BE00hqYsIGMNoJ/T5CIdMbAHBqlQE8VYHUOWFhAUCD39Tnw%2bOiYGflYr%2bvyuL%2bLIGOPi4tS88i4kPFyVMkdktEONF%2bAKpqnbw8OUBXgEB6jb9/ChpfgSgO3%2b8KJI/57gdvfS/vGzm97mA89sHYGQJltMM8ykGSaY4G%2bleRkM3TfvQmeJDiVl5YqmRjhiiQ1Jit4mEmiiuHE5QBBdQFyljLfSLzKSinJQKYFUlF7Vo2Pi2NeBFFJuR2w8KXIqAokT4lrJBOKYuHjiSyKCQ0ORRCA2%2bfpyXZIZob%2bk4A76eWRUNMdz3YQEhHBsBNt8zGYSL8PPHvrviZs%2biC5QyVBXS2Hpnu0NU7kFSXGpDDf3uoBGr4eP1YaXJ0/G5NdfJ9hUE9FzCG4VyMjJp0vU4o233sbg%2bkGKpJRWDFAo3tLSTIvyh39QKCZMnIhXX38TLzw/EfX1teTlWcgpLCHw1aFxaDNGjBzFyJOnkH0IXWf02PEIDQ1BREy8co3y/gMxZtxYDKqvQzpzkmpylvHPT8C48RORnJIMXz9Lt%2bU%2bkAIMNCONlQO0wo6oOV/xE4r%2brjmJObs0KpO74q4AoICemYcTMiJt/nSFoJAQ1VeUJwAk/Ty7AElitbiQiaAp84hrabr4h7yT0lun635%2biq4i4GniwYy0BJlLxkgp6yl%2bwLnMtEzZW0%2bEfygLcG%2bkDOXBSr1hKDMjul8sb1F33wAjNDRtYWry/3ui/dpB1YzZhchnjC0uLUN2Xj7qG5tQQfMXVykhQDYNG44kmq8PNylhbGBVDcoZyqoYvgqKilWiVFhUQCAdQvobBVtwGPozrBaVlClQbXl2OAYMHIDS8gG8%2bcFoaGhAbUMj0tPTfgJud/38LlvU9%2bD%2b94MjkyHy60Qyq3iGpCiGvlAdjHHk4qER3czsLgAaqG2LSj/ruAEx75ycLPp9lIrvSfTp9Mxs5JAPRBPAAkPDVJzPpkkHBYfAyJsSsxUMEOKUS7GQPseST5SUl6GxaZiK%2bUbG9nSmr0Jg8opKMWzYMCQnJyGO/CSb5EqwJpNzyhoCqHIYlzW5QquedVf4E0sSZuhiiPeCoJUKiCXlDCH1jONgBylmiBFGSV6Mxp%2bAoJFAExAcqjbuy0gRSMATOi08QMKQuINka2LyVpudGzAo95B2f0YOYZE%2bNGcXD5DNiAm7XCI6LkEpWJQawYRI5hbgFeUFUokClBIlQsnwBIiTUlIUbxDLk9BqDwnnGsEkVzoFprKug3Nm5eQo5WeQLBnuIULBDCVVJBzDvJ1uUOgN30orUhPSlQu4Eg3Xhq1kazX1jRg1aqRKnISeDqS5ilvUkurWNjQpuinj6gY3oJzmPm78BMb55zB8WBPKB1TSjIdi8muvoYhm78%2bQOXLc8xg36jmF9kNbRuHd997DS6%2b8gldefRXTvpyB50aOVFS2qmaQYnljmDDJ/xOPHjOWaw9AckY23p0yBfl5OaisG4xhQxupyERMeOF5rlWPF156BVOnvoepH/0NH055pxsvnAowm9DXRrOwOHmAp5EA5KOhCfkSfDxU8uAEJr26WQllAmKiRWFaEt/9aA3CC3wtzpsVAPXqiiAWWoEgs5FZnYQp5ZusC1%2bPT4hXFiIh0I83Jf3ltmWMiI3KEfeRWwwmpQ2PiICdViD9xQpsAU4wlZxArEuotHAG4RPSJlYiVuhHCeA4a1cf12VK3S0oPAo1vCUP%2bkZYZAwGEZjqeasxJCiSPGTSh4XFhYZxk/YglJaVUsqZLo9CFfMA2ZT4fjHbNLQYBzVfUlZGkpOo6G%2bcfDpjUhNHf5bwVlBUpMzWYHJSXpXHU5EuiuzKAkUxLlrrzOic1Nq76xuA%2bLUrW3T5veudV1e26MQAQ3e7q49rTVGkW3B4JEaNHqnSzBiCTEFhPpOHbKQyKxxEVK6qqlSoLiBktgUSrQvpo3FMdipV9peVk82kJkXFbT2xIrRfFOoHD1Ymq6NPikm%2b%2bPKLyMorYGKUqLI2ySqtdvsDxet7qfg/b39QEQWIVbiF5tciRQiE5O1d4UHlztReXw9PZf6Srnp1aVtcwOu%2bhMO963uBy7RcMVzdjtxsF3dwJVUm5hOaf8LY7ufw/4ja/ssKEAtw1L2IGMZeQXZp7Knd%2b2Pn/TfQs1/Pjd0/pmdMdkWTR3WQ/7MCEhtfRk11JWwEje64/5C38uD9jY/0Fh%2bFC/wvaziYeObNTrkAAAAASUVORK5CYII='%20/%3e%3c/svg%3e" alt="Prova" data-srcset="https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/73e0c/assets/static/sse-auto-reconnect.82a2fbd.b190f19887f331ddb680b6ba6bc4921e.gif 480w, https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/24679/assets/static/sse-auto-reconnect.7900e78.b190f19887f331ddb680b6ba6bc4921e.gif 836w" data-sizes="(max-width: 836px) 100vw, 836px" data-src="https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/24679/assets/static/sse-auto-reconnect.7900e78.b190f19887f331ddb680b6ba6bc4921e.gif"><noscript><img class="g-image g-image--lazy g-image--loaded" src="https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/24679/assets/static/sse-auto-reconnect.7900e78.b190f19887f331ddb680b6ba6bc4921e.gif" alt="Prova"></noscript></p>
- <h2 id="conclusion">Conclusion<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#conclusion"><span class="icon icon-link"></span></a></h2>
-
- <p>WebSockets are a big machinery built on top of HTTP and TCP to provide a set
- of extremely specific features, that is <strong>two-way</strong> and <strong>low latency</strong> communication.</p>
- <p>In order to do that they introduce a number of complications, which end up
- making both client and server implementations more complicated than solutions
- based entirely on HTTP.</p>
- <p>These complications and limitations have been addressed by new specs (<a href="https://tools.ietf.org/html/rfc7692" target="_blank" rel="nofollow noopener noreferrer">RFC 7692</a>, <a href="https://tools.ietf.org/html/rfc8441" target="_blank" rel="nofollow noopener noreferrer">RFC 8441</a>), and
- will slowly end up implemented in client and server libraries.</p>
-
- <p>However, even in a world where WebSockets have no technical downsides, they will
- still be a fairly complex technology, involving a large amount of additional code both on clients and servers.
- Therefore, you should carefully consider if the addeded complexity is worth it,
- or if you can solve your problem with a much simpler solution, such as Server-Sent Events.</p>
- <hr>
- <p>That’s all, folks! I hope you found this post interesting and maybe learned
- something new.</p>
- <p><a href="https://github.com/tyrion/sse-websockets-demo" target="_blank" rel="nofollow noopener noreferrer">Feel free to check out the code of the demo on GitHub</a>, if you want to experiment
- a bit with Server Sent Events and Websockets.</p>
- <p><a href="https://html.spec.whatwg.org/#server-sent-events" target="_blank" rel="nofollow noopener noreferrer">I also encourage you to read the spec</a>, because it surprisingly clear and contains
- many examples.</p>
- </article>
-
-
- <hr>
-
- <footer>
- <p>
- <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
- </svg> Accueil</a> •
- <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
- </svg> Suivre</a> •
- <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
- </svg> Pro</a> •
- <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
- </svg> Email</a> •
- <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
- </svg> Légal</abbr>
- </p>
- <template id="theme-selector">
- <form>
- <fieldset>
- <legend><svg class="icon icon-brightness-contrast">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
- </svg> Thème</legend>
- <label>
- <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
- </label>
- <label>
- <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
- </label>
- <label>
- <input type="radio" value="light" name="chosen-color-scheme"> Clair
- </label>
- </fieldset>
- </form>
- </template>
- </footer>
- <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
- <script>
- function loadThemeForm(templateName) {
- const themeSelectorTemplate = document.querySelector(templateName)
- const form = themeSelectorTemplate.content.firstElementChild
- themeSelectorTemplate.replaceWith(form)
-
- form.addEventListener('change', (e) => {
- const chosenColorScheme = e.target.value
- localStorage.setItem('theme', chosenColorScheme)
- toggleTheme(chosenColorScheme)
- })
-
- const selectedTheme = localStorage.getItem('theme')
- if (selectedTheme && selectedTheme !== 'undefined') {
- form.querySelector(`[value="${selectedTheme}"]`).checked = true
- }
- }
-
- const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
- window.addEventListener('load', () => {
- let hasDarkRules = false
- for (const styleSheet of Array.from(document.styleSheets)) {
- let mediaRules = []
- for (const cssRule of styleSheet.cssRules) {
- if (cssRule.type !== CSSRule.MEDIA_RULE) {
- continue
- }
- // WARNING: Safari does not have/supports `conditionText`.
- if (cssRule.conditionText) {
- if (cssRule.conditionText !== prefersColorSchemeDark) {
- continue
- }
- } else {
- if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
- continue
- }
- }
- mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
- }
-
- // WARNING: do not try to insert a Rule to a styleSheet you are
- // currently iterating on, otherwise the browser will be stuck
- // in a infinite loop…
- for (const mediaRule of mediaRules) {
- styleSheet.insertRule(mediaRule.cssText)
- hasDarkRules = true
- }
- }
- if (hasDarkRules) {
- loadThemeForm('#theme-selector')
- }
- })
- </script>
- </body>
- </html>
|