网络是怎样连接的---TCP/IP协议收发数据

TCP协议收发数据

在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的 IP 地址、端口号、通信操作的进行状态等。本来套接字就只是一个概念而已,并不存在实体,如果一定要赋予它一个实体,我们可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体。
协议栈是根据套接字中记录的控制信息来工作的。当浏览器调用 socket、connect 等 Socket 库中的程序组件时,协议栈内部是如何工作的。

  • 创建套接字
    应用程序调用 socket 申请创建套接字,协议栈根据应用程序的申请执行创建套接字的操作。在这个过程中,协议栈首先会分配用于存放一个套接字所需的内存空间,还需要往里面存入控制信息,套接字刚刚创建时,数据收发操作还没有开始,因此需要在套接字的内存空间中写入表示这一初始状态的控制信息。到这里,创建套接字的操作就完成了。
    接下来,需要将表示这个套接字的描述符告知应用程序。收到描述符之后,应用程序在向协议栈进行收发数据委托时就需要提供这个描述符。
    创建套接字之后,应用程序(浏览器)就会调用 connect,随后协议栈会将本地的套接字与服务器的套接字进行连接。
    连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。
    套接字刚刚创建完成的时候,里面并没有存放任何数据,也不知道通信的对象是谁。浏览器可以根据网址来查询服务器的 IP 地址,而且根据规则也知道应该使用 80 号端口,于是,我们需要让客户端向服务器告知必要的信息,比如“我想和你开始通信,我的 IP 地址是 xxx.xxx.xxx.xxx,端口号是 yyyy。”
    此外,当执行数据收发操作时,我们还需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区,它也是在连接操作的过程中分配的。
  • 连接
    具体的操作过程是从应用程序调用 Socket 库的 connect 开始的:connect(< 描述符 >, < 服务器 IP 地址和端口号 >, …)。调用提供了服务器的 IP 地址和端口号,这些信息会传递给协议栈中的 TCP 模块。
    首先,客户端先创建一个包含表示开始数据收发操作的控制信息的头部。头部包含很多字段,客户端的套接字就准确找到了服务器的套接字。然后,将头部中的控制位的 SYN 比特设置为 1,可以理解为表示连接。此外还需要设置适当的序号和窗口大小。
    当 TCP 头部创建好之后,接下来 TCP 模块会将信息传递给 IP 模块并委托它进行发送。当服务器找到对应的套接字之后,套接字中会写入相应的信息,并将状态改为正在连接,并返回响应,这个过程和客户端一样,需要在 TCP 头部中设置发送方和接收方端口号以及 SYN 比特 。此外,在返回响应时还需要将 ACK 控制位设为1,这表示已经接收到相应的网络包。如果由于某些原因不接受连接,那么将不设置 SYN,而是将 RST 比特设置为 1。
    然后,网络包就会返回到客户端,通过 IP 模块到达 TCP 模块,并通过 TCP 头部的信息确认连接服务器的操作是否成功。如果 SYN 为 1 则表示连接成功,这时会向套接字中写入服务器的 IP 地址、端口号等信息,同时还会将状态改为连接完毕。并将 ACK 比特设置为 1 并发回服务器,告诉服务器刚才的响应包已经收到。
    完成连接后,套接字就已经进入随时可以收发数据的状态了,可以认为这时有一根管子把两个套接字连接了起来。
  • 数据收发
    数据收发操作是从应用程序调用 write 将要发送的数据交给协议栈,协议栈收到数据后执行发送操作。
    首先,协议栈并不关心应用程序传来的数据是什么内容。在协议栈看来,要发送的数据就是一定长度的二进制字节序列而已。
    其次,协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。
    一次将多少数据交给协议栈是由应用程序自行决定的,协议栈并不能控制这一行为。如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去。
    协议栈会根据一个叫作 MTU 的参数来进行判断。MTU 表示一个网络包的最大长度,在以太网中一般是 1500 字节。MTU 是包含头部的总长度,因此需要从MTU 减去头部的长度,然后得到的长度就是一个网络包中所能容纳的最大数据长度,这一长度叫作 MSS。当从应用程序收到的数据长度超过或者接近 MSS 时再发送出去,就可以避免发送大量小包的问题了。
    当应用程序发送数据的频率不高的时候,如果每次都等到长度接近 MSS 时再发送,可能会因为等待时间太长而造成发送延迟。为此,协议栈的内部有一个计时器,当经过一定时间之后,就会把网络包发送出去。
    这是互相矛盾的。如果长度优先,那么网络的效率会提高,但可能会因为等待填满缓冲区而产生延迟;相反地,如果时间优先,那么延迟时间会变少,但又会降低网络的效率。进行发送操作时需要综合考虑这两个要素以达到平衡。
    HTTP 请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能超过一个网络包所能容纳的数据量。发送缓冲区中的数据就会超过 MSS 的长度,缓冲区中的数据会被以 MSS 长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给 IP 模块来执行发送数据的操作。
    序号和 ACK 号的用法
    TCP 模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在 TCP 头部中,“序号”字段就是派在这个用场上的。接收方可以用整个网络包的长度减去头部的长度就可以得到数据的长度。有了这两个数值,接收方能够检查收到的网络包有没有遗漏。如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入 TCP头部的 ACK 号中发送给发送方。但是数据收发是双向的。
    首先,客户端在连接时需要计算出与从客户端到服务器方向通信相关的序号初始值,并将这个值发送给服务器;接下来,服务器会通过这个初始值计算出 ACK 号并返回给客户端。同时,服务器也需要计算出与从服务器到客户端方向通信相关的序号初始值,并将这个值发送给客户端。接下来像刚才一样,客户端也需要根据服务器发来的初始值计算出 ACK 号并返回给服务器。
    数据双向传输时的情况序号和 ACK 号的交互
    TCP 采用这样的方式确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。如果对方没有返回某些包对应的 ACK 号,那么就重新发送这些包。
    不过,如果发生网络中断、服务器宕机等问题,那么无论 TCP 怎样重传都不管用。TCP 会在尝试几次重传无效之后强制结束通信,并向应用程序报错。
    当网络传输繁忙时就会发生拥塞,ACK 号的返回会变慢,这时我们就必须将等待时间设置得稍微长一点,否则可能会发生已经重传了包之后,前面的 ACK 号才姗姗来迟的情况。因此等待时间需要设定一个合适的值,TCP 采用了动态调整等待时间的方法,这个等待时间是根据 ACK 号返回所需的时间来判断的。TCP 会在发送数据的过程中持续测量 ACK 号的返回时间,如果 ACK 号返回变慢,则相应延长等待时间;相对地,如果 ACK 号马上就能返回,则相应缩短等待时间。
    每发送一个包就等待一个 ACK 号,但在等待 ACK 号的这段时间中,如果什么都不做那实在太浪费了。TCP 采用滑动窗口方式来管理数据发送和 ACK 号的操作。所谓滑动窗口,就是在发送一个包之后,不等待 ACK 号返回,而是直接发送后续的一系列包。
    滑动窗口
    但如果不等返回 ACK 号就连续发送包,就有可能会出现发送包的频率超过接收方处理能力的情况。当接收方的 TCP 收到包后,会先将数据存放到接收缓冲区中。然后,接收方需要计算 ACK 号,将数据块组装起来还原成原本的数据并传递给应用程序,如果这些操作还没完成下一个包就到了,下一个包也会被暂存在接收缓冲区中。如果数据到达的速率比处理这些数据并传递给应用程序的速率还要快,那么接收缓冲区中的数据就会越堆越多,最后就会溢出。因此,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制。当接收操作完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了,这时接收方会通过 TCP 头部中的窗口字段将自己能接收的数据量告知发送方。
    滑动窗口与接收缓冲区
    能够接收的最大数据量称为窗口大小。要提高收发数据的效率,还需要考虑另一个问题,那就是返回 ACK号和更新窗口的时机。
    更新窗口大小的时机应该是接收方从缓冲区中取出数据传递给应用程序的时候。。这个操作是接收方应用程序发出请求时才会进行的,而发送方不知道什么时候会进行这样的操作,因此当接收方将数据传递给应用程序,导致接收缓冲区剩余容量增加时,就需要告知发送方。
    当接收方收到数据时,如果确认内容没有问题,就应该向发送方返回 ACK 号,因此可以认为收到数据之后马上就应该进行这一操作。
    如果将两个因素结合起来看,首先,发送方的数据到达接收方,在接收操作完成之后就需要向发送方返回 ACK 号,而再经过一段时间,当数据传递给应用程序之后才需要更新窗口大小。因此,接收方在发送 ACK 号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间,在这个过程中很有可能会出现其他的通知操作,这样就可以把两种通知合并在一个包里面发送了。
    举个例子,在等待发送ACK 号的时候正好需要更新窗口,这时就可以把 ACK 号和窗口更新放在一个包里发送,从而减少包的数量。当需要连续发送多个 ACK 号时,也可以减少包的数量,这是因为 ACK 号表示的是已收到的数据量,也就是说,它是告诉发送方目前已接收的数据的最后位置在哪里,因此当需要连续发送 ACK 号时,只要发送最后一个 ACK 号就可以了,中间的可以全部省略。当需要连续发送多个窗口更新时也可以减少包的数量,因为连续发生窗口更新说明应用程序连续请求了数据,接收缓冲区的剩余空间连续增加。这种情况和 ACK 号一样,可以省略中间过程,只要发送最终的结果就可以了。
    发送 HTTP 请求消息后,接下来还需要等待 Web 服务器返回响应消息。对于响应消息,浏览器需要进行接收操作,这一操作也需要协议栈的参与。首先,协议栈会检查收到的数据块和 TCP 头部的内容,判断是否有数据丢失,如果没有问题则返回 ACK 号。然后,协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序。
  • 断开连接
    收发数据结束的时间点应该是应用程序判断所有数据都已经发送完毕的时候。协议栈在设计上允许任何一方先发起断开过程。完成数据发送的一方会发起断开过程,这里以服务器一方发起断开过程为例来进行讲解。首先,服务器一方的应用程序会调用 Socket 库的 close 程序。然后,将控制位中的 FIN 比特设为 1。接下来,协议栈会委托 IP 模块向客户端发送数据。
    当收到服务器发来的 FIN 为 1 的 TCP 头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态,并向服务器返回一个 ACK 号。
    应用程序调用 read 来读取数据时,协议栈会告知应用程序(浏览器)来自服务器的数据已经全部收到了。客户端应用程序会调用 close 来结束数据收发操作,这时客户端的协议栈也会和服务器一样,生成一个 FIN 比特为 1 的 TCP 包,然后委托 IP 模块发送给服务器。服务器会返回ACK 号,通信就全部结束了。
    断开连接
    和服务器的通信结束之后,用来通信的套接字也就不会再使用了,就可以删除这个套接字了。不过,套接字并不会立即被删除,而是会等待一段时间之后再被删除。
    等待这段时间是为了防止误操作,网络中可能存在重传的包,因此需要等待到重传完全结束。一般来说会等待几分钟之后再删除套接字。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值