详解TCP协议

TCP协议段格式

详解TCP协议_第1张图片

  • 序号和确认序号:在真实服务器和客服端通信过程中请求是并行执行的,这会导致到达是乱序的,所以才会有序号这个东西,确认序号是对方应答时返回的,例如序号发送到1,确认序号会返回2,表示2之前的都收到了(是连续的都收到了!)那为什么得专门分成序号和确认序号呢?因为是全双工的,client的序号和server的确认序号是一组,client的确认序号和server的序号是一组
  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题。此处的检验和不光包含TCP首部, 也包含TCP数据部分
  • 16位接收窗口:在真实通信中因为是并行的,对方的接收缓冲区可能被打满,我们需要得知对方的剩余接收缓冲区,这样就可以在全双工通信中实现流量控制
  • 6位标志位:

URG(带外数据): 紧急指针是否有效,配合紧急数据指针来使用,起始位置(有效载荷)+16位紧急数据指针(偏移量),后面的1个字节为紧急任务,recv设置flag为MSG_OOB,send也把flag设置为MSG_OOB这样就可以发送和接收带外数据
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段,通信过程中,连接单方面出现问题,例如服务器挂了,但是client还认为连接存在,我们在请求时server把RST位设置为1,告诉对方需要重新建立连接
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

1.如何封装解包

TCP有标准长度,先读取前20字节,因为是一个结构化的数据,所以可以提取4bit首都长度,这个4bit表示tcp报头总长度,所以取值范围是[0000,1111]→[0,15]但是这不对啊,tcp报头最少20字节,所以默认要*4,[0,60],因为最少是20,所以是[20,60],这样就可以得到后续选项的长度,报头处理完毕以后,最后剩下的就是有效载荷了。

2.如何分用?

根据目的端口号,就可以找到应用层进程

细心的读者会发现,UDP的长度是包括有效载荷的长度,而TCP并没有,这也是为什么TCP是面向字节流的原因

3.收到一个报文,是如何找到曾经bind特定port的进程的?

利用哈希可以快速定位

确认应答(ACK)机制

详解TCP协议_第2张图片

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

超时重传机制

情况一:

详解TCP协议_第3张图片

情况二:

详解TCP协议_第4张图片

因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果

重点:发送端,发出的数据必须要被维持一段时间,收到应答之后才可以删除!

超时时间规则如下:

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

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

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

连接管理机制

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

三次握手:

详解TCP协议_第5张图片

学了上面的东西,我们在重新理解一下这个过程,这里的SYN和ACK的设置就是更改报头的ACK和SYN标志位

四次挥手:

详解TCP协议_第6张图片

主动断开连接的一方,最终状态时TIME_WAIT,四次挥手动作已经完成,但是主动断开连接的一方要维持一段时间(2*MSL)的TIME_WAIT,为什么要维持这个状态?1.保证最后一个ACK尽可能被收到 2.双方在断开连接时,网络中还有滞留的报文——保证滞留报文消散

被动断开连接的一方,两次挥手完成是CLOSE_WAIT状态,可以通过不close服务端显示出这种情况

1.学了以上知识我们就可以解释为什么服务器会bind失败?

答:服务器是主动断开连接的一方,它会处于TIME_WAIT状态,而处于这个状态时,该端口和连接依旧被占用,所以无法bind

2.解决bind失败的方法

在这里插入图片描述

流量控制

前文我们知道TCP协议段格式中有一条叫做窗口大小,这个就是负责流量控制的,以免发送速度过慢或者过快。

1.我们如何第一次就知道对方缓冲区的大小的呢?

在通信之前,我们就做过了三次握手,交换窗口大小
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端

2.接收端如何把窗口大小告诉发送端呢?

回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;

滑动窗口

详解TCP协议_第7张图片

滑动窗口的大小是变化的!滑动窗口大小=min(对方的接收缓冲区,拥塞窗口),可能变大也可能变小

1.数据没丢,只是应答丢失了

详解TCP协议_第8张图片

2.数据真的丢了

如上图,那么就是应答错误的那个序号,滑动窗口不会向后滑动,连续收到3个包括3个就会触发重传机制

以上我们讨论的都是端到端的情况,也就是说我们讨论的都是双方的问题,没有考虑到网络的问题,所以就会有拥塞控制

拥塞控制

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据。 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题。因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的

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

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

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

详解TCP协议_第9张图片

  • 当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, 每次写一个字节; 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

TCP异常情况

  • 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
  • 机器重启: 和进程终止的情况相同.
  • 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释

TCP总结

可靠性:

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

提高性能:

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

其他:

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

基于TCP应用层协议

HTTP
HTTPS
SSH
Telnet
FTP
SMTP

学完以上知识,请问如果想用UDP实现可靠传输该怎么做呢?

1.引入序列号,保证数据顺序 2.引入确认应答 3.引入超时重传 等

listen第二个参数

listen(int socket, int backlog);

第二个参数backlog代表等待队列的最大长度

详解TCP协议_第10张图片

客户端:

当connect到来的时候无论等待队列有没有空余地方在客户端眼里都是连接成功的,因为一开始调用connect函数SYN请求,服务端立刻给予客户端SYN+ACK确认,客户端连接状态从SYN_SENT转换到ESTABLISHED之后再向服务端发ACK确认。也就是所谓的三次握手过程

服务端:

当客户端connect来到时,服务端进入SYN_RCVD状态并给予SYN+ACK响应。当下次客户端完成三次握手,收到客户端的ACK时:如果队列中有空间,则服务端的连接也建立成功,否则服务端眼里没有成功。每建立成功一次,要往队列中放入刚才建立好的连接,也就是队列空间-1,服务端状态从SYN_RCVD变为ESTABLISHED,如果不成功还是原来的SYN_RCVD状态。当服务端调用accept成功时,又是队列空间+1,从队列中拿走一个连接。

Linux内核协议栈为一个tcp连接管理使用两个队列:

  1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
  2. 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)

而全连接队列的长度会受到 listen 第二个参数的影响。全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了。这个队列的长度是 listen 的第二个参数 + 1。

测试:

把listen的第二个参数设置为1,并且把accept函数屏蔽掉,不让执行下去。这样保证迟早队列中会没有地方,导致有的连接不成功。

详解TCP协议_第11张图片

一共开了4个客户端,但从上图中可以看到只有两个服务端是成功的,另外两个还在上一个状态中,但是所有的客户端都是成功的。所以说明最大队列个数是backlog+1

你可能感兴趣的:(tcp/ip,网络,网络协议)