看到google 提交了http3 over QUIC 的标准化草案, 才发现2012年我在 UDP可靠传输那些事 https://blog.csdn.net/danscort2000/article/details/8432778 一文中给出的那些痛点,居然给出了解决方法, 最大的局限就是我当时说的, sendto 和recvfrom 函数, 2014年Linux核心添加了 recvmmsg 和 sendmmsg 函数,这样就和tcp send/recv函数一样,一次调用就可以读写多个数据包,解决了用户态和内核态之间频繁切换的问题, (但是, windows 目前还没有支持这两个函数)这样,如果只是单纯的做发包测试或者收报测试,运行在应用层[测试的时候优先级为默认]默认16包下,吞吐提升我估计大概会达到 16 *2 /3 也就是原先的10倍多一点,如果整合到具体的可靠传输协议中进行测试[包含包头的校验等一切算法], 按照当时的经验, 性能提升[也是按16包默认缓冲]估计会在35%左右,而google 的 QUICK 正是基于这两个recvmmsg sendmmsg 实现了高效率的UDP可靠传输.
QUIC简化了握手过程,包括TLS的握手过程,这样在处理海量的短连接的时候,性能优势是非常明显的 [请注意,是海量+短连接,要同时满足这两个条件] , 下面是QUIC旧版本的实现部分包头内容分解,说句老实话, 象QUIC这种UDP可靠传输,还是应该使用纯C来写更简洁明了(虽然我大部分时间也是用c++,但是这种协议层的实现确实应该使用纯C, c++ try catch不到位的话,很容易引发崩溃等各种漏洞或者攻击),这种c++写法我个人无法接受,当然google为了照顾go语言等面向对象的需要,可能就优先考虑c++了,但是真的看着挺乱的, 但是这里可能有问题,使用特制版的Linux使用RAW模式,应该比较容易发起性能攻击,这可能比TCP的DDOS更严重,因为UDP模式下,要伪造包太容易了, 攻击发起方需要的电脑数量比TCP少几何级,只是一种猜测,我没做测试,但是由于这种短握手机制,性能攻击虽然不是漏洞,但至少是缺陷,而且防火墙都不好处理.
QUIC_EXPORT_PRIVATE size_t
GetPacketHeaderSize(QuicVersion version,
QuicConnectionIdLength connection_id_length,
bool include_version,
bool include_diversification_nonce,
QuicPacketNumberLength packet_number_length);
// Index of the first byte in a QUIC packet of encrypted data.
QUIC_EXPORT_PRIVATE size_t
GetStartOfEncryptedData(QuicVersion version, const QuicPacketHeader& header);
QUIC_EXPORT_PRIVATE size_t
GetStartOfEncryptedData(QuicVersion version,
QuicConnectionIdLength connection_id_length,
bool include_version,
bool include_diversification_nonce,
QuicPacketNumberLength packet_number_length);
struct QUIC_EXPORT_PRIVATE QuicPacketPublicHeader {
QuicPacketPublicHeader();
QuicPacketPublicHeader(const QuicPacketPublicHeader& other);
~QuicPacketPublicHeader();
// Universal header. All QuicPacket headers will have a connection_id and
// public flags.
QuicConnectionId connection_id;
QuicConnectionIdLength connection_id_length;
bool reset_flag;
bool version_flag;
QuicPacketNumberLength packet_number_length;
QuicVersionVector versions;
// nonce contains an optional, 32-byte nonce value. If not included in the
// packet, |nonce| will be empty.
DiversificationNonce* nonce;
};
// Header for Data packets.
struct QUIC_EXPORT_PRIVATE QuicPacketHeader {
QuicPacketHeader();
explicit QuicPacketHeader(const QuicPacketPublicHeader& header);
QuicPacketHeader(const QuicPacketHeader& other);
QUIC_EXPORT_PRIVATE friend std::ostream& operator<<(
std::ostream& os,
const QuicPacketHeader& s);
QuicPacketPublicHeader public_header;
QuicPacketNumber packet_number;
};
至于QUIC 包头的实现,几乎是我当年编写的UDP可靠传输的翻版, QUIC使用了64位随机数ID作为连接,[我的代码是采用了32位无符号下标加 服务器端和客户端各32位随机数组成的加密[一共是128位,但是每包传输只需要用到96位],并在握手的时候又添加了一个可选的2个64位无符号加密数字,加服务器端一共是256位加密数字, 不知道QUIC只使用64位随机数做ID是否能适应未来],
QUIC彻底去掉了基于IP/PORT模式的认证[只基于64位ID随机数],这样就解决了移动端在切换基站导致ip/port发生变化时,可以无缝平滑的切换,而无须重新连接, 因此在移动端平台会有优于TCP的体验,这一点没有任何疑问,因为我当年测试中就已经有这个结果了,当然安全性嘛,这可能还需要经过长时间的验证,网络支付强烈建议不要使用QUIC,任何涉及到金钱或者强加密的内容我建议尽量不要使用QUIC.
QUIC采用了严格的递进packet number来标记发送包的序列号, 和TCP不同, tcp遇到丢包的时候需要重新投递相同标号的包, 这样在tcp收到包的时候,其实是无法区分究竟是原来的包还是重发的包,因此导致了重传时间计算的混淆,而QUIC采用了递进包编号,而包的内容是根据偏移来确定的,也就是offset, 因此如果两个包offset一样,而packet number不同,那么只需要计算packet number 之间的数字就能知道是否重传的包,并统计次数. 这里有个问题哈,个人觉得日后可能会引发安全问题,暂时跳过,但是根据我的经历,这种设计完全是画蛇添足,packet number 应该取消的,而是根据类型使用一个bit直接标记为是原始包还是重发包就足够了,因为如果发生了超过+3packet number, 通常都是网络切换导致的,如果按常规的拥堵算法,会直接导致一个卡顿,而且我们一般的延迟算法,对超过3次延迟普遍都归类到3次,实际并不会增加N倍延迟,而是有个上限的, 这个packet number 我觉得根本就是败笔,耗费了CPU计算资源,占用包头位置,空耗流量,没意义,一个bit足够了.
QUIC采用了stream 来当作整体块内容进行传输 , 这在实现单路连接模拟多路连接应用操作时,比TCP有效,由于tcp使用流模式,前面任何一个块里的片段丢失,会导致整个连接传输受阻,而QUIC 则没有这个问题,因为各个stream是独立的块,任何一块传输完成都可以可以被接收并执行处理.
QUIC 使用了和tcp滑动窗口控制拥塞不同的方法,而且严格禁止丢弃已经接收到的包,只要一个包被ack,那么就肯定被接收了,你不能因为缓冲满了就丢掉包让发送方重发.
QUIC的拥堵算法和TCP基本一样,不过做成了可选项,由于QUIC不是跑在内核层,如果有必要,可以针对无线网络高丢包环境,在QUIC层调整算法来实现更快的重发机制,这在TCP里是无法想像的.
QUIC已经自带加密,并支持TLS.
QUIC优点很多,UDP可靠传输的每个连接资源开销要远小于TCP,特别是处理海量连接的时候,更是明显,但是缺点也很明显,它没有跑在内核层,光靠recvmmsg sendmmsg优化[windows环境还没有这个优化],频繁的切换还是不可避免,而且目前也没有硬件针对QUIC进行优化,因此在处理大流量数据,或者需要更低的CPU开销的话,TCP可能更合适.
QUIC几乎是总结了之前所有UDP可靠传输实现的优缺点后重新造的一个轮子,不过凭借google这棵大树,加上在Linux内核开发的话语权,肯定会得到大范围的推广应用,但是,有很多缺点目前并没有暴露出来,包括安全方面. QUIC并不适合传输明文数据,明文传输使用QUIC是不够安全,必须结合TLS进行加密传输才能基本克服去掉IP/PORT引发的安全问题,目前也不能说用了TLS就绝对安全,因为大家都知道, TLS握手,真正的保密随机数可以说只有一个,现在没问题,不等于将来没问题.
如果你只是使用QUIC代替TCP做在线视频播放,我觉得目前是可行的,无线网络和移动端的实现只需要调整一下拥堵算法就能实现比TCP优秀的多感受[但是问题是,移动端运营商流量计费,如果遇到高丢包环境加错误的拥堵算法,可能会非常悲剧],至于其他方面,如果是网络游戏,那么不妨用QUIC代替其中的TCP,但是要用QUIC完全充当协议,肯定不行,它不是针对实时游戏开发的; 可以非常明显的看出,QUIC是特别针对无线网络的高丢包,ip地址动态切换的环境优化开发的,比较适合手机等无线移动平台的平滑使用,而且ip变化引发的流量损失会非常小,不需要全部重新加载,但是,为了弥补放弃ip/port带来的风险,对数据包采用了强加密强校验,而且这些加密校验都是需要CPU来完成的,没有硬件优化,因此不可避免的会导致移动平台CPU功耗相对TCP会大大增加,续航时间缩短等问题,如何取舍需要根据项目来决定的.
其实, UDP传输的最大优点除了可以实现类似TCP的可靠传输外,还可以支持时效性可靠传输等各种扩展,例如,在游戏开发中,有这样一种数据,希望能在一个短的时间,比如10秒内实现传输,如果没有传输成功,那么就丢弃它,还不能重复,这种实现我在自己的代码里管它叫时效性可靠传输,当然用在web上是没有用的,但是在很多场合,这种传输非常有效; 其次是多点互联,因为ipv6的崛起,完全带给我们不一样的网络环境,和IPV4环境下的多点互联不同[一把泪啊,各种的NAT,各种的穿透失败,除了内网,基本没法用], 在IPV6网络下,多点互联完全没有问题,这将给网络游戏的实现带来完全不同的场景,想像一下,比如网络游戏,本来只能支持30人每房间,现在我们通过互联P2P传输, 将原来服务器要承担的发送30个数据报,变成比如a/B各一个包,然后由A/B各自负责环状传播,这可以大大的节省服务器资源和带宽开销,一个房间支持更多的用户,带来更好的用户体验,但是QUIC并没有实现这个功能,还有其他很多扩展QUIC都没有实现,也许也是它的优点吧,专一实现可靠传输.
QUIC目前并没有普及,覆盖率还非常低,但是确实是一种趋势,但是UDP可靠传输并非万能,能清晰的知道它的优点和缺点,以及它可能的隐患是关键所在.