Contents

tcp 协议

认识TCP

还是那张图,tcp位于整个网络层次中的传输层

/img/net/1.png
工作流程

tcp报文长这个样子

/img/net/11.png
工作流程

  • 源端口:是指发起连接的端口,及客户端口
  • 目的端口:接受方端口,服务器端口

可以看出端口数量最大有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、序列号和窗口 大小称为连接。

/img/net/12.png
工作流程

  • 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 前必须先建立连接,而建立连接是通过三次握手来进行的。

/img/net/13.png
工作流程

  • 开始的时候,服务器和客户端都出于CLOSED状态。服务器监听某个端口,出于``LISTEN` 状态

  • 第一次握手:客户端是随机初始化序号(client_isn),并将这个序号填入TCP首部的序号字段中,同时把控制位SYN置为1,表示SYN报文。做完这些,将这个报文发给服务器,表示向服务器发起连接,该报文不包含应用数据,之后客户端出于SYN-SENT 状态

    /img/net/14.png
    工作流程

  • 第二次握手:服务端收到客户端的SYN 报文后,服务器也随机初始化一个序号(server_isn),将序号填充TCP首部的序号字段中,然后把client_isn+1填入TCP首部的「确认应答号」字段中,并把SYNACK 标志位置为1。最后把该报文发给客户端,该报文不包含应用数据,服务器此时处于SYN-RCVD 状态

    /img/net/15.png
    工作流程

  • 第三次握手:客户端收到服务器发送的SYN/ACK 报文后,还需要给服务器一个应答报文,及ACK 报文。首先该应答报文 TCP 首部 ACK 标志位置为1 ,其次「确认应答号」字段填入server_isn+1 最后将这个报文发个服务器,这次报文可以携带应用数据,之后客户端出于ESTABLISHED状态,服务器收到客户端的应答报文后,也进入ESTABLISHED状态 /img/net/16.png

一旦完成三次握手,双方都处于ESTABLISHED状态,此时连接已经建立完成,客户端和服务端就可以互相发数据了。TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

/img/net/17.png

为什么一定是三次握手,不是两次,不是四次

很常见的回答是:只有三次握手才能保证双方的接受能力和发送能力 这不是主要原因

  • 三次握手才可以阻止重复历史连接的初始化(主要原因)
  • 三次握手才可以同步双方的初始化序列号
  • 三次握手才可以避免资源浪费

原因一

三次握手最重要的原因是防止旧的重复连接初始化造成混乱 实际上的网络环境错综复杂,有可能旧的数据包先到达目的主机,这会造成连接混乱。为了规避这个问题,才有了三次握手 /img/net/18.png 在网络拥堵的情况下,客户端连续发起多次SYN建立连接的报文

  • 一个旧的SYN报文比最新的SYN报文早到达服务器
  • 此时服务会回一个SYN/ACK 报文给客户端
  • 客户端收到后根据自身的上下文,判断是否是个历史连接。三次握手巧就巧在,客户端可以根据情况来告诉服务器
    • 如果是历史连接,则发一个RST 报文,终止历史连接
    • 如果不是历史连接,则发一个ACK报文。双发进入ESTABLISHED

原因二

TCP 协议的通信双方,都必须维护一个序列号,序列号是可靠传输的关键因素

  • 接收方可以去除重复数据
  • 接收方可以根据数据包的序列号接收
  • 可以标识发出去的数据包中,哪些是已经被对方收到的

可以看见,序列号和应答号很重要,当客户端发送带有初始化序列号的SYN 时候,需要服务端回一个带有应答号的ACK报文,标识客户端的SYN报文已被服务器成功接收,当服务器发送初始化序列号SYN包给客户端时候,客户端也需要回一个ACK报文。这样一来一回才能确保双方的初始化序列号可能被可靠同步

原因三

四次握手可以满足tcp连接要求,但是没有必要

/img/net/19.png

原因四:避免资源浪费

如果是两次握手,当客户端的SYN报文在网络中阻塞,客户端由于没有收到服务的ACK报文,就会重新发送SYN报文。服务器收到一个SYN就会回一个ACK报文然后建立连接,就会造成资源浪费的情况 /img/net/20.png

认识 MTU 和 MSS

  • MTU:(Maximum Transmission Unit)最大传输单元,一个网络包的最大的长度,以太网一般为1500字节
  • MSS:除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度

/img/net/21.png

如果TCP的整个报文(头部+数据)交给IP层进行分片,会怎么样

当IP层有一个超过MTU大小的数据(TCP头部和TCP数据)要发送时,IP层就会进行分片,把数据分成若干片,保证每一分片都小于MTU,最后有目的进行重新组装。再交给上一层TCP传输层。这样有个问题如果一个IP分片丢失,整个IP报文的所有分片都要重传。因为一个大的TCP报文被MTU分片,那么只有第一个分片才具有TCP头部,后面的分片则没有TCP头部,接受方IP层只有重组装这些分片,才发现是个TCP报文,如果丢失了其中一个分片,接受方IP层不会把TCP报文丢给TCP层,会等待对方超时重传整个TCP报文。 /img/net/23.png

但是如果一个大的TCP报文被MSS分片,那么所有的分片都具有TCP头部,这样一来其中一个MSS分片丢失,只需要重传一个分片就可以了。 /img/net/22.png 为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过MSS 时,则就先会进行分片,当然由它形成的 IP 包的⻓度也就不会大于 MTU ,自然也就不用 IP 分片了。

SYN攻击

TCP的连接建立需要三次握手,假设攻击者短时间内伪造了不同的IP地址的SYN报文,服务器接收后就进入SYN_RCVD 状态。但服务端发送出去的ACK/SYN 报文,无法得到未知IP主机的ACK应答,久而久之就会占满服务端的SYN接收队列(未连接队列),使得服务器不能为正常的用户服务 /img/net/24.png

避免SYN攻击方法一

通过修改Linux 内核参数,控制队列大小和当队列满是应该怎么处理

查看内核参数

1
sysctl -a | grep netdev_max_backlog
  • 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队列取出连接。

/img/net/25.png

当应用程序处理,就会导致Accept队列被占满,SYN队列也被占满 /img/net/26.png 当服务端受到SYN攻击时,SYN队列会被占满 /img/net/27.png

内核参数tcp_syncookies 的方式可以应对SYN攻击

1
net.ipv4.tcp_syncookies = 1
  • 当SYN队列被占满后,后续的服务端收到SYN报文,不进入SYN队列
  • 计算出一个cookie 值,再以SYN+ACK 中的序列号返回客户端
  • 服务端接收客户端的ACK报文,服务端会检查ACK包的合法性。如果合法,直接Accept 队列
  • 最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。

/img/net/28.png

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状态,至此客户端已经完成了连接的关闭

/img/net/29.png 有一点需要注意的是:主动关闭连接的,才会有TIME_WAIT 状态

为什么一定要四次交互呢

  • 关闭连接的时候,客户端向服务端发送FIN 时,仅仅表示客户端不再发送数据了,但是还能接收。
  • 服务端收到客户端的FIN报文后,先回一个ACK报文,而服务端可能还有数据需要处理和发送,等服务器不再发送数据时,才发送FIN报文给客户端表示现在关闭连接

服务端通常要等待完成数据的处理和发送,所以ACK和FIN 报文会分开发送,这就是四次交互的由来

为什么需要TIME_WAIT

  • 防止具有相同的四元组的旧数据包被收到
  • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

原因一:防止旧连接的数据包 如果TIME_WAIT 没有等待时间或者时间很短,被延迟的数据包抵达会发什么什么?

/img/net/30.png

原因二:保证连接正确关闭 TIME_WAIT 作用是等待足够的时间以确保最后的ACK能让被动关闭方接收,从而帮助其正确关闭。

/img/net/31.png

如果 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:

1
2
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                                    state, about 60 seconds  */

如果要修改 TIME_WAIT 的时间⻓度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核

TIME_WAIT 过多有什么危害

  • 内存资源占用
  • 对端口资源的占用