程序员一定被问过这个问题:
我在浏览器输入一个网址,后面发生了什么?
有人要说了,这么老的问题也拿出来说:
就是HTTP协议呀,服务端也使用HTTP协议接收,我们就是这么做的,很简单。。。
上面的回答可能是很多工程师真实的工作体验,现在的技术领域分工高度发达,我们引入一个库就可以高效地开发应用了,完全不需要也没时间去了解底层发生了什么。
那么底层究竟发生了什么呢?事实上这个老问题真的不算简单,可延伸的幅度非常宽广,我们按照下面几个问题慢慢展开:
问题1: 生成HTTP报文后,怎么发出去的?
问题2: 网络为啥要分层呢?我直接发不就行了吗?
问题3: 怎么找到我要访问的服务器呢?
问题4: 我的数据丢了怎么办?
问题5:不是还有个Socket吗?我们用的是Socket长连接,它是第几层啊?
问题6: 我们用的是HTTPS,它和HTTP一样属于应用层吗?
HTTP(Hyper Text Transport Protocol)全称是超文本(扩展的文本)传输协议,HTTP协议诞生的目的就是传输HTML文本,后面逐渐迭代出HTTP/1.0、HTTP/1.1、HTTP/2 和 HTTP/3这些版本,迭代带来的是更好的性能和更加丰富的能力。
TIP: 协议(Ptotocol): 在计算机领域,理解为双方都要遵守的约定。
本文会高频地提到“协议”这个词(如HTTP、TCP、UDP等),为了解决不同场景的问题,计算机领域的前辈们制定了大量的协议,这些协议的凝结着解决问题的智慧。
先看一下HTTP协议的报文格式:
如果你有幸参与过HTTP服务的初期搭建,可能做过一些平时不需要关注的工作: 比如HTTP的版本选择、Header的生成和解析等,后面的同事运气足够好的话,常常会不知道这些东西的存在。
现在你应该知道了,一次HTTP请求,就是以上面的报文格式发出请求和收到响应的过程。
好的,文章结束了,告辞!
对这个解释有位同学非常不满意,甚至有点想骂人(能讲点我不知道的吗),然后抛出一个问题:
问题1: 生成HTTP报文后,怎么发出去的?
如果问老程序员,他会说发给了下层,下面还有好几层。。。
网络的分层模型
与很多大型项目解决方案类似,网络的基本处理框架也是分层。每一层负责自己的事情,处理完成后丢给上层(接收场景)或下层(发送场景)。越下层能力越通用。我们以实际使用的TCP/IP模型为例,如下图。
如上图所示,TCP/IP的模型分为5层,从下到上依次是: 物理层、MAC层、IP层、传输层、应用层。我们用到的HTTP协议就属于应用层。
很多人会把IP层叫网络层,把MAC层叫数据链路层,网络层和数据链路层的说法来自OSI模型(有7层),当然没必要太较真,了解就好。
基本模型了解后,我们就知道了,生成HTTP报文后,会发送到下面的传输层,然后一层一层地向下传输。那么一次HTTP请求的过程大概就是。。。这样:
好的,文章结束了,告辞!
对这个解释有位同学依然不满意,甚至有点想骂人(能说点我不知道的吗),然后抛出一个问题:
问题2: 网络为啥要分层呢?我直接发不就行了吗?
我们上面已经说了,HTTP报文不是直接发给接收端的HTTP,而是向下逐层传输,最终由物理层发出去,接收端也是从物理层接收到数据,然后向上逐层传输,最终到达应用层的HTTP。
经济学有一个基本理论:分工产生效能。
与所有复杂软件项目类似,网络处理是一个非常复杂的工程,每一层的协议都有自己的数据格式,按照分层的方式,不同的层只关心自己的工作,更高效也更容易排查问题。
现实世界里的网络建设已经进行了几十年,除非可以从软件到硬件重新实现一套标准且成功搭建,否则只能使用现有的分层网络模型,别无选择。
另外,假设可以不分层(事实上不行), 允许直接把HTTP报文传输到接收端,那么下层提供的能力你需要全部实现一遍:怎样找到服务器
、 网络包丢了怎么办
、 网络包顺序错了怎么办
,这会让本来只需要简单实现的代码变得极其复杂,维护过烂代码的人都知道,如果项目不分模块不解耦,随着程序的迭代,后续的程序将复杂到无法维护。
看到这里你若有所思:
“等一下,你刚刚说我只需要专注于应用层的HTTP?”
。。。
“那我还看这篇文章干嘛?告辞!”
有位同学还是不满意,面色凝重:“:刚才说HTTP把报文发给了下层,我怎么知道要发到哪里去呢?”
问题来了:
问题3: 怎么找到我要访问的服务器呢?
这里有一个隐藏的问题:在网络世界里,哪个属性是具有全局定位功能的?
>>答案: IP地址
不管是IPv4还是IPv6,分配了IP地址的设备才能被定位到; 在TCP/IP模型中,"IP"指的是IP协议(IP层协议),网络包通过IP协议发送到对应的IP地址的设备。
回到问题3,找到我要访问的服务器可以分两步:
1. 知道服务器在哪里:通过域名找到服务器的IP地址
我们在进行HTTP请求的时候,通常不会直接访问IP,而是访问一个域名。这样做有很多好处,同时需要解决一个问题: 我需要根据域名来找到服务器的IP。
DNS(Domain Name System)协议解决了这个问题, 服务商存储了域名与IP的映射关系,对外提供域名解析的服务,解析的过程有可能使用运营商提供的LocalDNS,也可能是公司自建或云服务商提供的HTTPDNS,总之它的作用就是告诉你域名对应的IP。DNS协议使用非常广泛,但不是本文的重点。
这里请注意,DNS和HTTP都是应用层的协议,HTTP协议本身不包括DNS的内容,但是实现HTTP的请求时用到了DNS协议。
2. 通过IP地址,把数据发到服务器
通过DNS协议已经找到了服务器的IP地址,接下来的问题是:
如何一步一步经过中间设备,最终成功访问到服务器IP呢?
其实,这是网络领域的核心问题,要解决这个问题,我们需要路由协议和一个耳熟能详的中间设备: 路由器。
路由器(Router)是一台网络设备,具有数据处理和运算能力,有多张网卡,每张网卡都同时是路由器的入口和出口。它的工作机制是:
工作中的路由器,会静态或动态地维护一个转发信息库(也叫路由表)
-
路由表中有多条路由规则,每条路由规则至少包含以下3个信息:
目标网络: 你要访问的目的IP地址
这个包要从哪个口出去?(用哪张网卡来转发)
下一个路由器的地址
一个IP数据包(IP Packet)从某一个网卡到达路由器时,查询路由表,然后从对应路由规则指定的网口发到下一个路由器。下一个路由器重复这个过程,直到数据包送达目标设备。
网络中的数据包就这样经过一个一个的路由器,最终达到目的地,这些路由器共同组成了整个网络世界。
一个路由器为什么知道我的数据包要怎么转发呢? 是因为它的路由表里包含了对应的路由规则。
那路由规则是什么时候写入的呢? 写入分静态路由和动态路由两种,由于网络信息变化太多,静态路由已经不适用了。动态路由的规则主要通过广播的形式通知其他路由器,其他路由器接收到信息之后,构建或更新基于图的数据结构。实际上,确定转发规则的时候,就是实现图中计算最短路径。比较流行的动态路由协议有BGP和OSPF等。
IP层的路由协议还有一个问题:不保证数据包送达。
转发错误丢包 路由器的路由规则存在更新延迟的情况,比如链路上的一个路由器挂了,或者数据包走了错误的链路达到最大的转发数,这些情况数据包都会被丢弃。
拥塞丢包 路由器作为一个硬件设备,有吞吐、存储和处理能力的极限,如果一段时间内达到了峰值,此时转发过来的包也会被丢弃。
我们当前的讨论(使用路由协议,把数据包发送到对应IP地址的设别)发生在IP层,也就是TCP/IP模型中的第三层,在IP层看来,人们看视频,刷网页,打游戏的过程,就是根据对应的路由协议,一群路由器不停转发的过程。
好的,你现在已经知道了,HTTP的请求就是这样一步一步找到了服务器,也把数据发送过去了,文章也就结束了,告辞!
有位同学说: “等一下先别走,你说IP包不能保证送达”
那问题来了:
问题4: 我的数据丢了怎么办?
你: IP啊,听说你不能保证网络包送达呀。。。
IP层: 我尽力了!
你: 。。。我要转账的,我怎么知道收没收到啊?
IP层: 我尽力了!
你: 。。。
为了解决这个问题,TCP协议登场了。
前面提到的HTTP是应用层协议(第五层),路由协议是IP层协议(第三层),新来的这位TCP老兄属于传输层协议(第四层)。
传输层协议最被人乐道的就是TCP(Transport Control Protocol 传输控制协议)和UDP(User Datagram Protocol 用户数据报协议),其中HTTP主要使用了TCP作为传输层协议,我们这里只介绍TCP。
TCP老兄说:既然让我解决这么难的问题,那就要按照我的套路来处理了,你们也别嫌我烦,我需要“三次握手”来建立连接,需要维护连接的数据结构,我需要建立一套复杂的机制来保证不丢包和不乱序,也需要“四次挥手”来断开连接,还有。。。
TCP的主要特性是可靠传输,它是通过面向连接来实现可靠的,至于TCP的连接,实际上是两端维护了一定的数据结构,在逻辑上维护了一个“连接”的状态,在这种状态下,双方都会接收对方发过来的数据。
使用TCP传输,首先要建立连接,典型的过程如下图:
连接建立后,我们就可以把HTTP请求里的数据通过TCP的连接发送到服务器了。
回到上面的问题,你TCP老兄是如何保证传输可靠的呢?TCP下面基于IP层的,我一个IP包丢了,你是怎么知道丢了,又是怎么处理的?
我们先讲一个无奈的逻辑,TCP的下层是基于IP层的,某个IP包(IP Packet)丢了这件事TCP一点办法都没有。它又没跟着这个包,它不会知道包丢了,只能在对方收到包的时候发动一个响应包,来间接地推断出包没丢。这也是连接(上图)中A和B建立连接的时间点不同的原因,它们并不是所谓“三次握手”完成后同时建立连接的。
讲到这里,很多文章上来就会说拥塞控制、滑动窗口,一下子能把人讲晕。这里用一个非典型的传输场景(下图),说一下TCP是怎么做到可靠的。没有了解过的朋友先感觉一下,至于更细节的内容,可以查看我的另一篇文章: 《关于TCP的几个问题》。
建议花几分钟看一下这个略显复杂的流程,按照数字序号和旁边的标注(重点关注丢包后的处理逻辑):
一图胜百言,我们发现为了保证可靠(避免丢包+乱序)TCP也是煞费苦心,没有任何黑科技,就是点名+记录+重传。
点到为止,这里介绍了TCP传输的基本逻辑,TCP处理的日常的场景中往往更加复杂,也有更多的策略。这里要得出一个结论:如果项目在技术上直接或间接地使用TCP作为你的传输层协议,那么丢包重传、乱序这些逻辑TCP已经全部实现了,你不需要在上层再加一层相应ACK的逻辑了(除非场景足够特殊)。
某一次在公司的接口文档中,看到一位程序员挑战技术方案:
方案是:
在基于TCP长连接的推送服务中,服务端要求客户端在收到推送消息时,向服务端发送ACK(应答)。
这个方案被老程序员挑战(非原话): 我们PUSH底层基于TCP,内部已经实现了丢包检测、重传等机制,上层再加一个ACK就是浪费流量。
看到这条写于2015年评论,我对这位老程序员肃然起敬。
每次聊到TCP内心就会涌出感慨: TCP协议作为TCP/IP模型的排头兵,凝聚了多少前辈的智慧,在不可靠的网络世界中,纯粹靠逻辑建起了一个(几乎)可靠的网络上层,TCP协议的使用者几乎等于全球的网民,几十年来,作为互联网可靠传输能力的保障,TCP协议的创造者到体验到的是怎样的成就感呢!
扯远了,关于TCP的连接断开(“四次挥手”)这里就不做介绍了,现在你(基本)知道为什么不怕丢包了,那文章也结束了,告辞~
有位同学说:“等一下,你刚刚说TCP长连接,我们的长连接是用Socket做的”
那么问题又来了:
问题5:不是还有个Socket吗?我们用的是Socket长连接,它是第几层啊?
这个概念经常被混淆,很多工程师只知道Socket不知道TCP。
Socket是系统提供给应用层的操作接口(更准确地说它是一个系统调用),用于让系统处理应用层以下逻辑,Socket不属于网络分层的概念。
那为什么要提供这个接口呢?
在Linux系统中,TCP/IP模型中实现了2到4层(数据链路层、IP层和传输层)协议的处理代码在内核里,只有应用层的实现需要应用自己做。过程中处理2到4层数据处于内核态,内核中对于网络包的处理不区分应用(这样的设计,为全局的流量监控和代理打下了基础),处理应用层数据处于用户态;这样两者就需要跨内核态和用户态通信,就需要一个系统调用完成这个衔接,这就是 Socket。也就是说,如果创建Socket时指定了SOCK_STREAM(TCP),那么调用socket.connect()
就会进行“三次握手”创建连接; 调用socket.close()
就会"四次挥手"关闭连接。
好,你现在知道什么和TCP的关系了,告辞~
这位同学又来了: “再等一下吧,我们用的是HTTPS,他和HTTP一样吗?”
所以问题还是来了:
问题6: 我们用的是HTTPS,它和HTTP一样属于应用层吗?
先补充一点点基础知识:
OSI, 另一种网络模型,有7层,其中应用层、表示层和会话层三层对应着TCP/IP模型的应用层。
回到问题,我们通常说的HTTPS,是指HTTP+TLS的组合。其中**TLS(Transport Layer Security)协议在OSI模型中,属于会话层(Session Layer),那么对应着TCP/IP模型,TLS属于应用层。
说起TLS,可以聊一下网络安全的问题,你可能听过它之前的版本SSL(Secure Sockets Layer),它们都是用来进行网络加密传输的协议。从文章上面的内容你可能也意识到了,我们无法控制IP数据包在网络中的传输路径,如果中途被人截获、篡改是非常危险的,在今天我们已经如此依赖网络,那么如何杜绝这种事情呢?
还是一样的困境:我们无法改变下层,只能改变自己,我们无法避免数据包被劫持和篡改,但如果能够检测到数据包被篡改了、能做到被截获之后对方无法解析出数据、能够识别到数据被恶意重传了。。。这样也能解决问题。
TLS就在做这件事, TLS会在传输层协议上(通常是TCP连接)建立一条TLS的连接,建立连接的过程会做很多事情,比如证书验证,密钥协商,其中会用到非对称加密和对称加密。连接建立之后的数据传输只使用对称加密。
互联网的世界里并非全是光明,网络攻防上演着一波又一波的博弈。即使在今天,HTTP劫持依然在发生,非对称加密的RSA算法在数学上证明了暴力破解需要(相对)无限长的时间,看起来能够保证安全了,Google已经把未使用HTTPS的网站标记为不安全了,很多新版本的手机系统也默认不允许明文(不经过加密)的数据传输。
现在,你知道了TLS属于哪一层了,甚至知道的有点多了,好的,告。。。算了,不告辞了,总结一下吧。
总结: 一个HTTP请求经历了什么?
能看到这里,同学你的好奇心是有点强啊!
如果之前没有了解过,你可能觉得挺有道理,但是脑子很懵,那我们就捋一捋,一个HTTP请求经历了什么。。。
使用DNS协议获取服务器IP地址。
把请求的数据组织成HTTP的Start Line、Body 和 Headers, HTTP的数据组织完成,发送到TCP处理。
TCP进行三次握手建立连接(也可以复用已建立的连接)。HTTP的数据可能会分成多个TCP Segment(TCP的数据包叫TCP Segment), 每个TCP Segment都会加上TCP的Headers,以实现TCP的能力(保证可靠),之后TCP Segment发送到IP层。
(可选)如果使用了HTTPS,这里会在TCP的基础上建立TLS连接。在HTTP 和 TCP之间,TLS要进行数据的加密和解密。
IP层拿到TCP Segment之后,加上IP层的Headers(提供了实现路由协议的数据),组织成IP Packet(IP层的数据包叫IP Packet), 然后发到MAC层。
MAC层拿到IP Packet后, 加上MAC层的Headers, 组织成Ethernet Frame(MAC层以太网数据包叫Ethernet Frame), 通过硬件网卡发出去。
经过中间设备的转发,最终到达服务器,然后一层一层地去掉Headers,向上传输,直到应用层。
好的,文章真的结束了,告辞!