Linux13 传输层UDP和TCP协议
传输层UDP和TCP协议
- 1. UDP 协议
- 1.1 UDP协议端格式
- 1.2 UDP特点
- 1.3 UDP 的缓冲区
- 1.4 UDP数据长度
- 1.5 基于 UDP 的应用层协议
- 2. TCP 协议
- TCP协议端格式
- 确认应答
- 序号和确认序号位
- 通信机制:
- 超时重传
- 连接管理
- 三次握手 - 建立连接
- 三次握手与TCPSocket
- 问题 - 为什么要有三次握手
- 四次挥手 - 断开连接
- 问题 - TIME_WAIT状态
- 流量控制
- 滑动窗口
- 概念
- 丢包情况
- 拥塞控制
- 概念
- 过程
- 慢启动
- 延迟应答
- 捎带应答
- 基于TCP应用层协议
- 粘包问题
- 描述
- 解决方法
- TCP、UDP对比
- UDP实现可靠传输
- 面向字节流
1. UDP 协议
1.1 UDP协议端格式
16位UDP长度,表示整个数据报(UDP首部 + UDP数据)的最大长度
如果校验和出错,直接丢弃
1.2 UDP特点
UDP 传输的过程类似于寄信
- 无连接:知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;(我只需要知道你的地址,就可以寄信给你)
- 不可靠:没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;(但我不能保证寄信的过程是否顺利)
- 面向数据报:不能够灵活的控制读写数据的次数和数量
- 应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并。
- 如果发送端调用一次 sendto, 发送 100 个字节, 那么接收端也必须调用对应的一次 recvfrom, 接收 100 个字节; 而不能循环调用 10 次 recvfrom, 每次接收 10 个字节;
1.3 UDP 的缓冲区
- UDP 没有真正意义上的 发送缓冲区. 调用 sendto 会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
- UDP 具有接收缓冲区. 但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致; 如果缓冲区满了, 再到达的 UDP 数据就会被丢弃
- UDP 的 socket 既能读, 也能写, 即全双工
1.4 UDP数据长度
- UDP 协议首部中有一个 16 位的最大长度. 也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部)
- 如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;
1.5 基于 UDP 的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议
- BOOTP: 启动协议(用于无盘设备启动) • DNS: 域名解析协议
2. TCP 协议
TCP协议端格式
- 源端口号(16 位):标识发送方应用程序的端口号。不同的应用程序在发送数据时会使用不同的端口号,通过源端口号,接收方可以知道数据是从哪个应用程序发送过来的。
- 目的端口号(16 位):标识接收方应用程序的端口号。接收方的操作系统会根据目的端口号将数据传递给相应的应用程序。
- 序列号(32 位):用于对发送的数据进行编号。在 TCP 连接中,每个字节的数据都有一个唯一的序列号。序列号的初始值是随机生成的,后续的数据序列号是上一个数据的最后一个字节的序列号加 1。通过序列号,接收方可以对收到的数据进行排序和确认。
- 确认号(32 位):是接收方期望收到的下一个字节的序列号。当接收方成功接收到数据后,会向发送方发送一个确认报文,其中的确认号就是接收方期望收到的下一个字节的序列号。发送方根据确认号可以知道接收方已经接收到了哪些数据,从而决定是否需要重传。
- 数据偏移(4 位):也称为首部长度,以 4 字节为单位,表示 TCP 头部的长度。由于 TCP 头部的长度是可变的(选项字段的长度可变),所以需要这个字段来确定数据的起始位置。最小的 TCP 头部长度是 20 字节(5 个 4 字节),如果有选项字段,头部长度会增加。
- 保留位(6 位):为了以后的扩展而设置,目前一般被设置为 0。
- 标志位(6 位):
- URG(紧急指针有效位):当该位为 1 时,表示紧急指针字段有效,说明当前数据中有紧急数据需要优先处理。
- ACK(确认应答位):当该位为 1 时,表示确认号字段有效,确认应答机制是 TCP 实现可靠传输的核心机制之一。
- PSH(推送位):当该位为 1 时,提示接收端应用程序立刻从 TCP 缓冲区把数据读走,而不是等到缓冲区满了再读取。
- RST(复位位):当该位为 1 时,表示对方要求重新建立连接,一般在连接出现错误时使用。
- SYN(同步位):在建立连接时使用,当该位为 1 时,表示请求建立连接,发送方会在建立连接的过程中发送带有 SYN 标志的报文。
- FIN(结束位):当该位为 1 时,表示通知对方,本端要关闭连接了,发送方会在关闭连接时发送带有 FIN 标志的报文。
- 窗口大小(16 位):表示接收端期望通过单次确认而收到的数据的大小,即接收窗口的大小。通过窗口大小,发送方可以控制发送数据的速度,避免发送方发送的数据过多导致接收方缓冲区溢出。
- 校验和(16 位):用于检验 TCP 头部和数据部分的完整性。发送方在发送数据时会计算出一个校验和,并将其包含在 TCP 头部中。接收方在接收到数据后,也会计算校验和,并与接收到的校验和进行比较,如果不一致,则说明数据在传输过程中出现了错误。
- 紧急指针(16 位):只有在 URG 标志位为 1 时才有效,指出紧急数据在数据包中的偏移量,即紧急数据相对于当前数据的起始位置的偏移量。
- 选项(可变长度):是可选的字段,用于协商一些额外的功能或参数,如最大报文段长度(MSS)、窗口扩大因子等。选项的长度是可变的,因此需要数据偏移字段来确定选项的结束位置和数据的起始位置。
- 数据:是 TCP 传输的实际数据内容,其长度是可变的,但要受到窗口大小和网络 MTU(最大传输单元)的限制。
确认应答
- 要收到了应答,就能保证我们的数据,对方一定收到了(保证可靠性)
- 不对应答做应答
序号和确认序号位
- 发送方在发送数据时,会为每一个字节的数据都进行编号,这个编号就是序列号(Sequence Number)。
- 接收方在成功接收到数据后,会向发送方返回一个确认应答报文,其中的确认号(Acknowledgment Number)是接收方期望收到的下一个字节的序列号。例如,发送方发送了序列号从 1 到 100 的数据,接收方成功接收后,返回的确认号就是 101,表示接收方期望下一次收到的是从序列号 101 开始的数据。
- 确认应答的报文ACK标志位为1
通信机制:
- 如果客户端发出1000,2000,3000,4000
- 收到一个应答序号为3001,意思是3000之前的全部收到
- 在网络传输过程中,由于不同的数据可能会经过不同的网络路径,导致到达接收方的顺序可能与发送方发送的顺序不同,即出现 “后发先至” 的情况。通过序列号和确认应答机制,接收方可以根据序列号对收到的数据进行排序,确保数据的正确顺序。即使数据乱序到达,接收方也可以根据序列号将其重新排序后再交给应用程序处理。
超时重传
- 发送方发送数据后,会启动一个定时器等待接收方的确认应答。如果在定时器超时之前没有收到确认应答,发送方就会认为数据丢失或确认应答丢失,从而触发超时重传机制,重新发送该数据。
- 随着重传次数的增加,超时时间会逐渐延长。例如在 Linux 系统中,超时时间以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。如果重发一次之后仍然得不到应答,等待时间会变为 2 * 500ms 后再进行重传;如果还是得不到应答,就等待 4 * 500ms 进行重传,依次类推,以指数形式递增
连接管理
三次握手 - 建立连接
- 第一次握手 - 客户端向服务器发送一个带有 SYN 标志位为 1 的报文,序列号假设为 x ,此时 ACK 标志位为 0,因为这是一个连接请求,还没有涉及确认应答。
- 第二次握手 - 服务器收到客户端的 SYN 请求后,会发送一个带有 SYN 和 ACK 标志位都为 1 的报文。这个报文的 SYN 标志位为 1 是为了同步服务器自己的序列号(假设为 y,随机生成),ACK 标志位为 1 是为了确认收到客户端的连接请求,同时告知客户端下一个期望收到的序列号(确认号为 x + 1)。
- 第三次握手 - 客户端收到服务器的 SYN - ACK 报文后,再发送一个 ACK 标志位为 1 的报文进行确认,确认号为 y + 1,此时连接正式建立,之后的数据传输阶段就按照正常的 ACK 置 1 的方式进行确认应答。
三次握手建立连接本质就是在赌最后客户端发给服务器的ACK一定收到
- 如果最后一个包丢了怎么办?
- 客户端在后续发送数据时,会收到来自服务器的RST(reset)标志位置一的报文
- 表示服务器让客户端把原来的链接释放掉,重新三次握手(链接重置)
三次握手与TCPSocket
- 根据之前的TCP套接字代码,客户端connect只是发起了三次握手
- 三次握手完成,服务器accept就会返回文件描述符 —— accept不参与三次握手的过程,仅是等待三次握手完成
- 三次握手是双方TCP协议层自主完成的
问题 - 为什么要有三次握手
- 验证全双工,验证网络连通性,用最小的次数验证自己能发能收
- 建立双方通信工时意愿,服务器发送了ACK时也发送了SYN(类似四次挥手)
- 协商双方的接受能力(与下面的流量控制有关)
四次挥手 - 断开连接
发起四次挥手的有可能是客户端,也有可能是服务器,以下描述仅针对客户端主动发起四次挥手。
- 第一次挥手 - 客户端发送一个 TCP 首部中 FIN(Finish)标志位被置为 1 的报文,用来关闭本端到对端的数据传送。发送完后,**客户端进入 FIN_WAIT_1(终止等待 1)**状态,表示在等待对方的确认应答。
- 第二次挥手 - 服务器收到 FIN 报文后,就向客户端发送一个 ACK 应答报文,确认序号为收到的序号加 1。此时服务器进入 CLOSE_WAIT(关闭等待)状态。在这个状态下,服务器会继续处理剩余的数据,准备关闭连接。而客户端收到 ACK 报文后,进入 FIN_WAIT_2(终止等待 2)状态,继续等待服务器的 FIN 报文。
- 第三次挥手 - 当服务器处理完所有数据后,就会向客户端发送一个 FIN 报文,用来关闭服务器到客户端的数据传送。发送完后,服务器进入 LAST_ACK(最后确认)状态,等待客户端的最后确认。
- 第四次挥手 - 客户端收到服务器的 FIN 报文后,回一个 ACK 应答报文,并将确认序号设置为收到的序号加 1。之后客户端进入 TIME_WAIT(时间等待)状态。经过 2 倍的最大报文段生存时间(2MSL)后,客户端自动进入 CLOSED状态,至此客户端完成连接的关闭。当服务器收到客户端的 ACK 报文后,也会进入 CLOSED状态,服务器方完成连接的关闭。
总结:
- 客户端向服务器发送完数据了,跟服务器打个招呼
- 服务器确认收到,但不一定服务器也发完数据了,所以要等服务器把数据发完
- 服务器也发完数据了,通知一下客户端
- 客户端确认收到
因此,四次挥手用最小的通信成本,建立了断开连接的共识(双方都不和对方通信了,且双方都知道对端不和自己通信了)
四次挥手也是由双方操作系统自主完成(由close(fd))触发
问题 - TIME_WAIT状态
由四次挥手的过程可知:客户端主动断开连接,最后会进入先TIME_WAIT状态,这个状态为什么会等待2MSL?
- 这是TCP报文最大生成时间
- 必须等待此报文过了生存期自主消散,防止过期报文再次被收到
- 保证客户端发送的最后一个ACK报文段能够到达服务端。
- 这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
流量控制
- 客户端根据服务器发送确认应答报文的16位窗口大小控制流量(服务器也根据客户端发送的确认应答报文的16位窗口大小控制流量)。
- 三次握手时,双方已经交换过了双方的接受能力;根据对方的接受能力,控制自身的发送数据的流量。
- 一个主机没有接受能力,另一个主机会进行窗口探测
滑动窗口
- 因为TCP有确认应答机制,对于每一个发送端的数据段,都要给一个ACK确认应答,收到ACK之后在发送下一个数据段,这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。
- 如果能一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
概念
- 滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,假设4000,每1000为一个段
- 那么发送前四个段的时候, 不需要等待任何 ACK, 直接发送
- 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推
- 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉
- 窗口越大, 则网络的吞吐率就越高
丢包情况
- 数据包抵达了,对端发来的ACK丢了
- 部分ACK丢了没关系,通过后续ACK进行确认
- 数据包丢了
- 发0 - 4000,如果只有1000 - 2000报文段丢了,发送端会一直收到1001这样的ACK,当主机连续收到三次同样的ACK,就会进行重新发送。(快重传机制)
- 接收端成功接收后,再次返回的就是4001,因为之前发送时这些已经被放到了接收端操作系统内核的接受缓冲区中
拥塞控制
概念
- 虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题。
- 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.
- TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
过程
- 发送开始的时候, 定义拥塞窗口大小为 1;
- 每次收到一个 ACK 应答, 拥塞窗口加 1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
- 即 滑动窗口大小 = min(拥塞窗口大小,应答窗口大小)
慢启动
- 设置一个叫值做慢启动的阈值
- 当拥塞窗口低于这个阈值的时候,按照指数方式增长
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小
假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率
每隔N个包(2)应答一次,每隔200ms应答一次
捎带应答
捎带应答(Piggyback Acknowledgment)是 TCP 协议中为了提高传输效率而采用的一种机制。在正常的 TCP 通信中,接收方需要对发送方发送的数据进行确认,这个确认过程如果单独进行会产生额外的开销。捎带应答机制允许接收方在向发送方发送数据时,将确认应答信息一起发送,从而节省网络资源和传输时间。
- 数据和确认的合并:假设客户端发送的数据序列号为 1 - 100,服务器收到后,在准备发送自己的数据(如服务器查询数据库后的结果)时,在 TCP 报文头部的确认号字段填入 101(表示已收到序列号 1 - 100 的数据,期望下一个收到的数据从序列号 101 开始),同时在报文的数据部分放入要发送给客户端的内容,如查询结果等信息。这样,一个 TCP 报文既包含了服务器发送的数据,又包含了对客户端数据的确认应答,实现了 “捎带” 的功能。
- 总结:ACK和数据一起发过来
基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
粘包问题
描述
- 粘包问题中的 “包” , 是指的应用层的数据包
- 在 TCP 的协议头中, 没有如同 UDP 一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
- 站在传输层的角度, TCP 是一个一个报文过来的. 按照序号排好序放在缓冲区中
- 站在应用层的角度, 看到的只是一串连续的字节数据
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包
- 对于 UDP, 如果还没有上层交付数据, UDP 的报文长度仍然在. 同时, UDP 是一个一个把数据交付给应用层. 就有很明确的数据边界
- 站在应用层的站在应用层的角度, 使用 UDP 的时候, 要么收到完整的 UDP 报文, 要么不收. 不会出现"半个"的情况
解决方法
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的 Request 结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)
TCP、UDP对比
- TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
- UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传输等. 另外 UDP 可以用于广播;
UDP实现可靠传输
- 引入序列号, 保证数据顺序;
- 引入确认应答, 确保对端收到了数据;
- 引入超时重传, 如果隔一段时间没有应答, 就重发数据
…根据TCP的一些机制引入UDP
面向字节流
- 创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区
- 调用 write(send) 时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区里数据多起来, 或者其他合适的时机发送出去
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用 read(recv) 从接收缓冲区拿数据;
- 另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
- 由于缓冲区的存在, TCP 程序的读和写不需要一一匹配
- 写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节;
- 读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次;