(二)用电信号传输 TCP/IP 数据

上一章介绍了浏览器生成消息并委托协议栈发送数据等步骤,那么协议栈具体是怎么操作的呢?所谓的套接字究竟是什么东西呢?



本章内容:

  • 创建套接字
  • 连接服务器
  • 收发数据
  • 从服务器断开连接并删除套接字
  • IP 与以太网的包收发操作
  • 用 UDP 协议收发数据的操作

一、创建套接字

协议栈的内部结构

  • 应用程序通过代码调用 Scoket 库的组件来让协议栈进行指定的操作
  • 协议栈的 TCP 和 UDP 部分负责接收应用程序的委托执行收发数据的操作。协议栈下半部分通过 IP 协议控制网络包的收发,以及将数据切分、发送。IP 协议中还包括 ICMP 协议和 ARP 协议,作用分别是确保网络包到达目的地、根据 IP 地址查询以太网 MAC 地址。
  • 网卡驱动负责控制网卡硬件
  • 网卡负责实际的收发操作,也就是直接跟网线交流。



    (还是觉得说得很牵强)

套接字究竟是什么

所谓创建套接字就是在协议栈里面创建一块内存空间,创建套接字就是为了通信,所以这块空间里面就存放着很多信息,比如通信对象的 IP 地址,端口号,通信状态等。

除此之外,还有发送数据的时间。发送数据时协议栈需要查看对方的 IP 地址和端口号,发了一条数据之后需要记录时间,过了很久没有收到就进行响应操作,因为总不能一直等待对方响应吧,说不定数据早就丢失了。



在控制台输入 netstat -ano 就可以看到下面这个图





这就是套接字的样子,显示的包括通信对象的 IP 地址,端口号,通信状态等。

创建套接字的过程

浏览器通过 Scoket 库向协议栈发出委托,在创建套接字时具体进行以下操作。

  • 协议栈首先分配一块内存空间
  • 向内存空间里面存入初始状态的控制信息,并不是通信对象的信息,以为收发数据还没有开始。
  • 将这个套接字的描述符告知应用程序,当需要收发数据时,应用程序只需要提供这个描述符就可以委托协议栈发送数据,而不用每次都告诉协议栈应该跟谁操作。

(所谓描述符应该就是这块内存空间的地址吧)

二、连接服务器

连接是什么意思

连接实际就是通信双方交换信息,并没有什么类似于连接的操作,只是为了方便人们理解,以及历史原因。
连接的时候做了三件事:

  • 因为创建套接字的时候并没有告诉协议栈通信对象的信息,所以这个时候就要把对方的 IP 地址和端口号告知协议栈。(注意,并没有写入套接字中)
  • 客户端找到服务器,并提供自己的信息,询问对方是否进行连接。
  • 创建缓冲区,用来临时存放准备收发的数据

(虽然是这么说,但是实际上应该复杂很多)

控制信息有哪些

控制信息分为两类

  1. 客户端和服务器相互联络是交换的控制信息
  2. 套接字(协议栈内存空间)中记录的信息

前一种控制信息在通信过程中必不可少,这些控制信息包括了目的地的信息。这些信息位于网络包开头,有 TCP 头部,以太网头部,IP 头部。TCP 头部中包括了发送方端口号,接收方端口号,序号,ACK 号等等。


后一种控制信息在不同的系统中不一样,都是根据需要来保存。

连接操作的实际过程

  1. 客户端创建一个头部,包含服务器的信息以便找到服务器。其中 SYN 设为 1,表示这是一个连接请求。
  2. 服务器的套接字一直都是创建好的,等待着客户端的连接。当收到客户端的连接请求的之后,如果愿意连接,就在 TCP 头部设置 SYN 为 1,并且设置 ACK 为去,表示已经接收到相应的网络包,最后将网络包发送给客户端。
  3. 客户端收到网络包,TCP 模块根据 TCP 头部的信息确定是否连接成功。如果 SYN 为 1,说明连接成功。这是会向套接字中写入服务器的 IP 地址、端口号等信息。然后还需要向服务器发送一个数据包,其中 ACK 为 1,告诉服务器刚才的响应包已经收到。




此时连接已经完成,这就是 TCP 的三次握手。
连接是抽象的,在断开连接之前都是可以收发数据的。

连接完成后,控制流程又回到了应用程序。

三、收发数据

收发数据分为

  • 将 HTTP 消息交给协议栈
  • 协议栈发送消息并接收响应
  • 将 HTTP 响应交给浏览器

其中,第二步是又分为很多步。

将 HTTP 消息交给协议栈

应用程序通过调用 write 组件将数据交给协议栈,协议栈并不在乎究竟是些什么数据,以为对它来说都是二进制字节序列而已。
协议栈会将收到的数据放到缓存区里面而不是马上发送出去,两种情况下将会发送。

  • 一个网络包的最大长度是 MTU,除去头部后一个网络包能容纳的 TCP 数据的最大长度叫做 MSS。当缓冲区里面的数据到达或超过 MSS 之后,协议栈就会发送出去。
  • 万一数据很少岂不是不发送了?所以还有一种情况下会发送出去。协议栈内部有一个定时器,当超过一定时间之后就会将数据发送出去。



除此之外,浏览器在发送数据的时候告诉协议栈不用等待直接发送,这样协议栈就会马上发送出去了。

对较大的数据进行拆分

当应用程序发过来的数据超过 MSS 之后,数据就会被拆分,分成小块之后,每一块都加上 TCP 头部,然后交给 IP 模块。

使用 ACK 号确认网络包已收到

发送数据包时,TCP 头部会包含这个数据包的序号,而接收方在接收的时候可以计算出这个数据包的长度。这样接收方就会回复 ACK 号,比如序号是 1,长度是1460,接收方回复的 ACK 号就是 1461。(这只是最简单的情况)


需要注意的是,序号并不是从 1 开始的,而是随机产生的,这是为了安全起见。



收发数据是双向的,在连接的时候双方都要生成一个随即序号并告诉对方,在之后的通信中都要用到这个序号。



在没有收到确认之前,发送过的数据都会保存的缓冲区里面,如果长时间没有收到确认号,就会重新发送,这是个非常强大的功能。

动态调整超时时间

长时间没有收到确认号就重新发送,可是多久算是长时间呢?

TCP 在发送数据的过程中持续检测 ACK 号的返回时间,当该时间变长的时候超时时间也变长,反之亦然。

滑动窗口

发送一条数据就等待 ACK 确认号回导致时间的浪费,但是不等待的话接收方可能会接受不了那么多数据导致数据的丢失。那怎么办呢?
TCP 是这样处理的,发送方有一个滑动窗口,从数据的开头开始发送,当发送一定数据之后就停止发送,等待接收方的确认。收到确认之后再继续发送。


那么问题来了,窗口的大小怎么定?实际中,接收方会告诉对方自己的接收缓冲区大小,让对方调整滑动窗口的大小。

ACK 和窗口的合并

这里实际就是减少发送的数据包。

  • 将更新窗口的消息和 ACK 号一起发送而不是分开发送。
  • 接收到多条数据之后,只发送最后一个数据包的确认信息。因为发送最后一个也就说明了之前的已经接收成功了。

接收 HTTP 响应消息

浏览器在委托协议栈发送消息之后,会调用 read 来获取响应消息。

和发送消息一样,协议栈会创建缓冲区,将收到的数据放在其中。

如果发送完数据之后还没有收到响应,协议栈就会将浏览器的委托暂时挂起,去完成别的任务。

收到数据时,协议栈会检查数据是否丢失,如果没有丢失就返回 ACK 号,然会将数据放入缓冲区,并将数据块连接起来还原成原始数据,然后交给应用程序。具体来说,协议栈将收到的数据复制到应用程序指定的内存地址中,然后将流程控制交回给应用程序。之后协议栈还需要找到合适的机会向发送方发送窗口更新的消息。

四、从服务器断开连接并删除套接字

数据发送完毕后断开连接

当收发数据的操作完成时,就可以断开连接了。下面以服务器发起断开请求为例,实际上任意一端都可以发起断开请求的。

  • 服务器发送 FIN=1 的 TCP 头部
  • 客户端的协议栈收到后将自己的套接字标记为断开操作状态,然后返回一个 ACK 确认号
  • 协议栈等待应用程序来 read 数据,会告知应用程序数据已经全部收到了
  • 客户端调用 close 结束数据的收发,然后发送一个 FIN=1 的 TCP 包给服务器
  • 服务器返回 ACK 确认号

删除套接字

正常来说,通信结束后就可以删除套接字了,但实际上协议栈并不会马上进行操作,这是为了防止出错,出错的情况有很多种,下面列举一种。

客户端说断开连接,服务器说好的。服务器也说断开连接,客户端也回复好的。可是最后一条消息丢失了,导致服务器重新说一次断开连接。如果本来的套接字已经删除,并且创建了一个使用之前端口号新的套接字,新的套接字就可能收到断开连接的消息。

数据收发小结

五、IP 和以太网的包收发操作

包的基本知识

网络包有两部分组成:

  • 头部
  • 内容

通常来讲,TCP 头部加上数据就是块就是包的内容。而内容加上 IP 头部之后就叫做 IP 包,IP 包加上 MAC 头部之后就叫做以太网包。

  • IP 头部有目的地的 IP 地址
  • MAC 头部要下一站的 MAC(物理地址)地址

先介绍一下路由器和集线器的概念

  • 路由器根据目标地址判断下一个路由的位置
  • 集线器在子网中将网络包传输到下一个路由



    当一个网络包创建好之后,已经有了 IP 和 MAC 头部,计算机将网络包发送给集线器,集线器里面有以太网表,根据 MAC 头部的地址在表中查找信息,判断发往哪个路由器。网络包到达路由器之后,将 MAC 头部丢弃,根据 IP 头部里面的 IP 在路由表里面查找下一跳的物理地址。然后为网络包添加新的 MAC 头部。以此类推,一直到发送给目的主机。

包收发操作概览

之前是说通过 IP 模块将数据发送出去,实际上 IP 模块是将二进制字节序列传递给网卡,然后网卡将数字信号转换成电信号发送出去。当收到数据时,网卡将电信号转化为数字信号交给 IP 模块,IP 模块将网络包的内容交给 TCP 模块。


虽说是让网卡发送的数据,但是在此之前 IP 模块也做了很多重要的工作。

  • 添加 MAC 头部:以太网用的头部,包含 MAC 地址
  • 添加IP 头部:IP 用的头部,包含 IP 地址。

需要注意,MAC 头部是由 IP 模块添加的,而且 IP 模块对于不同类型的包进行的都是一样的操作,因为对它来说那些都是二进制字节序号而已。

生成 IP 头部

IP 头部格式在这里就不详细说了,里面包括目的地的 IP 地址,还有发送方的 IP 地址。

这里的“发送方的 IP 地址”实际指的是网卡的 IP 地址,一台电脑可能有多个网卡,那么应该填写哪个网卡的地址呢?


计算机内部有一个 IP 表,在这个表里查找就可以知道这个目的 IP 应该通过哪个网卡发送出去了。



接下来还需要填写协议号,它表示的是包的内容来自哪个模块。如果是来自 TCP 委托的内容,就设置为 06(十六进制),如果是 UDP 就设置为 17(十六进制)。

生成 MAC 头部

生成 MAC 头部是需要设置三个字段:

  • 以太类型。IP 协议的值 0800(十六进制)
  • 发送方 MAC 地址。也就是本机网卡的 MAC 地址
  • 接收方 MAC 地址。路由表可以根据 IP 地址查询下一跳物理地址

ARP 查询目标路由器的 MAC 地址

路由器的路由表可能可以获取到下一跳的物理地址,但是一开始路由表是空的,这种情况下就需要通过 ARP 查询目标路由器的 MAC 地址了。



实际操作就是在子网内广播,询问所有设备“xxxx 这个 IP 是谁的?”,然后就会收到回复“xxxx 这个 IP 是我的,我的 MAC 地址是 yyyy”。这样就得到了想要的 MAC 地址了,也就可以将其写入 MAC 头部了。



如果每次放松数据包都要发送 ARP 包的话就太不方便了,所有有一个 ARP 缓存,发送之前查看里面有没有想要的信息,没有的话再发送广播。而这个 ARP 会定时清除数据,防止保存着错误的信息。

以太网的基本知识

以太网具有以下三个特点:

  • 将包发送到 MAC 头部的接收方 MAC 地址
  • 用发送方的 MAC 地址来识别发送方
  • 用以太类型来识别包的内容

将 IP 包转换成电或光信号发送出去

开机时,网卡驱动对网卡进行初始化操作,并将网卡 ROM 中保存的 MAC 地址写入网卡的 MAC 模块中。
当收到协议栈发来的数据时,网卡就将数据转换并发送出去。

给网络包加 3 个控制数据

网卡驱动从 IP 模块中获取包之后,会将其复制到网卡的缓冲区,然后向 MAC 模块发送发送包的命令,接下来就轮到 MAC 模块进行工作了。


MAC 模块会将包从缓冲区取出,并在开头加报头和起始帧分界符(SFD),在末尾加上用于检测错误的帧校验序列(FCS)。
(这部分太复杂看不懂…)

向集线器发送网络包

加上 3 个控制信息之后就可以将包通过网线发送出去了。发送信号的方式有两种,一种是使用集线器的半双工模式,一种是使用交换机的全双工模式。


在半双工模式中,先要判断网线中是否存在其他设备发送的信号,如果没有,MAC 模块就从报头开始将数字信息按每个比特转换成电信号,然后由 PHY 或者 MAU 模块发送出去。
(MAC 生成通用信号,然后由 PHY 或者 MAU 模块转换成可在网线中传输的格式,并通过网线发送出去)



注意,以太网是不会管对方有没有收到信号的



(PHY(MAU) 还有其他职责,在此省略…)

接收返回包

在使用集线器的半双工模式以太网中,一台设备发送的信号会到达连接到集线器的所有设备上。接收过程如下:

  • 把信号全都接收进来
  • 通过报头的波形同步时钟,然后遇到起始帧分界符开始将后面的信号转换成数字信息。
  • PHY(MAU) 模块将信号转换成通用格式并交给 MAC 模块。
  • MAC 模块从头开始将信号转换成数字信息,并放入缓冲区。到达末尾时还需要检查 FCS,如果出错就丢弃。
  • 检查 MAC 头部中接收方的 MAC 地址是否与自己的一致,如果不一致就丢弃,一直就将包放入缓冲区。
  • 通知计算机收到了一个包。(中断机制)

将服务器的响应包从 IP 传递给 TCP

假设 Wbe 服务器返回了一个响应包,协议栈会进行哪些操作呢?

  • 根据包的以太网类型,如果是 0800 网卡驱动就会交给 TCP/IP 栈来处理。
  • IP 模块检查 IP 头部,以及检查头部里的 IP 地址。确定无误之后就可以接收这个包了。
  • 如果 IP 地址不是我们的地址,那就是出错了,客户端计算机的 IP 模块会通过 IMCP 消息将错误告知发送方,而服务器收到不属于自己的包的时候,可能会像路由器一样进行转发。
  • 如果没问题的话 IP 模块就负责将网络包还原,因为有些数据包进行了分片。
  • 然后 IP 模块就将包交给 TCP 模块了,TCP 模块会执行相应的操作。

六、UDP 协议的收发操作

适用场景

  • 需要发送的数据不多时
  • 发送视频和音频数据时(比如直播)

    使用

  • 创建套接字,无需建立和断开连接
  • 将数据加上 UDP 头部交给 IP 模块发送
  • 接收时,找到相应套接字并将数据交给相应的应用程序

    UDP 头部中的控制信息

  • 发送方端口号
  • 接收方端口号
  • 数据长度
  • 校验和

(突然发现 UDP 和 TCP 头部中都没有 IP 地址,看来 IP 地址是值放在 IP 头部中的。第二章看完了,感觉有些懵逼,但是又不想继续看了…)