【Linux】网络传输层详解

1. 端口号

端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在这里插入图片描述
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看);

在这里插入图片描述

  • 端口号的划分范围

0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的

有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:

  • ssh服务器, 使用22端口
  • ftp服务器, 使用21端口
  • telnet服务器, 使用23端口
  • http服务器, 使用80端口
  • https服务器, 使用443
    执行下面的命令, 可以看到知名端口号

执行下面的命令, 可以看到知名端口号:

cat /etc/services
  • netstat
    netstat是一个用来查看网络状态的重要工具.
    常用选项:

  • n 拒绝显示别名,能显示数字的全部转化成数字

  • l 仅列出有在 Listen (监听) 的服務状态

  • p 显示建立相关链接的程序名

  • t (tcp)仅显示tcp相关选项

  • u (udp)仅显示udp相关选项

  • a (all)显示所有选项,默认不显示LISTEN相关


  • pidof

在查看服务器的进程id时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id


2. UDP协议

2.1 UDP协议端格式

在这里插入图片描述
任何协议,首先都要解决两个问题:

  1. 如何将自己的报头和有效载荷分离的问题
    UDP采用的是8字节定长报头,剩下的都是有效载荷
  2. 任何协议,都要解决自己的有效载荷交付给上层的哪一个协议
    UDP报头中包含有 16位目的端口号,这也解释了为什么port都是16位的以及为什么server需要bind

上面的图只是示意图而已,那么在linux内核中,udp协议到底是如何定义的?

下面是udp报头的实现,是一个结构体,成员变量的数据类型是“位段”。

struct udp_header{
   uint32_t src_port:16
   uint32_t dst_port:16
   uint32_t udp_length:16
   uint32_t src_check:16
};

所以所谓的填充报头,本质是在给对应的变量赋值。


2.2 UDP的特点

udp的传输过程与寄信类似

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

这里再讲一下数据报:用户层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。就像寄信一样,无论写了多少字,都一次寄出并接受。

比如UDP传输100个自己的数据,如果此时发送端调用一次sendto,发送了100字节,那么接收端也必须调用对应的一次recvfrom,接受100个字节,而不能循环调用10次recvfrom,每次接受10个字节。


2.3 UDP的缓冲区

udp的缓冲区是什么,这里我需要先补充一下:

以发送缓冲区为例:我们之前讲过的sendto,write,send等socket接口,并不是直接将数据发送到网络之 中,而是将数据先拷贝到udp/tcp的发送缓冲区中 或者将内核缓冲区中的数据,拷贝到用户。之后数据什么时候发,发多少,丢包问题 是有OS内的传输层协议决定的,这也是传输层存在的价值之一。

关于 发送缓冲区和接受缓冲区,我们在tcp协议中还会进一步解释。

在这里插入图片描述
但是由于UDP是面向数据报的:

  1. UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议之后进行后续的传输动作。
  2. UDP具有接受缓冲区,但是这个接受缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致,如果缓冲区满了,再送达的UDP数据就会被丢弃。
  3. UDP是全双工的,同一时间能读能写。

2.4 UDP使用注意事项

不难注意到,UDP协议首部具有一个16位的最大长度。这也就说明了一个udp能够传输的数据最大长度是64K(包含udp首部)。但是64K在当今互联网的环境之下并不是一个宽裕的数字,如果我们的待传输数据超过了64KB,就需要在应用层进行手动的分包,多次发送,并在接收端手动拼装。

2.5 基于UDP的应用层协议

  • NFS: 网络文件系统
  • TFTP: 简单文件传输协议
  • DHCP: 动态主机配置协议
  • BOOTP: 启动协议(用于无盘设备启动)
  • DNS: 域名解析协议

当然, 也包括你自己写UDP程序时自定义的应用层协议


3. TCP协议

TCP全称“传输控制协议”,简单来说就是对数据的传输进行一个详细的控制。

3.1 TCP协议段格式

在这里插入图片描述
任何协议,首先都要解决两个问题:

  1. 如何将自己的报头和有效载荷分离的问题

    • TCP报头中包含有 4位首部长度,表示该TCP头部有多少个32位bit。但是该4位首部长度的单位是 4字节,也就其表示的数值上限为 15(二进制下1111)*4=60字节 。 举个例子,已知一个20字节的报头,那么20/4=5,也就是说4位首部长度中存储的是0101。
    • 标准的tcp报头是20字节,而最大的报头长度是60字节,也就是说可以携带的选项是40字节。换句话说,报头的大小范围是[20,60] byte.
    • 定长报头+自描述字段(4位首部长度) 区分报头(包含选项)和有效载荷
  2. 任何协议,都要解决自己的有效载荷交付给上层的哪一个协议
    TCP报头中包含有 16位目的端口号


3.2 确认应答(ACK)机制

在这里插入图片描述

  • 什么是确认应答
    当host1向host2发送消息之后,host1是无法得知host2是否收到信息的,只有当host2向host1应答(此时应答是不覆盖有效信息)之后,host1才能够确定之前的发送是成功。这就可以理解为“确认应答”。

    所以确认应答机制,不是对最新数据的可靠性保证(也无法保证),而是对历史数据的可靠性保证。

  • 乱序问题
    那么,我们的确认应答就是主机发一条,对端就立刻应答一次吗**?并不是,实际上我们可以一次发多条报文,只要最后每一条都有应答就行(如下图)。
    在这里插入图片描述

    这里就会出现一个问题,报文的发送顺序不一定等于报文的接受顺序,就像我们网购时先下单的商品不一定最先到一样,每一个报文可能选择的路由路径会存在差异,而不同路径的通畅程度也是不同的。

    乱序显然是一种“不可靠”的表现,TCP是如何解决的?TCP给报头添加了序号,可以对报文进行顺序重排(注:在网络中传输的不是信息本身,而是tcp数据段,涵盖了tcp报头+有效载荷)

  • 序号与确认序号

    这里我们要引入TCP报头中两个新属性:32位序号32位确认序号

    在这里插入图片描述
    如果发送序号是10,那么对应的确认序号是11. 也就是确认序号会比发送序号多1.

    这里有的同学就会察觉到问题,无论对于请求报文还是响应报文来说,貌似一次只需要携带一个序号,那么我们设置一个序号参数不就行了吗?为什么报头中同时有 32位序号 和 32位确认序号?
    这当然不是冗余的设计,我们举个例子:当host2向host1发送数据的时候,除了要要对host1之前发送的数据做确认应答(确认应答不含有有效信息,只是确认收到)之外,还想发送一些有效信息。此时我们就会同时需要填写 序号 与 确认序号。(如下图)
    在这里插入图片描述


    那么这个所谓的序号是如何标定的呢?
    其实在发送(接收)缓冲区中(可以当作一个字符数组),TCP将每个字节的数据都进行了编号. 即为序列号在这里插入图片描述举个例子,我们将发送缓冲区的1~1000字节拷贝到报文的正文中,我们以1000作为序号填写到报头当中。那么对端发来的确认应答的编号就是1001…,也就是说,每一个ACK带有的确认序列号,意思是告诉发送者,我已经接收到了哪些数据,下一次你从哪里开始发。
    在这里插入图片描述


3.3 16位窗口大小

3.3.1 网络发送与接收的本质

这里我们先来纠正一个认知,之前我们在讲解socket编程时所使用的的IO接口 write,send,read,recv等,并不是调用之后,数据并不是直接在应用层就取到了,实际上是:

  1. send,write的本质是将用户数据拷贝到tcp的发送缓冲区
  2. read.recv的本质是将tcp缓冲区中的数据,拷贝到用户的缓冲区。

数据从缓冲区中发送出去的时机是由tcp决定的。所谓的数据发送,从OS角度,是将数据从发送缓冲区,通过网络转移到对端的接受缓冲区。
在这里插入图片描述

此时就会出现一个可能发送的数据量过大,导致对方无法接受。

3.3.2 流量控制

对于这种情况,tcp报头中设置了16位窗口大小,表明了自己的接受能力。通过互相通报自己的接受能力给对方,tcp报文中的16位窗口大小,以达到两个方向上的传输速度的控制,这个就叫作流量控制


3.4 同步标志位

在tcp的报头中,存在一些标志位:
在这里插入图片描述

3.4.1 SYN FIN ACK

SYN 叫作同步标志位,标识该报文时一个连接建立请求报文
SYN+ACK: 允许连接建立请求报文,ACK标识该报文也有对上一个报文的确认成分

其中,三次握手就包含如下过程:
在这里插入图片描述
为什么需要SYN标志位? 服务器收到tcp报文的时候,一定会在一个时间段,收到多种报文,接受方就需要区分这些报文,不同的报文有不同的处理手段:哪些是建立链接的,哪些是断开链接的,哪些是进行数据正常通信的,而SYN=1就是代表着建立链接请求报文,对应的,FIN=1就是表示着断开链接请求报文。

在这里插入图片描述

3.4.2 PSH

告知对方,尽快将接受缓冲区的数据取走,尽快让我们发送数据

3.4.3 URG

由于TCP是按序到达的,所以正常情况下无法“插队”,但是TCP提供了优先处理数据的能力,就是设置TCP中的URG标志位,又叫做紧急数据标志位,该标志位通常要配合报头中的16位紧急指针使用。
对于16位紧急指针,代表着“待插队”数据的起始位置,但是tcp规定 只能处理1字节的紧急数据。当我们使用send函数的时候,可以通过将第三个flag参数设置为MSG_OOB.就可以发送一个紧急数据,相应的,recv的时候也要将flag参数设置为MSG_OOB

在这里插入图片描述

3.4.4 RST

  • 场景引入

首先,我们需要明确,OS中存在大量的连接,这些连接需要被管理,需要时间和空间的成本,从代码上看,是一个个的链接结构体。
在这里插入图片描述
参照上图,在host1接收到了 允许连接请求之后,就认定此时连接建立成功了,同时发送ACK报文给host2,host2收到后认定连接建立成功。也就是说,双方认定连接建立成功的共识,是存在时间差的,同时,host1实际上并没有100%的把握host2已经确认连接(因为没有验证)。

如果发生了ACK报文丢失,但是host1此时实际已经认定为连接建立成功,生成连接结构体,并开始发送数据,但是很尴尬,host2此时因为报文丢失没有认定连接成功。此时发现异常的host2会发送报文,并设置标志位RST=1(复位标志位) ,host1收到后立刻销毁连接然后重新建立链接。

这只是RST的应用场景之一,只要通信双方发现连接异常,就可以向对端发送包含RST标志位的报文来进行链接重置。

在这里插入图片描述

  • 为什么不能是4次握手
    大家有没有想过,为什么建立连接时是三次握手,不是4,5次握手?

    根据我们上面说讲的,如果最后一个ACK失败,3次握手与4次握手致使 误认为连接成功的一方是不同的。
    在这里插入图片描述
    所以,现在问题的本质变成了最后一个ACK丢失之后,在连接重置之前的时间里,维护一个没有实际作用的连接结构体的成本由 client还是server承担。
    显然不能用server去承担这个成本,因为server与众多的client建立有连接,而client只与server建立连接。

    也就是说,3次握手是奇数次握手,意味着短暂链接建立的成本会嫁接在client端,那为什么同样是奇数的1,5,7…不可以呢?
    首先,TCP是全双工的,所以建立连接是要验证两端发收都是正常的,1次显然不行,至少3次。其次,网络通信需要花费时间,5次当然可以,但是没有必要,3次就够了。综上所述,是三次握手。


3.5 超时重传机制

  • 情况一
    在这里插入图片描述
    主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
    如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;

  • 情况二

在这里插入图片描述
主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.
这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.

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

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

3.6 连接管理机制

3.6.1 总状态图

在这里插入图片描述


3.6.2 TIME_WAIT状态

主动断开链接的一方,最终会进入一个TIME_WAIT状态,即在真正关闭链接之前等待一段时间。如果服务器中存在大量的close_wait,一定要注意检查服务器是否存在bug.

我们现在做一个测试,首先启动server,然后启动client,接着使用Ctrl+C终止server进程,紧接着又重新运行server,结果发现:
在这里插入图片描述

为什么会绑定失败呢?这是因为之前的server的应用程序虽然终止了,但是TCP协议层的连接实际上还没有断开,也就是server主动断开连接进入到TIME_WAIT状态了,一段时间后才会真正释放连接,所以短时间内我们无法监听相同的server端口,我们可以使用netstat命令查看:

在这里插入图片描述

  • TIME_WAIT的具体时长是什么

    TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.。关于MSL,是TCP报文的最大生存时间可以简单理解为数据从一段到另一端的最大时间,两个MSL就是一个来回。

    MSL 在RFCS1122中规定是2分钟,但是在不同的OS中是不同的,在centos7中默认是60秒。我们可以通过夏明的命令来查看MSL:

    cat /proc/sys/net/ipv4/tcp_fin_timeout
    

  • 为什么我们需要TIME_WAIT状态
    1. 为了保证历史数据在网络中消散。详细一点就是 保证在两个传输方向上尚未接受或者迟到的报文段都依旧消失(否者服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)
    2. 较大概率的保证最后一个ACK被对方收到。假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发。

  • 解决TIME_WAIT状态引起的bind失败的方法

为什么我们需要解决这个问题?又或者说TIME_WAIT会引发什么问题?

服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户
端来请求),这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接,由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目的ip, 目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现问题。

使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符:
在这里插入图片描述


3.7 滑动窗口

3.7.1 滑动窗口

之前我们在介绍确认应答机制时说过,主机通过一次发送多条数据来提高性能,这样可以将等待多个响应的时间重叠起来,且只需要保证最后数据都获得了应答即可。
在这里插入图片描述
我们又介绍了发送缓冲区,并且知道了缓冲区的每一个字节都被编号,那么缓冲区的数据具体时如何发送的呢,这就涉及一个新的概念:滑动窗口
在这里插入图片描述
在发送缓冲区中 可以发送 或者 已经发送但是暂时没有收到ACK的 部分数据 叫做滑动窗口。

窗口大小是指无需确认等待应答而可以继续发送数据的最大值,这个窗口的大小取决于对端的接受能力(报头中的16位窗口大小)。


滑动窗口的最大价值就是再有可以提高发送数据的效率。我们以上面的图片为例子:

  • 滑动窗口的大小等于对方窗口大小与自身拥塞窗口大小的较小值,因为发送数据时不仅要考虑对方的接收能力,还要考虑当前网络的状况。我们这里先不考虑拥塞窗口,并且假设对方的窗口大小一直固定为4000,此时发送方不用等待ACK一次所能发送的数据就是4000字节,因此滑动窗口的大小就是4000字节。(四个段)
  • 现在连续发送1001-2000、2001-3000、3001-4000、4001-5000这四个段的时候,不需要等待任何ACK,可以直接进行发送。
  • 当收到对方响应的确认序号为2001时,说明1001-2000这个数据段已经被对方收到了,此时该数据段应该被纳入发送缓冲区当中的第一部分,而由于我们假设对方的窗口大小一直是4000,因此滑动窗口现在可以向右移动,继续发送5001-6000的数据段,以此类推。
  • 滑动窗口越大,则网络的吞吐率越高,同时也说明对方的接收能力很强。

当发送方发送出去的数据段陆陆续续收到对应的ACK时,就可以将收到ACK的数据段归置到滑动窗口的左侧,并根据当前滑动窗口的大小来决定,是否需要将滑动窗口右侧的数据归置到滑动窗口当中。

  • 滑动窗口与 超时重传

    TCP的重传机制要求暂时保存发出但未收到确认的数据,而这部分数据实际就位于滑动窗口当中,只有滑动窗口左侧的数据才是可以被覆盖或删除的,因为这部分数据才是发送并被对方可靠的收到了,所以滑动窗口除了限定不收到ACK而可以直接发送的数据之外,滑动窗口也可以支持TCP的重传机制。

在这里插入图片描述

滑动窗口可以让发送方一次发送大量的数据,保证效率,另一方面,可以配合对端的接收能力,来完成流量控制。

  • 滑动窗口与流量控制

滑动窗口不一定会整体右移,且可以缩小与扩大。还是以刚才的例子为例,假设对方已经收到了1001-2000的数据段并进行了响应,但对方上层一直不从接收缓冲区当中读取数据,此时当对方收到1001-2000的数据段时,对方的窗口大小就由4000变为了3000。

当发送端收到对方的响应序号为2001时,就会将1001-2000的数据段归置到滑动窗口的左侧,但此时由于对方的接收能力变为了3000,而当1001-2000的数据段归置到滑动窗口的左侧后,滑动窗口的大小刚好就是3000,因此滑动窗口的右侧不能继续向右进行扩展。
在这里插入图片描述
所以我们可以总结:

  1. 对于ACK信息,滑动窗口会右移左边界
  2. 对于16位窗口大小信息,滑动窗口会改变右边界

  • 滑动窗口的实现

我们可以将滑动窗口理解为一个环形数组,两个指针 begin ,end分别控制左右边界,当收到对端的响应之后,如果确认响应序号为x,窗口大小为win,那么设置begin=x,end=begin+win.


3.7.2 高速重发控制

当发送端一次发送了多个报文,此时有两种情况可能出现丢包。

情况一: 数据包成功到达,ACK丢失
在这里插入图片描述
这种情况下,部分ACK丢失问题不大,可以通过后续的ACK进行确认。

以上图为例子:当1~ 1000, 2001~ 3000,3001~4000数据包对应的ACK丢失了,但是只要发送端接收到了5001 ~ 6000的ACK,也就可以确认6001之前的数据实际上都成功收到了。

情况二:数据包丢失

此时会触发“高速重发控制”(快重传)

在这里插入图片描述

  • 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001”
    一样;
  • 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
  • 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已
    经收到了, 被放到了接收端操作系统内核的接收缓冲区中;

这种机制就叫作“高速重传控制”。

此时,滑动窗口中又是什么情况呢?
在这里插入图片描述

需要注意的是,快重传需要在大量的数据重传和个别的数据重传之间做平衡,实际这个例子当中发送端并不知道是1001-2000这个数据包丢了,当发送端重复收到确认序号为1001的响应报文时,理论上发送端应该将1001-7000的数据全部进行重传,但这样可能会导致大量数据被重复传送,所以发送端可以尝试先把1001-2000的数据包进行重发,然后根据重发后的得到的确认序号继续决定是否需要重发其它数据包。


  • 快重传 和 超时重传

1 . 快重传能够快速进行 数据重发,当发送端连续收到三次相同的ACK就会触发。超时重传需要通过设置重传计时器,在一段时间之后才会重传。
2. 虽然快重传能够快速判定数据包丢失,但快重传并不能完全取待超时重传,因为有时数据包丢失后可能并没有收到对方三次重复的应答,此时快重传机制就触发不了,而只能进行超时重传。因此快重传虽然是一个效率上的提升,但超时重传却是所有重传机制的保底策略,也是必不可少的


3.8 流量控制

之前我们就提到过流量控制,现在我们来专门讲解。

接受端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被填满,这个时候如果发送端继续发送,就会造成丢包,继而引发丢包重传等等。

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫作 流量控制(Flow Control)。

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

在这里插入图片描述
当发送端得知接收端接收数据的能力为0时会停止发送数据,此时发送端会通过以下两种方式来得知何时可以继续发送数据:

  1. 等待告知。接收端上层将接收缓冲区当中的数据读走后,接收端向发送端发送一个TCP报文,主动将自己的窗口大小告知发送端,发送端得知接收端的接收缓冲区有空间后就可以继续发送数据了。
  2. 主动询问。发送端每隔一段时间向接收端发送报文(窗口探测),该报文不携带有效数据,只是为了询问发送端的窗口大小,直到接收端的接收缓冲区有空间后发送端就可以继续发送数据了。

  • 16位窗口字段的大小
    16位数字的最大表示是65535,但这并不是说TCP窗口的上限就是65535字节。实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;

  • 第一次向对方发送数据时如何得知对方的窗口大小
    双方在进行TCP通信之前需要先进行三次握手建立连接,而双方在握手时除了验证双方通信信道是否通畅以外,还进行了其他信息的交互,其中就包括告知对方自己的接收能力,因此在双方还没有正式开始通信之前就已经知道了对方接收数据能力,所以双方在发送数据时是不会出现缓冲区溢出的问题的


3.9 拥塞控制

  • 场景引入
    虽然TCP通过滑动窗口可以高效可靠的发送大量数据,但是如果在刚开始的阶段就发送大量数据,仍可能引发问题。因为网络中有很多计算机,假如当前的网络状态就已经比较拥堵了,在不清楚当前网络状态的情况下贸然发送大量数据,是很有可能造成更严重问题的。

    两个主机在进行TCP通信的过程中,出现个别数据包丢包的情况是很正常的,此时可以通过快重传或超时重发对数据包进行补发。但如果双方在通信时出现了大量丢包,此时就不能认为是正常现象了。
    也就是说,TCP不仅考虑了通信双端主机的问题,还考虑了网络的问题。双方网络通信时出现少量的丢包TCP是允许的,但一旦出现大量的丢包,此时TCP就不再推测是双方接收和发送数据的问题,而判断是双方通信信道网络出现了拥塞问题。

  • 如何解决网络拥塞问题?

网络出现大面积瘫痪时,通信双方作为网络当中两台小小的主机,看似并不能为此做些什么,但“雪崩的时候没有一片雪花是无辜的”,网络出现问题一定是网络中大部分主机共同作用的结果。

  • 如果网络中的主机在同一时间节点都大量向网络当中塞数据,此时位于网络中某些关键节点的路由器下就可能排了很长的报文,最终导致报文无法在超时时间内到达对端主机,此时也就导致了丢包问题。
  • 当网络出现拥塞问题时,通信双方虽然不能提出特别有效的解决方案,但双方主机可以做到不加重网络的负担。
  • 双方通信时如果出现大量丢包,不应该立即将这些报文进行重传,而应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率。

需要注意的是,网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞避免算法。

因此拥塞控制看似只是谈论的一台主机上的通信策略,实际这个策略是所有主机在网络崩溃后都会遵守的策略。一旦出现网络拥塞,该网络当中的所有主机都会受到影响,此时所有主机都要执行拥塞避免,这样才能有效缓解网络拥塞问题。通过这样的方式就能保证雪崩不会发生,或雪崩发生后可以尽快恢复

  • 拥塞控制

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

在这里插入图片描述

  • TCP除了有窗口大小和滑动窗口的概念以外,还有一个窗口叫做拥塞窗口。拥塞窗口是可能引起网络拥塞的阈值,如果一次发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。
  • 刚开始发送数据的时候拥塞窗口大小定义以为1,每收到一个ACK应答拥塞窗口的值就加一。
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送数据的窗口大小,即滑动窗口的大小。即 滑动窗口 = min(拥塞窗口,16为窗口大小)

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

为了不增加的那么快,因此不能是拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是改变为线性增长。

在这里插入图片描述

  1. 指数增长。 刚开始进行TCP通信的时候拥塞窗口的值为1,并不断按照指数的方式进行增长。
  2. 线性增大。慢启动的阈值初始时为对方窗口大小的最大值,图中慢启动阈值的初始值为16,因此当拥塞窗口的值增大到16时就不再按指数形式增长了,而变成了的线性增长。
  3. 乘法减小。拥塞窗口在线性增长的过程中,在增大到24时如果发生了网络拥塞,此时慢启动的阈值将变为当前拥塞窗口的一半,也就是12,并且拥塞窗口的值被重新设置为1,所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是12。

主机在进行网络通信时,实际就是在不断进行指数增长、加法增大和乘法减小。需要注意的是,不是所有的主机都是同时在进行指数增长、加法增大和乘法减小的。每台主机认为拥塞窗口的大小不一定是一样的,即便是同区域的两台主机在同一时刻认为拥塞窗口的大小也不一定是完全相同的。因此在同一时刻,可能一部分主机正在进行正常通信,而另一部分主机可能已经发生网络拥塞了。

3.10 延迟应答

如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。

  • 假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K。
  • 但实际接收端处理数据(即应用层从接受缓冲区取数据)的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了。
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
  • 如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的窗口大小就是1M。

需要注意的是,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。

在这里插入图片描述
此外,不是所有的数据包都可以延迟应答。

  • 数量限制:每个N个包就应答一次。
  • 时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)。

延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。

3.11 捎带应答

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

在这里插入图片描述

3.12 面向字节流

创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

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

由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

实际对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流


3.13 粘包问题

  • 什么是粘包
  • 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
  • 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段
  • 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
  • 站在应用层的角度, 看到的只是一串连续的字节数据.
  • 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.

  • 如何解决粘包问题

要解决粘包问题,本质就是要明确报文和报文之间的边界。

  • 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲
    区从头开始按sizeof(Request)依次读取即可;

  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;

  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔
    符不和正文冲突即可);

  • UDP是否存在粘包问题
    对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用
    层. 就有很明确的数据边界
    站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半
    个"的情况.


3.14 TCP异常情况

  • 进程终止

当客户端正常访问服务器时,如果客户端进程突然崩溃了,此时建立好的连接会怎么样?

当一个进程退出时,该进程曾经打开的文件描述符都会自动关闭,因此当客户端进程退出时,相当于自动调用了close函数关闭了对应的文件描述符,此时双方操作系统在底层会正常完成四次挥手,然后释放对应的连接资源。也就是说,进程终止时会释放文件描述符,TCP底层仍然可以发送FIN,和进程正常退出没有区别。

  • 机器重启

当客户端正常访问服务器时,如果将客户端主机重启,此时建立好的连接会怎么样?

当我们选择重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源

  • 机器掉电/网线断开

当客户端正常访问服务器时,如果将客户端突然掉线了,此时建立好的连接会怎么样?

当客户端掉线后,服务器端在短时间内无法知道客户端掉线了,因此在服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的。

服务器会定期客户端客户端的存在状况,检查对方是否在线,如果连续多次都没有收到ACK应答,此时服务器就会关闭这条连接。
此外,客户端也可能会定期向服务器“报平安”,如果服务器长时间没有收到客户端的消息,此时服务器也会将对应的连接关闭。
其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。


TCP小结

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

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

基于TCP的应用层协议

HTTP,HTTPS,SSH,Telnet,FTP,SMTP
当然, 也包括你自己写TCP程序时自定义的应用层协议;

4. TCP vs UDP

我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较:

  • TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
  • UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广

归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ornamrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值