当前位置: 首页 > 后端技术 > Node.js

QUIC对Node.js的支持

时间:2023-04-03 16:45:48 Node.js

作者:JamesSnell翻译:疯狂科技之家https://www.nearform.com/blog...2019年3月,在NearForm和ProtocolLabs的支持下,开始为Node.js写代码。js实现QUIC协议支持。这种新的基于UDP的传输协议旨在最终取代所有使用TCP的HTTP通信。熟悉UDP的人可能会持怀疑态度。众所周知UDP是不可靠的,经常出现数据包丢失、乱序、重复等情况,UDP不保证TCP支持的可靠性和有序性,而这些是HTTP等高层协议严格要求的。这就是QUIC的用武之地。QUIC协议在UDP之上定义了一个层,为UDP引入了错误处理、可靠性、流量控制和内置安全性(通过TLS1.3)。它实际上在UDP之上重新实现了TCP的大部分特殊效果,但有一个关键区别:与TCP不同,数据包仍然可以乱序传递。了解这一点对于理解QUIC优于TCP的原因至关重要。QUIC消除了队头阻塞的根本原因在HTTP1中,客户端和服务器之间交换的所有消息都是连续的、不间断的数据块。虽然可以通过单个TCP连接发送多个请求或响应,但在发送下一个完整消息之前必须等待前一个消息的完整传输。这意味着,如果要发送一个10兆字节的文件,然后再发送一个2兆字节的文件,则前者必须完全传输才能启动后者。这被称为线头阻塞,是大量延迟和网络带宽使用不当的根源。HTTP2试图通过引入多路复用来解决这个问题。HTTP2不是将请求和响应作为连续流传输,而是将请求和响应划分为称为帧的离散块,这些块可以与其他帧交错。TCP连接理论上可以处理无限数量的并发请求和响应流。虽然这在理论上是可能的,但HTTP2的设计并未考虑到TCP层的队头阻塞的可能性。TCP本身是一个严格有序的协议。数据包被序列化并以固定顺序通过网络发送。如果数据包未能到达其目的地,则整个数据包流将被阻塞,直到可以重新传输丢失的数据包。一个有效的序列是:发送数据包1,等待确认,发送数据包2,等待确认,发送数据包3....使用HTTP1,在任何给定时间只能传输一个HTTP消息,如果丢失一个TCP数据包,则重传只会影响一个HTTP请求/响应流。但是对于HTTP2,无限数量的并发HTTP请求/响应流的传输将被阻止,而不会丢失单个TCP数据包。当通过高延迟、低可靠性的网络与HTTP2通信时,与HTTP1相比,整体性能和网络吞吐量将急剧下降。在HTTP1中,请求将被阻塞,因为一次只能发送一条完整的消息。在HTTP2中,当单个TCP数据包丢失或损坏时,请求将被阻止。在QUIC中,数据包相互独立,可以按任何顺序发送(或重发)。幸运的是,对于QUIC,情况有所不同。当数据流以离散的UDP数据包传输时,任何单独的数据包都可以按任何顺序发送(或重新发送),而不会影响其他已发送的数据包。换句话说,基本上解决了线路阻塞问题。QUIC引入了灵活性、安全性和低延迟QUIC还引入了许多其他重要特性:QUIC连接独立于网络拓扑运行。建立QUIC连接后,源和目标IP地址和端口都可以更改,而无需重新建立连接。这对于频繁切换网络(例如LTE到WiFi)的移动设备特别有用。默认情况下,QUIC连接是安全和加密的。TLS1.3支持直接包含在协议中,所有QUIC通信都经过加密。QUIC为UDP添加了关键的流量控制和错误处理,并包含重要的安全机制以防止一系列拒绝服务攻击。QUIC增加了对零旅行HTTP请求的支持,这不同于基于TCP的HTTPTLS,后者需要在客户端和服务器之间进行多次数据交换以建立TLS会话,然后才能传输HTTP请求数据,QUIC允许HTTP请求标头作为TLS握手的一部分发送,大大减少了新连接的初始延迟。为Node.js核心实施QUIC为Node.js核心实施QUIC的工作始于2019年3月,由NearForm和ProtocolLabs共同赞助。我们利用优秀的ngtcp2库来提供大量低级实现。因为QUIC是很多TCP特性的重新实现,所以它对Node.js来说是有意义的,并且在Node.js中可以支持比当前TCP和HTTP更多的特性。同时向用户隐藏了很多复杂性。“quic”模块在实现新的QUIC支持的同时,我们正在使用新的顶级内置quic模块公开API。当功能登陆Node.js核心时,是否仍会使用这个顶级模块将在以后确定。但是在开发中使用实验性支持时,您可以将此API与require('quic')一起使用。const{createSocket}=require('quic')quic模块公开了一个导出:createSocket函数。该函数用于创建一个QuicSocket对象实例,供QUIC服务器和客户端使用。QUIC上的所有工作都在一个单独的GitHub存储库中进行,该存储库从Node.jsmaster分支派生并与之并行开发。如果你想使用新模块,或者贡献你自己的代码,你可以在那里获取源代码,请参阅Node.js构建说明。虽然它仍在进行中,但您一定会遇到错误。创建QUIC服务器QUIC服务器是配置为等待远程客户端发起新QUIC连接的QuicSocket实例。这是通过绑定到本地UDP端口并等待从对等方接收初始QUIC数据包来完成的。QuicSocket在收到一个QUIC数据包后,会检查是否有服务端的QuicSession对象可以用来处理这个数据包,如果没有,就会创建一个新的对象。一旦服务器的QuicSession对象可用,就会处理数据包并调用用户提供的回调。这里需要注意的是,处理QUIC协议的所有细节都由Node.js内部处理。const{createSocket}=require('quic')const{readFileSync}=require('fs')constkey=readFileSync('./key.pem')constcert=readFileSync('./cert.pem')constca=readFileSync('./ca.pem')constrequestCert=trueconstalpn='echo'constserver=createSocket({//绑定到本地UDP端口5678endpoint:{port:5678},//用于创建新的QuicServer会话实例默认配置服务器:{key,cert,ca,requestCertalpn}})server.listen()server.on('ready',()=>{console.log(`QUIC服务器正在监听${server.address.port}`)})server.on('session',(session)=>{session.on('stream',(stream)=>{//Echoserver!stream.pipe(stream)})conststream=session.openStream()stream.end('hellofromtheserver')})如前所述,QUIC协议是内置的,需要支持TLS1.3。这意味着每个QUIC连接都必须有一个与之关联的TLS密钥和证书。与传统的基于TCP的TLS连接相比,QUIC的独特之处在于QUIC中的TLS上下文与QuicSession相关联,而不是QuicSocket。如果你熟悉TLSSocket在Node.js中的用法,那么你一定注意到了这里的区别。QuicSocket(和QuicSession)之间的另一个主要区别是,与Node.js公开的现有net.Socket和tls.TLSSocket对象不同,QuicSocket和QuicSession都不是可读或可写流。也就是说,对象不能用于直接向连接的对等方发送数据或从其接收数据,因此必须使用QuicStream对象。在上面的例子中,一个QuicSocket被创建并绑定到本地UDP端口5678。然后这个QuicSocket被告知监听要启动的新QUIC连接。一旦QuicSocket开始侦听,将发出ready事件。当启动新的QUIC连接并创建相应服务器的QuicSession对象时,将发出会话事件。创建的QuicSession对象可用于侦听客户端服务器启动的新QuicStream实例。QUIC协议的一个更重要的特性是客户端可以在不打开初始流的情况下开始与服务器的新连接,并且服务器可以先启动自己的流而无需等待来自客户端的初始流。此功能允许许多非常有趣的游戏玩法,这是当前Node.js核心中的HTTP1和HTTP2无法实现的。创建QUIC客户端QUIC客户端和服务器之间几乎没有区别:const{createSocket}=require('quic')constfs=require('fs')constkey=readFileSync('./key.pem')constcert=readFileSync('./cert.pem')constca=readFileSync('./ca.pem')constrequestCert=trueconstalpn='echo'constservername='localhost'constsocket=createSocket({endpoint:{port:8765},client:{key,cert,ca,requestCertalpn,servername}})constreq=socket.connect({address:'localhost',port:5678,})req.on('stream',(stream)=>{stream.on('data',(chunk)=>{/.../})stream.on('end',()=>{/.../})})req.on('secure',()=>{conststream=req.openStream()constfile=fs.createReadStream(__filename)file.pipe(stream)stream.on('data',(chunk)=>{/.../})stream.on('end',()=>{/.../})stream.on('close',()=>{//优雅关闭socket.close()})stream.on('error',(err)=>{/.../})})对于服务端和客户端,都使用createSocket()函数来创建本地UDP端口上的QuicSocket实例对于QUIC客户端,仅在使用客户端身份验证时才需要提供TLS密钥和证书。在QuicSocket上调用connect()方法将新创建一个客户端QuicSession对象,并在相应的地址和端口上创建与服务器的新QUIC连接。连接启动后会发生TLS1.3握手。当握手完成时,客户端QuicSession对象发出一个安全事件,表明它现在可以使用了。与服务端类似,一旦创建了客户端QuicSession对象,就可以使用流事件监听服务端启动的新QuicStream实例,调用openStream()方法启动新的流。单向和双向流所有QuicStream实例都是双工流对象,这意味着它们实现了可读和可写流Node.jsAPI。然而,在QUIC中,每个流都可以是双向的或单向的。双向流在两个方向上都是可读和可写的,无论流是由客户端还是服务器发起的。单向流只能在一个方向上读写。客户端发起的单向流只能由客户端写入,只能由服务器读取;客户端不会发出任何数据事件。服务器发起的单向流只能由服务器写入,只能由客户端读取;服务器上不会发出任何数据事件。//创建一个双向流conststream=req.openStream()//创建一个单向流conststream=req.openStream({halfOpen:true})每当远程节点启动一个流时,服务器或客户端的QuicSession对象都会发出提供QuicStream对象的流事件。可用于检查此对象以确定其来源(客户端或服务器)及其方向(单向或双向)session.on('stream',(stream)=>{if(stream.clientInitiated)console.log('client启动流')if(stream.serverInitiated)console.log('serverinitiatedstream')if(stream.bidirectional)console.log('双向流')if(stream.unidirectional)console.log(''单向流')})本地发起的单向QuicStream的Readable端总是在创建QuicStream对象时立即关闭,因此永远不会发出数据事件。同样,远程启动的单向QuicStream的Writable端将在创建后立即关闭,因此对write()的调用将始终失败。就是这样。从上面的示例可以清楚地看出,从用户的角度来看,创建和使用QUIC相对简单。尽管协议本身很复杂,但这种复杂性几乎不会上升到面向用户的API。该实现包含一些高级功能和配置选项,这些功能和配置选项在上面的示例中没有说明,并且在通常情况下大部分是可选的。示例中未说明HTTP3支持。在基本QUIC协议实现之上实现HTTP3语义的工作正在进行中,并将在以后的帖子中介绍。QUIC协议的实现远未完成。在撰写本文时,IETF工作组仍在迭代QUIC规范,我们在Node.js中实现大部分QUIC的第三方依赖项正在发展,我们的实现远未完成,缺乏测试、基准、文档和案例.但作为Node.jsv14中的一项实验性新功能,这项工作正在逐步启动。希望Node.jsv15将完全支持QUIC和HTTP3支持。我们需要你的帮助!如果您有兴趣参与,请联系https://www.nearform.com/cont...!致谢在结束本文时,我要感谢NearForm和ProtocolLabs的经济赞助,使我能够全身心投入到QUIC的实施中。两家公司都对QUIC和HTTP3将如何推进点对点和传统Web应用程序开发特别感兴趣。一旦实施接近完成,我将写另一篇文章来解释QUIC协议的一些奇妙用例,以及使用QUIC相对于HTTP1、HTTP2、WebSockets和其他协议的优势。JamesSnell(@jasnell)领导NearFormResearch,该团队致力于研究和开发Node.js在性能和安全性方面的主要新功能,以及物联网和机器学习方面的进步。James在软件行业拥有超过20年的经验,是Node.js社区的知名人物。他是多个W3C语义Web和IETF互联网标准的作者、合著者、贡献者和编辑。他是Node.js项目的核心贡献者,是Node.js技术指导委员会(TSC)的成员,并曾作为TSC代表在Node.js基金会董事会任职。