tcp 协议
认识TCP
还是那张图,tcp位于整个网络层次中的传输层
tcp报文长这个样子
- 源端口:是指发起连接的端口,及客户端口
- 目的端口:接受方端口,服务器端口
可以看出端口数量最大有2^16 -1(65535)个
-
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
-
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数 据都已经被正常接收。用来解决不丢包的问题。
-
控制位:
-
ACK ,该位为1时,确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须 设置为 1 。
-
RST ,该位为1时,表示TCP连接中出现异常必须强制断开连接
-
SYN,该位为1时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
-
FIN,该位为1时,表示希望今后不会再有数据发送,希望断开连接,当通信结束希望断开连接时,通信双方的主 机之间就可以相互交换 FIN 位为 1 的 TCP 段。
-
事实上,tcp协议很复杂,因为tcp定位是可靠的传输协议。由于IP层不可靠,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:一定是一对一才能连接,是成对出现,不像UDP一样可以一个主机同时向多个主机发送消息。
- 可靠的:无论网络情况有多复杂,TCP 都可以保证一个报文一定能够到达接收端,
- 字节流:消息是没有边界,像流水一样。所以,需要应用层进行数据分割。http协议的换行符,和header里面的content-length 都是同一个目的,为了区分这次请求返回的数据界限。再比如,基于游戏服务的数据格式,需要留四位(int32)用于分包,处理正确的数据包。消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前 一个」消息没有收到的时候,即使它先收到了后面的字节,那么也不能扔给应用层去处理,同时对「复」的 报文会自动丢弃。
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口 大小称为连接。
- socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
TCP 通过四元组可以唯一确定一个链接
- 源IP
- 目的IP
- 源端口
- 目的端口
源IP和目的IP(32位)是通过IP协议发送IP数据报给对方主机
理论上client最多可以和65536-1(2^16-1)个server ip相连
理论上server最多可以和2^48( 2^32*2^16 )client相连,对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。当然这只是理论,实际上远远达不到这么多的连接数
- 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;
- 另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的。
TCP数据长度=IP总长度-IP首部长度-TCP首部长度
TCP 连接建立
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。
-
开始的时候,服务器和客户端都出于
CLOSED
状态。服务器监听某个端口,出于``LISTEN` 状态 -
第一次握手:客户端是随机初始化序号(client_isn),并将这个序号填入TCP首部的序号字段中,同时把控制位SYN置为1,表示SYN报文。做完这些,将这个报文发给服务器,表示向服务器发起连接,该报文不包含应用数据,之后客户端出于
SYN-SENT
状态 -
第二次握手:服务端收到客户端的
SYN
报文后,服务器也随机初始化一个序号(server_isn),将序号填充TCP首部的序号字段中,然后把client_isn+1
填入TCP首部的「确认应答号」字段中,并把SYN
和ACK
标志位置为1。最后把该报文发给客户端,该报文不包含应用数据,服务器此时处于SYN-RCVD
状态 -
第三次握手:客户端收到服务器发送的
SYN/ACK
报文后,还需要给服务器一个应答报文,及ACK
报文。首先该应答报文 TCP 首部 ACK 标志位置为1 ,其次「确认应答号」字段填入server_isn+1
最后将这个报文发个服务器,这次报文可以携带应用数据,之后客户端出于ESTABLISHED
状态,服务器收到客户端的应答报文后,也进入ESTABLISHED
状态
一旦完成三次握手,双方都处于ESTABLISHED
状态,此时连接已经建立完成,客户端和服务端就可以互相发数据了。TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
为什么一定是三次握手,不是两次,不是四次
很常见的回答是:只有三次握手才能保证双方的接受能力和发送能力 这不是主要原因
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始化序列号
- 三次握手才可以避免资源浪费
原因一
三次握手最重要的原因是防止旧的重复连接初始化造成混乱 实际上的网络环境错综复杂,有可能旧的数据包先到达目的主机,这会造成连接混乱。为了规避这个问题,才有了三次握手 在网络拥堵的情况下,客户端连续发起多次SYN建立连接的报文
- 一个旧的SYN报文比最新的SYN报文早到达服务器
- 此时服务会回一个SYN/ACK 报文给客户端
- 客户端收到后根据自身的上下文,判断是否是个历史连接。三次握手巧就巧在,客户端可以根据情况来告诉服务器
- 如果是历史连接,则发一个
RST
报文,终止历史连接 - 如果不是历史连接,则发一个
ACK
报文。双发进入ESTABLISHED
- 如果是历史连接,则发一个
原因二
TCP 协议的通信双方,都必须维护一个序列号,序列号是可靠传输的关键因素
- 接收方可以去除重复数据
- 接收方可以根据数据包的序列号接收
- 可以标识发出去的数据包中,哪些是已经被对方收到的
可以看见,序列号和应答号很重要,当客户端发送带有初始化序列号的SYN 时候,需要服务端回一个带有应答号的ACK报文,标识客户端的SYN报文已被服务器成功接收,当服务器发送初始化序列号SYN包给客户端时候,客户端也需要回一个ACK报文。这样一来一回才能确保双方的初始化序列号可能被可靠同步
原因三
四次握手可以满足tcp连接要求,但是没有必要
原因四:避免资源浪费
如果是两次握手,当客户端的SYN报文在网络中阻塞,客户端由于没有收到服务的ACK报文,就会重新发送SYN报文。服务器收到一个SYN就会回一个ACK报文然后建立连接,就会造成资源浪费的情况
认识 MTU 和 MSS
- MTU:(Maximum Transmission Unit)最大传输单元,一个网络包的最大的长度,以太网一般为1500字节
- MSS:除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度
如果TCP的整个报文(头部+数据)交给IP层进行分片,会怎么样
当IP层有一个超过MTU大小的数据(TCP头部和TCP数据)要发送时,IP层就会进行分片,把数据分成若干片,保证每一分片都小于MTU,最后有目的进行重新组装。再交给上一层TCP传输层。这样有个问题如果一个IP分片丢失,整个IP报文的所有分片都要重传。因为一个大的TCP报文被MTU分片,那么只有第一个分片才具有TCP头部,后面的分片则没有TCP头部,接受方IP层只有重组装这些分片,才发现是个TCP报文,如果丢失了其中一个分片,接受方IP层不会把TCP报文丢给TCP层,会等待对方超时重传整个TCP报文。
但是如果一个大的TCP报文被MSS分片,那么所有的分片都具有TCP头部,这样一来其中一个MSS分片丢失,只需要重传一个分片就可以了。 为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过MSS 时,则就先会进行分片,当然由它形成的 IP 包的⻓度也就不会大于 MTU ,自然也就不用 IP 分片了。
SYN攻击
TCP的连接建立需要三次握手,假设攻击者短时间内伪造了不同的IP地址的SYN报文,服务器接收后就进入SYN_RCVD 状态。但服务端发送出去的ACK/SYN 报文,无法得到未知IP主机的ACK应答,久而久之就会占满服务端的SYN接收队列(未连接队列),使得服务器不能为正常的用户服务
避免SYN攻击方法一
通过修改Linux 内核参数,控制队列大小和当队列满是应该怎么处理
查看内核参数
|
|
-
SYN_RCVD 状态连接的最大个数
1 2
$ cat /etc/sysctl.conf | grep net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_max_syn_backlog = 1024
-
超出处理能时,对新的 SYN 直接回报 RST,丢弃连接: 当TCP连接已经建立,并塞到程序监听backlog队列时,如果检测到backlog队列已经满员后,TCP连接状态会回退到
SYN+ACK
状态,假装TCP三次握手第三次客户端的ACK
包没收到,让客户端重传ACK
,以便快速进入ESTABLISHED
状态。如果设置了net.ipv4.tcp_abort_on_overflow
参数,那么在检测到监听backlog 队列已满时,直接发 RST 包给客户端终止此连接,此时客户端程序会收到104 Connection reset by peer
错误。这个参数很暴力,慎用1
net.ipv4.tcp_abort_on_overflow
-
1 2
#参数表示网卡接受数据包的队列最大长度,在阿里云服务器上,默认值是1000,可以适当调整。 net.core.netdev_max_backlog
避免SYN攻击方法二
Linux 内核的SYN (未完成连接建立)队列与Accept(已完成连接建立)队列是如何工作的? 正常流程:
- 当服务端接收到了客户端发的SYN报文时,会将其放入内核的SYN队列中
- 接着发送SYN+ACK 给客户端,等待客户端回应ACK报文
- 服务端接收到ACK报文后,从SYN队列移除到Accept队列
- 应用通过调用accept() socket 接口,从Accept队列取出连接。
当应用程序处理,就会导致Accept队列被占满,SYN队列也被占满 当服务端受到SYN攻击时,SYN队列会被占满
内核参数tcp_syncookies
的方式可以应对SYN攻击
|
|
- 当SYN队列被占满后,后续的服务端收到SYN报文,不进入SYN队列
- 计算出一个cookie 值,再以SYN+ACK 中的序列号返回客户端
- 服务端接收客户端的ACK报文,服务端会检查ACK包的合法性。如果合法,直接Accept 队列
- 最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。
TCP 的四次挥手,断开连接
双方都可以主动断开连接,断开后主机的资源将被释放
- 客户端打算关闭连接,此时会发送一个TCP头部
FIN
标志位被置为1 的报文,也就是FIN报文,之后客户端进入FIN_WAIT_1 状态 - 服务端收到报文后,就向客户端发送ACK应答报文,接着服务端进入CLOSED_WAIT 状态
- 客户端收到ACK报文后进入FIN_WAIT_2状态。
- 等服务端处理完数据后,会向客户端发送FIN报文,之后进入LAST_ACK 状态
- 客户端收到服务端的FIN报文后,回一个ACK报文,进入TIME_WAIT 状态
- 服务器收到了ACK报文后,进入CLOSED状态,至此服务端已经完成了连接的关闭
- 客户端在经过2MSL 一段时间后,自动进入CLOSED状态,至此客户端已经完成了连接的关闭
有一点需要注意的是:主动关闭连接的,才会有TIME_WAIT 状态
为什么一定要四次交互呢
- 关闭连接的时候,客户端向服务端发送FIN 时,仅仅表示客户端不再发送数据了,但是还能接收。
- 服务端收到客户端的FIN报文后,先回一个ACK报文,而服务端可能还有数据需要处理和发送,等服务器不再发送数据时,才发送FIN报文给客户端表示现在关闭连接
服务端通常要等待完成数据的处理和发送,所以ACK和FIN 报文会分开发送,这就是四次交互的由来
为什么需要TIME_WAIT
- 防止具有相同的四元组的旧数据包被收到
- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
原因一:防止旧连接的数据包 如果TIME_WAIT 没有等待时间或者时间很短,被延迟的数据包抵达会发什么什么?
原因二:保证连接正确关闭 TIME_WAIT 作用是等待足够的时间以确保最后的ACK能让被动关闭方接收,从而帮助其正确关闭。
如果 TIME_WAIT 等待足够长
- 服务端正常收到四次挥手的最后一个ACK报文,则服务器正常关闭连接
- 服务端没有收到四次挥手的最后一个ACK报文,最会重发FIN,并等待新的ACK报文
为什么TIME_WAIT 等待时间时2MSL
MSL 全称 Maximum Segment Lifetime,报文的最大生存时间,它是任何报文在网络上的存在的最长时间,超过这个时间的报文将被丢弃。因为TCP报文是基于IP协议的,而IP头部有个TTL字段,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器该值就会减一,当为0时,就会被丢弃,同时发送ICMP报文通知源主机。
MSL与TTL的区别:MSL的单位是时间,而TTL是经过路由的跳数。所以MSL应该大于等于TTL消耗为0 的时间,以确保报文已被自然消亡。
TIME_WAIT 等待2倍的MSL,比较合理的解释是:网络中可能存在来自对发送的数据包,当这些发送方的数据包被接收方处理又会向对方发送响应,一来二去正好2个MSL。
2MSL 的时间是从客户端发送FIN后发送ACK开始计时。如果在TIME_WAIT 时间内,因为客户端的ACK没有传输到服务器,客户端右接收到了服务端重发的FIN报文,那么2MSL时间将会重新计时。
在Linux系统里2MSL默认是60秒,其定义在Linux内核代码里面的名称为TCP_TIMEWAIT_LEN:
|
|
如果要修改 TIME_WAIT 的时间⻓度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核
TIME_WAIT 过多有什么危害
- 内存资源占用
- 对端口资源的占用