先直接给出 TCP 报文段的格式,如下图所示:(这里只介绍本章会用到的字段,图中有颜色标注出来的字段!!)
端口号:由于 TCP 提供端到端的连接,计算机中可以通过端口号唯一确定一个进程,所以 TCP 报文段的前 4 个字节是源端口号和目的端口号
序列号:在建立连接时由计算机生成随机数作为初始序列号,通过 SYN 包发送给接收端,每发送一次数据就将序列化累加一次该包中「数据字节数」的大小,用来解决网络包乱序的问题
确认应答号:指下一次期望收到数据的序列号,发送端收到这个确认应答号后可以认为这个序列号之前的数据都已经被正常接收,用来解决网络丢包的问题
举例:假设客户端初始序列号都是 0,第一次发送数据到服务器,其中:Seq = 0,数据大小为 520;服务器收到数据后,发送 ACK 包给客户端,其中:ack = 520,表示下一次期待收到序列号位 520 的包,并且序列号 520 之前的数据 ([0, 519]) 都已经正常接收
控制位:用来表示该 TCP 数据报的类型,每个控制位的大小为 1 bit
ACK:该位为 1 时,表示「确认应答」的字段有效,TCP 规定除了第一次握手的 SYN 包外,其余所有包该字段必须设置为 1
RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开
SYN:该位为 1 时,表示希望建立连接,并在序列号字段设置初始值
FIN:该位为 1 时,表示今后不会再发数据,希望断开连接
问题:TCP 报文段中有校验和字段,主要用于判断数据有无出错,也就是用于差错控制,如果判断有问题直接丢弃该包,但在链路层会添加帧尾,其中包含 CRC 校验,也用于差错控制,那么这两者有何区别?
首先当帧离开网卡后就进入物理链路中,帧尾的 CRC 校验主要用于判断在物理链路的传输中有无产生差错;而到达另一个主机的链路层后会向上传递给网络层,网络层的一些操作可能会使报文出错,所以需要在传输层再次判断是否存在差错
更详细解释可见 在计算机网络中数据链路层和传输层都有流量控制和差错控制他们有什么区别?又为啥需要在不同层设置相同功能?
TCP 是传输层协议,具有三个特点:
面向连接:在通信前需要通过三次握手建立连接,在通信结束时需要通过四次挥手断开连接
可靠:TCP 协议可以差错控制,可以保证一个报文从源主机顺利无差错的到达目的主机
基于字节流:如果 TCP 报文长度超过 MSS 会进行拆分,所以一条完整的应用层数据可能对应多个 TCP 报文;而 UDP 是基于报文,不会拆分,一条完整的应用层数据对应一个 UDP 报文
注意:TCP 报文会拆分;而 UDP 报文不会拆分,如果一条 UDP 报文长度超过 MSS,会在网络层进行拆分,在目的主机的网络层会拼装成完整 UDP 报文,然后才向上发送给传输层
问题:为什么 TCP 报文拆分,UDP 报文不拆分??
一般而言 TCP 报文较长,又要保证可靠传输,如果不拆分出现差错需要重传整条 TCP 报文,开销过大,将 TCP 报文拆分后只需要重传出错的部分即可;而 UDP 报文较短,整条报文重传开销也不算很大
UDP 是面向无连接、尽最大努力交付的数据传输服务,无法保证可靠,传输单位是用户数据报。UDP 报文头部固定 8 个字节,而 TCP 报文头部存在可变选项,在不算可变选项前提下 TCP 报文头部 20 字节
UDP 报文头部格式如下图所示:
端口号:虽然 UDP 不保证可靠传输,但依旧是端到端的通信,所以需要源端口号和目的端口号
包长度:该字段记录了 UDP 首部长度和数据长度之和
校验和:校验和是为了提供可靠的传输而设计的,虽然 UDP 不要求保证可靠,但也可以设计成可靠
连接
TCP 是面向连接的传输层协议,传输数据前先要建立连接
UDP 是面向无连接的传输层协议,随时随地可以传输数据
服务对象
TCP 是一对一的两点服务,一条连接只有两个端点
UDP 由于是无连接的,所以支持一对一、一对多、多对多的通信
可靠性
TCP 是可靠交付数据,数据可以无差错、不丢失、不重复、按序到达
UDP 是尽最大努力交付,不保证可靠交付数据,但可以基于 UDP 实现一个可靠的传输协议
拥塞控制、流量控制
TCP 有拥塞控制、流量控制,保证数据传输的安全性
UDP 没有这些机制,即使网络堵塞情况严重也不会影响 UDP 发送的速率
首部开销
TCP 报文的首部在没有使用选项的前提下是 20 字节,如果使用选项就会变长
UDP 报文的首部固定为 8 字节
传输方式
TCP 是流式传输,没有边界,但通过序列号保证顺序和可靠性
UDP 是面向报文传输,有边界,但可能会丢包和乱序
分片
如果 TCP 数据大于 MSS,会在传输层分片,目标主机收到后会在传输层组装成完整数据,会重传有问题的报文
如果 UDP 数据大于 MSS,不会在传输层分片,但会在网络层分片,目标主机收到后在网络层组装成完整数据,在向上传给传输层
由于 UDP 面向无连接,可以随时随地发送数据,再加上 UDP 本身的处理简单高效,所以常用于:
总包量较少的通信,如:DNS、SNMP 等
不需要严格可靠性的场景,如:视频、音频等
广播通信 (一对多)
由于 TCP 面向连接,提供可靠的数据传输,所以常用于:
FTP 文件传输
HTTP / HTTPS
问题一:为什么 UDP 首部没有首部长度,而 TCP 首部有首部长度字段?
UDP 首部固定为 8 个字节,而 TCP 首部有选项字段,长度不固定
问题二:UDP 和 TCP 可以使用同一个端口吗?
由于端口号是传输层用来唯一确定主机中的应用进程,UDP 和 TCP 有不同的处理模块,就算 UDP 和 TCP 使用同一个端口,也可以使用协议类型来区分报文属于哪个应用进程
TCP 是面向连接的协议,在通信前需要通过三次握手建立连接,三次握手的过程如下图所示:
三次握手的详细过程:
最开始,客户端和服务器都处于 CLOSE 状态,然后服务器主动监听某个端口,处于 LISTEN 状态
第一次握手:客户端随机初始化序列号 (client_isn),将它置于 TCP 首部的序列号字段,同时把标志位 SYN 置为 1,表示 SYN 报文,最后发送给服务器,之后客户端处于 SYN_SEND 状态
第二次握手:服务器收到客户端的 SYN 报文后,首先也随机初始化序列号 (server_isn),将它置于 TCP 首部的序列号字段,同时把标志位 SYN 置为 1,然后将收到的 SYN 报文中的序列号 + 1 置于 TCP 首部的确认应答号字段,同时把标志位 ACK 置为 1,表示 SYN-ACK 报文,最后发送给客户端,之后服务器处于 SYN_RECV 状态
第三次握手:客户端收到服务器的 SYN-ACK 报文后,然后将报文中的序列号 + 1 置于 TCP 首部的确认应答号字段,同时把标志位 ACK 置为 1,表示 ACK 报文,最后发送给服务器,之后客户端处于 ESTABLISHED 状态
最后服务器收到客户端的 ACK 报文后,也处于 ESTABLISHED 状态
注意一:客户端发送第三次握手后,不管服务器有没有收到,可以直接发送数据
注意二:前两次握手过程不可以携带数据,第三次握手过程可以携带数据
注意三:如果没有服务器没有收到第三次握手,对于客户端后续发送的数据,一般会包含 ACK,所以可以当作第三次握手。这也是 TCP 规定除了第一次握手的 SYN 包外,其余所有包 ACK 必须为 1 的原因
更详细解释可见 TCP第三次握手能携带数据吗?做个实验就知道!
为什么是三次握手,而不是两次握手或四次握手?主要有三个原因!!
三次握手的首要原因就是为了避免历史重复连接初始化造成混乱!!
下面模拟一种场景:客户端先发送了 SYN (Seq = 90) 报文,然后客户端宕机,而且这个 SYN 报文还被网络阻塞,服务器并没有收到,接收客户端重启后又重新建立连接,发送 SYN (Seq = 100) 报文
上面场景的三次握手过程如下图所示:
从上图可以看到,由于服务器需要收到客户端的第三次握手才会分配资源建立连接,但客户端收到服务器对历史连接的 SYN-ACK 报文后会发送一个 RST 报文中止连接,避免了服务器分配资源建立连接
如果只有两次握手,那么过程如下图所示:
区别:两次握手会先分配资源建立连接,后收到 RST 报文后中止连接,这样无疑白白增加了不必要的初始化开销!!
TCP 需要保证可靠传输,具体包括:数据可以无差错、不丢失、不重复、按序到达,可以根据序列号来判断报文的顺序以及是否重复
客户端和服务器在建立连接前都会随机初始化序列号,所以需要让双方知道对方的初始化序列号,才可以保证后续数据的不重复,按序到达
两次握手只能保证服务器知道客户端的初始序列号,但无法保证客户端知道服务器的初始序列号
问题:为什么客户端和服务器都需要随机初始化序列号,直接初始化为 0 不行吗?
每次建立连接时都需要随机初始化序列号,主要是为了:
防止历史报文被下一个相同四元组的连接接收
防止黑客伪造相同序列号的 TCP 报文被对方接收
假设客户端第一次发送的 SYN 报文由于网络阻塞长时间抵达服务器,经过一段时间后客户端超时重发 SYN 报文 (Seq 和第一个相同),如果只有两次握手,那么就有两次分配资源建立连接的过程
如果只有两次握手,那么过程如下图所示:
注意:宕机重启后重发的 SYN 报文序列号会发生变化;超时重发的 SYN 报文序列号不会发生变化
问题:为什么超时重发旧的连接不会被客户端识别到,然后发送 RST 报文中止连接?
由于只有两次握手,客户端会将旧的 SYN 报文认为是服务器重发报文,因为两个报文的序列号是一样的,所以不会发送 RST 报文中止连接
当客户端发送第一次握手后会处于 SYN_SEND 状态。如果第一次握手丢失,那么客户端会超时重发第一个 SYN 包,序列号相同
超时重发规则:内核会通过参数控制重发次数,默认为 5。第一次超时重发在 1s 后,第二次超时重发在 2s 后,第三次超时重发在 4s 后,第四次超时重发在 8s 后,第五次超时重发在 16s 后。第五次超时重发后会等待 32s,如果 32s 内依旧没有收到服务器的响应就会断开连接
对于客户端来说可能认为是自己发送的第一次握手服务器没有收到,就会按照第一次握手丢失的情况超时重发报文
对于服务器来说可能认为是自己发送的第二次握手客户端没有收到,就会按照相同的规则重发第二次握手
对于客户端来说不会重发 ACK 报文,而是由服务器重发第二次握手报文
TCP 是面向连接的协议,在通信结束前需要通过四次挥手断开连接,四次挥手的过程如下图所示:
四次挥手的详细过程:
第一次挥手:客户端已经发送完所有数据,打算断开连接,此时会发送一个 TCP 首部 FIN 标志位为 1 的报文,也称 FIN 报文,之后客户端处于 FIN_WAIT_1 状态
第二次挥手:服务器收到客户端的 FIN 报文后,向客户端发送一个 ACK 报文,之后服务器处于 CLOSE_WAIT 状态
第三次挥手:等待服务器处理完数据后,也向客户端发送一个 FIN 报文,之后服务器处于 LAST_ACK 状态
第四次挥手:客户端收到服务器的 FIN 报文后,向客户端发送一个 ACK 报文,之后客户端处于 TIME_WAIT 状态,等待 2MSL 时间后客户端处于 CLOSE 状态
服务器收到客户端的 ACK 报文后,处于 CLOSE 状态
问题:为什么是四次挥手?
首先彻底断开连接必须在双方都处理完数据的基础上,任意一方断开连接都不算真正的断开连接,只有当双方都主动断开连接后才算真正断开连接
一般断开连接都是客户端主动发起,它会向服务器发送一个 FIN 包,表示自己已经处理完所有数据,可以断开连接
当服务器收到 FIN 包后可能还需要处理剩余数据,当处理完剩余数据后向客户端发送 FIN 包表示自己也可以断开连接
当客户端发送第一次挥手后会处于 FIN_WAIT_1 状态。如果第一次挥手丢失,那么客户端会超时重发第一个 FIN 包
超时重发规则:内核会通过参数控制重发次数,假设为 5。第一次超时重发在 1s 后,第二次超时重发在 2s 后,第三次超时重发在 4s 后,第四次超时重发在 8s 后,第五次超时重发在 16s 后。第五次超时重发后会等待 32s,如果 32s 内依旧没有收到服务器的 ACK 包就会断开连接
第二次挥手是服务器向客户端发送 ACK 包,而 ACK 包是不会重发,所以如果第二次挥手丢失,客户端会认为是自己发送的第一次挥手服务器没有收到,就会按照第一次挥手丢失的情况超时重发报文
第三次挥手丢失类似于第一次挥手丢失,服务器会超时重发 FIN 包
第四次挥手丢失类似于第二次挥手丢失,服务器会超时重发第三次挥手的 FIN 包
MSL,全称 Maximum Segment Lifetime,表示报文最大生存时间,在网络中任何报文超过该时间都将会被丢弃,Linux 将 MSL 设置为 30s
客户端处于 TIME_WAIT 状态后需要等待 2MSL 后才关闭,主要是为了服务器也可以顺利关闭连接。当第四次握手丢失,服务器会超时重传一个 FIN 报文,最坏情况下只需要 2MSL 客户端就可以重新收到服务器重发的 FIN 报文,然后重新发送 ACK 包给服务器,此时 TIME_WAIT 等待时间重新计算。也就是说客户端可以允许一次报文丢失
至于不把等待时间设置为更长,是因为一次丢包的概率是 1/100,连续两次丢包的概率是 1/10000,概率太小,忽略它更具有性价比
主要有两个原因:
保证被动关闭的一方可以顺序关闭,原因同问题一
防止历史连接中的数据被后面相同四元组的连接错误的接收,等待 2MSL 可以保证本次连接中所有数据包要么都被成功接收,要么超时被丢弃
主要有两个原因:
占用系统资源,如:CPU、文件描述符、内存、线程等
占用端口资源,系统中端口资源有限
只有主动关闭的一方才会出现 TIME_WAIT,服务器出现大量的 TIME_WAIT 说明服务器主动关闭了连接,主要有三个原因:
HTTP 没有使用长连接,导致每次发送完一个数据后都会断开连接,但 HTTP/1.1 已经默认开始长连接
HTTP 长连接超时,系统一般会设置超时时间,如果一个长连接在规定时间内没有通信就会自动断开连接
HTTP 长连接数量达到上限,服务器会主动关闭长连接,就会出现 TIME_WAIT
只有被动关闭的一方才会出现 CLOSE_WAIT,服务器出现大量的 CLOSE_WAIT 说明服务器卡在了发送第三次挥手报文,也就是服务器程序没有调用close
函数关闭连接,只有客户端单方面关闭连接