传输层

本文深入解析TCP/IP协议,涵盖传输层核心功能,包括端到端数据传输机制,端口号的作用及划分,TCP与UDP协议特性对比,三次握手与四次挥手过程,以及TCP的可靠传输、流量控制和拥塞控制机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

传输层主要负责数据从发送端传输到接收端,即端到端的传输

 那么,要准确的到达,就需要双方的端口号,端口号标识了一个主机进行通信的不同的应用程序。在TCP/IP协议中,用“源IP”、“源端口号”、“目的IP“、”目的端口号“、”协议号“这样一个五元组来标识一个通信。

端口号范围划分
  • 0-1023 知名端口号,HTTP,FTP,SSH等,且他们的端口号都是固定的。
  • 1024-65535 操作系统动态分配的端口号,客户端程序的端口号,有操作系统分配。

 知名端口号:

  • ssh服务器 22
  • ftp服务器 21
  • telnet服务器 23
  • http服务器 80
  • https服务器 443

UDP

UDP协议端格式

16位源端口号16位目的端口号
16位UDP长度16位UDP检验和
数据(如果有)数据(如果有)
  • 16位源/目的端口号 :实现端与端的数据传输
  • 16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的长度;
  • 如果校验和出错, 就会直接丢弃

UDP特点

 无连接,不可靠,面向数据报

  • 无连接:直到对端的IP和端口号就直接进行传输,不需要建立连接
  • 不可靠:没有确认机制,没有重传机制,如果因为网络故障该段无法发送到对方,UDP协议层也不会给应用层返回任何错误信息
  • 面向数据报:不能够灵活的控制读写数据的次数和数量

 在面向数据报中,应用层交给UDP多长的报文,UDP原样发送,不会拆分不会合并,并整条交付给传输层。

UDP的缓冲区

  • UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
  • UDP具有接受缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序与发送的顺序一致,且如果缓冲区满了,再到达的UDP数据就会被丢失

基于UDP的应用层协议

  • DHCP: 动态主机配置协议
  • DNS: 域名解析协议

  

TCP

 TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制;
 所以,TCP的特性就是-面向连接,可靠传输,面向字节流

TCP协议段格式

  • 源/目的端口号:表示数据从哪个进城来,到那个进程去;
  • 32位序号/32位确认序号:保证TCP数据的有序交付(包序管理)
  • 4位TCP报头长度,表示该TCP头部由多少个32位bit,TCP报头的范围20~60个字节
  • 6位标志位
     URG: 紧急指针是否有效
     ACK: 确认是否有效
     PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
     RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
     SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
     FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
  • 16位窗口大小:用于实现滑动窗口机制
  • 16位检验和:发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也 包含TCP数据部分.
  • 16位紧急指针: 标识哪部分数据是紧急数据
  • 40字节头部选项: 用到的时候才会有,也就意味着TCP的报头长度不固定

面向连接

连接管理机制

建立连接协议(三次握手)

 在三次握手前,Client 创建套接字 – 绑定地址信息。Server 创建套接字 – 绑定地址信息 – 开始监听 – 有新的连接到来新建套接字。
在这里插入图片描述

  1. 第一次握手:客户端发送syn包(syn=x)的数据包到服务器,并进入SYN_SEND状态,等待服务器确认
  2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP连接都将被一直保持下去。

连接终止协议(四次挥手)

由于TCP连接是全双工的,因此每个方向都必须进行单独关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据,首先进行关闭的一方将执行主动关闭,而另一方执行被动关系。

  1. 第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可以接受数据
  2. 第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号, SYN 和 FIN 都有seq序号
  3. 第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了
  4. 第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。
状态装换图

状态说明
CLOSED: 这个表示初始状态。

LISTEN(服务器): 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。

SYN_RCVD(服务器): 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。

SYN_SENT: 这个状态与SYN_RCVD呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。

ESTABLISHED:这个容易理解了,表示连接已经建立了。

FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。

FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。

TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

注:MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值.RFC 1122建议是2分钟,但BSD传统实现采用了30秒.TIME_WAIT 状态最大保持时间是2 * MSL,也就是1-4分钟

结论:在TIME_WAIT下等待2MSL,只是为了尽最大努力保证四次握手正常关闭。确保老的报文段在网络中消失,不会影响新建立的连接.

CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。

LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。

为什么握手是三次,挥手是四次,为什么不是两次握手

 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

 3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
 现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在 S的应答分组在传输中被丢失的情况下,将不知道S是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的数据分组超时后,重复发送同样的数据分组。这样就形成了死锁。
就好比打电话, A打给B, A要告诉B ,我打给你了, B要回应给A听,OK,连接成功了;然后B也要得到A的确认,才能开始正式通话

三次握手失败

 tcp建立连接三次握手,主动方发送请求syn,server接收到信息,返回带有数据包的信息syn_sent,然后接收到信息的一方再发送确认信息ACK给server,第三次握手失败(超时)时,服务器并不会重传ack报文,server会发送RTS报文段并主动关闭至closed,以防止syn洪泛攻击。

syn洪泛攻击
通俗的理解是:当第三次握手没有发送确认信息时,等待一段时间后,主机就会断开之前的半开连接并回收资源,这为dos(deny of service)攻击埋下隐患,当主动方主动发送大量的syn数据包,但并不做出第三次握手响应,server就会为这些syn包分配资源(但并未使用),就会使server占用大量内存,使server连接环境耗尽,这就是syn洪泛攻击

TCP连接管理中的保活机制

 通信双方长时间没有数据来往(7200s),每隔一段时间(75s)则会给对方发送一个保活探测数据包,要求对方进行回复,若是收到回复,则认为连接正常,若是连续多次(9次)请求都没有回复,则认为连接断开。
 连接断开的体现:

  1. recv读完所有的数据之后,不在阻塞,返回0
  2. send会触发异常,进程退出(默认处理方式-SIGPIPE)

可靠传输

确认应答机制

 TCP将每个字节的数据都进行了编号,即为序列号,每一个ACK都带有对用的确认序列号,意思是告诉发送者,我们已经收到了那些数据,下一次你从哪里开始发。

 在TCP连接成功后,发送的每一条数据都可能会丢失,因此需要确认应答,以保证数据的完整性,同时也可以知道收到的是拿一条数据。

滑动窗口机制

 通信双方,在通信时会通过协议字段中的窗口字段协商窗口的大小,告诉对方一次可以发送的最大数据量,这些数据也不是一条就发送完,通信双方在三次握手阶段,还会协商一个数据 – MSS(最大数据段大小 – 数据报中数据的最大长度),发送方在发送数据时会将窗口大小的数据分成大小不大于MSS的数据报进行发送。

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

 出现了丢包

  • 情况一
    数据包已经到达,ACK丢失,这种情况下,部分ACK丢了不要紧,因为可以通过后续的ACK进行确认

  • 情况二(如上图的1001~2000数据段丢失)
    数据包直接丢了。
    当1001~2000段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是再提醒发送端”我想要的是 1001“ 一样。
    如果发送端主机连续三次收到同一个 “1001” 这样的应答们就会将对应数据1001~2000 重新发送
    这个时候接收端收到 1001 之后,再次返回的ACK就是最近最后一次接收到的数据段末尾数值+1,这个数值会被放到接收缓冲区中。

流量控制机制

 接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

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

 接收端如何将窗口大小告诉发送端呢?
 TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;但大小不仅仅是16位,即65535个字节大小,实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;

拥塞控制

 网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的
 TCP引入 慢启动,快增长 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据

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

 拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快

  • 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
  • 此处引入一个叫做慢启动的阈值
  • 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
  • 当TCP开始启动的时候, 慢启动阈值等于窗口大值
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1

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

延迟应答机制

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

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

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

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

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

捎带应答机制

 在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说 了 “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的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
  • 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
  • 站在应用层的角度, 看到的只是一串连续的字节数据.
  • 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层 数据包.

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

  • 对于定长的包, 保证每次都按固定大小读取即可
  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符
对于UDP协议来说,是否也存在“粘包问题”
  • 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用 层. 就有很明确的数据边界
  • 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半 个"的情况.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值