TCP
TCP 简介
传输层控制协议(Transport Control Protocol),TCP/IP 协议栈的核心之一。位于应用层与网络层之间,提供面向连接的、可靠的字节流服务。
记住关键词“面向连接”、“可靠”、“字节流”,这是学习掌握 TCP 的关键:
- 面向连接:客户端、服务端交换数据前,需要建立连接
- 可靠:通过特定机制,在不可靠的网络之上,确保报文准确送达
- 字节流:数据的最小单位为字节。至于字节中存储内容的含义,由于应用层的程序决定
TCP 如何确保服务可靠性
TCP 花了大量的功夫在确保传输层服务的可靠性上,具体举措包括(但不限于)以下:
- 应用数据切割:应用数据被分隔成 TCP 认为最适合发送的多个报文段(由特定的算法和机制来确认)
- 接收端确认:接收端收到报文段后,会向发送端发送确认报文;
- 超时重传机制:发送端发送一个报文段后,会启动定时器,等待接收端确认收到这个报文;如果没有及时收到确认,发送端会重新发送报文;
- 数据校验和:发送端发送的报文首部中,有个叫做校验和(checksum)的特殊字段,它是根据报文的首部、数据计算出来的。这是一个端到端的校验和,用来检测传输过程数据的变化。接收端收到报文后会对校验和进行检查,如果校验和存在差错,则丢弃这个报文,且不确认收到此报文(等待发送端超时重发)
- 报文段排序:TCP 报文包裹在 IP 数据包里进行传输,而 IP 数据包的到达次序是不固定的。接收端会对接收到的报文段重新排序,这个对应用层是无感知的;
- 去重复:接收端丢弃重复的报文(比如,因某些原因,虽然接收端已经收到报文,且给发送端发送了接收确认,但接收端没有收到该确认,超时后重新发送了同样的报文);
- 流量控制:TCP 连接双方都有固定大小的缓冲空间,且只允许发送端发送缓冲空间能够容纳的数据,避免缓冲区溢出;
TCP 首部
TCP 首部格式如下图所示,在不包含可选字段(其后会有一些填充 Padding)的情况下,大小通常为 20 个字节。
三次握手
为啥需要三次
- 确认双方的收发能力
- 第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
- 第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
- 第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
- 序列号可靠同步
如果是两次握手,服务端无法确定客户端是否已经接收到了自己发送的初始序列号,如果第二次握手报文丢失,那么客户端就无法知道服务端的初始序列号,那 TCP 的可靠性就无从谈起。 所以,只有三次握手才能确认双方的接收与发送能力是否正常。
- 阻止重复历史连接的初始化
客户端由于某种原因发送了两个不同序号的 SYN 包,我们知道网络环境是复杂的,旧的数据包有可能先到达服务器。如果是两次握手,服务器收到旧的 SYN
就会立刻建立连接,那么会造成网络异常。
如果是三次握手,服务器需要回复 SYN+ACK 包,客户端会对比应答的序号,如果发现是旧的报文,就会给服务器发 RST
报文,直到正常的 SYN
到达服务器后才正常建立连接。
所以三次握手才有足够的上下文信息来判断当前连接是否是历史连接。
- 安全问题
我们知道 TCP 新建连接时,内核会为连接分配一系列的内存资源,如果采用两次握手,就建立连接,那会放大 DDOS 攻击的。
初始序列号(ISN)
ISN 全称是 Initial Sequence Number,是 TCP 发送方的字节数据编号的原点,告诉对方我要开始发送数据的初始化序列号
ISN 不是固定的,如果固定,攻击者很容易猜出后续的确认序号,为了安全起见,避免被第三方猜到从而发送伪造的 RST
报文,因此 ISN 是动态生成的
半连接队列
服务器第一次收到客户端的 SYN
之后,就会处于 SYN_RCVD
状态,此时双方还没有完全建立连接。服务器会把这种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
第三次是否可以携带数据
第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN
报文中放入大量的数据,疯狂着重复发 SYN
报文,这会让服务器花费大量的内存空间来缓存这些报文,这样服务器就更容易被攻击了。
四次挥手
为啥需要四次
在主动关闭方发送 FIN
包后,被动方可能还要发送数据,不能立即关闭服务器端到客户端的数据通道,所以也就不能将服务器端的 FIN
包与对客户端的 ACK
包合并发送,只能先确认 ACK
,然后服务器待无需发送数据时再发送 FIN
包,所以四次挥手时必须是四次数据包的交互。
为什么 TIME_WAIT 状态需要经过 2MSL 才能返回到 CLOSE 状态
MSL
指的是报文在网络中最大生存时间。在客户端发送对服务器端的FIN
的确认包ACK
后,这个ACK
包是有可能不可达的,服务器端如果收不到ACK
的话需要重新发送FIN
包。
所以客户端发送 ACK
后需要留出 2MSL
时间(ACK
到达服务器 + 服务器发送 FIN
重传包,一来一回)等待确认服务器端确实收到了 ACK
包。
也就是说客户端如果等待 2MSL 时间也没有收到服务器端的重传包 FIN,说明可以确认服务器已经收到客户端发送的 ACK。
- 避免新旧连接混淆。
在客户端发送完最后一个 ACK
报文段后,在经过 2MSL
时间,就可以使本连接持续的时间内所产生的所有报文都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文。
TIME_WAIT 累积与端口耗尽
当某个 TCP 端点关闭 TCP 连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的 IP 地址和端口号。这类信息只会维持一小段时间,通常是所估计的最大分段使用期的两倍(称为 2MSL,通常为 2 分钟左右,以确保在这段时间内不会创建具有相同地址和端口号的新连接。
有些操作系统会将 2MSL 设置为一个较小的值,但修改此值时要特别小心。分组确实会被复制,如果来自之前连接的复制分组插入了具有相同连接值的新 TCP 流,会破坏 TCP 数据。
确保客户端和服务器在循环使用几个虚拟 IP 地址以增加更多的连接组合。
服务器出现大量 close_wait 的连接
close_wait 状态是在 TCP 四次挥手的时候收到 FIN 但是没有发送自己的 FIN 时出现的,服务器出现大量 close_wait 状态的原因有两种:
- 服务器内部业务处理占用了过多时间,都没能处理完业务;或者还有数据需要发送;或者服务器的业务逻辑有问题,没有执行 close()方法
- 服务器的父进程派生出子进程,子进程继承了 socket,收到 FIN 的时候子进程处理但父进程没有处理该信号,导致 socket 的引用不为 0 无法回收
处理方法:
- 停止应用程序
- 修改程序里的 bug
流量控制
滑动窗口协议,是 TCP 使用的一种流量控制方法。该协议允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。 只有在接收窗口向前滑动时(与此同时也发送了确认),发送窗口才有可能向前滑动。收发两端的窗口按照以上规律不断地向前滑动,因此这种协议又称为滑动窗口协议。
TCP 头里有一个字段叫 Window,也就是窗口大小,用于告知对方(发送方)本方的缓冲还能接收多少字节数据。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
所以,通常窗口的大小是由接收方的窗口大小(实际由发送端和接收端两个窗口的最小值)来决定的。
- cwnd:发送端窗口( congestion window )
- rwnd:接收端窗口(receiver window)
拥塞控制
- 发送端主动控制 cwnd,有慢启动(从 cwnd 初始为 1 开始启动,指数启动)
- 拥塞避免(到达 ssthresh(慢启动阈值) 后,为了避免拥塞开始尝试线性增长)
- 快重传(接收方每收到一个报文段都要回复一个当前最大连续位置的确认,发送方只要一连收到三个重复确认就知道接收方丢包了,快速重传丢包的报文,并且 TCP 马上把拥塞窗口 cwnd 减小到 1)
为什么要规定凑满 3 个呢?这是因为网络包有时会乱序,乱序的包一样会触发重复的 Ack,但是为了乱序而重传没有必要。由于一般乱序的距离不会相差太大,比如 2 号包也许会跑到 4 号包后面,但不太可能跑到 6 号包后面,所以限定成 3 个或以上可以在很大程度上避免因乱序而触发快速重传。
- 快恢复(当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把 ssthresh 门限减半(为了预防网络发生拥塞)。考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。)
差错控制
TCP 使用差错控制来提供可靠性。差错控制包括以下的一些机制:检测和重传受到损伤的报文段、重传丢失的报文段、保存失序到达的报文段直至缺失的报文到期,以及检测和丢弃重复的报文段。TCP 通过三个简单的工具来完成其差错控制:检验和、确认以及超时。
SYN 攻击
客户端向服务端发送请求链接数据包,服务端向客户端发送确认数据包,客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认 没有彻底根治的办法,除非不使用 TCP
SYN 攻击预防:
- 限制同时打开 SYN 半链接的数目
- 缩短 SYN 半链接的 Time out 时间
- 关闭不必要的服务
- 增加最大半连接数
- 过滤网关防护
- SYN cookies 技术
TCP KeepAlive
TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。主流的操作系统基本都在内核里支持了这个特性。
参考
https://www.cnblogs.com/chyingp/p/understanding-tcp.html