Quic support for node.js

Time:2021-9-13

By James Snell

Crazy technology house

https://www.nearform.com/blog…

In March 2019, by nearform andProtocol LabsI started to implement it for node.jsQuic protocolsupport. This new UDP based transport protocol is intended to eventually replace all HTTP communications using TCP.

Quic support for node.js

People familiar with UDP may be skeptical. As we all know, UDP is unreliable, and packets are often lost, out of order, repeated and so on. UDP does not guarantee the reliability and order supported by TCP, which is strictly required by advanced protocols such as HTTP. That’s where quic came in.

Quic protocol defines a layer above UDP, which introduces error handling, reliability, flow control and built-in security for UDP (through TLS 1.3). In fact, it re implements most TCP effects over UDP, but there is a key difference: unlike TCP, packets can still be transmitted out of order. Understanding this is essential to understand why quic is better than TCP.

Quic eliminates the root cause of team leader blocking

In HTTP 1, all messages exchanged between the client and the server are in the form of continuous and uninterrupted data blocks. Although multiple requests or responses can be sent through a single TCP connection, you must wait for the complete transmission of the previous message before sending the next complete message. This means that if you want to send a 10 megabyte file and then a 2 megabyte file, the former must be completely transferred before you can start the latter. This is the so-called queue head blocking, which is the root cause of a large number of delays and poor use of network bandwidth.

HTTP 2 attempts to solve this problem by introducing multiplexing. HTTP 2 does not stream the request and response as a continuous stream, but divides the request and response into discrete blocks called frames, which can be interleaved with other frames. A TCP connection can theoretically handle an unlimited number of concurrent request and response flows. Although this is feasible in theory, the design of HTTP 2 does not consider the possibility of queue head blocking in TCP layer.

TCP itself is a strictly ordered protocol. Packets are serialized and sent over the network in a fixed order. If a packet fails to reach its destination, the entire packet flow is blocked until the lost packet can be retransmitted. The valid sequence is: send packet 1, wait for confirmation, send packet 2, wait for confirmation, send packet 3. Using HTTP 1, only one HTTP message can be transmitted at any given time. If a single TCP packet is lost, retransmission will only affect a single HTTP request / response stream. However, with HTTP 2, the transmission of an unlimited number of concurrent HTTP request / response streams will be blocked when a single TCP packet is lost. When HTTP 2 communication is carried out through a high latency and low reliability network, the overall performance and network throughput will drop sharply compared with HTTP 1.

Quic support for node.js

In HTTP 1, the request is blocked because only one complete message can be sent at a time.

Quic support for node.js

In HTTP 2, when a single TCP packet is lost or corrupted, the request will be blocked.

Quic support for node.js

In quic, packets are independent of each other and can be sent (or retransmitted) in any order.

Fortunately, with quic, the situation is different. When the data stream is packaged into discrete UDP packets for transmission, any single packet can be sent (or retransmitted) in any order without affecting other sent packets. In other words, the line blocking problem has been solved to a great extent.

Quic introduces flexibility, security and low latency

Quic also introduces many other important features:

  • Quic connections operate independently of the network topology. After the quic connection is established, the source IP address and destination IP address and port can be changed without re establishing the connection. This is particularly useful for mobile devices that often perform network switching (e.g. LTE to WiFi).
  • The default quic connection is secure and encrypted. TLS 1.3 supports direct inclusion in the protocol, and all quic communications are encrypted.
  • Quic adds key flow control and error handling to UDP, and includes important security mechanisms to prevent a series of denial of service attacks.
  • Quic adds support for zero stroke HTTP requests, which is different from HTTP over TCP based TLS. The latter requires multiple data exchanges between the client and the server to establish a TLS session before transmitting HTTP request data. Quic allows the HTTP request header to be sent as part of the TLS handshake, thus greatly reducing the initial delay of a new connection.

Quic support for node.js

Implement quic for node.js kernel

The implementation of quic for the node.js kernel began in March 2019 and was co sponsored by nearform and protocol labs. We use the excellent ngtcp2 library to provide a large number of low-level implementations. Because quic is a re implementation of many TCP features, it is of great significance to node.js and can support more features than the current TCP and HTTP in node.js. At the same time, it hides a lot of complexity from users.

“Quic” module

While implementing the new quic support, we use a new top-level built-inquicModule to expose the API. When this function is implemented in the node.js core, whether this top-level module will still be used will be determined later. However, when using experimental support in development, you canrequire('quic')Use this API.

const { createSocket } = require('quic')

quicThe module exposes an export:createSocketFunction. This function is used to createQuicSocketObject that can be used for quic servers and clients.

All quic’s work is in a separateGitHub repositoryThe library fork is developed in parallel with the node.js master branch. If you want to use a new module or contribute your own code, you can get the source code from there, seeNode.js build description。 However, it is still an ongoing work, and you will encounter bugs.

Create quic server

Quic server is aQuicSocketInstance, configured to wait for the remote client to start a new quic connection. This is done by binding to the local UDP port and waiting for the initial quic packet to be received from the peer. After receiving the quic packet,QuicSocketIt will check whether there is a server that can be used to process the packetQuicSessionObject. If it does not exist, a new object will be created. Once the serverQuicSessionObject is available, the packet is processed and a user supplied callback is invoked. It is important that all the details of quic protocol are handled by node.js internally.

const { createSocket } = require('quic')
const { readFileSync } = require('fs')

const key = readFileSync('./key.pem')
const cert = readFileSync('./cert.pem')
const ca = readFileSync('./ca.pem')
const requestCert = true
const alpn = 'echo'

const server = createSocket({
  //Bind to local UDP 5678 port
  endpoint: { port: 5678 },
  //Create a default configuration for the new quicserver session instance
  server: {
    key,
    cert,
    ca,
    requestCert
    alpn 
  }
})

server.listen()
server.on('ready', () => {
  console.log(`QUIC server is listening on ${server.address.port}`)
})

server.on('session', (session) => {
  session.on('stream', (stream) => {
    // Echo server!
    stream.pipe(stream) 
  })

  const stream = session.openStream()
  stream.end('hello from the server')
})

As mentioned earlier, quic protocol is built-in and requires TLS 1.3 support. This means that each quic connection must have a TLS key and certificate associated with it. Compared with the traditional TCP based TLS connection, quic is unique in that the TLS context andQuicSessionRelated, notQuicSocket。 If you are familiar with node.jsTLSSocketThen you must notice the difference here.

QuicSocket(andQuicSession)Another key difference between node.js and node.js is the existingnet.Socketandtls.TLSSocketDifferent objects,QuicSocketandQuicSessionNone of themReadableorWritableFlow of. That is, an object cannot send or receive data directly to or from a connected peer, so it must be usedQuicStreamObject.

In the above example, aQuicSocketAnd bind it to port 5678 of local UDP. Then tell thisQuicSocketListen for new quic connections to start. onceQuicSocketWhen you start listening, an alarm will be issuedreadyevent.

When a new quic connection is started and the corresponding server is createdQuicSessionObject, ansessionevent. CreatedQuicSessionObject can be used to listen for new client-server initiatedQuicStreamexample.

One of the more important features of quic protocol is that the client can start a new connection with the server without opening the initial flow, and the server can start its own flow without waiting for the initial flow from the client. This function provides many interesting ways to play, which is impossible to provide in HTTP 1 and HTTP 2 in the current node.js kernel.

Create quic client

There is little difference between quic client and server:

const { createSocket } = require('quic')

const fs = require('fs')
const key = readFileSync('./key.pem')
const cert = readFileSync('./cert.pem')
const ca = readFileSync('./ca.pem')
const requestCert = true
const alpn = 'echo'

const servername = 'localhost'
const socket = createSocket({
  endpoint: { port: 8765 },
  client: {
    key,
    cert,
    ca,
    requestCert
    alpn,
    servername
  }
})

const req = socket.connect({
  address: 'localhost',
  port: 5678,
})

req.on('stream', (stream) => {
  stream.on('data', (chunk) => { /.../ })
  stream.on('end', () => { /.../ })
})

req.on('secure', () => {
  const stream = req.openStream()
  const file = fs.createReadStream(__filename)
  file.pipe(stream)
  stream.on('data', (chunk) => { /.../ })
  stream.on('end', () => { /.../ })
  stream.on('close', () => {
    // Graceful shutdown
    socket.close()
  })
  stream.on('error', (err) => { /.../ })
})

For servers and clients,createSocket()Function to create a binding to a local UDP portQuicSocketexample. For quic clients, TLS keys and certificates are required only when client authentication is used.

stayQuicSocketCall onconnect()Method will create a new clientQuicSessionObject and create a new quic connection with the server with the corresponding address and port. TLS 1.3 handshake after starting the connection. After the handshake is completed, the clientQuicSessionObjects emitsecureEvent, indicating that it can now be used.

Similar to the server side, once the client is createdQuicSessionObject, you can usestreamNew events started by listening serverQuicStreamInstance and can callopenStream()Method to start a new stream.

Unidirectional flow and bidirectional flow

be-allQuicStreamInstances are duplex stream objects, which means that they are all implementedReadableandWritableStream node.js API. However, in quic, each flow can be bidirectional or unidirectional.

A two-way flow is readable and writable in both directions, regardless of whether the flow is started by the client or the server. Unidirectional flow can only read and write in one direction. The one-way stream initiated by the client can only be written by the client and read by the server; No data events will be issued on the client. The one-way flow initiated by the server can only be written by the server and read by the client; No data events will be issued on the server.

//Create bidirectional flow
const stream = req.openStream()

//Create one-way flow
const stream = req.openStream({ halfOpen: true })

Whenever a remote peer starts a stream, both the server and the clientQuicSessionObjects are issued to provideQuicStreamObjectstreamevent. It can be used to examine this object to determine its source (client or server) and its direction (one-way or two-way)

session.on('stream', (stream) => {
  if (stream.clientInitiated)
    console.log('client initiated stream')
  if (stream.serverInitiated)
    console.log('server initiated stream')
  if (stream.bidirectional)
    console.log('bidirectional stream')
  if (stream.unidirectional)
    console.log(‘’unidirectional stream')
})

Locally initiated one-wayQuicStreamofReadableEnd in creationQuicStreamObjects are always closed immediately, so data events are never emitted. Similarly, remotely initiated one-wayQuicStreamofWritableThe end will be closed immediately after creation, sowrite()Calls to will always fail.

this is it

From the above example, it is clear that from the user’s point of view, creating and using quic is relatively simple. Although the protocol itself is complex, this complexity will hardly rise to user oriented APIs. The implementation contains some advanced functions and configuration options. These functions and configuration items are not described in the above example. In general, they are largely optional.

The support of HTTP 3 is not described in the example. The implementation of HTTP 3 semantics based on the basic quic protocol is in progress and will be introduced in future articles.

The implementation of quic protocol is far from complete. At the time of writing this article, the IETF working group was still iterating quic specifications. The third-party dependencies we used to implement most quics in node.js were also evolving, and our implementation was far from complete, lacking tests, benchmarks, documents and cases. However, as an experimental new function in node.js V14, this work is being carried out step by step. I hope quic and HTTP 3 support can be fully supported in node.js V15. We hope your help! If you are interested in participating, please contacthttps://www.nearform.com/cont…

Acknowledge

At the end of this article, I would like to thank nearform and protocol labs for their financial support, so that I can devote myself to the implementation of quic. Both companies are particularly interested in how quic and HTTP 3 will develop peer-to-peer and traditional web application development. Once the implementation is nearly complete, I will write another article to explain some wonderful use cases of quic protocol and the advantages of using quic over HTTP 1, HTTP 2, WebSockets and other methods.

James Snell( @jasnell)He is the head of nearform research. The team is committed to researching and developing the main new functions of node.js in terms of performance and security, as well as the progress of the Internet of things and machine learning. James has more than 20 years of experience in the software industry and is a well-known figure in the node.js community. He has been an author, co-author, contributor and editor of several W3C semantic web and IETF Internet standards. He is a core contributor to the node.js project, a member of the node.js Technical Steering Committee (TSC), and once served on the board of directors of the node.js foundation as a representative of the TSC.

Quic support for node.js


This article starts with WeChat official account: front-end pioneer.

Welcome to scan the two-dimensional code to pay attention to the official account, and push you every day to send fresh front-end technical articles.

Quic support for node.js


Welcome to continue reading other high praise articles in this column: