网络是怎么连接的 第二章 协议栈转发tcp

72 阅读18分钟

client TCP模块

client 发送 TCP overview

client 接入互联网部分总览

image.png

最早的 TCP/IP 协议原型设计相当于现在的 TCP 和 IP 合 在一起的样子,后来才拆分成为 TCP 和 IP 两个协议。

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    int sock;  // 用来存储套接字描述符
    struct sockaddr_in server_address;

    // 1. 创建套接字
    sock = socket(AF_INET, SOCK_STREAM, 0); // 创建一个 TCP 套接字
    if (sock == -1) {
        printf("创建套接字失败\n");
        return -1;
    }
    printf("套接字创建成功,描述符为:%d\n", sock);

    // 2. 配置服务器地址
    server_address.sin_family = AF_INET; // IPv4
    server_address.sin_port = htons(8080); // 端口号8080
    server_address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址

    // 3. 连接到服务器
    if (connect(sock, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
        printf("连接服务器失败\n");
        close(sock);
        return -1;
    }
    printf("连接成功\n");

    // 4. 发送数据
    const char *message = "Hello, Server!";
    send(sock, message, strlen(message), 0);
    printf("消息发送成功\n");

    // 5. 关闭套接字
    close(sock);
    printf("套接字已关闭\n");

    return 0;
}

image.png

之前第一章,讲解了浏览器通过发送DNS请求,获取到web server的IP,然后使用这个IP跟对方进行联系,首先就是创建一个socket 进行连接。

1 创建套接字

sock = socket(AF_INET, SOCK_STREAM, 0);

sock = socket(AF_INET, SOCK_STREAM, 0); 的返回值是一个 int 类型的值,它表示套接字描述符。这个描述符是操作系统分配的一个整数,用于标识一个特定的套接字实例。

socket() 是用来创建一个套接字的函数。它的返回值是一个描述符,用来唯一标识这个套接字。如果套接字创建成功,socket() 会返回一个非负整数(即描述符)。如果失败,会返回 -1

  • AF_INET
    • 这是地址族(Address Family)的参数,表示使用 IPv4 地址。
    • 如果你要使用 IPv6,可以用 AF_INET6
  • SOCK_STREAM
    • 这是套接字类型,表示使用 面向连接的流式传输协议
    • 对应的是 TCP 协议(Transmission Control Protocol),它确保数据传输的可靠性和顺序。
    • 如果需要无连接的通信(例如 UDP 协议),可以使用 SOCK_DGRAM
  • 0
    • 这是协议参数,通常设置为 0,表示使用默认协议。
    • 在这种情况下,默认协议是 TCP(对应于 SOCK_STREAM)。

2 创建连接

这是TCP的头部信息 image.png

1 需要把服务器的 IP 地址和端口号等信息告知协议栈, IP已经通过DNS知道,port 固定,http使用80, https使用443 \ 2 把 TCP包头的控制位设置为SYN=1,表示创建连接

3 发送数据

应用程序在 调用 write 时会指定发送数据的长度,在协议栈看来,要发送的数据就是 一定长度的二进制字节序列

协议栈并不是一收到数据就马上发送出去,而是会将数据存放 在内部的发送缓冲区中,等待一定的数量之后才发送;

  1. 第一个判断要素是每个网络包能容纳的数据长度,协议栈会根据一个 叫作 MTU,减去所有mac头部,IP header, TCP header之后,得到MSS,当应用程序传递给TCP的数据长度接近MSS之后才考虑发送出去;
  2. 发送时间也要考虑,如果都等待接近MSS长度才发送出去,那样会耽误非常长的时间;

判断要素就是这两个,但它们其实是互相矛盾的。如果长度优先,那 么网络的效率会提高,但可能会因为等待填满缓冲区而产生延迟;相反地, 如果时间优先,那么延迟时间会变少,但又会降低网络的效率。

有时候,即便缓冲区中的数据长度没有达到MSS,也应该果 断发送出去。为此,协议栈的内部有一个计时器,当经过一定时间之后, 就会把网络包发送出去

这句话的意思可以分解为以下几个要点:

  1. MTU(Maximum Transmission Unit,最大传输单元)

    • 定义:MTU 是网络中每个数据包所能容纳的最大字节数,它决定了链路层(如以太网)一次传输数据的最大长度。
    • 作用:协议栈会根据 MTU 确定每次发送的数据长度,确保不会超过网络链路的传输能力。
  2. TCP 包裹的数据长度

    • 在 TCP 协议中,发送的数据被称为 TCP 段(segment)。协议栈会根据 MTU 计算出 TCP 段中 有效负载(payload) 的最大长度。
    • 计算方式:需要从 MTU 中减去所有协议头的长度,因为头部占用了传输数据的部分空间。
  3. 需要减去哪些头部长度

    • 链路层头部(如以太网):一般为 14 字节(在没有 VLAN 的情况下)。

    • 网络层头部(如 IPv4):典型长度为 20 字节(没有选项时)。

    • 传输层头部(如 TCP):典型长度为 20 字节(没有选项时)。

    例如,对于一个 MTU 为 1500 字节的以太网网络,协议栈会将有效负载长度计算为:

    1500(MTU) - 14(以太网头部) - 20(IP头部) - 20(TCP头部) = 1446 字节
    

    这意味着单个 TCP 段的有效数据负载最多为 1446 字节。

  4. 在实际的通信中, 序号并不是从 1 开始的,而是需要用随机数计算出一个初始值,这是因为 如果序号都从 1 开始,通信过程就会非常容易预测

客户端和服务器双方都需要各自计算序号,因此双方需要在连接过程中互 相告知自己计算的序号初始值。

image.png

在 TCP 协议中,发送方通过“序号”标记报文段在字节流中的起始位置,而接收方则通过解析 IP 头部的总长度、IP 头部长度和 TCP 头部长度,使用公式 数据长度 = IP 总长度 - IP 头部长度 - TCP 头部长度 计算每个报文段的数据长度。尽管 TCP 头部不直接包含数据长度字段,但接收方仍能通过这些信息准确获取数据长度。

在将SYN 设为1 的同时,还需要同时设置序号字段的值,而这里的 值就代表序号的初始值

TCP是双工通信 其实也就是client and server 在收的同时也在发送

image.png

Web 中是先由客户端向服务器发送请求,序号也会跟随数据一起发送。然后,服务器收到数据后再返回ACK 号

TCP 采用这样的方式确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。

如果对方没有返回某些包对应 的ACK 号,那么就重新发送这些包。 TCP 会在尝试几次重传无效之后强制结束通信,并向应用程序报错

滑动窗口要解决的问题

发送一个包就等待一个ACK 号的方式, 实在太浪费时间了

def 滑动窗口(Sliding Window)它允许发送方在没有接收到确认(ACK)的情况下,继续发送一定数量的数据包,而不需要等待每个包的 ACK。这种方式能够有效地利用 往返时间(RTT),避免因等待确认而浪费时间。

但是这又造成一个新的问题:如果不等返回ACK 号就连续发送包,就有可能会出现发送包的频率超过接收方处理能力的情况

接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口方式的基本思路。

因为,

当接收方的TCP 收到包后,会先将数据存放到接收缓冲区中

接收方需要计算ACK 号,将数据块组装起来还原成原本的数据并传递给应用程序

如果server 来不及处理 下一个包也会被暂存在接收缓冲区中

缓冲区溢出之后,后面的数据就进不来了

在 TCP 通信中,接收方的处理能力有限,通常通过接收缓冲区来存储接收到的数据。如果数据到达的速度超过了接收方处理和传递给应用程序的速度,接收缓冲区可能会被填满,导致溢出。一旦缓冲区溢出,后续的数据将无法被接收,造成数据丢失,影响通信的可靠性。

为防止这种情况,TCP 引入了滑动窗口机制。接收方会在 TCP 报文的窗口大小字段中告知发送方其当前可用的接收缓冲区大小。发送方根据接收方通告的窗口大小,控制数据的发送速率,确保不会超出接收方的处理能力。这种机制实现了流量控制,保证了数据传输的可靠性和效率。

窗口大小,它是TCP 调优参数中非常有名的一个。

举个例子

  1. 初始状态:

   - B 的接收缓冲区为空,窗口大小为 500 字节。

   - A 可以发送最多 500 字节的数据而无需等待确认。

  1. A 发送数据:

   - A 发送了 300 字节的数据。

   - B 接收到这 300 字节,并将其存入接收缓冲区。

   - 此时,B 的接收缓冲区还剩余 200 字节的空间。

  1. B 通告新的窗口大小:

   - B 向 A 发送 ACK 确认,并通告新的窗口大小为 200 字节。

   - A 根据新的窗口大小,最多再发送 200 字节的数据。

  1. A 继续发送数据:

   - A 发送了 200 字节的数据。

   - B 接收到这 200 字节,并将其存入接收缓冲区。

   - 此时,B 的接收缓冲区已满,无法再接收新的数据。

  1. B 通告窗口关闭:

   - B 向 A 发送 ACK 确认,并通告窗口大小为 0,表示暂时无法接收新的数据。

   - A 停止发送数据,等待窗口重新打开。

  1. B 处理数据并释放缓冲区:

   - B 将缓冲区中的数据传递给应用程序,释放了 300 字节的空间。

   - B 向 A 通告新的窗口大小为 300 字节。

  1. A 恢复发送:

   - A 根据新的窗口大小,继续发送最多 300 字节的数据。

 

TCP 协议在每个连接的两端都维护着两个独立的缓冲区:发送缓冲区和接收缓冲区。

TCP 协议在每个连接的两端都维护着两个独立的缓冲区:发送缓冲区和接收缓冲区。

发送缓冲区(Send Buffer):

当应用程序调用 send()write() 函数发送数据时,数据首先被复制到内核的发送缓冲区中。随后,TCP 协议会根据网络状况和流量控制机制,将这些数据分段并发送到网络中。发送缓冲区的主要作用是:

  • 缓存待发送的数据:确保应用程序可以快速将数据提交,而无需等待数据实际发送到网络。

  • 实现可靠传输:在未收到对方确认(ACK)之前,数据会保留在发送缓冲区中,以便在需要时重新传输。

接收缓冲区(Receive Buffer):

当网络上的数据包到达时,TCP 协议会将其放入接收缓冲区中。应用程序调用 recv()read() 函数时,从该缓冲区读取数据。接收缓冲区的主要作用是:

  • 缓存已接收但未被应用程序处理的数据:确保数据不会因应用程序处理速度较慢而丢失。

  • 实现流量控制:通过通告窗口大小,告知发送方当前可接收的数据量,防止发送方发送过多数据导致接收方缓冲区溢出。

ACK和窗口更新合并发送

在 TCP 协议中,接收方在收到数据后,会经历delay confirm + 如果server 在此期间windows size changed will send updated windows size + ACK number to 发送方

因为,server 接收一个数据包会存到接收队列中等待处理,缓冲区中的数据随后被传递给服务器上的应用程序进行处理, 

之后才需要更新窗口大小; 其实就是接收一个数据包,等待app处理来释放windows size ,在此期间,不着急发送ACK 

示例:

假设发送方 A 向接收方 B 发送数据:

  • 步骤 1: A 发送数据包给 B。

  • 步骤 2: B 接收到数据包,但暂不立即发送 ACK,而是等待一段时间,看看是否有数据需要发送。

  • 步骤 3: 在等待期间,B 的应用程序处理了部分数据,接收缓冲区的可用空间增加,需要更新窗口大小。

  • 步骤 4: B 将 ACK 和窗口更新信息合并在一个报文中发送给 A。

 

当需要连续发送ACK 号时,只要发送最后一个ACK 号就可以了

可以减少包的数量

连接断开

当应用程序调用阻塞式的 read 函数时,如果接收缓冲区中没有可用数据,系统会将该调用挂起,应用程序会在此调用处等待,直到有数据到达并被读取为止。在此期间,应用程序的执行被阻塞,无法继续进行后续操作。

HTTP1.0 的情形

浏览器向Web 服务器

发送请求消息,Web 服务器再返回响应消息,这时收发数据的过程就全部

结束了,服务器一方会发起断开过程

在HTTP1.1 中,服务器返回响应消息之后,

客户端还可以继续发起下一个请求消息,如果接下来没有请求要发送了,

客户端一方会发起断开过程。

在http连接中,这个很重要

 客户端的端口号是从空闲的端口号中随意选择的

断开连接的操作,四次挥手之后,并不会马上删除套接字  因为如果马上删除 套接字对应的端口号就会被释放出来新的进程 创建新的 套接字 新套接字碰巧又被分配了同一个端口号 他马上会收到server 的FIN 

之所以不马上删  除套接字,就是为了防止这样的误操作

client IP模块

IP 模块负责添加如下两个头部。 (1)MAC 头部:以太网用的头部,包含 MAC 地址 (2)IP 头部:IP 用的头部,包含 IP 地址

这是IP的header

image.png

ARP协议查询mac地址

网卡并不是通上电之后就可以马上开始工作的,而是和其他硬件一样,

都需要进行初始化。也就是说,打开计算机启动操作系统的时候,网卡驱

动程序会对硬件进行初始化操作,然后硬件才进入可以使用的状态

网卡中的ROM并不是用作缓存设备,它的主要作用是存储固定的信息,例如网卡的MAC地址和某些网卡驱动程序需要的基本硬件信息。它是只读存储器

下面是MAC头部的信息

image.png

ARP(地址解析协议)主要用于局域网(LAN)中,将已知的IP地址解析为对应的MAC地址。在局域网内,数据链路层使用MAC地址进行通信,而ARP的作用是通过广播方式查询目标IP地址对应的MAC地址,以实现主机间的直接通信。

在广域网(WAN)中,数据传输通常通过路由器等网络设备进行,这些设备使用IP地址进行路由选择。由于不同子网之间的通信需要经过路由器的转发,ARP协议的广播机制无法跨越路由器,因此ARP协议主要在局域网内发挥作用。

详细解析文档  cloud.tencent.com/developer/a…

ARP的工作原理

假设HostA和HostB在同一个网段,HostA要向HostB发送IP包,其地址解析过程如下:

1.HostA首先查看自己的ARP表项,确定其中是否包含HostB的IP地址对应ARP表项。如果找到了对应的表项,则HostA直接理由ARP表项中的MAC地址对IP数据包封装成帧,并将帧发送给HostB。

2.如果HostA在ARP表中找不到对应的表项,则暂时缓存该数据包,然后以广播方式发送一个ARP轻轻。ARP请求报文中的发送端IP地址和发送端MAC地址为HostA的IP地址和MAC地址,目标IP地址HostB的IP地址,目标MAC地址为全0的MAC地址。

3.由于ARP请求报文以广播方式发送,该网段上的所有主机都可以接收到该请求。HostB比较自己的IP地址和ARP请求报文中的目标IP地址,由于两者相同,HostB将ARP请求报文中的发送端(HostA)IP地址和MAC地址存入自己的ARP表中,并以单播方式HostA发送ARP响应,其中包含了自己的MAC地址。其他主机发送请求的IP地址并非自己,于是都不做应答。

4.HostA收到ARP响应报文后,将HostB的MAC地址加入到自己的ARP表中,同时将IP数据包用此MAC地址为目的地址封装成帧并发送给HostB。

如果目标不在同一网段,数据包将转发到默认网关,由网关负责将数据包路由到适当的目的地

以太网的三个核心特性,即使经过多次演变,这些特性仍然保持不变:

目的地址:每个以太网帧都包含一个目标MAC地址,用于指明数据包的接收者。
源地址:帧中还包含一个源MAC地址,用于标识发送者的身份。
以太类型:帧内的以太类型字段用于指示数据包的协议类型,帮助接收方正确解析数据内容。

因此,具备上述三个特性的网络可被视为以太网。

将MAC 头部加在IP 头部的前面,整个包就完成了。到这里为止,整个打包的工作是由IP 模块负责的

DNS和ARP有相似性

DNS和 ARP协议的对比

在功能和机制上存在以下相似之处:

  1. 解析功能

   - ARP:将已知的IP地址解析为对应的MAC地址,确保数据在局域网内正确传输。

   - DNS:将人类易读的域名解析为对应的IP地址,方便用户访问互联网资源。

  1. 查询机制

   - ARP:通过在局域网内广播ARP请求,询问目标IP地址对应的MAC地址。

   - DNS:通过向DNS服务器发送查询请求,获取域名对应的IP地址。

  1. 缓存机制

   - ARP:将解析得到的IP地址与MAC地址的对应关系存入ARP缓存,以减少重复查询,提高通信效率。

   - DNS:将解析得到的域名与IP地址的对应关系存入DNS缓存,减少重复解析,加快域名解析速度。

MAc包的开头

前导0+ 接收方和发送方的mac+ 协议类型

image.png MAC数据包的起始数据如下

前导码由**7个字节(每字节8位)**组成

  • 比特序列:10101010 10101010 10101010 10101010 10101010 10101010 10101010

  • 总长度:7 × 8 = 56位

  • 后面还跟着一个“帧起始定界符”(Start Frame Delimiter, SFD),它的值为10101011,标志数据帧正式开始。

前导码的作用如下
便于时钟同步

  • 发送方在发送“10101010...”时,每个比特的高低电平交替变化,接收方可以轻松同步自己的时钟频率。

使用下面的命令输出当前操作系统支持的协议栈

cat /proc/net/protocols

client UDP模块

UDP 头部信息很短

image.png

不需要重发的数据用UDP 发送更高效,场景如下:

  1. DNS 查询等交换控制信息的操作基本上都可以在一个包的大小范围内解决,这种场景中就可以用UDP 来代替TCP
  2. 有另一个场景会使用UDP,就是发送音频和视频数据的时候,音频和视频数据中缺少了某些包并不会产生严重的问题,只会产生一些失真或者卡顿而已,一般都是可以接受的

在网络通信中,接收方的 IP 地址位于IP 头部,而TCP 头部中不包含 IP 地址信息。

IP 头部的主要字段包括:

  • 源 IP 地址:发送方的 IP 地址。
  • 目的 IP 地址:接收方的 IP 地址。

这些字段用于在网络层确定数据包的传输路径。

TCP 头部的主要字段包括:

  • 源端口号:发送方的端口号。
  • 目的端口号:接收方的端口号。

这些字段用于在传输层标识通信的应用程序或服务。