引言:上图是《图解 HTTP》这本书的目录,最近又重新读了一下这本书,然后打算写篇总结,来加深自己的理解,然而在做总结的时候觉得还是全面总结下网络相关的知识吧,所以又在网上找了大量的资料,最终完成了本文。
郑重声明:本文只是我的学习总结,不敢误人子弟,如果你看到了这篇文章,觉得有什么错误的地方还请指出。本文参考了大量的博客、文章等,如有侵权,请与我联系。谢谢
OSI vs TCP/IP
OSI
OSI全称为开放式系统互联通讯参考模型(Open System Interconnection Reference Model)。OSI 将计算机网络体系结构划分为 7 层。
- 物理层:在局部局域网上传送数据桢,它负责管理计算机管理通讯设备和网络媒体之间的互通。包括了针脚、电压、线缆电压、集线器、中继器、网卡、主机接口等。
- 数据链路层:负责网络寻址、错误侦测和改错。当表头和表尾被加之数据包时会形成桢。数据链表头(DLH)是包含了物理地址和错误侦测和改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)。
- 网络层:决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络数据,例如:互联网协议(IP)等。
- 传输层:把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息,例如:传输控制协议(TCP)等。
- 会话层:负责在数据传输中设置和维护计算机网络中两台计算机之间的通讯连接。
- 表示层:将数据转化为能与接收者的系统格式兼容并适合传输的格式。
- 应用层:提供为应用软件而设的接口,以设置与另一软件之间的通信。例如:HTTP、HTTPS、FTP、TELNET、POP3、SMTP、SSH等。
OSI 模型是一个标准而非实现,TCP/IP 四层模型由 OSI 7 层模型简化而来,是一个应用的具体实现。
下面具体说下四层模型,从下往上说起:
TCP/IP
链路层
又名数据链路层,网络接口层。用来处理网络连接的硬件部分。
- 以太网协议
以太网(Ethernet)规定,一组电信号构成一个数据包,叫做帧(Frame)。每一帧分成两部分:标头和数据。标头的长度固定为14字节,数据的长度范围为46~1500字节,如果数据不够长,则需要填充(比如ARP、RARP),如果过长,就必须分割成多个帧进行传输。
标头固定的14字节为:目的地址(6个字节)、源地址(6个字节)和类型(2个字节)。目的地址/源地址为MAC地址,类型:0x0800 是IP数据报,0x0806 是ARP请求,0x8035 是 RARP。末尾的CRC是校验码。
以太网是采用广播的形式发送数据包的。比如,一个子网络中有3台计算机,1号计算机想给2号计算机发送一个数据包,2号、3号计算机都会收到这个包,然后读取这个包的标头,获取目的MAC地址,和自身的MAC地址做匹配,如果相同则接收这个包,否则就丢弃这个包。
从上述广播可以看出,以太网数据包必须知道对方的MAC地址才可以传输数据,那么一个网卡是如何知道另一个网卡的MAC地址的呢?其实就是通过ARP协议获得的,后续文中将会说明。
网络层
又名网络互联层。用来处理在网络上流动的数据包。
IP协议
互联网是无数子网络共同组成的一个巨型网络。同一个子网络之间传输数据用MAC地址通过广播的方式传输,那么不同子网络之间呢?
网络层的诞生就是要引进一套新的地址,使得我们能够区分不同的计算机是否数据同一个子网络。
规定网络地址的协议,叫做IP协议,它所定义的地址称为IP地址。目前广泛采用的是IP协议第四版,简称IPv4。这个版本规定,网络地址由32个二进制位组成。习惯上,我们分成四段的十进制数表示IP地址,从0.0.0.0到255.255.255.255。
IP地址分为两个部分,前一部分代表网络,后一部分代表主机。但是单从IP地址我们无法判断网络部分到底是前24位还是前16位等等,这时候就需要用到子网掩码(subnet mask)了。所谓子网掩码就是表示子网络特征的一个参数。它在形式上等同与IP地址,也是一个32位二进制数,它的网络部分全部为1,主机部分全部为0。比如,IP地址为192.168.1.1,如果已知网络部分是前24位,主机部分是后8位,那么子网掩码就是11111111.11111111.11111111.00000000,对应十进制就是255.255.255.0。将两个IP地址分别和子网掩码进行AND运算,然后比较结果是否相同,就可以判断两个IP是否属于一个子网络了。
IP数据报格式
IP数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍。对于IPv4,4位版本字段是4。4位首部长度是以4字节为单位的,最小值是5,也就是说首部长度最小是4*5=20字节,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节。
8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃不用),还有4个位表示可选的服务类型(最小延迟、最大吞吐量、最小成本),还有一个位总是0。16位总长度是整个数据报(包括IP首部和IP层payload)的字节数,最大是65535字节,上面提到过以太网数据帧的数据部分最大为1500字节,因此,如果IP数据包超过了1500字节,就需要分割成几个以太网数据帧进行传输。
每传一个IP数据报,16位的标示增加1,可用于分片和重新组装数据报。3位标志和13位片偏移用于分片。
TTL(time to live)是这样用的:源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。协议字段指示上层协议是TCP、UDP、ICMP还是IGMP。然后是校验和,只校验IP首部,数据的校验由更高层协议负责。接下来是32位的IP地址。
ARP协议
看到这里,就知道了要想往对方机器传输数据需要IP地址和MAC地址,IP地址可以通过DNS服务获得,而MAC地址怎么获取呢?
这里需要分两种情况:
第一种情况,如果两台主机不在同一个子网络,其实是没有办法得到对方的MAC地址的,只能把数据包传送到两个子网络接处的网关(getaway),让网关去处理。
第二种情况,如果两台主机在同一个子网络,那么我们可以通过ARP协议,得到对方的MAC地址。简单来说就是在以太网首部目的MAC地址里面填写FF:FF:FF:FF:FF:FF,表示这是一个广播地址。收到这个广播的主机就会去匹配自己的IP地址,如果匹配则回复自己的MAC地址,否则丢弃这个包。
传输层
对应用层提供处于网络连接中两台计算机的数据传输。网络层是主机到主机之间的通信,而传输层是建立端口到端口的通信。代表:UDP、TCP。
UDP
UDP 首部比较简单,总共8字节。具体见上图。
TCP
和UDP协议一样也有源端口号和目的端口号,通讯的双方由IP地址和端口号标示。32位序号、32位确认号、窗口大小稍后详细解释。4位首部长度和IP协议头类似,标示TCP协议头的长度,如果没有选项字段,TCP协议头最短为20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位,稍后将解释SYN、ACK、FIN、RST,其它从略。
看一个简单的一问一答的例子:
三次握手
-
客户端发出段1(Client:SYN_SEND),标志位是SYN,序号是1000,这个序号在网络通讯中用作临时的地址,每发送一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没有发数据,但是由于发送了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端生命自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
-
服务器发出段2(Server:SYN_RECV),也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号位1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
-
客户端发出段3(ESTABLISHED),对服务器的连接请求进行应答,确认序号是8001。
在TCP通讯中,如果一方接收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。
数据传输
- 客户端发出段4,包含从序号1001开始的20个字节数据。
- 服务器发出段5,确认序号为1021,对序号1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称之为piggyback。
- 客户端发出段6,对服务器发来的序号8001-8010的数据表示确认收到,请求发送序号8001开始的数据。
在数据传输过程中,ACK和确认序号是非常重要的,应用层交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据之后,只有收到了对方的应答ACK段,才确认数据是成功发送了,并从缓存区中移除。如果等待超时,会进行重发。
四次挥手
- 客户端发出段7(Client:FIN_WAIT_1),FIN位表示关闭连接的请求。
- 服务器发出段8(Server:COSE_WAIT, Client:FIN_WAIT_2),应答客户端的关闭连接请求。
- 服务器发出段9(Server:LAST_ACK),其中也包含FIN位,向客户端发送关闭连接的请求。
- 客户端发出段10(Client:TIME_WAIT)应答服务器的关闭连接请求。
建立连接是三次握手,而关闭通常连接需要四段(也有三次的情况),服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。
客户端发出段10之后,不是立马关闭连接,而是进入TIME_WAIT状态,等待2MSL。MSL是Maximum Segment Lifetime的缩写,表示报文最大生存时间,当段10由于网络原因没有成功发送时,经过1MSL服务器会重发段9,段9传输最慢需要1MSL,所以需要等待2MSL来确保最后一次ACK成功发送。
滑动窗口:
我们知道UDP协议是不可靠的,当发送端发送的速度过快,接收端收到数据后处理的速度较慢,而缓冲区大小是固定的,就会丢失数据。TCP协议通过滑动窗口(Sliding Window)机制解决这一问题。
-
发送端发起连接,声明最大尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三次握手结束。
-
发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
-
接收端的应用程序提走2K数据,接收缓冲区又有了2K的空闲,接收端发送段10,在应答已收到6K数据的同时声明窗口大小为2K。
-
接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
-
发送端发出段12-13,每个段带2K的数据,段13同时还包含FIN位。
-
接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
-
接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
-
接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
-
接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
应用层
就是平时接触最多的一层,比如HTTP、DNS、FTP。
以 HTTP 为例,为了传输方便在传输层(TCP协议)把从应用层收到的数据(HTTP报文)进行分割,并在各个报文上打上标记序号及端口号后转发给网络层。
在网络层(IP协议),增加作为通讯目的地的 MAC 地址后转发给链路层。
接收端的服务器在链路层收到数据,按序往上层发送,一直到应用层。
HTTP 状态码
下面?的表格列出了常见的 HTTP 状态码
HTTP状态码302、303和307的故事HTTP/2
HTTP/2,简称h2(基于TLS/1.2或以上版本的加密连接)或h2c(非加密连接),是 HTTP 协议的第二个主要版本,主要基于 SPDY 协议。
SPDY 由 Google 研发,基于传输控制协议(TCP)的应用层协议,其主要设计目的是降低网页加载时间。通过优先级和多路复用,SPDY 使得只需要创建一个 TCP 连接即可传送网络内容和图片资源。SPDY 中广泛应用了 TLS 加密,传输内容也均以 gzip 或者 DEFLATE 格式压缩,另外 SPDY 还支持服务端主动推送数据到客户端。
HTTP/2 和 SPDY 的不同点
HTTP/2 的优点
1. 帧、消息和流
HTTP/2 采用了二进制而非文本来进行传输数据,这也是和 HTTP/1.x 的最大区别。HTTP/2 将一个 TCP 连接分为若干个流(Stream),每一个流中可以传输若干个消息(Message),每一个消息由若干个最小的二进制帧(Frame)组成。
流(Stream)
如上图所示,流一般表示的是每个请求和响应,在 HTTP/2 中可以多个流同时传输。
消息(Message)
消息是一组帧,表示某一个具体的请求或者响应。
帧(Frame)
帧是 HTTP/2 数据传输中的最小单位。每一帧都包含标头和数据,头部(固定9个字节)整个帧格式如下:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
复制代码
-
length 占3个字节表示的是整个帧的大小。
-
type 表示类型,目前规定了10种类型,具体如下:
- 0x0:DATA 数据帧类型
- 0x1:HEADERS 请求头类型
- 0x2:PRIORITY 设置流的优先级
- 0x3:RST_STREAM 终止流
- 0x4:SETTINGS 设置此连接的参数
- 0x5:PUSH_PROMISE 服务器推送
- 0x6:PING 测试 RTT
- 0x7:GOAWAY 终止连接
- 0x8:WINDOW_UPDATE 流量控制
- 0x9:CONTINUATION 继续传输头部数据
-
flag 是标志位,常见的有 END_STREAM 、 END_HEADERS 和 PADDED。详细参加这里
-
R 是保留位
-
identifier 标示符,用于跟踪逻辑流的帧成员关系。简单的说就是标示的哪个流。
2.多路复用
知道了上述流的概念之后,多路复用就好理解了。HTTP/1.0 每次都需要经过三次握手才可以传输数据,HTTP/1.1 中虽然增加了keep-alive可以不用每次建立连接,但是还是需要等待上一个响应返回之后,才可以发送新的请求,而在 HTTP/2.0 中只增加请求只需要增加一个流即可,数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送。
3.优先级排序
由于可以进行多路复用,服务器和客户端的帧都是交错发送的,对于发送给服务器的帧,为了解决处理的先后问题,因此引入了数据流的优先级。通过优先级类型(PRIORITY)的帧或者标头帧可以给流设置优先级。也可以定义依赖关系,允许在一个资源之前加载另一个资源。还可以将优先级组合到一个依赖树中,让开发者控制每个流的重要性。
在上图?中,字母标示流标示符,数字表示分配给每个流的权重。树的根是流 A,首先会向它分配资源,然后才向依赖它的流 B 和 C 分配资源。为流 B 分配了 40% 的权重,流 C 分配了 60% 的权重,即流 B 和 C 分别占用 40% 和 60% 的可用资源。依次往下。
4.头压缩
专门为 HTTP/2 定制的 HPACK 算法为头压缩提供了很好的支持。它使用了一份 索引表 来定义常用的 HTTP Header,把常用的 HTTP Header 存放在表里,请求的时候只需要发送对应的索引即可,举个? :method=GET
对应索引值为2,path=/index.html
索引值为5。那么只需要给服务端发送一个 Frame,该 Frame 的 payload 是 0x8285,Frame 的 type 设置为 HEADERS 类型,便可以知道这是一个请求头,内容是 GET /index.html
。既节俭了流量,又提高了传输效率。
0x8285 而不是 0x0205?这是因为高位设置为1表示这个字节是一个完全索引值(key 和 value 都在索引中)。类似的,通过高位的标志位可以区分出这个字节是属于一个完全索引值,还是仅索引了key,还是 key 和 value 都没有索引。(具体可以看下这篇文章(1.3 首部字段及首部块的表示))因为索引表的大小是有限的,它仅保存了一些常用的 HTTP Header,同时每次请求还可以在表尾动态追加新的 HTTP Header 缓存。动态部分称之为 Dynamic Table。Static Table 和 Dynamic Table 在一起组成了索引表。
<---------- Index Address Space ---------->
<-- Static Table --> <-- Dynamic Table -->
+---+-----------+---+ +---+-----------+---+
| 1 | ... | s | |s+1| ... |s+k|
+---+-----------+---+ +---+-----------+---+
^ |
| V
Insertion Point Dropping Point
复制代码
HPACK 不仅通过索引值来降低数据量,同时还会将字符串进行 霍夫曼编码 来压缩字符串的大小。
5.服务端推送
作为 HTTP/2 的一个重磅功能,也不是服务器想推就可以推的,服务器要遵循请求-响应这个模型,只不过服务器对同一个请求可以推送多个响应。客户端在交换 SETTINGS Frame 时,设置字段 SETTINGS_ENABLE_PUSH (0x2) 为1显示允许服务器推送。
当服务端需要主动推送某个资源时,便会发送一个 Frame type 为 PUSH_PROMISE 的 Frame,里面带了 PUSH 需要新建的 Stream ID。意思时告诉客户端“接下来我要用这个ID向你发送东西,你准好接着”。
参考: