使用电或光信号传输TCP/IP数据

TCP/IP软件分层结构

  • 应用程序 -> Socket库(解析器)
  • 操作系统协议栈
    • TCP (需要连接) | UDP (不需要连接)
    • IP(传输网络包、确定路由)
      • ICMP ARP
        • 驱动程序 - 网卡驱动
        • 硬件 - 网卡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//应用程序:获取IP地址
<内存地址> = gethostbyname(...);//Socket库
//协议栈: UDP模块 - IP模块 - DNS服务器

//应用程序:创建套接字
<描述符>=socket(使用ipv4,使用tcp...)//Socket库
//协议栈:TCP模块创建套接字返回

//应用程序: 连接
connect(<描述符>,<服务器IP和端口号>,...)//套接字连接
//协议栈:TCP模块 - IP模块 - web服务器
//连接成功

//应用程序:写数据
.write(xxx)
//协议栈:TCP模块-IP模块-Web服务器

//应用程序:读入套接字数据至缓存
<接收数据长度>=read(xxx)

//应用程序:断开
close(<描述符>)
//协议栈:TCP模块 - IP模块 - web服务器 <断开> - IP模块 - TCP模块 <断开> <删除套接字> - 应用程序

TCP 模块

创建套接字

套接字实体

协议栈内部的内存空间,记录通信操作的控制信息,例如IP、端口、通信操作状态、时间等。

通过 netstat命令显示套接字

调用socket程序组件时的操作
  • 应用程序调用Socket库中的socket组件时,(操作系统内存管理)在协议栈内存中创建套接字(分配空间,写入初始状态),返回套接字描述符。
  • 应用程序收到描述符,因为套接字中记录了通信双发的信息及状态等信息。后续操作就可以通过这个描述符进行。

连接服务器

套接字刚刚创建完成时,里边并没有存放任何数据。连接实际上是通信双方交换控制信息(比如客户端的IP和端口),在套接字中记录这些必要信息并准备数据收发的一系列操作。

TCP头部格式(20个字节)

  • 发送方端口 16bit
  • 接收方端口 16bit
  • 序号(发送数据的顺序编号) 32bit 随机数
  • ACK号(接受数据的顺序号)32bit 接收方告知发送方已经收到了所有数据的第几个字节
  • 数据偏移 4bit 数据部分的起始位置
  • 保留 6bit
  • 控制位 6bit
  • 窗口16bit 接收方告知发送方窗口大小
  • 校验和 16bit 检查数据是否正确
  • 紧急指针 16bit 紧急处理的数据位置
  • 可选字段 可变长度 长度可变 很少使用
控制信息分类
  • TCP头部中 记录的信息,客户端和服务器相互联络时交换的控制信息
    • 内容在TCP协议中进行定义。在连接,收发,断开各阶段都需要
      • 发送方/接收方端口号,序号(发送数据的顺序编号),ack号(接收数据的顺序编号),数据偏移量,保留位,控制位(URG-紧急指针有效、ACK-数据接收有效、PSH-通过flush操作发送数据、RST-强制断开连接、SYN-连接操作、FIN-断开连接)、窗口、校验和-检查是否出错、紧急指针、可选字段
  • 套接字中记录等信息,依赖控制协议栈操作的信息
    • 应用程序传递来的信息以及从通信对象接收到的信息
    • 收发数据操作的执行状态等信息
连接的过程

从调用Socket库的connect开始

1
connect(<套接字描述符>,<服务器IP地址和端口号>,...)

这些信息会传递给协议栈的TCP模块,然后TCP模块会与该IP地址对应的服务器的TCP模块交换控制信息。

所谓的 3次握手

  • 在TCP模块处创建表示连接控制信息的头部,通过TCP头部的发送方和接收方的端口号可以找到要连接的套接字。随机生成序号seq = x,控制位syn=1
  • TCP头创建成功后,传递给IP模块并委托它发送
  • IP模块执行网络包发送后,网络包通过网络到达服务器
  • 服务器的IP模块将接收到的网络包传递给TCP模块
  • 服务器的TCP模块根据TCP的头部信息找到端口号对应的套接字
  • 在套接字中写入相应的信息,将状态改为正在连接
  • 服务器的TCP模块响应,在TCP头部写上发送方和接收方的端口号以及控制位syn=1,ack=x+1seq=y表示接收到网络包
  • 服务器TCP模块将TCP头部传递给IP模块并委托IP模块向客户端返回响应
  • 网络包到达客户端,通过IP模块到达TCP模块
  • 客户端通过TCP头部信息确认连接服务器操作是否成功
    • 如果SYN为1表示连接成功
  • 连接成功时,会向套接字中写入服务器的IP地址,端口等信息;同时将连接状态改为连接完毕
  • 客户端将ACK设置为1,ack=y+1并返回服务器,告诉服务器响应包收到。
  • 服务器将状态改为连接完毕,操作完成。流程控制被交回到应用程序。

收发数据

控制流程从connect回到应用程序后,就进入数据收发阶段了。

  • 协议栈不关心应用程序传来的数据是什么内容。应用程序在调用write时会指定数据长度。
  • 协议栈并不是一收到数据就马上发送出去,而是先将数据放入内部的发送缓冲区中,并等待应用程序的下一段数据。
    • 不同程序实现不同,有些可能一次性传递所有数据,有些则会逐字节或逐行传递数据。

缓存区积累多少数据发送,不同系统有所不同,通过以下几个要素来判断

  • MTU :每个网络包能容纳的数据长度,协议栈会根据一个叫做MTU的参数进行判断。MTU表示一个网络包的最大长度,在以太网中一般是1500字节。MTU是包含头部的总长度,因此需要从MTU减去头部的长度,然后得到的长度就是网络包所能容纳的最大数据长度,这个长度叫做MSS(最大分段大小)
  • 时间:当应用程序发送数据的频率不高的时候,如果每次都等到长度接近MSS时再发送,可能会因为等待时间太长而造成发送延迟,协议栈内部有一个定时器,经过一定的时间(以毫秒为单位计算)之后,网络包会发送出去。

如果仅靠协议栈来判断发送时机可能会带来一些问题,因此 协议栈也给应用程序保留了控制发送时机的余地。应用程序在发送数据时可以指定一些选项,比如不使用缓存区直接发送

对较大数据进行拆分发送。

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

例如应用程序数据较大,分3段进行发送,MSS 1460字节。

注意:示例中控制位ACK = 1,还有个环节服务端的seq,客户端的ack号没有写。客户端单向通信。

第一段 第二段 第三段
1~1460 1461 ~ 2920 2921 ~ 3000
  • 第一段
    • 客户端 : seq = x ,mss=1460
    • 服务端: ack = x + 1460 (x‘)
  • 第二段
    • 客户端:seq = x‘,mss=1460 ack
    • 服务端:ack=x’ + 1460 (x‘’)
  • 第三段
    • 客户端:seq = x‘’ mss= 1460
    • 服务端:ack = x‘’ + 1460

TCP采用这种方式确认对方是否收到了数据,在得到对方确认之前,数据会保存在发送缓冲区,如果对方没有返回某些包的ack号,那么就重新发送这些包,尝试几次之后强制结束通信,并向应用程序报错

实际上网络的错误检查和补偿机制非常复杂。

返回ACK号的等待时间(超时时间):当网络传输繁忙时会发生阻塞,ACK号的返回就会变慢。将等待时间设置得稍微长些。TCP采用动态调整等待时间的方法

使用窗口有效管理ACK号

等待ACK号时,TCP采用滑动窗口方式来管理数据发送和ACK号的 操作。即:发送一个包后,不用等待ACK号返回,直接发送后续的一系列包。接收方将自己能接收的数量(窗口大小)告诉发送方,发送方不会发送过多的数据

接收方会缓存。多个ACK号发送时,更新窗口大小在最后一个包里即可,发最后一个即可。中间可以省略

断开连接

以服务端为例,4次挥手

  • 服务端应用程序调用Socket库中的close程序,服务器协议栈生成包含断开信息的TCP头部,控制位的FIN=1,同时服务器的套接字也会记录下断开操作的相关信息,发送客户端 (1)
  • 客户端接收到FIN=1的TCP信息时候,客户端协议栈会将自己的套接字标记为进入断开状态,返回给服务器一个ACK号 (2)
  • 完成以上操作时,应用程序会调用read读取数据。协议栈告诉应用程序服务器的数据已全部收到
  • 客户端调用close结束数据收发,此时协议栈同服务器一样。会产生一个控制位的FIN = 1 包,然后委托IP模块发送给服务器 (3)
  • 服务器返回ACK号。此时客户端和服务端的通信就全部结束了 (4)

删除套接字

服务器的通道结束之后,套接字就不使用了。不过套接字不会马上被删除,而是等待一段时间之后再删除。这样是防止误操作。比如以下顺序

(1)客户端发送FIN

(2)服务器返回ACK号

(3)服务器发起FIN

(4)客户端返回ACK号

误操作原因:如果第(4)步的客户端返回ACK号丢失了,服务端没有收到ACK号,需要客户端重发一次。但是客户端套接字已经删除了。信息也消失,端口释放。。碰巧别的程序使用了同一个端口创建套接字,这时候重发的FIN信息会错误的跑到新的套接字里。

删除套接字的等待时间和包重传的操作方式有关。通常等待几分钟。

IP与以太网的包收发操作

包的基础知识

TCP模块在执行连接、收发、断开等各阶段操作时,都需要委托IP模块将数据封装成包发送给通信对象。

包是由头部和数据两部分构成,头部包含目的地址等控制信息

TCP / IP包

MAC头部 IP头部 TCP头部 数据包
以太网控制信息 IP 控制信息 TCP头部和数据块加起来就是包的内容
包的内容 包的内容
IP 包 IP 包 IP 包
以太网包 以太网包 以太网包 以太网包
包的收发操作
  • 包收发操作的起点是TCP模块委托IP模块发送包的操作,TCP模块会指定通信对象的IP地址。

  • 收到委托后,IP模块会将包的内容当作一整块数据,在前边加上包含控制信息的头部:IP头部和MAC头部

    • IP头部包含IP协议规定的、根据IP地址将包发往目的地所需的控制信息
    • MAC头部包含通过以太网的局域网将包传输至最近的路由器所需的控制信息
  • 封装好的包会被交给网络硬件,例如以太网、无限局域网,统称网卡。传递给网卡的包由0和1组成的数字信息,网卡将这些数字信息转换成电信号或光信号,并通过网线发出去
  • 信息到达集线器、路由器,再由设备一步一步地送达接收方
  • 包送达对方之后,对方作出响应。返回的包也会通过转发设备发送回来,然后我们接受这个包,过程是相反的。
  • 由网卡将其转换为数字信息并传递给IP模块,IP模块将TCP头部加上数据块,传递给TCP模块

    IP模块的包收发操作都是相同,并不关心TCP头部信息和数据内容,只负责接收和发送

生产包含IP地址的IP头部

IP模块接受TCP模块的委托负责包的收发工作,他会生成IP头部控制信息并添加在TCP头部前边。

IP头部包含的接收方IP地址,是由TCP模块告知的,而TCP是执行套接字连接时由应用程序那里获知的,因此该地址最初来源于应用程序

IP头部格式 (20个字节)
  • 版本号 4bit 目前版本是4
  • 头部长度IHL 4bit 可选字段导致长度变化,这里需要指定头部长度
  • 服务类型ToS 8bit 表示包传输优先级。
  • 总长度 16bit 表示IP消息的总长度
  • ID号 16bit 用于识别包的编号,一般是包的序列号。如果一个包被IP分片,则所有分片都有相投的ID号
  • 标志Flag 3bit 2个bit有效,分别表示是否允许分片以及当前包是否为分片包
  • 分片偏移量 13bit 表示当前包的内容为整个IP消息的第几个字节开始的内容
  • 生存时间TTL 8bit 表示包的生存时间,为了避免网络回环时包在网络中打转,每经过一个路由器这个值就会减1,直到为0时,包被丢弃
  • 协议号 8bit 16进制:TCP 06 | UDP 11 | ICMP 01
  • 头部校验和 16bit 用于检查错误,现在不使用
  • 发送方IP地址 32bit 网络包发送方的IP地址
  • 接收方IP地址 32bit 网络包接收方的IP地址
  • 可选字段 长度可变 可选字段,一般不用
发送方IP

协议栈的IP模块与路由器中负责包收发的部分都是根据IP协议规则来进行收发操作的。 “IP表”叫路由表

  • 对套接字中记录的目的地IP地址与路由表的目的地栏进行比较,找到匹配行
  • 匹配行中,Interface列表示网卡的网络接口,Gateway表示路由器的IP地址

这样我们就能判断出应该使用哪块网卡来发送包了,然后可以在IP头部的发送方IP地址中填上这块网卡对应的IP地址

协议号

协议号表示包的内容来自哪个模块,例如:TCP模块委托内容,则设置为06 (十六进制)

生产以太网用的MAC头部

生产IP头部之后,接下来IP模块还需要在IP头部之前添加MAC头部。

IP头部中的接收方IP地址表示网络包的目的地,通过这个地址我们可以判断包要发到哪里。但在以太网中,采用匹配的方式将包发往目的地,MAC头部就是干这个的

MAC头部字段 (14字节)
  • 接收方MAC地址 48bit 网络包接收方的MAC地址,在局域网中使用这一地址来传输网络包
  • 发送方MAC地址 48bit 网络包发送方的MAC地址,接收方通过他来判断是谁发送了这个包
  • 以太类型 16bit 常用(十六进制):IP协议 0800 | ARP协议 0806 | IPv6 86DD
发送方MAC

多网卡的情况下,在获取发送方IP时候,可以找到网卡,此处将对应网卡的mac写入即可

接收方MAC

搞清楚把包发给谁,需要查路由表。在路由表中找到相匹配的条目,然后把包发给对应Gateway路由就可以了。现在只需要将路由的MAC填上就可以了。

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

ARP利用广播的形式,将包发给连在同一个以太网的所有设备。

查询结果会放在一个叫做ARP缓存的内存空间中,发ARP广播时,先查缓存,如果找到了对应的MAC,直接使用。

1
2
arp -a # 显示所有ARP缓存
arp -d ip #删除某个缓存

ARP缓存的删除,一般经过几分钟,简单粗暴。不管ARP缓存的内容是否有效。将MAC头部加载IP头部的前边,整个包就完成了。打包的工作由IP模块负责

网卡

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

IP模块生成的网络包知识存放在内存中的一串数字信息,需要转换成电或光信号才能在网上传输。

负责这个操作的就是网卡 + 驱动程序。

系统启动时,网卡驱动程序对硬件进行初始化操作。然后硬件才进入可试用状态。这些初始化操作包括:

  • 硬件错误检查
  • 初始化设置等步骤
  • MAC模块中设置MAC地址

网卡的ROM中保存着唯一MAC地址,这是生产网卡时写入的。也有些特殊的方法,比如从命令或者配置文件读取MAC地址并分配给MAC模块,此时网卡会忽略ROM中的MAC地址

给网络包添加3个控制数据

网卡驱动从IP模块获取包之后,

  • 会将其复制到网卡内的缓冲区中,然后向MAC模块发 发送包 的命令。
  • 接下来MAC模块工作
    • 将包从缓冲区取出
    • 在开头加上报头和起始帧分隔符SFD,在末尾加上用于检查错误的帧校验序列FCS

报头是一串010001类似出现的比特序列,长度为56bit。作用是确定包的读取时机。这些比特序列会转换成特定的 波形,接收方收到信号时,遇到这种波形就可以判断读取数据的时机。

通过时钟信号与数据信号的叠加读取信号

在报头添加用来测量时钟信号的特殊信号,起始帧分隔符用来表示包起始位置。FCS用来检查包传输过程中因噪声导致的波形紊乱、数据错误。采用CRC 循环冗余校验 码。

向集线器发送网络包

网卡加上以上3个控制数据后,就通过 集线器的半双工模式,交换机的全双工模式 可以将包发出去了。

发送和接收同时并行的方式叫全双工,某一个时刻只能进行发送或接收一种操作的叫半双工

在半双工模式下,为了避免信号碰撞,首先判断网络中是否存在设备发送信号。如果有,则等待该信号传输完成。当没有信号传输的情况下,才开始发送信号。

网卡的MAC模块生成通用信号,然后由PHY(MAU)模块转换成可在网线中传输的格式,并通过网线发送出去。PHY(MAU)的指责不仅是将MAC模块的信号通过网线发送出去,还监控接受线路中有没有信号进来。以太网不会确认发送的信号对方有没有收到。万一发生错误,协议栈的TCP回负责搞定。

接收返回包

以太网的包接收和发送一样,和设备类型、TCP的工作阶段以及应用程序的种类无关,是共通的

半双工模式下,一台设备发送的信号会到达连接在集线器上的所有设备。

  • 信号的开头是报头,通过报头的波形同步时钟
  • 然后遇到起始帧分隔符开始将后边的信号转化成数字信息。
  • 网卡

    • PHY(MAU)模块先开始工作,将信号转换成通用格式转给MAC模块

    • 然后MAC模块再从头开始将信号转化成数字信号,并放入缓冲区

    • 信号到达末尾时,检查FCS

      • 绝大多数情况是正确的
      • 错误的情况下,包被丢弃
    • MAC头部中接收方的MAC地址与网卡的MAC地址是否相同

      • 相同,则将包放入缓冲区中
      • 不同,不是自己的,直接丢弃
    • 通过中断机制,通知计算机

      • 网卡向扩展总线中的中断信号发送信号
  • 该信号通过计算机的中断控制程序连接到CPU
  • CPU暂时挂起正在处理的任务,切换到操作系统中的中断处理程序
  • 中断处理程序调用网卡驱动,控制网卡执行响应的接收操作
  • 网卡驱动被中断程序调用后,会从网卡的缓冲区中取到数据,并通过MAC头部的一台类型字段判断协议类型
  • 如果使用的是TCP/IP。网卡驱动就会把这些包交给TCP/IP协议栈
    • 如果不存在协议栈,则视为错误,丢弃
    • 协议栈会判断这个包交给哪个应用程序

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

假设web服务器返回了一个网络包,服务器返回的包的以太类型应该是0800,因此网卡驱动将其交给TCP/IP协议栈来处理。

  • IP模块工作
    • 检查IP头部,确认格式是否正确
    • 检查接收方IP地址
      • 如果接收方的IP地址与网卡的IP地址一致,则可以接收该包
      • 如果接收方IP地址不是自己的地址,IP模块会通过ICMP消息告知发送方
    • IP协议中一个叫分片的功能,将他们还原成原始包
    • IP模块工作结束,交给TCP模块
  • TCP模块
    • 根据IP头部的接收方和发送方IP(IP模块附加参数),以及TCP头部的接收方和发送方端口号来查找响应的套接字
    • 找到套接字之后,根据套接字中记录的通信状态,执行响应的操作
      • 如果包的内容是应用程序数据,则返回确认接收的包,并将包放入缓冲区等待应用程序读取
      • 如果是建立/断开连接的控制包,则返回响应的响应,并告知应用程序建立和断开连接的操作状态

UDP协议的收发操作

不需要重发的数据,使用UDP更高效。

控制用的端数据,适合使用UDP。像DNS查询等交换控制信息基本上可以在一个包的大小范围内解决,这种情况使用UDP较合理。不需要接收确认,窗口等机制。直接加上UDP头部就可以发送了。接收也简单,只要根据IP头部的接收方和发送方的IP地址,以及UDP头部的接收方和发送方端口号,就可以找到响应的套接字,并将数据交给应用程序就可以了。

UDP头部的控制信息 (8字节)

  • 发送方端口号 16bit
  • 接收方端口号 16bit
  • 数据长度 16bit udp头部后面数据的长度
  • 校验和 16bit 用于校验错误

音频和视频数据

实际场景,如果音频/视频在规定的时间不能送达,就会错过播放时机,导致声音图像的卡顿。如果像TCP那样确认响应检查错误并重发会消耗一定的时间,一旦错过了播放时机,重发是没用的。音视频中少了某些包只会造成一些失真或卡顿,也是可以接受的。因此使用UDP发送数据的效率更高

UDP经常被防火墙阻拦