当前位置: 首页 > news >正文

【Linux】传输层协议TCP

目录

一、TCP协议

1.1 TCP协议段格式

二、确认应答(ACK)机制

三、超时重传机制

四、连接管理机制

4.1 理解TIME_WAIT状态

4.2 解决TIME_WAIT状态引起的bind失败的方法

4.3 理解CLOSE_WAIT状态

五、滑动窗口

六、流量控制

七、拥塞控制

八、延迟应答

九、捎带应答

十、面向字节流

十一、粘包问题

十二、TCP异常情况

十三、TCP总结

十四、基于TCP应用层协议

十五、TCP/UDP对比

十六、UDP实现可靠传输


一、TCP协议

TCP全程为传输控制协议(Transmission Control Protocol)。跟名字一样,要对数据的传输进行详细的控制。

1.1 TCP协议段格式

  • 源/目的端口号:表示数据是从哪个进程来,到哪个进程去。
  • 32位序号/32位确认序号:后面详讲。
  • 4位首部长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60.
  • 6位标志位:
  1. URG:紧急指针是否有效
  2. ACK:确认号是否有效
  3. PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
  4. RST:对方要求重新建立连接,我们把携带RST标志位的称为复位报文段
  5. SYN:请求建立连接,我们把携带SYN标志位的称为同步报文段
  6. FIN:通知对方,本端要关闭了,我​​​​​​​们把携带SYN标志位的称为结束报文段
  • 16位窗口大小:后面详讲
  • 16位检验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题,此处的检验和不光包含TCP首部,也包含TCP数据部分
  • 16位紧急指针:标识哪部分数据是紧急数据
  • 40字节头部选项:暂时忽略

二、确认应答(ACK)机制

TCP将每个字节的数据都进行了编号,即为序列号。

每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。

三、超时重传机制

  • 如果主机A发送数据给主机B之后,可能因为网络拥堵等原因,数据无法到达主机B。
  • 如果主机A在一个特定的时间间隔内没有收到主机B发来的确认应答,就会进行重发。

但是,主机A未收到主机B发来的确认应答,也可能是因为ACK丢失。

因此,主机B会收到很多重复的数据。那么TCP协议需要能够识别出哪些包是重复的包,并且把重复的包丢弃。

这时候我们可以利用前面提到的序列号,就可以很容易的做到去重的效果。

那么,超时的时间如果确定?

  • 最理想的情况下,找一个最小的时间,保证“确认应答一定能在这个时间内返回”。
  • 但是这个时间的长短,随着网络环境的不同,是有差异的。
  • 如果超时时间设的太长,会影响整体的重传效率。
  • 如果超时时间设的太短,有可能频繁发送重复的包。

TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。

  • Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判断超时重发的超时时间都是500ms的整数倍。
  • 如果重发一次后仍然得不到应答,等待2 * 500ms后再进行重传。
  • 如果仍然得不到应答,等待4 * 500ms进行重传。以此类推,以指数形式进行递增。
  • 累积到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。

四、连接管理机制

在正常情况下,TCP要经历三次握手建立连接,四次挥手断开连接。

服务端状态转化

  • [CLOSED -> LISTEN]:服务器调用 listen 后进入 LISTEN 状态,等待客户端连接。
  • [LISTEN -> SYN_RCVD]:一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送 SYN 确认报文。
  • [SYN_RCVD -> ESTABLISHED]:服务端一旦收到客户端的确认报文,就进入 ESTABLISHED 状态,可以进行读写数据了。
  • [ESTABLISHED -> CLOSE_WAIT]:当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入 CLOSE_WAIT 状态。
  • [CLOSE_WAIT -> LAST_ACK]:进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据),当服务器真正调用 close 关闭连接时,会向客户端发送 FIN,此时服务器进入 LAST_ACK 状态,等待最后一个 ACK 到来(这个ACK是客户端确认收到了 FIN)。
  • [LAST_ACK -> CLOSED]:服务器收到了对 FIN 的 ACK,彻底关闭连接。

客户端状态转化

  • [CLOSED -> SYN_SENT]:客户端调用 connect,发送同步报文段。
  • [SYN_SENT -> ESTABLISHED]:connect 调用成功,则进入 ESTABLISHED 状态,开始读写数据。
  • [ESTABLISHED -> FIN_WAIT_1]:客户端主动调用 close 时,向服务器发送结束报文段,同时进入 FIN_WAIT_1。
  • [FIN_WAIT_1 -> FIN_WAIT_2]:客户端收到服务器对结束报文段的确认,则进入 FIN_WAIT_2,开始等待服务器的结束报文段。
  • [FIN_WAIT_2 -> TIME_WAIT]:客户端收到服务器发来的结束报文段,进入 TIME_WAIT,并发出 LAST_ACK。
  • [TIME_WAIT -> CLOSED]:客户端要等待一个 2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入 CLOSED 状态。

下图是TCP状态转化的一个汇总:

  • 较粗的虚线表示服务端的状态变化情况。
  • 较粗的实线表示客户端的状态变化情况。
  • CLOSED是一个假想的起始点,不是真实状态。

4.1 理解TIME_WAIT状态

现在做一个测试,首先启动 server,然后启动 client,然后用 Ctrl + c 使 server 终止,这时马上在运行 server,结果是:

这时因为,server的应用程序终止了,但TCP协议层的连接还没有完全断开,因此不能再次监听同样的server端口。我们用 netstat 命令查看一下:

  • TCP协议规定,主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态。
  • 我们使用了 Ctrl + c 终止了 server,所以 server 是主动关闭连接的一方,在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口。
  • MSL 在 RFC1122中规定为两分钟,但是各操作系统的实现不同,在Centos/Ubuntu上默认配置为60s。
  • 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值。

思考一下,为什么 TIME_WAIT 的时间是 2MSL?

  • MSL 是 TCP 报文最大的生存时间,因此 TIME_WIAT 持续存在 2MSL 的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这个数据很有可能是错误的)。
  • 同时也在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失,那么服务器再重发一个 FIN。这时虽然客户端的进程不在了,但是 TCP 连接还在,仍然可以重发 LAST_ACK)。

4.2 解决TIME_WAIT状态引起的bind失败的方法

在 server 的 TCP 没有完全断开之前不允许重新监听,某些情况下可能是不合理的。

  • 服务器需要处理非常大量的客户端连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求)。
  • 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量 TIME_WAIT 连接。
  • 由于我们的请求量很大,就可能会导致 TIME_WAIT 的连接数很多,每个连接都会占用一个通信五元组(源IP,目的IP,源端口,目的端口,协议)。其中服务器的IP、端口和协议是固定的,如果新来的客户端连接的IP和端口和TIME_WAIT占用的连接重复了,就会出现问题。

使用 setsockopt() 设置 socket 的描述符的选项 SO_REUSEADDR 为 1,表示允许创建端口号相同但ip地址不同的多个 socket 描述符。

int opt = 1;

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

4.3 理解CLOSE_WAIT状态

我们关闭了服务器,这时客户端进入 CLOSE_WAIT 状态,结合我们四次挥手的流程图,可以认为四次挥手没有正确完成。

原因:客户端没有正确的关闭 socket,导致四次挥手没有正确完成,只需要加上对应的 close 即可解决问题。

同样,服务器上出现大量的 CLOSE_WAIT 状态,也是同样的原因,一样的解决方案。

五、滑动窗口

刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个 ACK 确认应答。收到 ACK 后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候。

既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起)。

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)。
  • 发送前四个段的时候,不需要等待任何 ACK,直接发送。
  • 收到第一个 ACK 后,滑动窗口向后滑动,继续发送第五个段的数据,以此类推。
  • 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答,只有确认应答的数据,才能从发送缓冲区中删除。
  • 窗口越大,网络的吞吐量越高。

那么如果出现了丢包,如何进行重传?这里分两种情况讨论:

情况一:数据包已经抵达,ACK丢了

这种情况下,部分 ACK 丢了不要紧,因为可以通过后续的 ACK 确认。

情况二:数据包就直接丢了

  • 当某一段报文段弄丢了之后,发送端会一直收到 1001 这样的 ACK,就像在提醒发送端“我想要的是 1001”一样。
  • 如果发送端主机连续3次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 - 2000 重新发送。
  • 这个时候接收端收到了 1001 之后,再次返回的 ACK 就是 7001了(因为 2001 - 7000 接收端之前已经收到了,被放到了接收端操作系统内核的 接收缓冲区 了)。

这种机制被称为 “快速重发控制”(也叫“快重传”)。

六、流量控制

接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等一系列连锁反应。

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做“流量控制”(Flow Control)。

  • 接收端将自己能够接收的缓冲区的剩余空间大小放入 TCP首部中的“窗口大小”字段,通过 ACK 通知发送端。
  • 窗口大小字段越大,说明网络的吞吐量越高。
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。
  • 发送端接收到这个窗口之后,就会减慢自己的发送速度。
  • 如果接收端缓冲区满了,就会将窗口设置为0,这时候发送端不会再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告知发送端。

接收端如何把窗口大小告诉发送端呢?回忆我们的TCP首部中,有一个16位窗口字段,就是存放了窗口大小信息。

那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节吗?

实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移 M 位。

七、拥塞控制

虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量数据。但是如果在刚开始阶段就发送大量数据,仍然可能引发问题。

因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。

TCP引入 慢启动 机制,先发少量数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

  • 此处引入一个概念称为拥塞窗口
  • 发送开始的时候,定义拥塞窗口大小为1.
  • 每收到一个一个 ACK 应答,拥塞窗口大小加1.
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为发送的窗口。

像上面这样的拥塞窗口增长速度,是指数级别的。“慢启动”是指初始时慢,但是增长速度非常快。

  • 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。
  • 此处引入一个叫做慢启动的阈值。
  • 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。

  • 当TCP开始启动的时候,慢启动阈值等于窗口最大值。
  • 在每次超时重发的时候,慢启动阈值会变为原来的一半,同时拥塞窗口置为1.

少量的丢包,我们仅仅是触发超时重传,大量的丢包,我们就认为网络拥塞。

当TCP开始通信后,网络吞吐量会逐渐上升,随着网络发生拥堵,吞吐量会立刻下降。

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力而做的折中方案。

八、延迟应答

如果接收数据的主机立刻返回 ACK 应答,这个时候返回的窗口可能比较小。

  • 假设接收端缓冲区为 1M。一次收到了 500K 的数据,如果立刻应答,返回的窗口就是 500K。
  • 但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区中消费掉了。
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
  • 如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么这个时候返回的窗口大小就是 1M。

一定要记得,窗口越大,网络吞吐量越高,传输效率越高。我们的目标是保证网络不拥塞的情况下尽量提高传输效率。

那么所有的包都可以延迟应答吗?肯定也不是。

  • 数量限制:每个 N 个包,就应答一次。
  • 时间限制:超过最大延迟时间应答一次。

具体的数量和超时时间,依操作系统不同也有差异,一般 N 取 2,超时时间取 200ms。

九、捎带应答

在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是“一发一收”的。意味着,客户端给服务器说了 "How are you",服务器也会给客户端回一个 "Fine, Thank you"。

那么这个时候 ACK 就可以搭顺风车,和服务器回应的 "Fine, Thank you" 一起回给客户端。

十、面向字节流

创建一个TCP的 socket,同时在内核中创建一个 发送缓冲区接收缓冲区

  • 调用 write 时,数据会先写入发送缓冲区。
  • 如果发送的字节数太长,会被拆分成多个TCP数据包发送。
  • 如果发送的字节数太短,就会现在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区。
  • 然后应用程序可以调用 read 从接收缓冲区中读取数据。
  • 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对这一个连接,既可以读数据,也可以写数据,这个概念叫全双工

由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:

  • 写 100 个字节数据时,可以调用一个 write 写 100 个字节,也可以调用 100 次 write,每次写 1 个字节。
  • 读取 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次 read 100个字节,也可以一次 read 一个字节,重复 100 次。

十一、粘包问题

  • 首先要明确,粘包问题中的“包”,是指应用层的包。
  • 在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段,但是有一个序号这样的字段。
  • 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区。
  • 站在应用层的角度,看到的只是一串连续的字节数据。
  • 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。

那么如何避免粘包问题呢?归根结底就是一句话:明确两个包之间的边界

  • 对于定长的包,保证每次都按固定大小读取即可;例如上面的 Request 结构,是固定大小的,那么就从缓冲区从头开始按 sizoef(Request) 依次读取即可。
  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。
  • 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序员自己来定的,只要保证分隔符不和正文冲突即可)。

思考:对于UDP协议来说,是否也存在“粘包问题”呢?

  • 对于UDP,如果还没有上层交付数据,UDP的报文长度仍在。同时,UDP是一个一个把数据交付给应用层的,就有很明确的数据边界。
  • 站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况。

十二、TCP异常情况

进程终止:进程终止会释放文件描述符,仍然可以发送 FIN,和正常关闭没有什么区别。

机器重启:和进程终止的情况相同。

机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行 reset。即使没有写入操作,TCP自己也内置了一个保活计时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。

另外,应用层的某些协议,也有一些这样的检测机制。例如 HTTP 长连接中,也会定期检测对方的状态,例如QQ,在QQ断线之后,也会定期尝试重新连接。

十三、TCP总结

为什么TCP这么复杂?因为要保证可靠性,同时还要尽可能提高性能。

可靠性:

  • 校验和
  • 序列号(按需到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他:

  • 定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)

十四、基于TCP应用层协议

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP

当然,也包括自己写的TCP程序时自定义的应用层协议。

十五、TCP/UDP对比

我们说了TCP是可靠连接,那么是不是TCP就一定优于UDP呢?TCP和UDP之间的优点和缺点,不能简单,绝对的进行比较。

  • TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景。
  • UDP用于对高速传输和实时性要求较高的通信领域,例如:早期的QQ,视频领域等,另外UDP可以用于广播。

归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的场景需求去判定。

十六、UDP实现可靠传输

参考TCP的可靠机制,在应用层实现类似的逻辑。

例如:

  • 引入序列号,保证数据顺序。
  • 引入确认应答,确保对端收到数据。
  • 引入超时重传,如果隔一段时间没有应答,就重发数据。
  • ......
http://www.dtcms.com/a/531898.html

相关文章:

  • 前端监控:错误捕获与行为日志全解析
  • 第一部分:网络基础
  • Socket详解
  • Ceph存储
  • [人工智能-大模型-87]:模型层技术 - “神经网络架构演进的全景地图”,“从简单到复杂、从单一到智能” - 通俗易懂版。
  • windows 2003 取消网站访问密码wordpress黑镜百度云盘
  • Spring Boot3零基础教程,自定义 starter,把项目封装成依赖给别人使用,笔记65
  • 建设足球网站的心得和意义渠道分销管理系统
  • 【PLC】汇川InoTouchPad在Win11上显示太小
  • OpenHarmony蓝牙技术全解析:从设备发现到数据传输的完整流程
  • 解压版MySQL的安装与卸载
  • C++编程基础(五):字符数组和字符串
  • 在线旅游网站平台有哪些山东泰安房价2023最新价格
  • [3D Max 基础知识分享]—多孔结构模型编辑
  • 【C++篇】C++11入门:踏入C++新世界的大门
  • 爬虫数据清洗可视化案例之全球灾害数据
  • QT(c++)开发自学笔记:4.Qt 3D简易实现
  • Vue3 自定义事件
  • 上海住房和城乡建设厅网站个人备案网站可以做产品推广
  • Android OpenGLES视频剪辑示例源码
  • 做淘宝客导购网站推广wordpress 明星
  • WebForms 页面
  • Leetcode 39
  • 【STM32项目开源】基于STM32的智能水质检测系统
  • 设计模式-迭代器模式(Iterator)
  • GitHub等平台形成的开源文化正在重塑天热e
  • 做网站需要用什么开发软件有哪些制作视频的软件
  • github中获得Personal Access Token
  • 从RDPDD!DrvEscape到RDPWD!ShareClass::UPSendOrders
  • RiPro数据转换CeoMax插件