知识犹如人体的血液一样宝贵。——高士其
本文内容参考一些网络文章:Henry_Jeannie
OSI七层网络协议和TCP/IP四层网络协议
首先需要理解OSI七层和TCO/IP四层网络协议,这个4/7模型是网络知识的基础。
但是我觉得,首先,我们可以思考一下,为什么要弄得这么麻烦,这么多层去传输数据?为什么不可以在物理层就设计好呢?
——这个想法是合理的,但是网络的发展过程中实际上是理论基础不强的,都是解决一个一个小问题来发展的。所以这是历史发展的角度,当时为了解决这些问题才出现这个层的。虽然从理论上可能能够更好,但是当时为了解决当时的问题才出现这个层和结构的。
其他的,比如我们的java语言,支持泛型这一点就是在过去的基础上堆砌出来的,而不是从头设计的机制。
OSI开放式互联参考模型(OSI七层参考模型)
OSI七层网络模型如下图所示:
整个运行的流程是先自上而下,后自下而上地处理数据头部。
由下往上来看:
- 物理层:定义网络设备的标准,包括:网线类型、接口类型、传输介质的类型,将他们转化成电流强弱来进行传输,比特转换(数模和模数转换)
- 数据链路层:传输数据的时候会产生传输的数据不完整等可能。数据链路层定义了如何格式化数据来进行传输。以及控制如何对物理介质进行访问。也提供错误检测和纠正,以确保数据传输的可靠性。这一层把比特数据转换成了“帧”,交换机工作在这一层,并对帧进行解码。根据帧中包含的信息把数据发送到正确的接收方。
- 网络层:这一层的协议把网络地址翻译成对应的物理地址,并决定如何将发送的数据从发送方路由到接收方。由于路由器工作在网络各段,所以路由器工作在这一层。(关注IP协议)
- 传输层(OSI模型中最重要的一层):解决主机间的数据传输问题。为了保证传输体量大的数据的准确性,可能需要对数据进行切割,切割成一个一个的段落进行发送。(关注TCP和UDP协议)
- 会话层:可以不用每次都去寻址,发送底层的包。会话层是用来建立和管理应用程序之间的通信。
- 表示层:比如win的很多文件在linux上不能执行,vice verser, 用来解决不同操作系统之间的通信语法问题。这一层中数据将按照网络能理解的方式进行格式化,
- 应用层:规定发送方和接收方必须使用一个固定长度的消息头,消息头也必须使用某种固定的结构。消息头必须记录消息的长度等一系列信息,以方便接收方能接受发送方所发送的数据。应用层方便用户利用网络中接收到的数据。没有这一层,你在不同电脑中传递的数据都是“1”和“0”. 这一层中要关注的是 TCP/IP中的HTTP协议
需要注意,OSI不是一个标准,而是制定标准的时候使用的概念性框架。
记忆方法,就记开头的第一个字,更容易记住!
应、表、会、传、网、数、物
OSI的实现:TCP/IP模型(TCP/IP四层参考模型)
TCP传输协议
TCP协议简介
TCP协议是一个基于连接的协议,在连接的基础上可以达到一个可靠的传输。怎样是可靠的传输?我们的数据包是按照顺序收到的,也就是说我发出的数据包的顺序和你收到数据包的顺序一定是一样的。
-此外,我们要保证我发出的包和你收到的包是一样的,是不出错的。
-还有其他的重要的功能,包括流量控制和拥塞控制。
流量控制可以保证不会发生:发送端发送数据太快,接收端来不及接收。拥塞控制指网络出现拥塞的时候,情况不会恶化。
TCP 链接一旦建立,就可以在链接上进行双向的通信。
具体细节:
- 面向连接的、可靠的、基于字节流(不是基于报文,可以根据字节大小进行编号)的传输层通信协议
- 将应用层的数据流分割成报文段并发送给目标节点的TCP层
- 数据包都有序号,对方受到则发送ACK确认,未收到则重传
TCP的报文格式:
接下来介绍TCP的三次握手和四次挥手。
首先,我们要用网络化的思想方法(这个网络化的思想方法是非常工程化的思想方法)。我们首先承认现实,网络是不可靠的,任何包都可能丢,我们设计TCP协议的过程中,一定要牢记这句话,因为如果不是基于这个事实,那么所有的设计其实都是过于复杂的,都是没有意义的。
然后,我们需要秉持工程化的思路,我们就是会不断遇到问题,然后解决问题。之后很可能再遇到新的问题,再解决这个新的问题。通过这种思想,我们会不断遇到问题,解决问题,不断迭代。通过这样做,我们得到的其实不是一个完美的协议,而是一个能用就好的东西。
这就是网络相关问题的现实:不断遇到问题,然后解决问题,长此以往不断迭代,迭代到能用即可。
这种思想不会是普适的,有些问题是需要我们通过理论上完美解决才好的,比如数据库,数据库就是通过理论去解决问题的。
TCP三次握手和四次挥手需要用到的符号变量
- 序列号
seq
: 用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;序列号seq
就是这个报文段中的第一个字节的数据编号。
- 确认号
ack
: 期待收到对方下一个报文段的第一个数据字节的序号;序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号。
- 确认
ACK
: 仅当ACK=1
时,确认号字段才有效。ACK=0
时,确认号无效
同步
SYN
: 连接建立时用于同步序号。当SYN=1
,ACK=0
时表示:这是一个连接请求报文段。若同意连接,则在响应报文段中使得SYN=1
,ACK=1
。因此,SYN=1
表示这是一个连接请求,或连接接受报文。SYN
这个标志位只有在TCP
建产连接时才会被置1,握手完成后SYN
标志位被置0。终止
FIN
: 用来释放一个连接。FIN=1
表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接
TCP三次握手(建立连接)
首先需要记得,TCP的三次握手,需要用到两个类型的变量:SYN、ACK。它们的作用分别是:
- ACK:确认。仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置1.
- FIN:终止。用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接。
接下来看 TCP 建连的三次握手。TCP 是基于链接的,所以在传输数据前需要先建立链接,TCP 在传输上是双工传输,不区分 Client 端与 Server 端,为了便于理解,我们把主动发起建连请求的一端称作 Client 端,把被动建立链接的一端称作 Server 端。
一开始,Server端先监听端口,其初始状态是LISTEN。Client端主动打开连接,开始三次握手。
第一次握手:客户端向服务器发出连接请求报文,这时报文首部中的同部位SYN=1
,同时随机生成初始序列号 seq=x
,此时,客户端进程进入了 SYN-SENT
状态,等待服务器的确认。
第二次握手:服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1
,SYN=1
,确认号是ack=x+1
,同时也要为自己随机初始化一个序列号 seq=y
,此时,服务器进程进入了SYN-RCVD
状态,询问客户端是否做好准备。;
第三次握手:客户端进程收到确认后,还要向服务器给出确认。确认报文的ACK=1
,ack=y+1
,此时,连接建立,客户端进入ESTABLISHED
状态,服务器端也进入ESTABLISHED
状态。
后面双方可以进行数据传输。
三次握手的简化流程图为:(建连的时序是从上到下,左右两边的绿色字分别代表 Client 端与 Server 端当时的链接状态。)
在面试时需要明白三次握手是为了建立双向的链接,需要记住 Client 端和 Server 端的链接状态变化。
其他问题:
- 首次握手时候的隐患:SYN超时——针对SYN Flood的防护措施。
——面试回答建连的问题时,可以提到 SYN 洪水攻击发生的原因,就是 Server 端收到 Client 端的 SYN 请求后,发送了 ACK 和 SYN,但是 Client 端不进行回复,导致 Server 端大量的链接处在 SYN_RCVD 状态,进而影响其他正常请求的建连。可以设置 tcp_synack_retries = 0 加快半链接的回收速度,或者调大 tcp_max_syn_backlog 来应对少量的 SYN 洪水攻击。因为是Server端接收到了Client端的SYN包之后的回复的时候出现的问题,所以可以认为是首次握手时候产生的隐患。
- 建立连接后,Client出现故障怎么办?
——保活机制:1.相对方发送保活探测报文,如果未接收到相应则继续发送。2.尝试次数达到了设定的保活探测次数但是仍未收到响应的话,则中断连接。
TCP四次挥手(终止连接)
TCP四次挥手同样需要使用两个类型的变量:FIN和ACK。
从图中看出,通信中 Client 和 Server 两端的链接都是 ESTABLISHED 状态。实际上,TCP 链接的关闭,通信双方都可以先发起,我们暂且把先发起的一方看作 Client
第一次挥手:客户端进程发出连接释放FIN
报文,并且停止发送数据。释放数据报文首部,FIN=1
,其序列号为seq=x
,此时,客户端进入FIN_WAIT_1
(终止等待1)状态。
第二次挥手:服务端进程收到连接释放FIN
报文,发出确认ACK
报文,ACK=1
,ack=x+1
,并且带上自己的序列号seq=y
,此时,服务端就进入了CLOSE_WAIT
(关闭等待)状态。此时,服务端通知高层的应用进程,客户端向服务端的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务端若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE_WAIT
状态持续的时间。客户端收到服务端的确认请求后,此时,客户端就进入FIN_WAIT_2
(终止等待2)状态,等待服务器发送连接释放报文,在这之前依然可以接收服务端发送过来的最后的数据。
第三次挥手:服务端将最后的数据发送给客户端完成后,就向客户端发送连接释放FIN
报文,FIN=1
,ack=x+1
,此时的序列号为seq=z
,此时,服务端就进入了LAST-ACK
(最后确认)状态,等待客户端的确认。
第四次挥手:客户端接收到服务端的连接释放FIN
报文后,必须发出确认报文,ACK=1
,ack=z+1
,而自己的序列号是seq=x+1
,此时,客户端就进入了TIME_WAIT
(时间等待)状态。此时服务端收到客户端发送过来的确认报文,就立即撤销自己的传输控制块TCB
,进入CLOSED
状态,注意此时的TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,客户端没有收到服务端发来的任何数据,证明服务端已正常关闭,此时客户端会撤销相应传输控制块TCB
后,进入CLOSED
状态。至此,TCP的连接才真正的断开了。(服务端结束TCP连接的时间要比客户端稍微早一些)
这里面试官比较可能会问:为什么需要等待2倍的最大报文生存时间之后Client再关闭链接?原因有两个:
- 保证TCP协议的全双工连接能够可靠关闭;
- 保证这次连接的重复数据段从网络中消失,防止端口被重用时可能产生数据混淆。
TCP四次挥手的简化图:
需要注意,在四次挥手的过程中,可以提到在实际应用中有可能遇到大量Socket处在TIME_WAIT或者CLOSE_WAIT状态的问题。一般开启 tcp_tw_reuse
和 tcp_tw_recycle
能够加快 TIME-WAIT 的 Sockets 回收;而大量 CLOSE_WAIT 可能是被动关闭的一方存在代码 bug,没有正确关闭链接导致的。
TCP三次握手和四次挥手的对比总结
无论是建连还是断连,都是需要在两个方向上进行,只不过建连时,Server 端的 SYN 和 ACK 合并为一次发送,而断链时,两个方向上数据发送停止的时间可能不同,所以不能合并发送 FIN 和 ACK。
TCP要三次握手的原因
TCP的握手为什么要三次呢?最后一次不要了,改为两次握手,可以么?
假如现在客户端想向服务端进行握手,它发送了第一个连接的请求报文,但是由于网络信号差或者服务器负载过多,这个请求没有立即到达服务端,而是在某个网络节点中长时间的滞留了,以至于滞留到客户端连接释放以后的某个时间点才到达服务端,那么这就是一个失效的报文,但是服务端接收到这个失效的请求报文后,就误认为客户端又发了一次连接请求,服务端就会想向客户端发出确认的报文,表示同意建立连接。
假如不采用三次握手,那么只要服务端发出确认,表示新的建立就连接了。但是现在客户端并没有发出建立连接的请求,其实这个请求是失效的请求,一切都是服务端在自相情愿,因此客户端是不会理睬服务端的确认信息,也不会向服务端发送确认的请求,但是服务器却认为新的连接已经建立起来了,并一直等待客户端发来数据,这样的情况下,服务端的很多资源就没白白浪费掉了。
采用三次握手的办法就是为了防止上述这种情况的发生,比如就在刚才的情况下,客户端不会向服务端发出确认的请求,服务端会因为收不到确认的报文,就知道客户端并没有要建立连接,那么服务端也就不会去建立连接,这就是三次握手的作用。
NOTE:用更专业的内容可以说,之前发送过程中滞留的包,是”已失效的连接请求报文段“
TCP要四次挥手的原因
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工 模式,这就意味着,在客户端想要断开连接时,客户端向服务端发送FIN
报文,只是表示客户端已经没有数据要发送了,但是这个时候客户端还是可以接收来自服务端的数据。
当服务端接收到FIN
报文,并返回ACK
报文,表示服务端已经知道了客户端要断开连接,客户端已经没有数据要发送了,但是这个时候服务端可能依然有数据要传输给客户端。
当服务端的数据传输完之后,服务端会发送FIN
报文给客户端,表示服务端也没有数据要传输了,服务端同意关闭连接,之后,客户端收到FIN
报文,立即发送给客户端一个ACK
报文,确定关闭连接。在之后,客户端和服务端彼此就愉快的断开了这次的TCP
连接。
或许会有疑问,为什么服务端的ACK
报文和FIN
报文都是分开发送的,但是在三次握手的时候却是ACK
报文和SYN
报文是一起发送的,因为在三次握手的过程中,当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN
+ACK
报文。其中ACK
报文是用来应答的,SYN
报文是用来同步的。但是在关闭连接时,当服务端接收到FIN
报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK
报文,告诉客户端,你发的FIN
报文我收到了,只有等到服务端所有的数据都发送完了,才能发送FIN
报文,因此ACK
报文和FIN
报文不能一起发送。所以断开连接的时候才需要四次挥手来完成。
UDP传输协议
UDP报文
可以看出,UDP和TCP报文一样,都有源端口号、目的端口号等内容。
UDP协议特点
- 面向非连接
- 不维护连接状态,支持同时向多个客户端传输相同的消息
- 数据包报头只有8个字节,额外开销较小
- 吞吐量只受限于数据生成速率、传输速率以及机器性能
- 尽最大努力交付,不保证可靠交付,不需要维持复杂度的连接状态表
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并
TCP和UDP的比较
这个问题比较大,但是其实比较好回答,结合以下几个方面回答即可:
- 面向连接 vs 无连接
- 可靠性
- 有序性
- 速度
- 量级
TCP滑动窗口
TCP滑动窗口解决了什么问题
解决的问题,最基本的就是,如果我们每发送一个包都要得到ACK,那么网络延迟很长的话会很浪费。于是我们可以采用滑动窗口,在窗口中发送过去,让它来ACK,这样可以更好地利用我们的带宽。此外,它还能做到流量控制(流控),我们可以保证两边的收发速度相对一致。
TCP滑动窗口原理介绍
TCP主要使用滑动窗口机制来实现流量控制,这个滑动窗口对于有包发送和接收顺序要求的TCP来说,非常重要。
首先,对于TCP,假设如果没有滑动窗口,每个包只能一个一个,按照序号发送,那么为了满足TCP这个要求,我们只能如图这么设计:
但是这个方案缺点很明显,如果1号没发成功,2号根本不能发,否则乱序了,导致吞吐量太低。
使用滑动窗口方案改进之后的示意图如下:
但是,一次发送2个包过去,如何能够解决确认收到的问题呢?
这就是用到了滑动窗口了,滑动窗口是一个长度被设定好的”窗口”,举例子说明:
灰色是已经发送了的包。黄色和绿色都是在窗口里面的包,黄色是在窗口中已经发送但是没有收到确认的包。绿色是在窗口中同时没有发送的包。
过一段时间之后,情况变成下图所示:
新加入了没有发送的11号包,然后在这段时间内,又发送了8和9号包。
再往后,5发送之后,会读入12;6发送之后,会再读入13,以此类推。
再来看看丢包的情况:我这个包发送过去对方发了ACK,但是我没收到。或者我这个包根本就没法送出去。总之,从发送方角度来看,就是我没有收到ACK。
而做法就是,我如果等了一段时间,没有收到ACK,我会把10和11号包也都发送掉。
但是此时我的窗口已经发满了,不能再读入12,我就只能等到ACK的接收。
如果我始终得不到,就会激发超时重传。
需要注意,ACK的按照顺序的ACK,对方一定会等到5号包出现才会发送ACK,哪怕先收到了6号,7号的包,在TCP协议下,对方也不会发送ACK
如此一来,我们就能保证滑动窗口发送和接收的顺序。
此时可能发生:
此时是之前6,7和8都已经收到了,并且对方想要发送ACK,但是无奈5号包没有收到。此时在我们这边发送了5号包过去,对方收到了之后,会把5,6,7,8一起发送过来,并且 新把12,13,14,15加入到窗口中。
窗口协议的概念整体就是这样的,书上写得比较复杂,但是从工程学角度还是很好理解的。
我们为了优化TCP四次握手的吞吐量问题,我们把数据包一起发送,此时会有窗口的问题产生,我们为了更加有效运用吞吐量,我们使用滑动窗口这个数据结构来解决。这个其实是很朴素的想法。
油管上搬运来的TCP滑动窗口的实现示意动画: 点击此处
TCP滑动窗口具体变量和计算过程
首先,滑动窗口有两个计算的变量:RTT和RTO
- RTT:发送一个数据包到收到对应的ACK所花费的时间。就是发送数据到接收到ACK的时间差
- RTO:重传时间间隔。通过RTT计算得到。设置一个RTO作为间隔时间,超过了RTO就重传,没超过RTO就等待ACK
相关计算公式和示意图:
需要注意,TCP的滑动窗口大小可以为0.当然这是一种特殊情况,如果接收方正在忙,可以先让滑动窗口的大小为0,等到后面再重新调整。
HTTP
HTTP全名为“超文本传输协议”,支持客户/服务器模式,即Client发送请求给Server,Server发送响应回去。大部分目前都是基于HTTP1.1的。
HTTP的请求结构
请求报文如下图所示:
举一个用Wireshark抓HTTP请求报文的例子:
HTTP的响应结构
响应报文如下图所示:
举一个用Wireshark抓HTTP响应报文的例子:
HTTP状态码
一共有五种取值可能:
- 1xx:指示信息——表示请求已经接收,继续处理,但是整个请求还没成功
- 2xx:成功——表示请求已被成功接收、理解、接受,已经成功处理了请求的状态代码
- 3xx:重定向——要完成请求,但是需要进一步操作(往往要再跳转一步)
- 4xx:客户端错误——请求有语法错误或请求无法实现
- 5xx:服务器错误——服务器未能实现合法的请求,表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求的错误。
常见HTTP状态码及其作用:
200 OK:正常返回信息
400 Bad Request:客户端请求有语法错误,不能被服务器所理解
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden:服务器收到请求,但拒绝提供服务(比如IP被禁了)
404 Note Found:请求资源不存在,eg.输入了错误的URL
500 Internal Server Error:服务器发生不可预期的错误
503 Server Unabaliable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
GET请求和POST请求的区别
可以从三个层面解释:
- HTTP报文层面:GET将请求信息放在URL中,POST放在报文体中。
- 数据库层面:GET符合幂等性 (对数据库的多次操作效果一样,PUT也是幂等的)和安全性 (操作不会改变数据库中的数据),POST不符合。
- 其他层面:GET可以被缓存、被存储,而POST不行
Cookie和Session
Cookie简介
举一个使用Cookie的最简单的例子:登录某个网站后,勾选了同意记录信息只有,客户端发送HTTP响应的同时也会发送客户端的信息,下次再登录就可以通过记忆,不用再输入密码了。
- Cookie由服务器发送给客户端的特殊信息,以文本的形式存放在客户端
- 客户端再次请求的时候,会把Cookie回发
- 服务器接收到后,会解析Cookie生成与客户端相对应的内容
Cookie实现方式
示意图如下:
Session简介
- 服务器端的机制,在服务器上保存的信息
- 解析客户端请求并操作session id,按需保存状态信息
Session实现方式
如下图:
实际上,Session有两种实现方式:
- 使用Cookie实现
- 使用URL回写实现
TOMCAT一开始同时支持Cookie和URL回写两种方式,之后如果发现浏览器支持Cookie,就停止URL回写。
Cookie和Session的区别
- Cookie数据存放在客户端的浏览器上,Session数据放在服务器上
- Session相对于Cookie更安全(Session保存在服务器端,更安全,但是占用服务器的资源)
- 若考虑减轻服务器负担,应当使用Cookie
HTTP和HTTPS
简而言之,HTTPS是更加安全版本的HTTP,多了一层SSL或TLS。
HTTP和HTTPS结构图如下所示:
SSL
SSL(Security Sockets Layer,安全套阶层)
- 为网络通信提供安全及数据完整性的一种安全协议
- 操作系统对外的API,SSL3.0后更名为TLS
- 采用身份验证和数据加密保证网络通信的安全和数据的完整性
加密方式
- 对称加密:加密和解密都使用同一个秘钥
- 非对称加密(区块链常用):加密使用的秘钥和解密使用的秘钥是不相同的
- 哈希算法(常用MD5):将任意长度的信息转换为固定长度的值,算法不可逆
- 数字签名:证明某个消息或者文件是某人发出/认同的
HTTPS数据传输流程
- 浏览器将支持的加密算法信息发送给服务器
- 服务器选择一套浏览器支持的加密算法,以证书的形式回发给浏览器
- 浏览器验证证书合法性,并验证证书公钥加密信息发送给服务器
- 服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
- 浏览器解密响应信息,并对消息进行验真,之后进行加密交互数据
HTTP和HTTPS的区别
- HTTPS需要到CA申请证书,HTTP不需要
- HTTPS密文传输,HTTP明文传输
- 连接方式不同,HTTPS默认使用443端口,HTTP默认使用80端口
- HTTPS=HTTP+加密+认证+完整性保护,比HTTP更安全
HTTPS真的很安全么
也不能肯定,很多浏览器默认会填充http://,网页需要从HTTP跳转到HTTPS,这个过程有被劫持的风险
可以使用HSTS(HTTP Strict Transport Security)进行优化
常见题:在浏览器输入URL,按下回车之后经历的流程
答案:一共有六步
- DNS解析。——浏览器进行DNS解析
- TCP连接。——浏览器向服务器发送TCP连接,过程参考TCP三次握手
- 发送HTTP请求——浏览器向服务器发送HTTP请求,内容参考前面HTTP请求包结构
- 服务器处理请求并返回HTTP报文——服务器返回HTTP报文,结构参考前面HTTP响应包结构
- 浏览器解析渲染页面
- 连接结束——参考TCP四次挥手
需要注意,这里的第五步和第六步几乎是同时进行的。
DNS缓存由近到远分别是:浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存根,域名服务器缓存,顶级域名服务器缓存,过程中从哪个缓存中找到了对应的服务器IP,就直接返回,不再继续查询后面的缓存。