tcp协议

259 阅读21分钟

tcp 协议的功能

  • 面向连接
  • 可靠传输(错误确认和重传)
  • 拥塞控制
  • 流量控制 (发送方和传送方的传输速率协调)

tcp 报文格式

tcp报文格式.png

标志位的讲解

SYN:当本字段为1时,表示这是一个连接请求或者连接接受报文。 ACK:确认标志,当本字段为1时,表示请求已收到。 PSH:tcp模块的发送方自己决定何时发送数据包,并且使PSH值设置为1。 数据包到达接收端以后,接收端不对其进行队列处理,而尽可能将其转发给应用程序(触发应用程序调用read()函数的返回)。 FIN: 当本字段为1时,表示此报文段的数据已经发送完毕,要求释放连接。 RST: 如果要向主机发送数据包以建立连接,并且没有这样的服务等待在远程主机上回答,则主 机将自动拒绝请求,然后发送回复RST标志置1。这表示远程主机已重置连接。

窗口字段

WIN: 接收窗口。用来告知发送端自己所能接收的数据量。

序号字段

含义

tcp连接 字节流之中每一个字节都有一个编号,本字段的值指的是本报文段所发送数据部分第一个字节的序号。

确认号

含义

下一个报文段数据部分的第一个字节的序号,序号为ack-1以及以前的字节都被收到。

tcp 报文类型

PUSH 报文

PUSH 报文 定义(判断规则)

tcp报文 的 PUSH 标志 取值为1

PUSH 报文 作用

发送方 使用 该报文 通知 接收方 将所收到的数据全部提交给接收进程。

复位报文(RST 报文)

复位报文的定义

tcp 报文 的 RST 标志 取值为1

复位报文的作用

复位报文用来 异常终止 一个连接。发送复位报文的一端,会丢弃发送缓冲区和接收缓冲区之中的数据。 发送FIN 报文,是正常终止 一个连接。

FIN 报文

FIN 报文的定义

tcp 报文的 FIN 标志位为 1, 表示为FIN 报文。

FIN 报文的作用

FIN 报文 表示一方需要终止连接。

Linux 手动 发送 tcp 报文的命令行工具

hping3

tcp 连接

tcp连接的属性

  • 目的ip
  • 目的端口
  • 本地ip
  • 本地端口
  • 连接状态

tcp连接状态有哪些

  • CLOSED:连接处于关闭状态
  • LISTEN :监听来自远方的建立连接请求。
  • SYN_SENT :客户端 发送 请求建立连接报文 后转换为此状态(客户端SYN_SENT状态)
  • STN_RECV:服务端 收到 客户端发送的 请求建立连接报文后转换为此状态。
  • ESTABLISHED :连接建立
  • CLOSE_WAIT:处于ESTABLISHED状态的服务端 收到 客户端发送的FIN 包,变换为此状态。
  • FIN_WAIT_1: 处于ESTABLISHED状态,主动发送FIN 报文的一端,状态进 FIN_WAIT_1。
  • FIN_WAIT_2:主动关闭端接到ACK后,状态进入了FIN-WAIT-2。
  • LAST_ACK:处于CLOSE_WAIT状态的被动关闭端一段时间后,它所对应的应用程序,调用CLOSE关闭连接,进入LAST-ACK 状态。
  • TIME_WAIT:在主动关闭端接收到FIN后,发送ACK包,并进入TIME-WAIT状态。

Linux 上 tcp连接的唯一性标志

目的ip 目的端口 本地ip 本地端口 合在一起作为tcp连接的唯一性标志

Linux 上 一个tcp连接的实质

自己的理解:Linux 上建立一个tcp连接,会使用一个socket来存储tcp连接的信息,socket 在Linux 上以文件表示。但是并不表示,出现socket文件,就是建立了tcp连接。因为 调用 socket创建函数,也可以创建socket。

Linux 上 一个tcp连接会占据哪些资源

1:连接跟踪表之中的连接数 (如果tcp 启用连接跟踪) 2:内存 3:文件描述符(Linux上所能打开的最大文件数是有限的,一个进程所能打开的文件数也是有限的)

Linux 上 tcp连接数是否是有限制的?

是的。受限于 连接跟踪表 内存 Linux 上所能打开的文件的文件数

tcp 三次握手建立连接

过程

tcp三次握手建立连接.png

问题

tcp连接 第一次握手失败 会发生什么(即发送了SYN报文,但是没有收到ACK 确认报文)

自己对于第一次握手成功 和 失败 的理解: 第一次握手成功 是在指定的时间内收到另一端发送的ACK报文。 第一次握手失败 是在指定的时间内没有收到另一端发送的ACK报文。

第一次握手失败会按照重试的次数,每次等待指定的时间。 默认重试次数会6次,第一次等待的时间为 2^0 =1秒 第二次为 2^1 = 2秒,加上重试6次,一共会重试7次。总时间为127秒,如果重试6次,还是不能建立连接,那么连接建立失败。

内核参数 net.ipv4.tcp_syn_retries 用来表示 SYN 请求建立连接 的 重发次数。

第一次SYN报文 用以判断 是否超时的时间标准

1秒

tcp连接 第二次握手失败 会有什么结果?

自己对于 tcp连接第二次握手失败的理解为,客户端未收到服务端发送的 SYN+ACK 报文。 那么客户端会认为自己发送的SYN 报文丢失,会进行SYN 报文重传。

tcp连接 第三次握手失败 会有什么结果?

自己对于第三次握手失败的理解为:服务端未收到客户端发送的AC确认报文。 服务端会不断重新发送SYN+ACK报文。 重试次数由net.ipv4.tcp_synack_retries 内核参数控制。 net.ipv4.tcp_synack_retries 参数含义:参数表示服务器 重发 SYN+ACK报文的重试次数。重试时间间隔 也是 第一次等待的时间为 2^0 =1秒 第二次为 2^1 = 2秒。

Linux tcp 连接队列

  • tcp 半连接队列
  • tcp 全连接队列

tcp 半连接队列

tcp 半连接队列之中元素

半连接队列之中保存 SYN_RECV 状态的连接。

tcp 半连接队列 入队

服务端接收到第一次握手 SYN 报文,在半连接队列之中保存 SYN_RECV 状态的连接。

tcp 半连接队列 出队

服务端接收到 客户端 发送的 ACK 确认报文,将对应的连接移出,放入到全连接队列之中。

tcp 半连接队列最大长度

获取 tcp 半连接队列最大长度 设置值

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

修改 tcp 半连接队列最大长度

修改 /proc/sys/net/ipv4/tcp_max_syn_backlog 文件之中的值,注意这是临时生效的。 持久化的修改 需要修改此内核参数,并固定下来

查看当前 tcp 半连接队列之中元素的个数

netstat -antp | grep SYN_RECV | wc -l

tcp半连接队列溢出处理

服务端 将 客户端发送的多余的 SYN请求建立连接报文 丢弃。

tcp半连接队列溢出监控

执行以下命令,如果有信息输出 表示tcp 半连接 存在溢出 netstat -s | grep 'SYNs to LISTEN'

tcp 全连接队列

tcp 全连接队列之中的元素

处于 ESTABLISHED状态 的tcp连接

tcp全连接队列入队

服务端 收到 客户端 发送的 ACK确认报文,将连接从半连接队列移至全连接队列

tcp 全连接队列出队

应用层调用 accept()方法,从全连接队列移出连接。

tcp 全连接队列 最大长度

获得tcp全连接队列最大长度

计算公式为:min(backlog, /proc/sys/net/core/somaxconn) backlog 是应用层 设置socket 监听的时候,传入的参数。 /proc/sys/net/core/somaxconn 是内核参数

设置tcp全连接队列最大长度

min(backlog, /proc/sys/net/core/somaxconn)。 要么设置backlog 要么设置内核参数

tcp 全连接队列溢出处理策略

与 内核参数 net.ipv4.tcp_abort_on_overflow 的取值有关。 取值为 0:如果全连接队列满了,那么 server 扔掉 client 发过来的 ack ; 取值为 1:如果全连接队列满了,server 发送一个 reset 包给 client,表示废掉这个握手过程和这个连接;

tcp 全连接队列 监控

查看当前tcp全连接队列之中的连接个数

使用 ss -lnt 来获得 tcp全连接队列的长度。 输出信息

State     Recv-Q     Send-Q          Local Address:Port      Peer Address:Port             
LISTEN   0             128                  0.0.0.0:9090              0.0.0.0:*                

列名解析: Recv-Q列 表示的是处于监听状态的socket的全连接队列之中的连接数目。 Send-Q列 表示的是处于监听状态的socket的全连接队列的最大长度。

查看 当前tcp 全连接队列是否发生溢出

执行以下命令,如果有信息输出 表示 tcp全连接 存在溢出。 netstat -s | grep overflow

Linux syn_cookie 机制

syn_cookie 机制的产生原因

针对可能产生的 SYN 洪水攻击,所以提出了 syn_cookie 机制。

syn_cookie 的原理

syn_cookie 机制,会修改服务端的三次握手机制,当服务端接收到客户端发送的SYN报文时,不再服务端分配一个专门的数据区,而是根据这个 SYN 包计算出一个 cookie 值。这个 cookie作为将要返回的 SYN+ACK 包的初始序列号。当客户端返回一个 ACK 包时,根据包头信息计算 cookie,与返回的确认序列号(初始序列号 + 1)进行对比,如果相同,则是一个正常连接,然后,分配资源,建立连接。

查看 syn_cookie 机制 是否开启

检查内核参数 net.ipv4.tcp_syncookies 参数取值: 0:表示关闭这个功能 1:表示仅当 SYN 半连接队列放不下时,再启用它。 2:表示无条件开启功能。

tcp协议 数据传输

tcp 协议如何实现安全传输

  • 1:如何保证不丢包 超时重传
  • 2:如何保证 不会重复发送数据包 确认号
  • 3:如何保证 接收端 不会重复接收数据包 序列号
  • 4:如何解决接收端接收的数据包乱序的问题 序列号 + 确认号
  • 5:如何保证收到的数据包没有差错 tcp报文具有校验和字段,来验证数据在传输过程之中没有变化。

MSS

MSS是什么

MSS(Maximum Segment Size,最大报文长度),是TCP协议定义的一个选项, MSS选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。 MSS 的初始值 = MTU - IP报文段的头(20字节) - tcp报文段的头(20字节) 在三次握手建立连接的同时,也是相互之间协商MSS大小的时机。

MSS的作用

当发送缓冲区之中的数据超过MSS 的大小时,将数据按照MSS的大小进行分割。例如,当应用程序一次性向缓冲区写入大量的数据的时候。

问题

1: 是不是只有当 tcp 发送缓冲区之中的数据量 大于 MSS 值的时候,tcp模块才会将缓冲区之中的数据通过网卡发送出去? 不是。自己通过java 程序验证,在socket的 OutputStream 之中写入3个字节,另一端仍然是收到了数据。

RTT

RTT 的定义

tcp协议之中,一个 数据包发送时刻 与 接收到确认数据包时刻之间 的 差值。 注意:RTT 指标也可以作为判断网络拥塞程度的标志。

RTT 的监控

执行 ss -ti 可以查看rtt指标的信息状况

RTO

RTO 的定义

RTO: Retransmission Time Out, 超时重传时间RTO。 RTO也就是tcp在发送一个数据包之后,会启动一个重传定时器,而RTO就是这个定时器的重传时间。 一般来说RTO_MIN的值应该比RTT大,否则就会出现无谓的超时重传了。

超时重传

超时重传(RTO)的时间

TCP超时时间初始值是根据RTT时间计算得到,随着重传次数增加重传时间间隔也增加。

超时重传次数

Linux 内核会有两个参数 tcp_retries1 tcp_retries2 用以计算超时重传的总时间。每次重传时会判断 当前时间 距离 第一次发送数据包的时间 是否大于 超时重传的总时间,如果大于,那么就不会重传了,小于的话,会进行重传。

过了超时重传次数,还是失败,最终的结果是什么?

放弃重传,发送端会关闭tcp连接,不会进入 TIME_WAIT状态。

快速重传

快速重传的定义

当发送端接收到三个重复的ACK确认报文的时候,触发数据包的重传。

快速重传所针对的问题

数据包超时重传 所等待的时间太长,影响吞吐量。

快速重传的监控

netstat -s | grep retran 输出信息: segments retransmitted 重传的报文数 fast retransmits 快速重传数

问题

1: 有时候 Linux 内核抓包发现,即使只是收到一次 重复的ACK, 但是也会触发超时重传,这是什么原因? 因为 SACK 机制,发送端接收到 SACK 确认报文后,可以了解服务端到底缺少哪一个报文,Linux 内核也有内部的判断逻辑,如果 缺失的数据量 > net.ipv4.tcp_reordering * MSS,那么会触发快速重传,是有一个阈值的。

ACK 确认

SACK 机制

SACK 解决的问题

SACK 解决快速重传时 需要重传哪几个数据包的问题,或者说针对所丢失的数据包,进行重传,而不是一股脑全部重传

SACK 的实现原理

SACK报文之中,除了有ACK 字段,还有其他字段,表示接收端接收到的数据包序列,两者一比,就可以知道缺失的数据包序列是哪些。

Linux 下 查看 SACK 机制是否开启

内核参数 net.ipv4.tcp_sack 的值 表示是否开启 1 为开启 0 为关闭

导致数据包重传的原因

  • 1:发送端所发送的数据包在网络之中丢失,触发发送端的超时重传。
  • 2:发送端的数据包到达接收端的时候出现乱序,触发发送端快速重传。

导致 数据包乱序的原因

  • 1: 外部网络的问题
  • 2:本机所发送数据包 出现乱序

tcp 数据包重传监控

netstat -s | grep retran 输出信息: segments retransmitted 重传的报文数 fast retransmits 快速重传数

Linux 下 命令行工具 用于测试网络状况 对 数据包重传 的影响

iperf3 -c 服务端地址 输出的信息之中 有一列 Retr,表示重传的数据包数。

tcp 流量控制

tcp 流量控制产生的原因

发送端 和 接收端 的收发速率可能不一致,需要进行平衡。

tcp 流量控制的作用

使发送方发送的数据不要太快,让接收方来得及接收数据。(进行端到端之间的控制)

tcp 流量控制的实现

使用滑动窗口机制来实现 tcp 流量控制

拥塞控制

拥塞控制的作用

自己的理解:避免发送方的数据充满这个网络。当网络发生拥塞的时候,调整发送方的数据发送行为。

拥塞窗口

拥塞窗口的定义

拥塞窗口 cwnd 是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。 网络拥塞程度高 cwnd 越小。

初始化拥塞窗口 的值

有Linux 内核代码定义。自己搜索得知是10 转换到字节的大小 需要 ✖️ MSS

Linux 下 如何查看一个tcp 连接当前的拥塞窗口大小

ss -nti 命令的输出信息 有 cwnd ,cwnd * MSS 是拥塞窗口的大小(单位 字节)

拥塞窗口大小的意义(如何看待拥塞窗口大小 这个指标)

看到拥塞窗口这个指标,可以用来判断网络的拥塞程度。 当然导致网络拥塞的原因 还要探索 是否是 网络层 或者网卡队列拥塞导致的,还是外部的网络拥塞导致的。

拥塞窗口大小 与 发送窗口大小的关系

发送窗口的大小= min(另一端接收窗口的大小,cwnd * MSS)

滑动窗口机制

滑动窗口机制的实现

发送端和接收端都有两个窗口,一个发送窗口,一个接收窗口。 发送端发送窗口的大小 取决于 接收端在tcp报文之中所发送的接收端的接收窗口的大小。 发送端 和 接收端 接收窗口的大小 会受到 tcp连接的接收缓冲区剩余空间大小的影响。 如果发送端 接收到 ACK确认报文之中 接收端的接收窗口大小为0,那么 发送端停止发送数据,同时发送端会启动一个坚持定时器,等待一定时间后发送探测报文,并将坚持计时器的等待时间加倍并复位,直到收到对方的回复为止(等待时间到达一定的限制后,不会加倍了,保持不变)。

keep alive 计时器 (保活计时器)

keep alive 计时器产生的原因

在长时间没有交互的过程之中,交互双方都有可能出现 死机 掉电 异常重启 等意外。 当意外发生的时候,tcp连接还没来得及释放,另一端并不知道这个情况,另一端会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成系统资源的消耗和浪费。 注意: 用来检测连接是否可用,这个是重点。

keep alive 计时器的工作原理

在一定时间内 服务端没有收到客户端发送的数据,服务端就向客户端按照一定的时间间隔多次发送探测报文,如果多次发送的探测报文都没有收到回复,则终止连接。如果收到,keep alive 计时器复位。

keep alive 计时器的相关参数

内核参数 net.ipv4.tcp_keepalive_time : keep alive 计时器等待的时间长度,单位是秒 net.ipv4.tcp_keepalive_probes : keep alive 计时器 发送探测报文的次数 net.ipv4.tcp_keepalive_intvl: keep alive 计时器发送探测报文的时间间隔 单位是秒

tcp keep alive 计时器是否开启

KeepAlive默认不是开启的,需要在应用层设置 SO_KEEPALIVE, 才可以生效。 linux的内核参数会包含 tcp keep-alive 的相关参数

粘包

粘包的定义

接收端一次读取可能读取客户端发送的多个数据包的内容。客户端所发送的多个数据包就像粘在了一起,成为了一个数据包。 (应用程序调用 一次socket的read()方法,返回的数据可能包含多个数据包的数据)

粘包的产生原因

tcp接收缓存区之中是字节流,没有明显的用以判断数据包分隔的符号。

半包

半包的定义

接收端一次读取,读取的数据内容不到完整的一个数据包。 (应用程序调用 一次socket的read()方法,返回的数据只是一个完整数据包的一小部分)

半包的产生原因

tcp接收缓存区之中是字节流,没有明显的用以判断数据包分隔的符号。

tcp 四次挥手断开连接

tcp四次挥手断开连接.png

问题

1: 四次挥手之中,如果第一次发送端的FIN 报文丢失了,如何进行处理

无论是第一次发送的FIN报文在网络之中被丢弃,还是超过时间没有收到ACK 报文,都会被发送端认为是报文丢失,会触发发送端的超时重传机制。

FIN 报文的重传次数 由 内核参数 /proc/sys/net/ipv4/tcp_orphan_retries 决定,如果取值为0,默认的重传次数为8 (外部搜索得知,内核代码之中的写法)

2:四次挥手之中,如果第二次挥手,所发送的ACK 确认报文丢失了,结果会如何?

无论是因为 ACK 报文在网络上传输时被丢弃,还是因为网络延时,没有按时到达, 都会触发 发送端 超时重传 FIN 报文。

3:四次挥手之中,第三次挥手的 FIN报文 丢失了,结果会如何?

触发发送端超时重传。

4:四次挥手之中,第四次挥手的 ACK报文 丢失了,结果会如何?

触发第三次挥手的FIN报文 超时重传。

5: 如果第三次挥手的FIN 报文一直不发送,处于FIN_WAIT_2状态的那一端会如何?

由于另一端一直不发送 FIN报文,那么接收端会处于 FIN_WAIT_2状态, 接收端的FIN_WAIT_2状态会存在一定的时间,如果在这个时间内,仍然没有收到FIN报文,那么接收端不会转入 TIME_WAITING状态就会直接关闭。发送端的套接字还是会处于 CLOSE_WAIT状态,不会变化。 接收端FIN_WAIT_2存在时间由内核参数 net.ipv4.tcp_fin_timeout 决定, 单位是秒

6:为什么 tcp 主动发起连接的一端,会在TIME_WAIT状态等待 2 * MSL的时间?

可以让tcp 重发最后的ACK,防止ACK确认报文丢失而带来的问题。

7:tcp连接 在 TIME_WAIT状态等待 2 * MSL的时间,带来的影响

意味着发起关闭的客户端的 套接字(ip+端口)在 2 * MSL 内无法复用。 实际的例子,写java代码,客户端socket绑定本地端口,执行一次连接, 然后重新执行,会抛出端口已绑定的异常。

8: 限制服务器 处于 TIME_WAIT 状态的连接数 参数

/proc/sys/net/ipv4/tcp_max_tw_buckets TIME_WAIT 状态的连接数 超出限制 则内核会报错。

Linux tcp 信息 查看工具

  • netstat
  • ss

tcp监控

1:查看当前 Linux 上的 tcp 连接数(已建立连接)

 netstat -ant | grep ESTABLISHED | wc -l

2:查看当前 Linux 上 各进程的 tcp连接数(已建立连接)

netstat -antp  | grep ESTABLISHED |  awk '{print $7}' | awk -F '|'  '{count[$1]++;} END {for(i in count) {print i,count[i]}}'

3: 查看当前 Linux 上 tcp连接信息

 ss -nti

tcp 全局监控指标

  • 1:数据包重传数
  • 2:所接受的数据包乱序数。

实验

tcp 三次连接 重传机制实验

目的: 验证 SYN 重传机制。 步骤: 1:在iptables之中增加 对 127.0.0.1 5000端口 TCP协议 SYN数据包的丢弃规则。

   iptables -t filter -I INPUT -p tcp -s 127.0.0.1 --dport 5000 --syn -j DROP 

2: 使用tcpdump 对本地回环网卡接口 进行抓包,查看报文的数据

  tcpdump -n -i lo -Ss0  src 127.0.0.1 and dst 127.0.0.1 and port 5000

3:设置 SYN报文重传次数为1

     echo 1 > /proc/sys/net/ipv4/tcp_syn_retries

4:新开一个终端,使用telnet建立连接, 记录命令的执行耗时

     date '+ %F %T'; telnet 127.0.0.1 5000; date '+ %F %T';

5:验证两个SYN 报文之间的时间间隔是否为1秒,telnet 程序的耗时是否为3秒

tcp SYN+ACK 重传的时间间隔探索实验

目的:查看第一次 SYN+ACK 的超时的时间 步骤: 1:终端1 nc -l 8090 建立服务端

 nc -l 8090

2: 终端2 iptbales 增加丢失 SYN包的规则

iptables -t filter -I INPUT -p tcp --tcp-flags ACK ACK --dport 8090 -j DROP

3: 终端2 tcpdump 抓包

tcpdump -n -i lo 'port 8090'

4: 终端3 nc 127.0.0.1 8090 建立连接 nc 127.0.0.1 8090

5: 查看终端2 上 SYN+ACK 报文的输出时间间隔

tcp 数据包 超时重传机制 超过限制会如何处理

目的:查看 tcp 数据包 超时重传机制 超过限制次数的处理结果 步骤: 1:调整内核参数 net.ipv4.tcp_retries1 ipv4.tcp_retries2 的值 echo 2 > /proc/sys/net/ipv4/tcp_retries2 1: 在终端1 建立服务端 nc -l 8090 2: 在终端2 建立客户端 nc 127.0.0.1 8090 3: 在终端3 增加iptables 规则 丢弃 客户端发送的数据包 iptables -t filter -I INPUT -p tcp --tcp-flags PSH PSH --dport 8090 -j DROP 4:在终端3 网卡抓包 tcpdump -n -i lo 'port 8090' 5:在终端2 的客户端发送数据 6: 观察 终端的状况 发现 终端2 的客户端断开了。 执行 ss -nt | grep 8090 ,另一端的连接也断开了。

问题

1: PUSH 报文 是由 用户进程控制发出 还是由操作系统控制发出? 操作系统 2: 内核的tcp模块 什么时候将发送缓冲区之中的数据发送出去? 这个内核的发送机制 留待以后探索 3:内核的tcp模块 什么时候将接收缓冲区之中的数据 发送给应用程序。 这个内核的接收机制 留待以后探索。