Linux网络学习第六部分:tcp协议详解

TCP协议格式

源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去。
在这里插入图片描述

TCP协议中的4位首部长度

注意:4位首部长度的每一位的单位是4字节
TCP协议根据接收到的报文去掉首部长度就可以计算出发送的数据长度,就可以将数据完整的提取出来
用TCP协议发送时,由于TCP是数据流协议,因此TCP数据的大小是没有限制的
在这里插入图片描述

TCP协议中的32位序列号

在这里插入图片描述

TCP协议中的32位确认号

在这里插入图片描述

为什么协议既要有序列号,也要有确认号,一个不够吗?

答:因为TCP是全双工的,因此我再给你回确认消息的同时我也可以给你发消息,发消息就需要用到序列号,保证接收的顺序,
所以序列号,确认号都需要

TCP协议中的16位窗口大小

在这里插入图片描述

理解TCP的缓冲区(重要)

在这里插入图片描述

插一个小问题:端口号找到进程的原理是通过哈希的策略,但为什么用端口号来标示进程?而不直接用进程PID呢?

因为设计理念要统一:
端口号是传输层实现,进程号是操作系统的内部实现是应用层,从分层隔离的角度来看, 网络层不应与应用层高度耦合。
与上面为什么要拷贝到缓冲区的设计理念相同

TCP协议中的6个标志位

先做个总结然后一一解释:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

SYN请求建立连接

我们再进行通信的时候我们肯定首先要建立链接,那么如何确认我们成功建立了连接呢?见下图:
在这里插入图片描述

ACK确认号是否有效

在这里插入图片描述

RST重置异常链接

在这里插入图片描述

PSH提示接收端应用程序立刻从TCP缓冲区把数据读走

当接收方缓冲区满了就不接受了,发送方可以发送PSH来催接收方的应用层赶紧把缓冲区数据拷贝走,我还有好多要发过来呢!

URG紧急指针是否有效

在这里插入图片描述

FIN通知对方, 本端要关闭了

在这里插入图片描述

TCP的各项机制

超时重传机制

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传.
如果仍然得不到应答, 等待 4
500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接

一般超时重传有以下两个场景:
在这里插入图片描述
在这里插入图片描述

连接管理机制

三次握手、四次挥手都是OS中TCP完成的,我们用户完全不参与,当我们使用connect函数,底层OS就帮我们完成了三次挥手的工作
这就是缓冲区的优势,实现了与用户层解耦。
在这里插入图片描述

为什么要三次握手而不是1、2、4、5、6等次呢?

在这里插入图片描述

为什么要四次握手而不是1、2、3、5、6等次呢?

答:断开连接本质是双方达成链接都应该断开的共识,就是一个通知对方的机制
在这里插入图片描述

四次挥手中的TIME_WAIT状态

在这里插入图片描述
在这里插入图片描述

因为不方便做实验验证,我就直接说结论了。
1.主动断开连接的一方会进入TIME_WAIT状态
2.进入TIME_WAIT状态后并不会立刻断开链接,会等待2MSL即两个报文的发送最大时间
[RFC0793]中规定MSL为2分钟,然而在实际应用中可以为30秒、1分钟或者两分钟。在绝大多数的情况下,该值是可以修改的。
等待2MSL的原因如下:
1.尽量保证历史发送的网络数据在网络中消散,当我发送第四次握手的ACK后我会等待2MSL,而之前在历史上的数据正常发送的话最多在网络中一个来回的时间即2MSL。
2.尽量的保证,最后一个ACK被对方收到,如果我们发完ACK后进入等待,结果又收到了FIN,则说明我们的ACK发送失败了,我们还有补救的机会,当等了2MSL都没有收到FIN,则我们默认对方成功收到ACK了。
尽量是因为我们有可能对方先给我们发了个FIN我们ACK发不出去了,对方突然FIN也发不出去了,两边就不断在互相发FIN和ACK,结果两边啥都收不到,这就没办法了,所以只能尽量。

bind eror

我们之前做的实验发现,如果服务器先断开的话,如果立刻重启服务器就会发现bind eror的问题,结合上面刚讲过的知识,这回我们就理解了,原因为:
服务器作为先断开的一方要进入TIME_WAIT状态,而这个状态要持续2MSL,这就是为什么服务器关掉了,立刻重启用不了,其实是服务器并没有完全关闭,还要等带2MSL之后才会彻底关闭!
在这里插入图片描述

如何解决bind eror

想要解决bind eror,说白了就是让断开的一方进入TIME_WAIT状态后将他绑定的端口号允许其他人用,这样就不会出现bind eror了
对应的修改函数:
在这里插入图片描述
固定套路,原理就相当于将判断该端口号是否可用修改为true。
但是服务端还是会继续TIME_WAIT到2MSL之后再退出,并不是直接断开链接了!!!
在这里插入图片描述
现象就是之后关闭连接后立刻重启就不会bind eror了
在这里插入图片描述

四次挥手中的CLOSE_WAIT状态

在这里插入图片描述
close_wait状态给我们的信息就是:
在这里插入图片描述

滑动窗口(是动态增大减小的,不是固定的!)

如果通信时是发一个数据等一个ACK的话,通信效率将有所下降
因此,真正通信时是允许一次连续发送多个数据的,发送的数据大小取决于当前对放的接受能力,通过设置滑动窗口大小来动态修改发送数据的上限,窗口越大, 则网络的吞吐率就越高;

正常通信的情况:
在这里插入图片描述
滑动窗口:
在这里插入图片描述
在这里插入图片描述
滑动窗口的机制可以有效地提高我们的传输效率,接下来将详细理出滑动窗口的特点:
1.在我们TCP三次握手的时候,会收到对方发送的16位窗口大小,里面包含了我们各自缓冲区的接受能力,
这个接受能力称之为MSS(最大分段大小)互相知道对方的接收能力后我们就可以设置滑动窗口的大小了,举例如下图:

在这里插入图片描述
2.当开始通信后,如果一放的接收缓冲区被清空了或增加了一部分,即接受能力增大或减小,那么会给对方发送最新的接受能力,如:
在这里插入图片描述

快重传触发场景

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

在这里插入图片描述
处理方式:
在这里插入图片描述

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

在这里插入图片描述
处理方式:
在这里插入图片描述
上述当接收端连续发了三次同样的接收确认报文后,就会将对应的报文重新发送,这种机制被称为 “高速重发控制”(也叫 “快重传”).

超时重传vs快重传

快重传又快效率又高,为啥还要超时重传呢?
试想:当我的发送缓冲区只剩1个字节了,我没有收到正常顺序数据,但我都无法给对方发送完整的接收报文了,对方就更不可能触发快重传的机制,这时候就是超时重传的优势了,对方等一段时间会触发超时重传,保证了正常通信!
说明,超时重传是最后的兜底保证,不能没有他!!

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,
就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
1.接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
2.窗口大小字段越大, 说明网络的吞吐量越高;
3.接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
4.发送端接受到这个窗口之后, 就会减慢自己的发送速度;
5.如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
在这里插入图片描述

流量控制一些问题

1.什么时候流量控制?意思我第一次给你发数据的时候我咋知道你的接受能力的?
答:前面滑动窗口图里讲过,三次握手的时候互相会通知对方的接受能力
在这里插入图片描述
2.如果我的接收缓冲区窗口大小为0怎么办?
由上图可知,当我的接收大小为0,如果对方过了超时重传的时间还没收到确认的通知,会发一个探测报文比如携带PSH标志位催对方赶紧接收数据呀,来询问我们的接受能力是多少?因为探测报文是不站空间的,因此TCP协议可以进行处理,来通知发送方我最新的接受能力。
在这里插入图片描述

拥塞控制

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,
是很有可能引起雪上加霜的.
试想:如我我连续给对方发送100个数据,对方只会给我前两个的确认ACK,我是应该继续重传后98个还是认为是网络问题做一些解决措施呢?当人是后者!!!
因此接下来介绍TCP协议为了处理网络问题而设计的机制–>慢启动机制!

慢启动机制与拥塞窗口

TCP引入慢启动机制:先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
当我们发送的数据一大半都没有被确认接收,那么此时TCP默认触发慢启动机制,接下来的处理方法如下图:
在这里插入图片描述
此处引入一个概念程为拥塞窗口
1.发送开始的时候, 定义拥塞窗口大小为1;
2.每次收到一个ACK应答, 拥塞窗口加1(仅是举例实际是先指数级后线性增长);
3.每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口,也就是滑动窗口。

像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快.
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

在这里插入图片描述
结合上图可知:
1.慢启动是从1开始然后指数级增长,当增长到ssthresh(慢启动阈值)的初始值后便改成匀速增长,不断地试探最大的传输速度
2.当再次网络拥塞后,重新慢启动从1开始传输,此时慢启动阈值更新为上次发生网络拥塞时拥塞窗口的一半,这样周而复始不断地重复。

上述的传输手段我们称之为拥塞控制!
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.

TCP的三个窗口(1.滑动窗口 2.报文中的窗口 3.拥塞窗口)

延迟应答

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

一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率
那么所有的包都可以延迟应答么? 肯定也不是;
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms

在这里插入图片描述

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”; 那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端,提高通信效率。
在这里插入图片描述

TCP小结

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能
可靠性:
1.校验和
2.序列号(按序到达)
3.确认应答
4.超时重传
5.连接管理
6.流量控制
7.拥塞控制
提高性能:
1.滑动窗口
2.快速重传
3.延迟应答
4.捎带应答
其他:
定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

理解字节流

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

由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

接下来我总结一下我认为的字节流:
我们在用户层比如调用write函数将一批数据拷贝到缓冲区,我们可以理解为将一大盆水倒入缓冲区,之后我们就不用管了,OS会根据TCP协议来将我们的这一大盆水交给其他主机,那么是一口气将这一大盆水全送走呢,还是一杯一杯的将这盆水送走呢,我们作为用户完全不关心,我只要你给我发过去就行,那么OS会关心我们的数据内容吗,完全不关心!在他眼里就是一堆数字,他只会按照网络状态将这批数据交给接收方,那有没有可能,比如我发了一句话“我有一个亿,我骗你的”,OS因为不关心内容,先将“我有一个亿”发送到对方的缓冲区呢?
完全有可能!那会不会影响我们在用户层上读取数据时误读了呢?不会的!因为我们可以通过协议的规则将数据的内容从缓冲区完整的读上来的,当缓冲区只有“我有一个亿”时应用层调用read是没反应的,因为只有将完整的数据发如缓冲区OS才会允许我们读走!

粘包问题

首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
1.在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
2.站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
3.站在应用层的角度, 看到的只是一串连续的字节数据.
5.那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
1.对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
2.对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
3.对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);
思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?
1.对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
2.站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.
总结:解决粘包问题是我们应用层的工作所以此会有特定的类似http应用层协议,就是为了能够完整的读取数据避免发生粘包问题

TCP异常情况

进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ
断线之后, 也会定期尝试重新连接

基于TCP应用层协议

HTTP
HTTPS
SSH 远程登陆安全协议
Telnet 远程登陆、网络连接协议
FTP 文件传输协议
SMTP 邮件传输协议

TCP/UDP对比

TCP和UDP相当于两个极端,TCP偏向于传输稳定,UDP偏向于传输速率。
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定

用UDP实现可靠传输(经典面试题)

想到可靠性,就要想到TCP,想要让UDP具有可靠性,就要具有TCP相关的特性机制
例如:
1.引入序列号, 保证数据顺序
2.引入确认应答, 确保对端收到了数据;
3.引入超时重传, 如果隔一段时间没有应答, 就重发数据;

理解 listen 的第二个参数(第一个参数作为监听套接字,监听有没有新连接)

这个最后讲是为了有了前面的知识做铺垫才好理解,下面将listen的参数进行设置:
1.将linsten第二个参数设置为1
在这里插入图片描述
2.server执行listen后不accept
在这里插入图片描述
3.因为只有一台主机所以我复制了三个会话来访问server,结果如下
在这里插入图片描述
结论:
客户端状态正常, 但是服务器端出现了 SYN_RECV 状态, 而不是 ESTABLISHED 状态
这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:
1.半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
2.全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响.
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.
这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1

至于为什么是 listen 的第二个参数 + 1这个和底层的原因有关系,我们就不必要多考虑了。下面解释一下全连接的作用。

为什么要维护全连接队列?为什么这个队列不能太长?为什么不能没有?

下面以生活中的场景来解释全连接的:
门口的接待员就是listen第一个参数起到监听作用!
在这里插入图片描述

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值