网络-软件架构设计

概述

网络协议有很多种,但对互联网来说,用的最多的就是HTTP协议。HTTP主要有1.0、1.1、2三个版本,在HTTP之上有HTTPS。
1996年,HTTP1.0协议规范RFC 1945发布;
1999年,HTTP1.1协议规范RFC 2616发布。
2015年,HTTP/2协议规范RFC 7540/7541发布。
HTTP/2还比较新,目前远没有达到普及的程度。在过去的近20年间,主流的协议一直是http1.1。接下来将对HTTP协议的发展脉络进行梳理。

HTTP1.0

HTTP1.0的问题

HTTP协议的基本特点是“一来一回”。什么意思呢户端发起一个TCP连接,在连接上面发一个HTTP Request到服务器,服务器返回一个HTTP Response,然后连接关闭。每来一个请求,就要开一个连接,请求完了,连接关闭。
这样的协议有两个问题:

  1. 性能问题。连接的建立、关闭都是耗时操作。对应一个网页来说除了网页本身的HTML请求,页面里面的JS、CSS、img资源,都是一个个的HTTP请求。现在的互联网上的页面,一个页面上有几十个资源文件是很常见的事。每来一个请求就开一个TCP连接时非常耗时的。虽然可以同时开多个连接,并发的发送请求,但连接数毕竟是有限的。
  2. 服务器推送问题。不支持“一来多回”,服务器无法在客户端没有请求的情况下主动向客户端推送消息。但很多的应用恰恰都需要服务器在某些事情完成后主动通知客户端。
    针对这两个问题,来看HTTP在发展过程中是怎么解决的。

Keep-Alive机制与Content-Length属性

为了解决上面提及的第一个问题,HTTP1.0设计了一个Keep-Alive机制来实现TCP连接的复用。具体来说,就是客户端在HTTP请求的头部加上一个字段Connection:Keep-Alive。服务器收到带有这样字段的请求,在处理完请求之后不会关闭连接,同时在HTTP的Response里面也和加上该字段,然后等待客户端在该连接上发送下一个请求。
当然,这会给服务器带来一个问题:连接数有限。如果每个连接都不关闭的话,一段时间之后,服务器的连接数就耗光了。因此,服务器会有一个Keep-Alive timeout参数,过一段时间之后,如果该链接上上没有新的请求进来,则连接就会关闭。
连接复用之后又产生了一个新问题:以前一个连接就只发送一个请求,返回一个响应,服务器处理完毕,把连接关闭,这个时候客户端就知道连接的请求处理结束了。但现在,即使一个请求处理完了,连接也不关闭,那么客户端怎么知道连接处理结束了呢者说,客户端怎么知道接收回来的数据包是完整的呢br> 答案是在HTTP Response的头部,返回一个Content-Length:xxx的字段,这个字段可以告诉客户端HTTP Response的Body共有多少个字节,客户端接收到这么多字节之后,就知道响应成功接收完毕。

HTTP 1.1

连接复用与Chunk机制

从上面的分析可以看出,连接复用非常有必要,所以到了HTTP 1.1之后,就把连接复用变成了一个默认属性。即使不加Connection:Keep-Alive属性,服务器也会在请求处理完毕之后不关闭连接。除非在请求头部显示地加上Connection:Close属性,服务器才会在请求处理完毕之后主动关闭连接。
在HTTP 1.0里面可以利用Content-Length字段,让客户端判断一个请求的响应成功是否接收完毕。但Content-Length有个问题,如果服务器返回的数据时动态语言生成的内容,则要计算Content-Length,这点对服务器来说比较困难。即使能够计算,也需要服务器在内存中渲染出整个页面,然后计算长度,非常耗时。
为此,在HTTP1.1中引用了Chunk机制(Http Streaming)。具体来说,就是在响应的头部加上Transfer-Encoding:chunked属性,其目的是告诉客户端,响应的Body是分成一块块的,块与块之间有分隔符,所有块的结尾也有一个特殊标记。这样,即使没有Content-Length字段,也能方便客户判断出响应的末尾。
下面显示了一个简单的具体Chunk机制的HTTP响应,头部没有Content-Length字段,而是Transfer-Encoding:chunked字段。该响应包含4个chunk,数字25(16机制)表示第一个chunk的字节数,1C(16进制)表示第二个chunk的字节数…最后的数字0表示整个响应的末尾。

Pipeline与Head-of-line Blocking问题

有了“连接复用”之后,减少了建立连接、关闭连接的开销。但还存在一个问题,在同一个连接上,请求是串行的,客户端发送一个请求,收到响应,然后发送下一个请求,再收到响应。这种串行的方式导致并发度不够。
为此,HTTP1.1引入了Pipeline机制。在同一个TCP连接上面,可以在一个请求发出去之后、响应没有回来之前,就可以发送下一个、再下一个,这样就提高了在同一个TCP连接上面的处理请求的效率。如下图所示,展示了在同一个TCP连接上面,串行和Pipeline的对比。

网络-软件架构设计
接下来从最基础的对称加密讲起,一步步分析SSL/TLS背后的原理和协议本身。

对称加密的问题

对称加密的想法很简单,如下图所示。客户端和服务器知道同一个密钥,客户端给服务器发消息,客户端用此密钥加密,服务器用此密钥解密;反过来,服务器给客户端发消息时,是相反的过程。

网络-软件架构设计
客户端、服务器把自己的公钥公开出去,自己保留私钥。这样一来客户端就知道了服务器的公钥,服务器也知道了客户端的公钥。
当客户端给服务器发送信息时,就用自己的私钥PriA签名,再用服务器的公钥PubB加密。所谓的“签名”相当于自己盖了一个章,或者说签了一个字,证明这个信息是客户端发送的,客户端不能抵赖;用服务器的公钥PubB加密,意味着只有服务器B可以用自己的私钥PriB解密。即使这个信息被C截获了,C没有B的私钥,也无法解密这个信息。
服务器收到信息后,先用自己的私钥PriB解密,再用客户端的公钥验签(证明信息是客户端发出的)。反向过程同理:服务器给客户端发送信息时,先用自己的私钥PriB签名,然后用PubA加密;客户端收到服务器的信息后,先用自己的私钥PriA解密,再用服务器的公钥PubB验签。
在这个过程中,存在着签名和验签与加密和解密两个过程。
  1. 签名和验签:私钥签名,公钥验签,目的是防止数据被篡改。如果第三方截取到信息之后篡改,则接收方验签肯定过不了。同时也防止抵赖,既然没人可篡改,只能是发送方自己发出的。
  2. 加密和解密:公钥加密,私钥解密。目的是防止信息被第三方拦截和偷听。第三方即便能截获到信息,但如果没有私钥,也解密不了。
    在双向非对称加密中,客户端需要提前知道服务器的公钥,服务器需要指定客户端的公钥。和对此加密一样,同样面临公钥如何传输的问题。
    在继续探讨之前,先看一下单向非对称加密。

单向非对称加密

在互联网上,网站对外是完全公开的,网站的提供者没有办法去验证每个客户端的合法性;只有客户端可以验证网站的合法性。比如用户访问百度或者淘宝网站,需要验证所访问的是不是真的百度或者淘宝,防止被钓鱼。
在这种情况下,客户端并不需要公钥和私钥对,只有服务器有一对公钥和私钥。如下图所示,客户端没有公钥和私钥对,只有服务器有。服务器把公钥给到客户端,客户端给服务器发送消息时,使用公钥加密,然后服务器私钥解密。反过来,服务器给客户端发送的消息,采用明文发送。

网络-软件架构设计
客户端对服务器说:“Hi,我们的对称加密密钥是xxx,接下来就用这个密钥通信。”这句话是通过Pub加密的,所以只有服务器能用自己的PriB解密。然后服务器回复一句明文:“好的,我知道了”。虽然是明文,但没有任何密钥信息在里面,所以采用明文发送也没关系。接下来,双方就可以基于对称加密的密钥进行通信了,这个密钥在内存里面,不会落地存储,所以也不存在被盗的问题,而这就是SSL/TLS的原型。

中间人攻击

通过上面的分析可以发现,我们并不需要双向的非对称加密,而用单向的非对称加密就能达到传输的目的。
但无论是双向还是单向,都存在着公钥如何安全传输的问题。下面就以一个典型的“中间人攻击”的案例为例,来看一下这个问题是如何被解决的。
如下图所示,本来客户端和服务器要交换公钥,各自把自己的公钥发给对方。但被中间人劫持了,劫持过程如下:

网络-软件架构设计
反过来也同理,如下图所示,客户端用自己的公钥PubA通过CA换取一个证书,相当于客户端的身份证,客户端把这个证书发给服务器,服务器就能验证整个证书是否为客户端下发的。
网络-软件架构设计

证书信任链的验证过程

客户端要验证服务器的合法性,需要拿着服务器的证书C3,到CA2处去验证(C3是CA2颁发的,验证方法是拿着CA2的公钥,去验证证书C3的有效性);
客户端要验证CA2的合法性,需要拿着CA2的证书C2,到CA1处去验证(C2是CA1颁发的);
客户端要验证CA1的合法性,需要拿着CA1的证书C1,到CA0处去验证(C1是CA0颁发的);
而CA0呢,只能无条件信任。怎么做到无条件信任呢oot CA机构都是一些世界上公认的机构,在用户的操作系统、浏览器发布的时候,里面就已经嵌入了这些机构的Root证书。你信任这个操作系统,信任这个浏览器,也就信任了这些Root证书。

证书信任链的颁发过程

颁发过程与验证过程刚好是逆向的,上一级CA给下一级CA颁发证书。从根CA(CA0)开始,CA0给CA1颁发证书,CA1给CA2颁发证书,CA2给应用服务器颁发证书。
最终,证书成为网络上每个通信实体的“身份证”,在网络上传输的都是证书,而不再是原始的那个公钥。把这套体系标准化之后,就是在网络安全领域经常见到的一个词,PKI(public key infrastructure)。
想一想在现实生活中的例子:

  • 你出生在一个小镇上,怎么证明你是你呢/li>
  • 镇派出所给你发个身份证,证明你是你;
  • 镇派出所为什么可以被信任呢为经过了县公安局授权;
  • 县公安局为什么可以被信任呢为经过了市公安局授权;

  • 一级级回溯,最后,信任的根是什么是国家最高机构!

SSL/TLS协议:四次握手

到此为止,我们理解了对称加密、非对称加密、证书、根证书等概念后,再来看看SSL/TLS协议就很简单了,如下图所示:

网络-软件架构设计
  1. TCP连接的建立。
  2. SSL/TLS四次握手协商出对称加密的密钥。
  3. 基于密钥,在TCP连接上对所有的HTTP Request/Response进行加密和解密。
    其中阶段1和阶段2只在连接建立时做1次,之后只要连接不关闭,每个请求只需要经过阶段3,因此相比HTTP,性能没有太大损失。
    最后,分析一下HTTP/2和HTTPS的关系:HTTP/2主要是解决性能问题,HTTPS主要解决安全问题。从理论上讲,两者没有必然的关系,HTTP/2可以不依赖于HTTPS;反过来也如此。把两者同时放在整个网络分层体系中,如下图所示:
    网络-软件架构设计
    网络-软件架构设计
    • 图中的ACK的意思和之前所讲的稍微有些差异:前文中的ACK=7,表示告诉对方编号小于或等于7的包都收到了;这里ACK=x+1,表示小于或等于x的包都收到了,接下来要接收x+1。所以,虽然意思相同,但换了一种说法。
    • seq=x表示发出去的包的编号是x。因为TCP是全双工的,通信双方一方面要发送自己的编号的包,一方面要确认对方的包,为了优化传输,会把两个包合在一起传输,所以就有了同一个包里,同时包含seq=y,ack=x+1。表示当前这个包是发出去的第y个包,同时也是对对方的第x个包的确认(接下来要接收x+1)。
      从图中可以看出,客户端的状态转移过程是CLOSED->SYN_SENT->ESTABLISHED;服务器的状态转移过程是CLOSED->LISTEN->SYN_RCVD->ESTABLISHED。
      那为什么是三次握手呢起来好像两次就够了,我们来分析一下。
      客户端:“Hi,服务器,我想建立一个连接。”
      服务器:“好的,可以。”
      但问题是,服务器知道“好的,可以”这句话它发出去了,但是客户端是否收到,服务器是不确定的。所以两次不够,需要改成三次,同样的问题,客户端没法确认最后一次ACK(第三次)对方是否收到了。
      这就是经典的网络的2将军问题:无论是两次,还是三次,还是四次…永远都不知道最后发出去的那个数据包对方是否收到了。想要指定最后一次是否收到,只有让对方回复一个ACK,但是回复的这个ACK是否收到,只能让对方为这个ACK再回复一个ACK,如此循环往复,问题无解。
      网络的2将军问题非常关键,在网络通信中几乎是无处不在的,在应用层也存在同样的问题:
      客户端给服务器发送了一个HTTP请求,或者说客户端向DB写入一条记录,然后超时了没有得到响应,请问服务器是写入成功了,还是没有成功呢案是不确定的。
      场景1:该请求服务器根本没有收到,发送时网络有问题。
      场景2:该请求服务器收到了,服务器写入成功了,但回复给客户端时,网络有问题。
      场景3:网络没有问题,服务器接收到了请求,写入成功了,但回复给客户端时,服务器宕机了。
      无论哪种场景,客户端看到的结果是一样的:它发出的数据没有得到响应。对于客户端来说,只有一个办法,就是再重试,直到服务器返回成功,客户端才能确认请求被成功处理了。
      无论是两次握手,还是三次握手、四次握手,都绕不开网络的2将军问题,那为什么是三次呢br> 因为三次握手恰好可以保证客户端和服务器对自己的发送、接收能力做了一次确认。第一次,客户端给服务器发了seq=x,无法得到对方是否收到;第二次,对方回复了seq=y,ack=x+1。这时客户端知道自己的发送和接收能力没有问题,但服务器只知道自己的接收能力没问题;第三次,客户端发送了ack=y+1,服务器收到后知道自己第二次发的ACK对方收到了,发送能力也没有问题。

    四次挥手

    相比于建立连接的三次握手,关闭连接的四次挥手根据复杂。如下图所示:

    网络-软件架构设计
    这里有一个问题:关了就关了,为何不能直接进入CLOSED状态,而要做一个TIME_WAIT状态,非要等待一段时间之后,才能进入CLOSED状态呢两个原因:
    1. 所谓的“连接”是假的,物理层面没有连接。这意味着当双方都进入CLOSED状态后,仍可能有数据包还在网络上“闲逛”,此时如果收到了这些闲逛的数据包,丢掉即可,但问题是连接可能重开。
      一个连接是由(客户端IP、客户端Port、服务器IP、服务器Port)4元组唯一标识的,连接关闭之后在重开,应该是一个新的连接,但用4元组无法区分出新连接和老连接。这会导致,之前闲逛的数据包在新连接打开后被当做新的数据包,这样一来,老连接上的数据包会“串”到新连接上面,这是不能接受的。怎么解决这个问题呢br> 在整个TCP/IP网络上,定义了一个值叫做MSL(Maximum Segment Lifetime),任何一个IP数据包在网络上逗留的最长时间是MSL,这个值默认是120s。意味着一个数据包必须最多在MSL时间内,从源点传输到目的地,如果超出了这个时间,中间的路由节点就会把该数据包丢弃。
      有了这个限定之后,一个连接保持在TIME_WAIT状态,在等待2*MSL的时间进入CLOSED状态,就好完全避免旧的连接上面存在闲逛的数据包串到新的连接上。为什么是2倍的MSL的时间呢涉及到下面这个原因。
    2. 因为网络的2将军问题,4次握手第四次发送的数据包,服务器是否收到是不确定的。服务器采取的方法是无法收到第四次的情况下重新发送第三次的数据包,客户端重新收到第三次数据包,再次发送第四次的数据包。第四次数据包的传输时间+服务器重新发送发送第四次数据包的时间,最长是两个MSL,所以要让客户端在TIME_WAIT状态等待2MSL的时间。
      还有一个问题:客户端处于TIME_WAIT状态,要等待2
      MSL时间进入CLOSED;但服务器收到第四次的ACK之后,立即进入了CLOSED状态。为什么不让服务器也进入TIME_WAIT状态呢因是没有必要。任何一个连接都是一个4元组,同时关联了客户端和服务器,客户端处于TIME_WAIT状态后,意味着这个连接要到2MSL时间之后才能重新启用(也就是在2MSL时间里,即使创建连接该端口的连接也是创建不了的,因为有一端还没有进入CLOSED状态),服务器端即使想立马使用也无法实现。
      通过分析发现,一个连接并不是想关就能立刻关闭的,关闭后还要等2*MSL时间才能重开。这就会造成一个问题,如果频繁地创建连接(同一个ip可以使用不同的端口创建连接),最后可能导致大量的连接处于TIME_WAIT状态。最终耗光所有的连接资源(比如服务器的端口资源)。为了避免出现这种问题,可以采取如下措施:
    • 不要让服务器主动关闭连接。这样服务器的连接就不会处于TIME_WAIT状态。
    • 客户端做连接池,复用连接,而不要频繁地创建和关闭,这其实也是HTTP1.1和HTTP/2采用的思路。

    文章知识点与官方知识档案匹配,可进一步学习相关知识网络技能树跨区域网络的通信学习网络层的作用22111 人正在系统学习中

    来源:融极

    声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年1月20日
下一篇 2022年1月21日

相关推荐