首页 > 代码库 > 从HTTP 2.0想到的关于传输层协议的一些事

从HTTP 2.0想到的关于传输层协议的一些事

0.HTTP协议的历史

我也不知道...

1.关于HTTP 2.0

收到了订阅的邮件,头版是说HTTP 2.0的内容,我本人不是很关注HTTP这一块儿,但是闲得无聊时也会瞟两眼的。HTTP 2.0的最大改进我觉得有两点:
第一:新增了帧层
帧层的好处在于重新分发流信息,服务器处理顺序可以不再依赖用户提交请求的顺序了。另外就是不必一定用TCP传输HTTP了,实际上规范一开始就是这么说的。
第二:HTTP头的内容可以增量交互了
很多的HTTP头里面的信息都是参数的协商,每次都要携带,如key/value的形式,HTTP 2.0改变了,它只携带新增的或者变化的内容。既然HTTP是基于请求/应答会话的,那么何必不把参数保存在会话中呢?类似SSL/TLS的方式。即便是IPSec这种无连接的协议族,也只是每次传输一个SPI索引而已。

2.不需要设计一种新的传输层协议

在传输层,只有TCP和UDP就够了,如果想扩展,那就在UDP上扩展好了。

3.几个关于TCP和UDP的误区

在协议改造之前,首先要澄清几个误区,那就是你要知道为何要使用UDP而不再使用TCP,或者反过来,你始终认为TCP比UDP好:

误区一:UDP更高效

很多人以为是因为UDP效率高,因为不需要ACK,这最有可能是从教科书上学到的,要么就是老师亲口告诉你的,但是看看你马上就要做的事,让UDP变得可靠,按序到达,那么除非你发明一种不要ACK的按序到达机制,否则你的ACK机制可能比TCP的更低效。

误区二:TCP更加健壮防攻击

这是彻头彻尾的误区,TCP协议头里面有什么保证了安全?校验和吗?大多数情况是IP层先替你挡了一刀而已,如果IP层去掉了防火墙,校验和,手无寸铁的TCP将面对什么!序列号吗?一个32位的数字而已,想让对端接收你伪造的TCP段,你只需要构造一个序列号在合理窗口范围内且匹配五元组的段即可。事实上,TCP的协议规范中从来就没有安全机制,虽然你确实可以在TCP选项中放一些和PKI体系相关的东西。

误区三:TCP和UDP都不安全

这实际上是一个伪命题。问题根本就不在点子上,可能IPv6的出现更加助长了持这种观点的人的威风。TCP也好,UDP也好,只是一个传输层协议,它提供的是端到端的节点通信,在IP之上提供应用的多路复用机制,而已。安全不是它的职责,当然也不是IP层的职责。按照严格分层模型,身份认证属于会话层的职责,而数据完整性则是表示层的职责。

误区四:TCP的低效在于其握手,慢启动等协商反馈机制

相反,基于反馈的慢启动和快速重传/快速恢复都是合理的,它们的目的正是探测端到端路径的传输能力,TCP没有使用显式的NAK机制,而将丢包视为拥塞,将连续收到相同ACK视为丢包,这一环套一环的反馈机制已经是TCP可以使用的最高效的方式了,更高效的方式当然是丢包点或者拥堵点直接发送源抑制反馈信号,但那样会破坏简单性原则。有人为了减少TCP短连接的握手开销,发明了TCP重用,即TCP快速打开机制,使得在SYN包中都可以携带数据,这并不是一种好方式,为何要在相同两台端到端主机之间频繁开启短连接呢?为何不在期间开一条长连接呢,然后将短连接的应用请求附着而上呢?用长连接平滑掉握手开销才是正解,握手是为了协商参数,这点开销都承受不起,免费的午餐一定是剩饭!难道所谓能重用的连接不是以前遗留下来的吗?
       HTTP 1.1支持长连接,但是支持的不够好,每个请求必须按照请求发出的顺序被回应,如果两个请求是关联性很小的请求,第一个请求的处理延迟使得第二个请求被连累,这是伤不起的。这如何解释??个人认为这是传输层协议的使用方式不对的问题而不是协议设计的问题,传输层只是在整个层次上提供了上层的多路复用机制,而这个上层并不是说一定就得是应用层。谁让你把两个GET请求塞到一个TCP连接了啊,好吧,那你可以用两条TCP连接,也是,完全可以用多条TCP连接!但是还是有问题,问题在哪儿呢?我们来看下应用的模式。

4.应用模式的演进

最初的史前年代,是没有所谓的应用的,如何非得说有,那就是两类,一类是文件下载,另一类就是终端登录,看看那些著名的史前协议吧,FTP,Telnet...TCP当时和IP是在一起的,并没有分成两个层级,也没有必要分开,因为不管是文件下载还是登录,都需要严格的数据按序到达,那时的网络其实就是一个数据精确按序到达的网络,但当它们发展到第三个版本的时候,它们分手了,至于说它们为什么分手,说好听点就是分层模型的每一层只负责一类处理的职责原则,说得不那么好听但很容易理解一点就是出现了小三,因为当时出现了一种需求,并不需要提供按序到达的机制,但要提供数据边界,属于一种发后不管的需求,很多的控制报文的投递属于这一种,它们不需要一个长连接,但需要数据边界,即一个报文从哪里开始在哪里结束,这些控制报文本身提供额外的校验机制,并不需要网络的反馈。显然TCP-IP无法满足这些需求,于是IP就纳妾了,从而变成了TCP/IP,实际上是(TCP-UDP)/IP。
       按照抢先性原则,以后的应用要么使用TCP,要么使用UDP,也有很多不需要多路复用的直接使用IP,比如很多路由协议。随着应用的蓬勃发展,一直到HTTP 2.0之前,TCP和UDP都能应对,即便出现了很多的效率问题,但总体上都有解决的方法。但是HTTP 2.0带来一个全新的时代,一切和以前都不同了。
       一个应用要请求一台主机上的很多资源,按照以往对会话的理解,需要建立多个会话,但是为了效率,提倡共享一条TCP连接,HTTP 2.0和以往不同的是,它加入了一个新的帧层,将每一个HTTP数据包封装在一个帧中,每一个帧都携带一个流标号,这就意味着HTTP的回应不必按照请求的顺序处理了,流标号可以识别一个HTTP回应对应哪个请求,然后问题还是存在,不过是出在了TCP层。虽然应用服务器不必再按照请求顺序将处理排队,但是由于TCP连接是严格按序到达的,因此万一有丢包,其后续的数据将全部被阻塞,直到丢包到达,这是TCP一直都存在的颠簸问题,玩过祖马小游戏的都应该体会过。在信号不稳定的无线环境下,一个TCP流的颠簸带来的问题是全面且严重的。HTTP 2.0 over TCP带来的问题是TCP的阻塞代替了应用服务器的阻塞。那么怎么解决呢?

5.会话传输层设想

如果能有一个端到端协议,每个连接占据一个五元组,在该连接上提供帧边界的按序可靠到达,或者在该连接上复用多个流,每个流上按序可靠到达,这不就解决了HTTP 2.0的问题了吗?同时解决的还有端口资源占用的问题,再也不用为端口不够用犯愁了。
       OSI模型中有会话层,但是TCP/IP模型没有,实际上这个层还是必要的,如果HTTP 2.0承载于会话层而不是传输层,那么传输层就可以用轻量级的UDP了,只要在会话层确保每一个会话按序处理即可,如果承载于传输层,那么对于TCP而言,一个WEB应用的所有会话都要共享一个TCP连接,在按序处理上造成了互相牵制。

6.实现思路与措施

不知道你对OpenVPN的Reliable层有没有了解,这是一个活生生的例子,经过我个人的改造,在它之上实现一个基于UDP的多个流多路复用是很简单的,协议封装图如下:
基于Reliable层的stream1----基于Reliable层的stream2----基于Reliable层的stream3
----------------------------------------------------------------------------------------------------------------------------
                          UDP协议(我使用了一对OpenSSL的BIO来代替)
----------------------------------------------------------------------------------------------------------------------------
依照上图,如果stream2中的一个中间数据包没有到达,它只会阻塞stream2的后续数据包向应用提交,而stream1和stream3的数据包即便是在没有到达的stream2的数据包后面到达的,只要是按序的,就可以尽快提交给应用层。我的这个stream的协议头很简单,为了方便就两个字段,一个标识session ID,一个标识长度,其实这个长度也是不需要的,因为Reliable层中本来就有长度。按照这个思想,还有一个引申出的协议,即按照边界标识来提供按序到达的语义,此时协议头中的长度字段就是必须的了,这个引申的意义是,我只要保证协议头中指示的长度为length的这些数据按序到达即可。如果这样,每一个数据包都需要一个序列号,但是只有length范围内的序列号用于按序到达语义,如果序列号落到了length范围内,则按照按序语义处理,否则直接提交给应用。在我的测试中,我没有提供性能优化措施,我只是调通了而已,因为我觉得优化总是有办法的,再说用BIO来模拟丢包也不是很准确。
      之所以使用OpenVPN的Reliable层,不是因为它有多么好,而是我比较熟悉它而已,据我对世界历史的了解,轮子只在美索不达米亚被发明过一次,借用总比重新来一遍要好。整个过程就是对OpenVPN的裁剪过程,最终剩下的C文件是:
buffer.c  error.c  interval.c  list.c  mbuf.c  memcmp.c  otime.c  packet_id.c  reliable.c  schedule.c  session_id.c  ssl.c
相应的H文件和Makefile也要改,我只是在bio_read/write和Reliable层之间加了一个复用的机制而已。BIO真是一个好东西,模拟UDP再好不过了,

6.看到些曙光了吗

写到这里,我有点脱离HTTP 2.0了,其实本文的大部分都不是在说HTTP 2.0,而完全是针对TCP的,HTTP 2.0运行于TCP之上真的不合适了,因为TCP的逻辑太简单又太复杂。说它简单是因为它依然和几十年前一样,仅仅适合于文件传输和远程长连接登录,说它复杂是因为针对这两类需求以及少量的这两类需求之外,TCP衍生了各种复杂的基于反馈的算法,要知道,只要是反馈系统就是复杂的,动物正是有了负反馈系统而和植物区分开来!因此,如果应用逻辑越来越复杂的时候,比如如今的各类WEB应用,TCP能否作为一匹好马就值得怀疑了,HTTP 2.0正是为了应对复杂的WEB应用而出世的,绝不能被TCP牵绊住,总的来讲,TCP太重了,HTTP 2.0同样也不轻,TCP的诸多优化并不是针对WEB的。举一个最简单的例子,TCP的目标是填充带宽延时乘机的管道,为此它和端到端的流控机制有了冲突,接收端只可以接收N个数据,难道发送端只能发N个吗?要知道N个数据还要在网络上跑一会儿呢!所以发送端应该发送的是N*延时个数据,可是如果延时很长的话,你能保证延时期间接收端的接收窗口保持不变吗?特别在复杂WEB应用环境下,这个接收窗口转瞬即变,因此又需要反馈...如果是用于文件传输,就没有问题了,因为文件传输的目的是尽量填满整个带宽,实现最大吞吐量。
       好了,说了这么多TCP怎么不适合HTTP 2.0,那么我的上面的想法就一定好吗?我不敢说,但是我觉得起码解决了几个问题,第一个就是你不用建立多个连接了(这实际上是HTTP 2.0解决的问题,但是本文不限于HTTP 2.0),要知道对于同一个目标,你的机器上的每个IP地址只能使用65535个端口,这不是你的机器的限制,而是协议的限制,在带宽稀缺且数据量不大(载荷率很低)的年代,协议头里面的16位端口字段要比32端口字段节省很多的带宽!也许你会觉得,你在一个端口上复用多个session ID,道理不也一样吗? 难道session ID就不怕浪费吗?是的,它也不是取之不尽的,问题的关键来了,这个关键可能推翻IPv6的糟糕架构!
       问题是你愿意横向扩展还是愿意纵向扩展。我不得不用IP层的一个现实来说明问题,因此我不得不扯到IP层。以IPv4为例,它的地址空间总是被认为有限的,人们一直担心它会耗尽,于是IPv6的思想之一很简单,那就是加大地址空间,一下子变成了128位,说什么地球上每一平方可以拥有多少地址之类的宣传性言语,其无聊性不亚于经济大萧条时美国总统承诺每人锅里面有一只鸡!这么一来IPv6是解决了问题,但是协议栈不兼容了,整个IP层要重写,在重写其间需要截断整个上下层的通信,但是这是不能截断的,因为IP目前基本成了所有通信的必经之路,这个很不像修路时那样,可以向左向右改道,IP实际上是一个双向2车道的路径。
       有没有办法呢?有!那就是LISP的思想,即将IP分成两层,外层标识位置,内层标识设备,这样是不是更好呢?类似IPv6的那种宣传,LISP可以说,每一个位置都可以有2的32次方个IP地址,是不是更诱人呢?毕竟并没有说一个位置有多大!!和IPv6不同,实现这个很简单,只需要提供一个兼容层即可,过渡很方便,如果支持LISP,那就处理,如果不支持,那就剥掉外衣直通。注意,我这里说的可不是标准的LISP,而是基于LISP思想而对IPv4的改进。
       回到TCP/UDP的问题,我们如果采用类似的思想,是不是也不错呢?如果一个session ID为32位,那么一个UDP端口就可以承载2的32次方个session!完全满足了当前复杂的应用需求,应用可以连接同一个UDP端点,但是发送不同的业务数据,每一个业务或者说事务都是session独立的,互不影响。