【网络】TCP协议理论(2)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 【网络】TCP协议理论

七、连接管理机制

TCP的各种可靠性机制是基于连接的,与连接是强相关的。比如一台服务器启动后可能有多个客户端前来访问,如果TCP不是基于连接的,也就意味着服务器端只有一个接收缓冲区,此时各个客户端发来的数据都会拷贝到这个接收缓冲区当中,此时这些数据就可能会互相干扰,所以我们在进行TCP通信之前需要先建立连接。

1、操作系统对连接的管理

一台机器上可能会存在大量的TCP连接,此时操作系统就必须对这些连接进行管理。

在操作系统中有一个描述连接的结构体,该结构体当中包含了连接的各种属性字段,所有定义出来的连接结构体最终都会以某种数据结构组织起来,此时操作系统对连接的管理就变成了对该数据结构的增删查改。

  • 建立连接,实际就是在操作系统中用该结构体定义一个结构体变量,然后填充连接的各种属性字段,最后将其插入到管理连接的数据结构当中即可。
  • 断开连接,实际就是将某个连接从管理连接的数据结构当中删除,释放该连接曾经占用的各种资源。

因此连接的管理也是有成本的,这个成本就是管理连接结构体的时间成本,以及存储连接结构体的空间成本。

2、三次握手

一开始,客户端和服务端都处于 CLOSE 状态。

  1. 先是服务端主动监听某个端口,处于 LISTEN 状态,然后调用accept()等待客户端连接。
  2. 然后客户端调用 socket()函数,分配一个文件描述符,然后调用 connect()发起连接请求,正式开始三次握手。


三次握手的过程,实际是由双方的操作系统中的TCP层自主完成的!

  • connect函数,触发连接,等待完成。
  • accept,等待建立完成,获取连接。

在编码角度看:


在原理角度看:

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

  • 第二次握手:服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1(服务端向客户端发起连接建立请求并对客户端发来的连接请求进行响应)。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN_RCVD 状态。

  • 第三次握手:客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。

  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

需要注意的是:

  1. 客户端向服务器发起的建立连接请求,是请求建立从客户端到服务器方向的通信连接,而TCP是全双工通信,因此服务器在收到客户端发来的建立连接请求后,服务器也需要向客户端发起建立连接请求,请求建立从服务器到客户端方向的通信连接。
  2. 从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的
  3. 连接的建立不是百分之百能成功的,通信双方在进行三次握手时,其中前两次握手能够保证被对方收到,因为前两次握手都有对应的响应,但第三次握手是没有对应的响应报文的,如果第三次握手时客户端发送的ACK报文丢失了,那么连接建立就会失败。

如果第三次握手的ACK报文丢失导致,服务端也就不会进入ESTABLISHED 状态,但是客户端已经进入了ESTABLISHED 状态,然后客户端给服务器发送数据时,服务端就会检测到双方认知状态不一致,于是向客户端发送一个RST被置为1的TCP报文,让客户端重新建立连接。

3、为什么是三次握手

我们来进行依次考虑:

一次握手

TCP是全双工通信,需要两个方向上的通信信道都没有问题,一次握手在成功的情况下只能保证一个方向的通信信道没有问题,没有办法实现双向通信。

两次握手

如果只有两次握手,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到服务端的 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接

这会造成什么情况呢?

如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

而且在这种情况下,建立连接时的异常连接是挂在服务端的,不管客户端有没有成功收到服务端发送的 SYN + ACK,服务端自己是先要维护好连接的(哪怕这是一个异常的连接),然后客户端收到服务端发送的 SYN + ACK以后才会建立连接,如果没有收到,客户端就不必建立连接进行维护。

于是利用这个特点就可以对TCP进行「SYN洪水攻击」:

攻击者用大量的假IP地址发送初始连接请求(SYN)数据包,让服务端建立连接,然后切换IP继续发,于是导致服务端上面挂满了异常连接,而攻击者的机器上是不需要维护任何连接的,这样被攻击的服务器的CPU和内存资源会迅速耗尽,就无法响应任何新的请求了。

所以两次握手:可能会造成双方资源的浪费,同时也是不安全的。

三次握手

  • 没有明显的设计漏洞,一旦建立连接出现异常,异常连接成本嫁接到客户端。
  • 握手的本质是在验证双方通信信道的通畅情况,而三次握手是验证全双工通信信道通畅的最小成本!

  • 客户端通过①和②就能够验证自己的通信的收和发的通信信道是没有问题的。
  • 服务端通过①和③就能够验证自己的通信的收和发的通信信道是没有问题的。

四次握手

三次握手在理论上就已经能够完成可靠连接建立,所以不需要使用更多的通信次数。

4、四次挥手

由于双方维护连接都是需要成本的,因此当双方TCP通信结束之后就需要断开连接(双方都可以主动断开连接),断开连接后主机中的「资源」将被释放,断开连接的这个过程我们称之为四次挥手。

这里我们还是以服务器和客户端为例,当客户端与服务器通信结束后,需要与服务器断开连接,此时就需要进行四次挥手。

  • 第一次挥手:客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 第二次挥手:服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 第三次挥手:等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 第四次挥手:客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态。
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL (两个报文最大生存时间,单位是秒) 时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

报文最大生存时间在Linux中是可以查看的,通过下面的命令

cat /proc/sys/net/ipv4/tcp_fin_timeout

当然这个值也是可以修改的,修改以后想要生效需要重新编译 Linux 内核。


至此四次挥手结束后双方的连接才算真正断开,此外你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

为什么是四次挥手?

由于TCP是全双工的,建立连接的时候需要建立双方的连接,断开连接时也同样如此。

  • 关闭连接时(客户端主动调用close函数),客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据的。
  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时(服务器主动调用close函数),才发送 FIN 报文给客户端,表示表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,因此是需要四次挥手。但是在特定情况下,四次挥手是可以变成三次挥手的

5、四次挥手中的一些状态

CLOSE_WAIT

  • 双方在进行四次挥手时,如果只有客户端调用了close函数,而服务器不调用close函数,此时服务器就会进入CLOSE_WAIT状态,而客户端则会进入到FIN_WAIT_2状态。
  • 但只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器没有主动关闭不需要的文件描述符,随着是时间的积累,在服务器端就会存在大量处于CLOSE_WAIT状态的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少,因此一定要及时关闭不用的文件描述符!!!

LAST_ACK

如果客户端给服务器发送了FIN报文并收到了ACK以后会进入FIN_WAIT_2状态,这个状态并不稳定,这个状态会等待服务端发送FIN报文,如果服务端一直长时间不调用close函数不发送FIN报文,客户端会强制断开连接直接进入CLOSE状态,于是等客户端已经进入CLOSE状态了,服务器才调用close函数,此时,服务器的状态变为LAST_ACK,但是客户端是不会再对服务器进行响应(ACK),由于服务端一直收不到ACK,就会不断进行重传(此时一直处于LAST_ACK状态),直到达到指定次数,于是服务器也就主动进入了CLOSE状态了。

当服务器中存在大量CLOSE_WAIT的情况下关闭服务器进程(文件描述符是随进程的,进程关闭相当于主动调用close)就会发现CLOSE_WAIT状态变为了大量的LAST_ACK状态。

TIME_WAIT

主动断开连接的一方在发送完最后一个ACK以后,就会进入TIME_WAIT状态,然后等待2MSL时间以后才会进入CLOSE状态。

那么为什么要有TIME_WAIT状态呢,为什么不是发送完最后一个ACK以后直接进入进入CLOSE状态呢?

主要有下面的两个原因:

  • 保证「被动关闭连接」的一方,能被正确的关闭

如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。

假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,对于收到服务端重传的 FIN 报文不会进行响应,于是服务器在经过若干次超时重发后得不到响应,最终也会将对应的连接关闭,但在服务器不断进行超时重传期间还需要维护这条废弃的连接,这样对服务器是非常不友好的。

  • 保证双方通信信道上的数据在网络中尽可能的消散

假设 TIME-WAIT 没有等待时间或时间过短,当一个服务器重启以后,服务端以相同的四元组(源IP,目的IP,源端口,目的端口)重新打开了新连接,重启之前的被延迟的数据包这时抵达了服务端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。

为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的

TIME_WAIT的优化

TIME_WAIT状态的设计是好事,但是在有些情况下却可能带来糟糕的影响,例如当我们的服务器挂掉了,这时主动断开连接的一方就变成了服务器,由于TIME_WAIT状态导致我们的服务器不能够立即启动,这时我们重新启动服务器就会显示端口已经被占用了。

由于我们是服务器程序,我们又不能随意改变端口,于是我们只能等待120s,但是这对于服务器程序来说是不允许的,因此我们要想办法让服务器程序在成为主动断开连接的一方时也能够忽略TIME_WAIT状态立即启动。

我们知道计算机中被使用的端口,还是可以继续对另外一个主机发起连接的,于是Linux给我们提供了一个函数,通过这个函数我们能够复用套接字。

#include <sys/types.h>        
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • 作用: 设置套接字有关信息
  • 参数 :
    sockfd:将要被设置的套接字。
    level:要设置的选项所在的协议层,这里我们设置为SOL_SOCKET
    optname:需要访问的选项名,这里我们设置为SO_REUSEADDR
    optval:对选项要设置的的值的起始地址,这里我们定义一个变量opt为1,然后填写这个opt变量的地址就行了。
    optlenoptval指向的缓冲区的长度,这里的长度为sizeof(opt)
  • 返回值
    成功返回0,失败返回-1,错误码被设置。

至此TCP的连接管理机制讲解完毕,下面是一张TCP的完整的连接过程:

八、滑动窗口

我们在前面说过:双方在进行TCP通信时可以一次向对方发送多条报文,这样可以将发送和等待多个响应的时间重叠起来,进而提高数据通信的效率。

虽然双方在进行TCP通信时可以一次向对方发送多条的报文,但是其必须收到「流量控制」的限制,即一次发送多条数据时不能超出接收方的接收能力。

1、滑动窗口的一般原理介绍

首先滑动窗口是在发送方的发送缓冲区里面的,为了便于理解我们可以将发送缓冲区看作是一个char类型的数组(便于体现其字节流的特性)其有两个下标,winstart指向滑动窗口的起始位置,winend指向滑动窗口的结束位置。

窗口里面的数据就是指这部分数据是可以进行并发发送,暂时不用收到应答的数据

根据滑动窗口可以将发送缓冲区当中的数据分为三部分:

  • 已经发送并且已经收到ACK的数据。
  • 已经发送还但没有收到ACK的数据。
  • 还没有发送的数据。

问:那么这个滑动窗口的大小是多少呢?

由于TCP有「流量控制」和「拥塞控制」,所以滑动窗口的机制不能违背其他机制,结论是其大小为:

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
2天前
|
数据采集 存储 JSON
从零到一构建网络爬虫帝国:HTTP协议+Python requests库深度解析
在网络数据的海洋中,网络爬虫遵循HTTP协议,穿梭于互联网各处,收集宝贵信息。本文将从零开始,使用Python的requests库,深入解析HTTP协议,助你构建自己的网络爬虫帝国。首先介绍HTTP协议基础,包括请求与响应结构;然后详细介绍requests库的安装与使用,演示如何发送GET和POST请求并处理响应;最后概述爬虫构建流程及挑战,帮助你逐步掌握核心技术,畅游数据海洋。
15 3
|
7天前
|
数据采集 网络协议 API
HTTP协议大揭秘!Python requests库实战,让网络请求变得简单高效
【9月更文挑战第13天】在数字化时代,互联网成为信息传输的核心平台,HTTP协议作为基石,定义了客户端与服务器间的数据传输规则。直接处理HTTP请求复杂繁琐,但Python的`requests`库提供了一个简洁强大的接口,简化了这一过程。HTTP协议采用请求与响应模式,无状态且结构化设计,使其能灵活处理各种数据交换。
35 8
|
6天前
|
网络协议
UDP协议在网络通信中的独特应用与优势
UDP(用户数据报协议)作为关键的传输层协议,在网络通信中展现出独特优势。本文探讨UDP的无连接性及低开销特性,使其在实时性要求高的场景如视频流、在线游戏中表现优异;其不保证可靠交付的特性赋予应用程序自定义传输策略的灵活性;面向报文的高效处理能力及短小的包头设计进一步提升了数据传输效率。总之,UDP适用于高速、实时性强且对可靠性要求不高的应用场景,为网络通信提供了多样化的选择。
|
7天前
|
网络协议 网络架构 数据格式
TCP/IP基础:工作原理、协议栈与网络层
TCP/IP(传输控制协议/互联网协议)是互联网通信的基础协议,支持数据传输和网络连接。本文详细阐述了其工作原理、协议栈构成及网络层功能。TCP/IP采用客户端/服务器模型,通过四个层次——应用层、传输层、网络层和数据链路层,确保数据可靠传输。网络层负责IP寻址、路由选择、分片重组及数据包传输,是TCP/IP的核心部分。理解TCP/IP有助于深入掌握互联网底层机制。
33 2
|
16天前
|
网络协议 C语言
C语言 网络编程(十三)并发的TCP服务端-以进程完成功能
这段代码实现了一个基于TCP协议的多进程并发服务端和客户端程序。服务端通过创建子进程来处理多个客户端连接,解决了粘包问题,并支持不定长数据传输。客户端则循环发送数据并接收服务端回传的信息,同样处理了粘包问题。程序通过自定义的数据长度前缀确保了数据的完整性和准确性。
|
16天前
|
网络协议 C语言
C语言 网络编程(十一)TCP通信创建流程---服务端
在服务器流程中,新增了绑定IP地址与端口号、建立监听队列及接受连接并创建新文件描述符等步骤。`bind`函数用于绑定IP地址与端口,`listen`函数建立监听队列并设置监听状态,`accept`函数则接受连接请求并创建新的文件描述符用于数据传输。套接字状态包括关闭(CLOSED)、同步发送(SYN-SENT)、同步接收(SYN-RECEIVE)和已建立连接(ESTABLISHED)。示例代码展示了TCP服务端程序如何初始化socket、绑定地址、监听连接请求以及接收和发送数据。
|
16天前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
16天前
|
网络协议 C语言
C语言 网络编程(十二)TCP通信创建-粘包
TCP通信中的“粘包”现象指的是由于协议特性,发送方的数据包被拆分并在接收方按序组装,导致多个数据包粘连或单个数据包分割。为避免粘包,可采用定长数据包或先传送数据长度再传送数据的方式。示例代码展示了通过在发送前添加数据长度信息,并在接收时先读取长度后读取数据的具体实现方法。此方案适用于长度不固定的数据传输场景。
|
16天前
|
缓存 网络协议 网络性能优化
C语言 网络编程(二)TCP 协议
TCP(传输控制协议)是一种面向连接、可靠的传输层协议,通过校验和、序列号、确认应答等机制确保数据完整性和可靠性。通信双方需先建立连接,再进行通信,采用三次握手建立连接,四次挥手断开连接。TCP支持任意字节长度的数据传输,具备超时重传、流量控制及拥塞控制机制。三次握手用于同步序列号和确认双方通信能力,四次挥手则确保双方均能完成连接关闭操作,保证数据传输的可靠性。
|
16天前
|
网络协议 C语言
C语言 网络编程(十)TCP通信创建流程---客户端
在TCP通信中,客户端需通过一系列步骤与服务器建立连接并进行数据传输。首先使用 `socket()` 函数创建一个流式套接字,然后通过 `connect()` 函数连接服务器。连接成功后,可以使用 `send()` 和 `recv()` 函数进行数据发送和接收。最后展示了一个完整的客户端示例代码,实现了与服务器的通信过程。