传输层(TCP与UDP)

Posted 楠c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了传输层(TCP与UDP)相关的知识,希望对你有一定的参考价值。

UDP协议

特点

  • 无链接,知道对端ip和端口号直接进行传输,不需要建立链接
  • 不可靠,假如因为某种原因发送失败也不会返回应用层任何信息
  • 面向数据报,有明显的分界,不能灵活控制读写数据的次数和数量

面向数据报:应用层交给UDP的思数据,UDP原样发送,既不拆分也不合并。
假设发送端,sendto100个字节的数据,那么接收端也必须调用recvfrom一次接收100个字节,而不是循环接收10次一次10个字节

协议格式

  • 16位UDP总长度,表示整个数据报的最大长度
  • 如果校验和出错,就会直接丢弃。

怎么将报头,有效载荷分离?
定长报头,先读走8个字节。剩下16位长度减8,就是数据。
交给哪个协议
目的端口号决定

UDP的缓冲区

  • UDP没有发送缓冲区,因为他要做的事很简单,加个报头调用sendto直接交给内核,由内核将数据传给网络层协议进行后续传输动作。
  • UDP具有接收缓冲区,但是接收缓冲区不能保证收到的UDP报文与发送时的顺序。加入缓冲区满再来的udp就会被丢弃。
  • UDP是全双工,socket即能读也能写。

注意

由于UDP的16位最大长度,也就是说UDP共计(首部+数据)一共64k。比较小,假如传输的数据超过64k
就需要我们自己在应用层,发送端手动的分包,接收端拼装。

基于UDP的应用层协议

NFS:网络文件系统
TFTP:简单文件传输
DHCP:动态主机配置
BOOTP:启动协议
DNS:域名解析

TCP协议

特点

  • 有链接
  • 可靠
  • 面向字节流

协议格式


  • 源/目的端口:从哪个进程来,到哪个进程去
  • 4位首部长度:表示该TCP报头有多少个4字节。可表示的最大十进制值是15. 所以当IP首部长度为1111(就是十进制15),首部长度就达到最大值 60字节.由于标准长度至少为20字节,所以首部长度最小值为5,表示5个四字节
  • 选项:最大为 40字节,60-20(标准长度)。
  • 保留:用作后面的扩用
  • 校验和:发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分
  • 紧急指针:和URG标志位配合

和UDP相比没有数据总长度,只有首部长度,是因为他不需要总长。

怎么将报头,有效载荷分离?
首先无脑读20字节。分析4位首部长度。20字节说明剩下都是数据,比20字节大那就在读取x-20。
向上交付
目的端口号

32位序号

发送端------>接收端
一个个报文发出去,每个数据的路径不同,就算为了提高效率一起发送一批报文也不能保证按序到达
那么发送时给每个数据安排上序号,到达对端进行排序。保证按序到达

确认序号

接收端----->发送端

连续发送了3个报文,我就要给你3个应答,假如3个应答都丢了呢?
假如3个报文序号为6,7,8。(32位序号)。应答也有编号叫确认序号,分别为7,8,9.(32位确认序号)。假如我收到了确认序号7,代表之前的6我已收到。假如我收到了确认序号9,我认为序号为6,7,8的报文我都收到了。也就是说TCP允许少量丢包。

  1. 为啥要有32位序号和确认序号?
    1.按序到达(乱序)
    2.确认应答(丢包)

  2. 那么为啥要两组序号呢。我向你发的时候这个字段代表序号。你给我回应的时候这个字段代表确认序号不行吗?
    其实很简单不行的,因为TCP全双工,我向你发的时候,你也有可能向我发,显然双方也会互相回应。

他们两个与确认应答机制相关。

16位窗口大小

TCP既有接收缓冲区,又有发送缓冲区。而16位窗口大小是接收缓冲区剩余的大小。谁发这个报文,谁填写 即告诉你我的接收能力,让你心里有数。
与流量控制相关。

标志位

1表示被设置
SYN:建立链接请求
FIN:断开链接报文
ACK:确认报文
PSH:发送方发数据的时候设置标志位,催促接收方尽快处理交付数据。
RST:点击跳转
URG:点击跳转

RST标志位

跳转到的地方

前两个丢了没事,但最后一个丢了有个问题。因为客户端觉得自己发出去了,而只要发出去了,客户端就会认为自己已经建立成功了(因为服务器不会再回应ack)。

但是服务器没有收到。假如客户端直接开始发数据,服务器傻眼了,建立链接才进行了两次握手,你就要和我传数据。服务器会觉得任务客户端出现异常,然后就会向他发送RST标志位,使客户端重新准备三次握手。

URG标志位

TCP要保证按序到达,就像我们排队,先来就在前面。但有特殊情况的同学就需要插队,对应这个URG标志位,与16位紧急指针配合使用。即优先读取数据中对应偏移量。一个指针所以一次只能读取一个字节。倒也合理因为插队又不能符合常理。
对应recv系统调用的最后一个参数

确认应答机制

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

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

超时重传机制

我发数据,你给个回应。我收到了回应,就知道我刚才发的数据你收到了。
互联网没有绝对的可靠,因为永远有最新一条信息发送出去,那就需要一直给予回应。但可以保证相对可靠,确认保证在这之前的消息他都收到了。回应也是数据假如“回应”没有被收到怎么办,不需要对ACK在ACK。

我发出数据,不管是数据丢了,还是你收到数据,然后你给我的回应丢了。
只要主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发。


假如两个都没丢,只是在网络传输的时间稍微长了一点,超出了这个时间间隔,所以可能会接收到大量重复数据。那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。

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

链接管理


从左往右状态:

三次握手

SYS_SENT:同步发送
SYS_RECV:同步接收
ESTABUSHED:建立成功,当第二次握手完成,客户端发出ACK,就处于建立成功状态。
ESTABUSHED:建立成功,当服务器接收到ACK

四次挥手
FIN_WAIT_1:
CLOSE_WAIT:
FIN_WAIT_2:

LAST_ACK
TIME_WAIT
CLOSED(服务器)
CLOSED(客户端)

CLOSE_WAIT

当我们应用层代码中,close服务器文件描述符,服务器会处于LAST_ACK状态,发送FIN到客户端。但假如我们忘记close,那么就不会处于LAST_ACK状态,也就是会一直处于上一个CLOSE_WAIT状态,没有变化。

TIME_WAIT

假如没有这个状态,客户端发一个ACK,从此自己进入CLOSED状态。但是万一对刚才发过来的FIN的ACK丢失了呢。服务器还在傻傻的等着你,服务器会以为自己的FIN没有发过去,进行重传,可是你已经关闭了链接。浪费服务器资源。
协议要求主动关闭的一方处于TIME_WAIT状态,这里就是客户端,会发送一个ACK向服务器。假如ACK丢失,服务器以为自己的FIN没有发过去,进行重发FIN,这下可以,因为客户端还没有关闭连接,可以收到了,同时重发ACK。服务器CLOSED,客户端CLOSED。

总结:

  1. 尽量保证最后一个ACK被对方收到,让服务器尽快进入CLOSED。
  2. 等待网络中的历史数据进行消散。
    MSL:单向最大传送时间。
    TIME_WAIT的时间:一个来回2MSL。

由于某些场景,服务器主动关闭连接,这个时候服务器就会有很多链接进入TIME_WAIT状态,此时有很多新连接过来,虽然调用了close接口,但你又处于TIME_WAIT状态没有完全退出。新连接想用你的端口,被占用,就会出现问题。这是就需要一个接口设置端口可复用。

使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
监听套接字,传输层,可复用,opt为1开启这个功能,opt大小

滑动窗口

刚才虽然一次性也发了一批数据,但是经过回复ACK以后才能发送下一批,是串行的。我们一次发送多批数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)而一次发多批,当然也要回复多批ACK,同样只要最后一次ACK收到,假如之前的ACK丢了,我们也能容忍少量丢包。
窗口大小指的是暂时无需等待确认以前的数据,我先发出去再说。滑动窗口大小就是发数据的最大值。
滑动窗口是发送缓冲区的一部分,窗口左边的数据是已经发出去,而且已经收到ACK,窗口内的数据是已经发送,但未收到ACK。之后是尚未发送的数据。窗口内的数据收到ACK,向右移动。就像前面说的,收到4001的ACK时,代表之前都已经收到,直接全向右移动。

1.收到数据,但ACK丢了。
不影响,因为他后续有ACK。
2.数据丢了
ACK丢了,我们可以容忍少量。但数据呢,数据丢了,由于没有收到对应数据的ACK那么会超时重传,但其实还有一种办法我们就会对他的上一批重复3次ACK。发送端发现不对劲,就会重传,这叫做快重传,快重传一般是解决大量数据的时候,一般与超时重传配合使用。 但当滑动窗口不足3位,都没有发三个报文,肯定都支持不了3次的重复ACK呢,所以说超时重传是一种兜底策略。
滑动窗口的大小=min(拥塞窗口,接收窗口)
拥塞窗口:发送不会引起网络拥塞的最大数据
接收窗口:对端的接受能力

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)。报头的16位窗口大小。
假如接收端自己的缓冲区快满了,发送一个报文填着自己的缓冲区大小,告诉发送端发慢点。
假如接收端接收缓冲区已经满了,将窗口设置为0,告诉发送端,别发了。但是会有一个探测报文,隔一小会告诉发送端目前的窗口大小。

拥塞控制

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据。

此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1

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

延迟应答

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

那么所有的包都可以延迟应答么? 肯定也不是;
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms

捎带应答

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

三次握手与四次挥手

三次握手

一次握手行吗 发一次SYN就认为双方建立链接,那和UDP有什么区别。
两次握手行吗 发一次SYN客户端建立链接,发SYN+ACK服务器建立链接。和第一种也无太大差别。
建立链接是有成本的,这个成本体现在空间和时间的花费,这两种方式会引起一种SYN泛洪攻击,你发送一个SYN,我就回应一个SYN+ACK认为链接建立成功的话(一次握手不回应直接建立)。假如此时有大量链接请求涌入就会不断吃掉服务器资源。
三次握手建立链接 1.验证全双工通信信道。客户端与服务器至少各收发一次。
2.第一个SYN,第二个报文SYN+ACK丢失。不用怕,因为并没有任意一方建立链接。第三个丢失,但客户端已经发出去了,客户端认为我的信道已经建立。因为第三个丢了么,所以服务器不认为自己的信道已经建立
四次握手建立链接 四次握手的场景下,前三个报文丢失不用怕,因为并没有任意一方建立链接 。第 四个报文丢失,但服务器发出去了,服务器认为我的信道已经建立。发出最后一个链接的一方,一经发送就认为自己的信道已经建立,四次会导致服务器建立信道,浪费服务器的资源,服务器的资源很宝贵。既然无法避免,那我们尽量让客户端来承受,因为没有那么多人连接客户端,你的资源浪费就浪费了。

基于上述,奇数次连接会使客户端发出最后一次报文,而这一次报文丢失不会影响服务器,除了1那么3次就是最小的奇数次连接。

四次挥手

全双工,双方都需要关闭链接。需要注意,调用close接口后,指的是应用层不在发送数据,底层还要进行四次挥手所需要的含有FIN与ACK的报文。CLOSED状态才是真正结束,释放数据结构。

TCP总结

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

  • 可靠性:
    校验和
    序列号(按序到达)
    确认应答
    超时重发
    连接管理
    流量控制
    拥塞控制
  • 效率:
    滑动窗口
    快速重传
    延迟应答
    捎带应答
    其他:
    定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等

常见问题

理解面向字节流

写100个字节数据,可以调用write写100,也可以调用100次write。
读的时候,也像写的时候一样。

粘包问题

udp面向数据报,应用层发了多少,就必须一次读取多少。而且由于报文中的总长度是定长的所以他明确了边界。不存在粘包问题。
TCP面向字节流,应用层发送多少,它可以自由读取。
由于TCP的随心所欲,他会面临一个问题,怎么明确边界?
传输层是知道TCP一个个报文过来的,因为有序号能区分。但是站在应用层只能看到一个个字节流,读的时候随意读,所以会存在粘包问题。
但是在HTTP,由于header里存在content-lenth,所以应用层明确了两个包之间的边界,这个协议细节可以有程序员自己定制。

TCP异常情况

进程终止: 进程终止会释放文件描述符(但还没有进入CLOSED状态), 仍然可以发送FIN,然后经过四次挥手进入CLOSED状态,所以进程终止和正常关闭没有什么区别。
机器重启:和进程终止类似,重启也是先终止进程。
机器断电/网线断开:因为是突发情况,所以并没有告诉对方,他没有时间去进行四次挥手,对端认为链接还在。他虽然立即反应不过来,假如有数据过来,对端此时发现链接没了,reset重新建立链接。即使没数据,TCP也有个保活定时器,定期监测对方,假如链接失效,自己也相应释放连接。

Listen的第二个参数

因为内部资源不够,所以要排队,但不宜太长,会让顾客等的不耐烦。不宜太短,处理完任务可能会空出来位子没人填上去。这就是他的第二个参数,全链接队列的长度,长度为参数+1。全连接队列用来保存处于established状态,且应用层没有调用accept取走的链接。还有一个半连接队列用来保存SYN_SENT和SYN_RECV状态的链接。
就像之前HTTP博客的图片,内核中有这两个链接的数据结构。但是为啥全连接队列的长度为参数+1呢。本质是因为他底层的判定逻辑

分段与分片

MSS:最大传输段 (TCP告诉网络层(IP)的)
MTU:最大传输单元 (数据链路层告诉IP层的)

当TCP的报文大于MSS(MSS位于报文中的可选项)就会分段,分出多个TCP报文。
当IP层的报文大于MTU就会分片。

有了IP层分片,为啥TCP还要分段?
假如不分段的话在IP层需要分片,但是TCP不知道你分片了,假如某个分片之后的数据丢了,则需要重传分片之前的报文,效率很低。在TCP进行分段之后,尽量的在IP层不要进行分片,这样即使丢失了,重传对应分段之后的数据就可以。

UDP没办法,自己没能力分段,超过MTU的话只能靠IP层分片。

以上是关于传输层(TCP与UDP)的主要内容,如果未能解决你的问题,请参考以下文章

TCP 与 UDP 协议简介

王道考研 计算机网络19 传输层 传输层的寻址与端口 TCP UDP

TCP与UDP的区别

传输层(TCP与UDP)

传输层(TCP与UDP)

传输层协议之TCP/UDP