美文网首页
计算机网络

计算机网络

作者: upup果 | 来源:发表于2020-12-18 17:01 被阅读0次

    TCP/IP相关

    TCP基本认识

    • TCP头部格式
      序列号:用来解决网络包乱序问题,保证所有传输的数据按照正常的次序进行重组。在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。
      确认应答号:用来解决丢包的问题。指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。
      控制位:
      ACK:该位为 1 时,「确认应答」的字段为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
      RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接,重新连接
      SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
      FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。
      URG:该位为1,紧急指针有效,需要优先处理
      PSH:提示接收端应用程序立即从TCP缓冲区读取数据
    • 为什么需要 TCP 协议?TCP 工作在哪一层?
      IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
    • 什么是 TCP ?
      TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
      面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
      可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
      字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。
    • 什么是 TCP 连接?
      用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。Socket:由 IP 地址和端口号组成.序列号:用来解决乱序问题等.窗口大小:用来做流量控制.
    • 如何唯一确定一个 TCP 连接呢?
      TCP 四元组可以唯一的确定一个连接,四元组包括如下:源地址,源端口,目的地址,目的端口.源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
    • 有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?
      理论最大TCP连接数=客户端的IP数X客户端的端口数
      对 IPv4,客户端的 IP 数最多为 232 次方,客户端的端口数最多为 216 次方,也就是服务端单机最大 TCP 连接数,约为 248 次方。当然,服务端最大并发 TCP 连接数远不能达到理论上限。首先主要是文件描述符限制,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统是有限的。
    • UDP 和 TCP 有什么区别呢?分别的应用场景是?
      UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。
      UDP 协议头部只有 8 个字节( 64 位),UDP 的头部格式如下,每个字段各16位:
      目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
      包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
      校验和:校验和是为了提供可靠的 UDP 首部和数据而设计。
      TCP 和 UDP 区别:
      1.连接
      TCP 是面向连接的传输层协议,传输数据前先要建立连接。
      UDP 是不需要连接,即刻传输数据。
      2.服务对象
      TCP 是一对一的两点服务,即一条连接只有两个端点。
      UDP 支持一对一、一对多、多对多的交互通信
      3.可靠性
      TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
      UDP 是尽最大努力交付,不保证可靠交付数据。
      4.拥塞控制、流量控制
      TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
      UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
      5.首部开销
      TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
      TCP 和 UDP 应用场景:
      由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:FTP 文件传输,HTTP / HTTPS.
      由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
      包总量较少的通信,如 DNS 、SNMP 等,视频、音频等多媒体通信,广播通信.
    • 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?
      因为TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。
    • 为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?
      (TCP 数据的长度=IP总长度-IP首部长度-TCP首部长度,其中 IP 总长度 和 IP 首部长度,在 IP 首部格式是已知的。TCP 首部长度,则是在 TCP 首部格式已知的,所以就可以求得 TCP 数据的长度。)
      因为为了网络设备硬件设计和处理方便,首部长度需要是 4字节的整数倍。如果去掉 UDP 「包长度」字段,那 UDP 首部长度就不是 4 字节的整数倍了,所以这可能是为了补全 UDP 首部长度是 4 字节的整数倍,才补充了「包长度」字段。

    TCP连接建立

    TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。

    • TCP 三次握手过程和状态变迁


      TCP 三次握手.png

      一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态.


      第一个报文--SYN报文.png
      客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
      第二个报文--SYN+ACK报文.png

      服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。


      第三个报文--ACK报文.png
      客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
      服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
      一旦完成三次握手,双方都处于 ESTABLISHED 状态,此致连接就已建立完成,客户端和服务端就可以相互发送数据
      (从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的)
    • 如何在 Linux 系统中查看 TCP 状态?
      TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。


      linux下tcp状态.png
    • 为什么是三次握手?不是两次、四次?
    1. 三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。由于网络拥堵等原因,可能会使旧的数据包,先到达目标主机.
      三次握手避免重复连接.png
      客户端连续发送多次 SYN 建立连接的报文,在网络拥堵等情况下:
      一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
      那么此时服务端就会回一个 SYN + ACK 报文给客户端;
      客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。
      如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
      如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接;
      如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接;
      所以, TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接.
      2.原因二:同步双方初始序列号
      TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
      接收方可以去除重复的数据;
      接收方可以根据数据包的序列号按序接收;
      可以标识发送出去的数据包中, 哪些是已经被对方收到的;
      可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步.
      四次握手和三次握手.png
      四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
      3.原因三:避免资源浪费
      两次握手.png
      如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接.如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
      4.小结
      TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
      不使用「两次握手」和「四次握手」的原因:
      「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
      「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
      不使用「两次握手」和「四次握手」的原因:
      「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
      「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
    • 为什么客户端和服务端的初始序列号 ISN 是不相同的?
      因为网络中的报文会延迟、会复制重发、也有可能丢失,这样会造成不同连接之间产生互相影响,所以为了避免互相影响,客户端和服务端的初始序列号是随机且不同的。
    • 初始序列号 ISN 是如何随机产生的?
      起始 ISN 是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。
      RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
      ISN = M + F (localhost, localport, remotehost, remoteport)
      M 是一个计时器,这个计时器每隔 4 毫秒加 1。
      F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
    • 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
    MTU 与 MSS.png

    MTU:一个网络包的最大长度,以太网中一般为 1500 字节;
    MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;
    如果TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,在交给上一层 TCP 传输层。如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应ACK 给对方,那么发送方的 TCP 在超时后,就会重发「整个 TCP 报文(头部 + 数据)」。因此,由 IP 层进行分片传输,是非常没有效率的。所以,为了达到最佳的传输效能, TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

    • 什么是SYN共攻击?
      TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
    • 如何避免SYN攻击?
    1. 通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。
      当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:net.core.netdev_max_backlog:SYN_RCVD 状态连接的最大个数:net.ipv4.tcp_max_syn_backlog:
      超出处理时,对新的 SYN 直接回 RST:net.ipv4.tcp_abort_on_overflow
    2. 正常流程:
      当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
      接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
      服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
      应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。
      如果应用程序过慢时,就会导致「 Accept 队列」被占满。
      如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满
      tcp_syncookies 的方式可以应对 SYN 攻击:net.ipv4.tcp_syncookies = 1
      当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
      计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
      服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
      最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。

    TCP断开连接

    TCP 断开连接是通过四次挥手方式。
    双方都可以主动断开连接,断开连接后主机中的「资源」将被释放

    • TCP 四次挥手过程和状态变迁


      客户端主动断开连接--TCP四次握手.png

      1)客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
      2)服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
      3)客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
      4)等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
      5)客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
      6)服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
      7)客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
      每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
      这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

    • 为什么断开需要四次?
      关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
      服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
      从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。
    • 为什么 TIME_WAIT 等待的时间是 2MSL?
      MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
      MSL 与 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
      TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
      比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
      2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
      在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
      其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:#define TCP_TIMEWAIT_LEN (60HZ)
      /
      how long to wait to destroy TIME-WAIT state, about 60 seconds */
      如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。
    • 为什么需要 TIME_WAIT 状态?
      主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
      需要 TIME-WAIT 状态,主要是两个原因:
      防止具有相同「四元组」的「旧」数据包被收到;
      保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;
      原因一:防止旧连接的数据包


      接收到历史数据的异常.png

      如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
      这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
      原因二:保证连接正确关闭


      没有确保正常断开的异常.png
      如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSE 状态了,那么服务端则会一直处在 LASE-ACK 状态。
      当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:
      服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
      服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。
      所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭。
    • TIME_WAIT 过多有什么危害?
      如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。
      过多的 TIME-WAIT 状态主要的危害有两种:
      第一是内存资源占用;
      第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口;
      第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过如下参数设置指定:net.ipv4.ip_local_port_range
      如果服务端 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
    • 如何优化 TIME_WAIT?
      这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:
      打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
      net.ipv4.tcp_max_tw_buckets
      程序中使用 SO_LINGER ,应用强制使用 RST 关闭。
      方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps
      如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
      net.ipv4.tcp_tw_reuse = 1
      使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
      net.ipv4.tcp_timestamps=1(默认即为 1)
      这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。温馨提醒:net.ipv4.tcp_tw_reuse要慎用,因为使用了它就必然要打开时间戳的支持 net.ipv4.tcp_timestamps,当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉。敖丙在工作中就遇到过。。。排查了非常的久
      方式二:net.ipv4.tcp_max_tw_buckets
      这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置。这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。
      方式三:程序中使用 SO_LINGER
      我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
      struct linger so_linger;
      so_linger.l_onoff = 1;
      so_linger.l_linger = 0;
      setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
      如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。但为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
    • 如果已经建立了连接,但是客户端突然出现故障了怎么办?
      TCP 有一个机制是保活机制。这个机制的原理是这样的:
      定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
      在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
      net.ipv4.tcp_keepalive_time=7200
      net.ipv4.tcp_keepalive_intvl=75
      net.ipv4.tcp_keepalive_probes=9
      tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
      tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
      tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
      也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
      这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。
      如果开启了 TCP 保活,需要考虑以下几种情况:
      第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
      第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
      第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
    • 挥手一定需要四次吗?
      假设 client 已经没有数据发送给 server 了,所以它发送 FIN 给 server 表明自己数据发完了,不再发了,如果这时候 server 还是有数据要发送给 client 那么它就是先回复 ack ,然后继续发送数据。
      等 server 数据发送完了之后再向 client 发送 FIN 表明它也发完了,然后等 client 的 ACK 这种情况下就会有四次挥手。
      那么假设 client 发送 FIN 给 server 的时候 server 也没数据给 client,那么 server 就可以将 ACK 和它的 FIN 一起发给client ,然后等待 client 的 ACK,这样就是三次挥手.

    重传机制

    TCP 实现可靠传输的方式之一,是通过序列号与确认应答。在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。但在错综复杂的网络,并不一定能能正常的数据传输,数据在传输过程中可能会丢失.所以 TCP 针对数据包丢失的情况,会用重传机制解决。常见的重传机制:超时重传,快速重传,SACK,D-SACK.
    超时重传
    重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。TCP 会在以下两种情况发生超时重传:
    数据包丢失
    确认应答丢失
    超时时间应该设置为多少呢?
    RTT (Round-Trip Time)就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。精确的测量超时时间 RTO 的值是非常重要的,这可重传机制更高效。根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。估计往返时间,通常需要采样以下两个:
    需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

    快速重传
    TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。

    SACK 方法
    还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认)。
    这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
    如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。


    选择性确认

    Duplicate SACK
    Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。


    ACK 丢包

    「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK。这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。

    网络延时

    数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。可见,D-SACK 有这么几个好处:
    可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
    可以知道是不是「发送方」的数据包被网络延迟了;
    可以知道网络中是不是把「发送方」的数据包给复制了;
    在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

    • 滑动窗口
      窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。


      用滑动窗口方式并行处理

      上图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通话下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。

    • 窗口大小由哪一方决定?
      TCP 头里有一个字段叫 Window,也就是窗口大小。
      这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以,通常窗口的大小是由接收方的决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
    • 发送方的滑动窗口
      下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:



      #1 是已发送并收到 ACK确认的数据:1~31 字节
      #2 是已发送但未收到 ACK确认的数据:32~45 字节
      #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
      #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

    在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。


    可用窗口耗尽

    在下图,当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。


    [32 ~ 36 字节已确认]
    • 程序是如何表示发送方的四个部分的呢?
      TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。


      SND.WND、SND.UN、SND.NXT

      SND.WND:表示发送窗口的大小(大小是由接收方指定的);
      SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
      SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
      指向#4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。那么可用窗口大小的计算就可以是:
      可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

    • 接收方的滑动窗口


      接收窗口

      接收窗口相对简单一些,根据处理的情况划分成三个部分:
      #1 + #2 是已成功接收并确认的数据(等待应用进程读取);
      #3 是未收到数据但可以接收的数据;
      #4 未收到数据并不可以接收的数据;
      其中三个接收部分,使用两个指针进行划分:
      RCV.WND:表示接收窗口的大小,它会通告给发送方。
      RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
      指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

    • 接收窗口和发送窗口的大小是相等的吗?
      并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
      因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系

    流量控制

    发送方不能无脑的发数据给接收方,要考虑接收方处理能力。如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。


    流量控制

    根据上图的流量控制,说明下每个过程:

    1.客户端向服务端发送请求数据报文。这里要说明下,本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。
    2.服务端收到请求报文后,发送确认报文和 80 字节的数据,于是可用窗口 Usable 减少为 120 字节,同时 SND.NXT 指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。
    3.客户端收到 80 字节数据后,于是接收窗口往右移动 80 字节,RCV.NXT 也就指向 321,这意味着客户端期望的下一个报文的序列号是 321,接着发送确认报文给服务端。
    4.服务端再次发送了 120 字节数据,于是可用窗口耗尽为 0,服务端无法在继续发送数据。
    5.客户端收到 120 字节的数据后,于是接收窗口往右移动 120 字节,RCV.NXT 也就指向 441,接着发送确认报文给服务端。
    6.服务端收到对 80 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 321,于是可用窗口 Usable 增大到 80。
    7.服务端收到对 120 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 441,于是可用窗口 Usable 增大到 200。
    8.服务端可以继续发送了,于是发送了 160 字节的数据后,SND.NXT 指向 601,于是可用窗口 Usable 减少到 40。
    9.客户端收到 160 字节后,接收窗口往右移动了 160 字节,RCV.NXT 也就是指向了 601,接着发送确认报文给服务端。
    10.服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是 SND.UNA 指针偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。

    • 操作系统缓冲区与滑动窗口的关系
      发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

    • 操作系统的缓冲区,是如何影响发送窗口和接收窗口的呢?


      image.png

      1.客户端发送 140 字节数据后,可用窗口变为 220 (360 - 140)。
      2.服务端收到 140 字节数据,但是服务端非常繁忙,应用进程只读取了 40 个字节,还有 100 字节占用着缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通过给客户端。
      3.客户端收到确认和窗口通告报文后,发送窗口减少为 260。
      4.客户端发送 180 字节数据,此时可用窗口减少到 80。
      5.服务端收到 180 字节数据,但是应用程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并在发送确认信息时,通过窗口大小给客户端。
      6.客户端收到确认和窗口通告报文后,发送窗口减少为 80。
      7.客户端发送 80 字节数据后,可用窗口耗尽。
      8.服务端收到 80 字节数据,但是应用程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端。
      9.客户端收到确认和窗口通告报文后,发送窗口减少为 0。

    可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变,

    当服务端系统资源非常紧张的时候,操心系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。

    image.png

    Socket编程

    相关文章

      网友评论

          本文标题:计算机网络

          本文链接:https://www.haomeiwen.com/subject/szuqgktx.html