- 应用层:通过不同的应用层协议为不同的网络应用提供服务。
- 表示层:使通信的应用程序能够解释交换数据的含义。
- 会话层:负责建立、管理和终止表示层实体之间的通信会话。
- 传输层:为两台主机进程之间的通信提供服务,建立端到端的连接。
- 网络层:选择合适的路由和 IP 选址,确保数据按时成功传送。
- 链路层:将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。
- 物理层:实现计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。
应用层、传输层、网络层常见协议
- HTTP:超文本传输协议,是一个简单的请求-响应协议,它通常运行在 TCP 之上。
- SMTP:提供可靠且有效的电子邮件传输的协议。SMTP 是建立在 FTP 上的一种邮件服务,主要用于系统之间的邮件信息传递,并提供有关来信的通知。
哪些应用层协议使用了TCP,哪些使用了UDP?
上面列举的常用应用层协议中,只有 DNS 使用的是 UDP。因为用DNS域名解析时间更短~
- Host:请求的web服务器域名地址。
- Accept-Encoding:代表客户端能支持的内容压缩编码类型。
- Accept-Language:代表客户端用来展示返回信息的优先语言
- cookie:HTTP请求发送时,会把保存在该请求域名下的所有 cookie 值一起发送给web服务器。
-
响应状态:例如 HTTP/ 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。
Nginx
就是性能非常好的反向代理服务器,用来做负载均衡。从浏览器输入url后都经历了什么
① DNS解析:当用户输入一个网址并按下回车键的时候,浏览器获得一个域名,而在实际通信过程中,我们需要的是一个 IP 地址,因此我们需要先把域名转换成相应 IP 地址。
② TCP连接:浏览器通过 DNS 获取到 Web 服务器真正的地址后,便向 Web 服务器发起 TCP 连接请求,通过三次握手建立好连接后,浏览器便可以将 HTTP 请求数据发送给服务器了。
③ 发送HTTP请求:浏览器向 Web 服务器发起一个 HTTP 请求,HTTP 协议是建立在 TCP 协议之上的应用层协议,其本质是在建立好的 TCP 连接中,按照 HTTP 协议标准发送一个索要网页的请求。这一过程中,会涉及到负载均衡等操作。
④ 处理请求并返回:服务器获取到客户端的 HTTP 请求后,会根据 HTTP 请求中的内容来决定如何获取相应的文件,并将文件发送给服务器。
⑤ 浏览器渲染:浏览器根据响应开始显示页面,首先解析 HTML 文件构建 DOM 树,然后解析 CSS 文件渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将它绘制到屏幕上。
⑥ 断开连接:客户端和服务器通过四次挥手终止 TCP 连接。
为什么有了TCP还需要UDP?各自适用场景?
因为UDP有简单、传输快的优势,在越来越多场景下取代了TCP,如实时游戏。现代网速的提升使得UDP的丢包率降低,如果使用应用层重传,还能够确保传输的可靠性。
如果采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,接收重传的包后再继续发送,延时会越来越大。
- TCP适用场景:适用于要求可靠运输的应用,例如:文件传输、邮件传输。
- UPD适用场景:适用于实时应用,例如:直播、即时通信、域名转换。
UDP实现可靠传输的设计思路?
在应用层模仿传输层TCP的可靠性传输。
- 添加seq/ack机制,确保数据发送到对端
- 添加发送和接收缓冲区。
UPD首部字段和长度?
16位源端口、16位目标端口、16位UDP长度、16位UDP校验和(一共 8 个字节)。
深入理解 TCP 协议:从原理到实战
一句话描述TCP协议:TCP是一个可靠的、面向连接的、基于字节流的、全双工的协议。
-
面向连接的协议要求正式发送数据之前需要通过握手建立一个逻辑连接,结束通信时也是通过有序的四次挥手来断开连接
-
IP是一种无连接、不可靠的协议,TCP想要在IP基础上构建可靠的传输层协议,必须有一个复杂的机制来保障。主要有:
- 数据分块:应用数据被分割成 TCP 认为最适合发送的数据块。
- 校验和:每个TCP包的首部中都有两字节用来表示校验和,防止在传输过程中有损坏。如果收到一个校验和有差错的报文,TCP不会发送任何确定直接丢弃它,等待发送端重传。
- 包的序列号:解决了接收数据的乱序、重复问题。
- 超时重传:当 TCP 发出一个报文段后,它启动一个定时器;如果超过某个时间还没有收到确认,将重发这个报文段。
- ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
- 流量控制:TCP 连接的双方都有一个固定大小的缓冲空间,发送方发送的数据量不能超过接收端缓冲区的大小。当接收方来不及处理发送方的数据,会提示发送方降低发送的速率,防止产生丢包。
- 拥塞控制:当网络某个节点发生拥塞时,减少数据的发送。
TCP是一种字节流(byte-stream)协议,流的含义是没有固定的报文边界。
假设你调用2次write函数往socket里一次写500字节、800字节。write函数只是把字节拷贝到内核缓冲区,最终会以多少条报文发送出去是不确定的。
在TCP中发送端和接收端可以是客户端/服务端,也可以是服务端/客户端,通信的双方在任意时刻既可接收数据也可以是发送数据。
TCP头部是支撑 TCP 复杂功能的基石。完整的TCP头部如下图所示:
TCP报文头部里没有源 ip 和目标 ip 地址,只有源端口号和目标端口号。因为那是 IP 层协议的事,TCP 层只有源端口和目标端口。
源IP、源端口、目标IP、目标端口构成了 TCP 连接的四元组。一个四元组可以唯一标识一个连接。
TCP 是面向字节流的协议,通过 TCP 传输的每个字节都分配了序列号,序列号指的是本报文段 第一个字节的序列号。
序列号加上报文的长度,就可以确定传输的是哪一段数据。
因为网络层(IP层)不保证包的顺序,TCP 协议利用序列号来解决网络包乱序、重复的问题,以保证数据表以正确的顺序组装传递给上层应用。
初始序列号(Initial Sequence Number, ISN):在建立连接之初,通信双方都会各自选择一个序列号,称之为初始序列号。在建立连接时,通信双方通过SYN报文交换彼此的ISN。
TCP使用确认号ACK 来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。
关于ACK的几个注意点:
- 不是所有的包都需要确认
- 不是收到了数据包就要立马确认,可以延迟一会再确认
- ACK包本身不需要被确认,否则就会无限循环了
- 确认号永远是表示小于此确认号的字节都已经收到
TCP有很多种标记,有些用来发起连接同步初始序列号,有些用来确认数据包,还有些用来结束连接。
TCP定义了一个8位的字段用来表示Flags,大部分只用到了后6个,如下图所示
-
- SYN(synchronized):用于发起连接数据表同步双方的初始序列号
- RST(Reset):这个标记用来强制断开连接
- FIN(Finish):通知对方我发完了所有数据,准备断开连接,后面就不会发数据包给你了。
- PSH(Push):告知对方这些数据包收到以后应该马上交给上层应用,不能缓存下来。
这也太小了,因此TCP协议引入了【TCP窗口缩放】选项作为窗口缩放的比例因子,比例因子的范围是0~14,其中最小值0表示不缩放。比例因子可以将窗口扩大到原来的2的n次方。
例如:窗口大小缩放前为1050,缩放因子为7,则真正的窗口大小为^=134400。
常用的选项有以下几个:
- MSS:TCP允许的从对方接收的最大报文段
- SACK:选择确定选项
- IP数据包长度在超过链路的MTU时,发送之前需要分片。而TCP层为了IP层不用分片主动将包分割为MSS大小。
三次握手的最重要的是交换彼此的 ISN(初始序列号)。
SYN 报文不携带数据,但是却占用一个序号,下一次发送时数据序列号要加一。
除了交换彼此的 ISN,三次握手的另一个作用是交换一些辅助信息,比如 MSS、窗口大小、窗口缩放因子、是否支持选择确认等。
在握手之前,主动打开连接的客户端结束 CLOSE 阶段,被动打开的服务器也结束 CLOSE 阶段,并进入 LISTEN 阶段。随后进入三次握手阶段:
① 首先客户端向服务器发送一个 SYN 包,并等待服务器确认。随后客户端进入 SYN-SENT
阶段。
- 标识位为 SYN,表示请求建立连接;
② 服务器接收到客户端发来的 SYN 包后,并返回一段 TCP 报文,结束 Listen 状态,进入 SYN-RECV
阶段。
- 标识位为 SYN 和 Ack,表示确认客户端的报文 Seq 序号有效,并同意创建新连接;
- 确认号为 Ack = x + 1,表示收到客户端的序号 Seq,并将其加 1 作为自己确认号 Ack 的值;
③ 客户端收到 SYN+ACK 包后,确认了自己能正常发送数据,从而进入到 ESTABLISHED
阶段,并返回最后一段报文。
- 标识位为 ACK,表示确认收到服务端同意连接的信号;
- 序号为 Seq = x + 1,表示收到服务端的确认号 Ack,并将其值作为自己的序号值;
- 确认号为 Ack = y + 1,表示收到服务端的序号 Seq,并将其加 1 作为自己的确认号 Ack 的值。
当服务器收到客户端的最后一段报文后,确认自己能正常发送信息到客户端,从而进入到 ESTABLISHED
阶段,自此完成三次握手。
为什么第 2 次握手传回 ACK,还要传回 SYN?
ACK 是为了告诉客户端发来的数据已经接收到了。
而传回 SYN 是为了告诉客户端,服务端收到的消息确实是客户端发送的消息。
为什么需要三次握手,两次不行吗?那能不能四次握手?
三次握手的主要目的是确认自己和对方的发送和接收都是正常的,从而保证了双方能够进行可靠通信。若采用两次握手,当第二次握手后就建立连接的话,此时客户端知道服务器能够正常接收到自己发送的数据,而服务器并不知道客户端是否能够收到自己发送的数据。
可以四次握手。但是可以合并,这样可以提高连接的速度与效率。
① 客户端调用 close 方法,主动关闭,发送一个 FIN 报文给服务端。随后客户端进入 FIN-WAIT-1
状态,即【半关闭】阶段,并且停止向服务端发送通信数据。
- 标记位为 FIN,表示请求释放连接;
② 服务端收到请求断开连接的 FIN 报文后,服务端进入 CLOSE_WAIT
状态,并返回一段 TCP 报文。
- 标记位为 ACK,表示接收到客户端释放连接的请求;
- 确认号为 Ack = u + 1,表示是在收到客户端报文的基础上,将其序号值加 1 作为报文确认号 Ack 的值。
客户端收到服务器发送的 TCP 报文后,确认服务器已经收到了释放连接的请求了,随后客户端进入 FIN-WAIT-2
状态。
③ 服务端在发出 Ack 确认报文后,服务器端会将遗留的待传数据发给客户端,待传输完成后便也做好了关闭连接的准备了,随后进入 LAST-ACK
阶段,停止发送数据。并再次向客户端发送一段 TCP 报文。
- 标记位为 FIN 和 ACK,表示服务器已经准备好释放连接;
- 确认号仍然为 Ack = u + 1,表示是在收到客户端报文的基础上,将其序号值加 1 作为报文确认号 Ack 的值。
④ 客户端收到从服务器发来的 TCP 报文,确认了服务器已经做好了释放连接的准备,于是进入 TIME-WAIT
阶段,并向服务器发送一段报文。
- 标记位为 ACK,表示接收到服务器释放连接的信号;
- 序号为 Seq = u + 1,将其确认号 Ack 值作为本段序号的值。
- 确认号为 Ack = w + 1,表示是在收到服务器报文的基础上,将其序号 Seq 的值作为本段报文确认号的值。
发送后客户端在 TIME-WAIT
状态等待 2MSL之后,进入 CLOSED
阶段。服务端收到客户端发出的 TCP 报文后,也进入 CLOSED
阶段。由此完成四次挥手。
为什么FIN和SYN要消耗一个序列号呢?
因为 FIN 和 SYN 信号都是需要 ACK 的,也就是必须回复这个信号,如果它不占用一个字节的话,是判断不出这个 ACK 是回复这个信号 还是回复这个信号之前的数据包的。
例如:如果 FIN 信号不占用一个字节,回复 FIN 的 ACK 包可能被认为是 之前发送的数据包被重新发了一次,第二次挥手无法完成,连接也就无法正常关闭了。
为什么挥手要四次,三次可以吗?
释放 TCP 连接时之所以需要四次挥手,是因为 FIN 释放连接报文和 ACK 确认接收报文是分别在两次挥手中传输的。 当主动方在数据传送结束后发出连接释放的通知,由于 被动方可能还有必要的数据要处理,所以会先返回 ACK 确认收到报文。当被动方也没有数据再发送的时候,才发出连接释放通知。
例如打电话,我说 “我没东西说了”,对方回答 “知道了”,但是对方可能还有没说完的话,我不能要求对方跟着自己的节奏结束电话,于是对方又说了一通,最后对方说 “我说完了”,我回答 “知道了”,这样通话才算结束。
CLOSE-WAIT 状态是服务端第三次挥手,把剩余的待发送的数据发送给客户端后进入的状态。因此,CLOSE-WAIT 状态的意义就是为了 保证服务器在关闭连接之前将待发送的数据发送完成。
- 首先检查是不是代码的问题,看是否服务端程序忘记关闭连接。如果是,则修改代码。
- 调整系统参数,一般一个 CLOSE_WAIT 会维持至少 2 个小时的时间,我们可以通过调整参数来缩短这个时间。
TIME-WAIT 发生在第四次挥手,只有 主动断开的那一方才会进入 TIME_WAIT 状态,且会在那个状态持续 2 个 MSL。原因:
- 若没有该状态,即客户端在收到服务端的 FIN 报文后立即关闭连接,此时服务端的端口并没有关闭,若客户端在相同的端口立即建立新的连接,则有可能收到上一次连接中残留的数据包,从而导致不可预料的异常出现。
- 除此之外,假设客户端最后发送的 Ack 包传输时丢失了,服务端没有收到确认。由于 TCP 协议的超时重传机制,服务端将重发 FIN 报文,若客户端直接关闭了。收到服务端重新发送的 FIN 包时,客户端就会用 RST 包来响应服务端,这将会使对方认为是有错误发生,然而这只是正常的关闭过程,并没有出现异常情况。
- 如果服务器在 1MSL 后仍然没有收到客户端的 Ack 确认报文,那么它会向客户端重传 FIN 报文,对客户端而言,从客户端发出 ACK 报文起,重传的 FIN 报文的最晚到达时间是 2MSL。
- 因此 2MSL 为的是确认服务器能否接收到客户端发出的 ACK 确认报文。
大量TIME_WAIT造成的影响,怎么优化?
导致的问题:在 高并发短连接 的 TCP 服务器上,例如京东、淘宝等,当服务器处理完请求后主动请求关闭连接,这样服务器上会有大量的连接处于 TIME_WAIT
状态,服务器维护每一个连接需要一个 socket,而 socket 的使用是有上限的,如果持续高并发,会导致一些正常的 连接失败。
解决方案:修改配置使 处于 TIME_WAIT 状态下的 socket ,可以能够快速回收和重用。 也可以采用长连接的方式,但是长连接的业务中并发量一般不会太高。
TCP 最大连接数限制
client 在每次发起 TCP 连接请求时,如果自己并不指定端口的话,系统会随机选择一个本地端口,该端口是独占的,不能和其他 TCP 连接共享。本地端口个数最大只有 65536,除了端口 0 不能使用外,其他端口在空闲时都可以正常使用,这样可用端口最多有 65535 个。
在实际环境中,一些 IP 地址和端口具有特殊含义,没有对外开放,还有机器资源等限制,一般通过增加内存、配置参数等方式,单机最大并发 TCP 能超过 10 万。
SYN泛洪攻击是一种 DoS(拒绝服务攻击)。
想象一个场景:客户端大量伪造 IP 发送 SYN 包,服务端回复的 ACK+SYN 去到了一个未知的 IP 地址。会造成服务端大量的连接处于 SYN_RCVD 状态,而服务器的半连接队列大小也是有限制的,如果半连接队列满,也会出现无法处理正常请求的情况。
- 现在服务器上的tcp_syncookies默认等于1,表示连接队列满时启用。0表示禁用、2表示始终启用。
- 原理就是:在三次握手的最后阶段才分配连接资源。
- 永远记住 ACK 是表示这之前的包都已经全部收到
如果发送5000个字节的数据包,因为MSS的限制每次传输1000个字节,分5段传输。
数据包 1 发送的数据正常到达接收端,接收端回复 ACK 1001。如果数据包 2 因为某些原因未能到达服务器,其他包正常到达,这时接收端也不能ack 3 4 5数据包,因为数据包 2 还没收到,接收端只能回复 ACK1001。
快速重传机制与 SACK
快速重传的含义是:当接收端收到一个不按序到达的数据段时,TCP 立刻发送 1 个重复 ACK,当发送端收到 3 个或以上重复 ACK,就意识到之前发的包可能丢了,于是马上进行重传,不用等到重传定时器超时再重传。
这里有一个问题,发送 3、4、5 包收到的全部是 ACK=1001,快速重传解决了一个问题: 需要重传。因为除了 2 号包,3、4、5包也可能丢失,那到底是只重传 2 号包还是重传 2,3,4,5 所有包呢?
聪明的网络协议设计者,想到了一个好办法:
- 收到 3 号包的时候在 ACK 包中告诉发送端。我目前收到的最大连续的包序号是1000(ACK=1001),[1:1001]、[]区间的包我也收到了
- 收到 4 号包的时候在 ACK 包中告诉发送端。我目前收到的最大连续的包序号是1000(ACK=1001),[1:1001]、[]区间的包我也收到了
- 收到 5 号包的时候在 ACK 包中告诉发送端。我目前收到的最大连续的包序号是1000(ACK=1001),[1:1001]、[]区间的包我也收到了
这样发送端就清楚知道只用重传 2 号数据包就可以了,数据包3、4、5已经确认无误的被对端收到了。这种方式被称为 SACK(Selective Acknowledgment)
TCP流量控制(滑动窗口)
TCP 会把要发送的数据放入发送缓冲区(Send Buffer),接收到的数据放入接收缓冲区(Receive Buffer),应用程序会不停的读取接收缓冲区的内容进行处理。
流量控制做的事情就是,如果接受缓冲区已满,发送端应该停止发送数据。
为了控制发送端的速率,接收端会告知客户端自己的接收窗口(rwnd),也就是接受缓冲区中空闲的部分。
TCP在收到数据包回复的 ACK 包里会带上自己接收窗口的大小,发送端会根据这个值调整发送策略。
当对方的 ACK 包中表明自己的接收窗口大小后,发送端会把自己的 [发送窗口] 限制在这个大小以内。
如果处理能力有限,导致接收缓冲区满,接收窗口大小为 0,发送端应该停止发送数据。
从 TCP 角度而言,数据包的状态可以分为四种:
- 粉色部分#1:表示已发送并且已经收到 确认ACK 的数据包
- 蓝色部分#2:表示已发送但是还没收到 确认ACK 的数据包。如果在一段时间内没有收到 ACK,发送端需要重传这部分数据包。
- 绿色部分#3:表示未发送,但接收端有空间可以接收的数据包
- 黄色部分#4:表示未发送,而且这部分接收端没有空间接收的数据包
发送窗口表示在某个时刻,发送端被允许发送的最大数据包大小,包括还没收到确认ACK的数据包(#2、#3区域)
可用窗口是发送端还能发送的最大数据包大小(#3区域)
如果上图中的可用窗口的46~51发送出去,可用窗口区间减小到 0,这个时候只有收到接收端的 ACK 数据,否则发送端将不能发送数据。
因此,流量控制实际上是对【发送方数据流量】的控制。
流量控制这种机制确实可以防止发送端向接收端过多的发送数据,但是它只关注了发送端和接收端自身的状况,而没有考虑整个网络的通信状况。于是出现了我们今天要讲的拥塞处理。
拥塞处理涉及下面这几个算法:
为了实现上面算法,TCP的每条连接都有两个核心值:
拥塞窗口(cwnd)和接收窗口(rwnd)有什么区别呢?
- 接收窗口是接收端的限制,是接收端还能接收的数据量大小
- 拥塞窗口是发送端的限制,是发送端在还未收到对端 ACK 之前还能发送的数据量大小
我们在TCP头部的windows字段讲的是 接收窗口大小。
拥塞窗口初始值等于操作系统的一个变量 initcwnd,最新的 linux 系统 initcwnd 默认值等于 10。
- 真正的发送窗口大小 = 【接收端窗口大小】 与 【发送端自己的拥塞窗口大小】两者的最小值。
如果接收窗口比拥塞窗口小,表示接收端处理能力不足。如果拥塞窗口小于接收窗口,表示接收端有处理能力,但网络拥塞。
因为发送端能发送多少数据,取决于两个因素:
- 对方能接收多少(接收窗口)
- 自己为了避免网络拥塞主动减少数据量(拥塞窗口)
发送端和接收端不会交换 拥塞窗口cwnd 这个值,这个值是维护在发送端本地内存中的一个值。
拥塞控制的算法本质是控制拥塞窗口(cwnd)的变化。
拥塞处理算法之:慢启动
拥塞控制是从整个网络的大局观来控制的,如果有足够的带宽,你可以选择用最快的速度传输数据。但是如果是一个缓慢的移动网络,发送数据过多,只是造成更大的网络延迟。
每个 TCP 连接都有一个拥塞窗口的限制,最初这个值很小,随着时间的推移,每次发送的数据量如果在不丢包的情况下,“慢慢”的递增,这种机制被称为【慢启动】
- 第一步,三次握手以后,双方通过 ACK 告诉对方自己接收窗口(rwnd)大小,准备就绪。
- 第二步,通信双方各自初始化自己的 [拥塞窗口](cwnd)大小
- 第三步,cwnd初始值较小时,每收到一个ACK,cwnd+1;每经过一个 RTT(往返时延),cwnd变为之前的两倍。
慢启动算法的拥塞窗口(cwnd)肯定不能永无止境的指数级增长下去,它的阙值称为【慢启动阙值】。
与慢启动的区别在于:每经过一个 RTT 才将拥塞窗口加 1,不管期间收到多少个 ACK。
拥塞处理算法之:快速重传 和 快速恢复
快恢复算法是和快重传算法配合使用的,该算法主要有以下两个要点:
① 当发送方连续收到三个重复确认,拥塞阙值 ssthresh 值减半
② 由于发送方可能认为网络现在没有拥塞,因此与慢开始不同,把 cwnd 值设置为 ssthresh 减半之后的值,然后执行拥塞避免算法,线性增大 cwnd。