[Node.js] HTTP2
https://btholt.github.io/complete-intro-to-realtime/intro-to-http2-push
- long-running HTTP call
- HTTP2 PUSH
FE:
async function getNewMsgs() {
let reader;
const utf8Decoder = new TextDecoder("utf-8");
try {
const res = await fetch("/msgs");
reader = res.body.getReader();
} catch (e) {
console.log("connection error", e);
}
let done;
presence.innerText = "🟢";
do {
let readerResponse;
try {
readerResponse = await reader.read();
} catch (e) {
console.error("reader failed", e);
presence.innerText = "🔴";
return;
}
done = readerResponse.done;
const chunk = utf8Decoder.decode(readerResponse.value, { stream: true });
if (chunk) {
try {
const json = JSON.parse(chunk);
allChat = json.msg;
render();
} catch (e) {
console.error("parse error", e);
}
}
console.log("done", done);
} while (!done);
// in theory, if our http2 connection closed, `done` would come back
// as true and we'd no longer be connected
presence.innerText = "🔴";
}
A few things here:
- We're still using fetch, but instead of just saying res.json(), we're opening a readable stream with getReader() and now we can expect multiple responses. Up front we're just logging out the first chunk we get back, but we can now expect that to respond multiple times.
- We're using the green and red circle to show the user if they're still connected to the socket. If it's red, we know we've disconnected. If that happens, you just need to refresh the page. In a production app, you'd just need to reconnect a new socket and keep listening. But you can do that on your own time.
- We need to decode the response that comes over the socket. That's what the utf8Decoder does.
- I'm not making you do the POST again. It's the same logic as last time.
- If you look at the network request in your network console, notice that there isn't a status code or anything. According to the browser, this request is still actually in flight.
BE:
import http2 from "http2";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import handler from "serve-handler";
import nanobuffer from "nanobuffer";
let connections = [];
const msg = new nanobuffer(50);
const getMsgs = () => Array.from(msg).reverse();
msg.push({
user: "brian?",
text: "hi",
time: Date.now(),
});
// openssl req -new -newkey rsa:2048 -new -nodes -keyout key.pem -out csr.pem
// openssl x509 -req -days 365 -in csr.pem -signkey key.pem -out server.crt
// http2 only works over HTTPS
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const server = http2.createSecureServer({
cert: fs.readFileSync(path.join(__dirname, "/../server.crt")),
key: fs.readFileSync(path.join(__dirname, "/../key.pem")),
});
server.on("stream", (stream, headers) => {
const method = headers[":method"];
const path = headers[":path"];
// streams will open for everything, we want just GETs on /msgs
if (path === "/msgs" && method === "GET") {
// immediately respond with 200 OK and encoding
stream.respond({
":status": 200,
"content-type": "text/plain; charset=utf-8",
});
// write the first response
stream.write(JSON.stringify({ msg: getMsgs() }));
// keep track of the connection
connections.push(stream);
// when the connection closes, stop keeping track of it
stream.on("close", () => {
connections = connections.filter((s) => s !== stream);
});
}
});
server.on("request", async (req, res) => {
const path = req.headers[":path"];
const method = req.headers[":method"];
if (path !== "/msgs") {
// handle the static assets
return handler(req, res, {
public: "./frontend",
});
} else if (method === "POST") {
// get data out of post
const buffers = [];
for await (const chunk of req) {
buffers.push(chunk);
}
const data = Buffer.concat(buffers).toString();
const { user, text } = JSON.parse(data);
msg.push({
user,
text,
time: Date.now(),
});
// all done with the request
res.end();
// notify all connected users
connections.forEach((stream) => {
stream.write(JSON.stringify({ msg: getMsgs() }));
});
}
});
// start listening
const port = process.env.PORT || 8080;
server.listen(port, () =>
console.log(
`Server running at https://localhost:${port} - make sure you're on httpS, not http`
)
);