Linux高级--2.4.1 网络概念(分层、TCP)
关于网络分层理解的难点
对于一般人(不参与设计和维护网络协议栈的人)来讲,物理层和应用层很容易理解,也很好记住。首先,物理层是看的到的网线、基站的实体。再者,应用层是用户自己参与编写的程序。
而那些不长接触的数据链路层、IP层、网络层 确实最难理解和记住的。下面的内容会重点讲这三层。
应该很多人有这样的疑问:IP层和网络层为什么不合并在一起,既然分层那这两层的界限在哪?数据链路层又和IP层之间的界限又在哪?
IP层和网络层为什么不合并在一起?
1. 有些协议只工作在IP层,比如IPv4/IPv6、ICMP、ARP、RIP、OSPF、BGP、
IGMP等。(这些协议操作是针对IP的存在相关的)
2. IP层对于包的传输有自己的策略:上层给的长度大于MTU,则分片,收到分片的包要
合并,一个片丢了则所有片都丢掉。校验不过则丢掉,没有重传、备份、确认机制。
这样尽可能保证IP层任务处理的简单性,最小化性---这也是保证运行在IP层协议的
轻量性。至于数据完整性,则靠上层协议自己保证--》使得上层协议的的多样性。
因为上层协议就是可能必须要可靠传输(TCP),或者必须要不可靠传输(UDP)。
IP层和MAC层为什么不合并?
1. 有的协议只在MAC层工作:主要涉及物理地址和链路层的控制,如Ethernet、gPTP、
HDLC、PPP、Wi-Fi、VLAN、Frame Relay、FDDI等。(这些协议是针对设备的操
作,因为一个设备通常是一个网卡)
2. MAC层没有分包和组包,因为网卡为了保证传输的速度只管收包,且MAC一般依赖网
卡的DMA技术快速缓存移动(不依赖CPU),没法分包和组包。所以分包和组包的
工作就交给上层了。
3. MAC层有重传,对设备来说是守大门的,对于最基本的CRC校验错误的包,不能让流
转回上层,否则就浪费了资源和时间。
// 一个IPv4数据包的结构
struct ip_packet {
uint8_t version; // IP版本(IPv4或IPv6)
uint8_t ihl; // IP头部长度
uint8_t tos; // 服务类型
uint16_t total_len; // 总长度(IP头部 + 数据部分)
uint16_t id; // 标识
uint16_t frag_off; // 分片偏移
uint8_t ttl; // 生存时间
uint8_t protocol; // 协议类型(TCP或UDP等)
uint16_t checksum; // 校验和
uint32_t src_ip; // 源IP地址
uint32_t dest_ip; // 目的IP地址
uint8_t *data; // 数据部分(载荷),指向TCP或UDP段
};
struct tcp_segment {
uint16_t src_port; // 源端口
uint16_t dest_port; // 目的端口
uint32_t seq_num; // 序列号
uint32_t ack_num; // 确认号
uint8_t data_offset; // 数据偏移(TCP头部长度)
uint8_t flags; // 控制标志位(如SYN, ACK, FIN等)
uint16_t window_size; // 窗口大小
uint16_t checksum; // 校验和
uint16_t urgent_pointer; // 紧急指针
uint8_t *data; // 数据部分(载荷)
};
struct udp_datagram {
uint16_t src_port; // 源端口
uint16_t dest_port; // 目的端口
uint16_t length; // UDP长度(头部 + 数据)
uint16_t checksum; // 校验和
uint8_t *data; // 数据部分(载荷)
};
struct sock {
struct socket *socket; // 套接字的实际结构
struct net_device *dev; // 绑定的网络设备
struct sockaddr_in local_addr; // 本地地址和端口
struct sockaddr_in remote_addr; // 远程地址和端口
unsigned short state; // 套接字的状态
unsigned int flags; // 套接字的标志
struct sk_buff_head sk_receive_queue; // 用于接收数据的队列
struct sk_buff_head sk_send_queue; // 用于发送数据的队列
int protocol; // 协议类型(UDP,TCP等)
...
};
struct tcb {
unsigned long seq; // 当前的发送序列号
unsigned long ack; // 当前的接收确认号
unsigned long snd_wnd; // 发送窗口大小
unsigned long rcv_wnd; // 接收窗口大小
unsigned long snd_nxt; // 下一次要发送的序列号
unsigned long rcv_nxt; // 下一次要接收的序列号
unsigned long rcv_buf_size; // 接收缓冲区大小
struct sockaddr_in src_addr; // 源地址
struct sockaddr_in dest_addr; // 目标地址
int state; // 连接的当前状态(例如 ESTABLISHED, SYN_SENT 等)
// 其他字段,包括缓冲区、计时器、重传机制等
};
struct sk_buff {
struct sk_buff *next; // 链表中的下一个skb,用于链表或队列中的组织
struct sk_buff *prev; // 链表中的前一个skb
struct net_device *dev; // 指向网络设备的指针(网络接口卡)
unsigned int len; // 数据包长度
unsigned char *data; // 数据指针,指向包的有效载荷
unsigned char *tail; // 数据包尾指针
unsigned char *end; // 数据包的结束指针
unsigned char *head; // 数据包的起始指针
struct socket *sk; // 套接字,指向相关的socket
unsigned int protocol; // 协议字段,标识当前skb的数据类型(比如IP,ARP等)
unsigned int priority; // 包的优先级
struct dst_entry *dst; // 路由目的地信息(用于IP层)
struct sk_buff *next_free; // 空闲链表中的下一个skb
...
};
网络层与传输层概述
网络层:
- 抽象概念:网络层是基于 IP 的抽象概念,与数据链路层用 MAC 地址标记设备不同。MAC 地址是一种具体化的概念,绑定于所在的物理网络,而 IP 地址可以是固定的,也可以通过路由动态变化。
- 功能:所有“网络”的“网”是基于 IP 的抽象概念,而链路层的“路”是基于 MAC 的具体化概念。
传输层:
- 协议决定内容:传输层与物理信号的形式(光信号、电信号、无线信号)或目的地无关,传输层的协议决定了传输的内容是可靠的还是不可靠的。例如:
- 不可靠传输协议:适用于允许丢帧和数据丢失的流媒体传输。
- 可靠传输协议:适用于需要保密、稳定且不允许数据损坏或丢失的场景,并能控制传输的速度和质量。
数据链路层的作用与网卡驱动
数据链路层的命名
数据链路层之所以称为“链路层”,是因为它负责在物理链路上传输数据。链路指网络中相邻节点之间的物理连接(例如两台计算机或交换机)。
数据链路层的主要作用
-
帧的封装和拆解:
- 把网络层的数据打包成帧(Frame)。
- 在接收端解包帧,提取网络层数据。
-
差错检测与纠正:
- 使用校验和机制(如 CRC)检测传输过程中的错误。
- 部分情况下还能纠正错误。
-
流量控制与重传:
- 控制数据流速,防止发送过快导致接收端无法处理。
- 在数据帧丢失时请求重传。
-
MAC 地址管理:
- 确保数据在局域网内正确发送到目标设备。
数据链路层与网卡驱动的关系
- 数据链路层的职责:定义如何在物理链路上传输帧,并确保数据传输的可靠性。
- 网卡驱动的职责:
- 是数据链路层的一部分。
- 负责从网卡接收数据并传递给网络层。
- 与操作系统协作完成数据链路层功能。
数据链路层与网络层的关系
- 网络层的职责:负责逻辑寻址和跨网络的数据路由。
- 网络层的工作方式:
- 接收来自链路层的帧,解封装出 IP 数据包。
- 基于 IP 地址和路由信息,将数据传递到目的地。
TCP/IP 协议栈
tcp组包时 校验和 计算 -- 按照协议中指出,校验和本身在校验时 作为0计算。
校验和前面 还包含了 伪包头 包含了IP地址,是为了防止路由错IP
主要组成部分
- 链路层(Link Layer):对应 OSI 模型中的数据链路层。
- 网络层(Internet Layer):主要协议是 IP,负责跨网络的数据路由。
- 传输层(Transport Layer):如 TCP 和 UDP,提供端到端的数据传输。
- 应用层(Application Layer):处理应用数据和服务,如 HTTP、FTP。
数据包的传递流程
- 链路层负责在物理网络中传输数据帧。
- 网络层通过路由实现数据的跨网络传递。
- 传输层提供可靠或不可靠的数据传输服务。
网卡驱动与 sk_buff
结构体
sk_buff
的作用
- 是 Linux 内核中描述网络数据包的重要数据结构。
- 用于存储和管理网络协议栈的所有数据包。
典型的数据包接收流程
- 网卡驱动接收数据:
- 网卡驱动接收到数据包后,分配一个
sk_buff
。 - 将数据存储到
sk_buff
的缓冲区中。
- 网卡驱动接收到数据包后,分配一个
- 传递给协议栈:
- 通过
netif_rx()
等函数将sk_buff
传递给网络层。
- 通过
- 网络层处理:
- 解封装数据,提取 IP 包进行进一步处理。
sk_buff
的主要字段
data
:指向数据部分。len
:数据长度。protocol
:上层协议类型。dev
:关联的网络设备。- 头部信息指针:
mac_header
、network_header
、transport_header
。
sk_buff
示例
struct sk_buff *skb = netdev_alloc_skb(dev, len);
if (!skb) {
return -ENOMEM;
}
memcpy(skb_put(skb, len), data, len);
skb->protocol = eth_type_trans(skb, dev);
netif_rx(skb);
传输层与 TCB 结构体
TCB 的作用
- 传输控制块(TCB, Transmission Control Block)用于管理和维护 TCP 连接。
-
在Linux内核中,TCB(Transmission Control Block)是专门为TCP通信服务的,它是TCP连接的核心数据结构,用来维护和管理每个TCP连接的状态。在UDP中并没有类似于TCP的TCB结构,因为UDP是无连接的协议,它不需要维护像TCP那样的复杂状态。
对于TCP,TCB通常是指Linux内核中的
,TCB 主要由struct tcp_sock
,它继承自struct inet_sock
,而后者又继承自struct sock
。这些结构体联合起来负责维护一个TCP连接的各种状态和信息。struct tcp_sock
表示。
tcp_sock
定义位置
- 定义于
include/net/tcp.h
。
TCB(struct tcp_sock
)的主要成员变量:
-
发送和接收窗口:
snd_wnd
:发送窗口的大小。rcv_wnd
:接收窗口的大小。snd_nxt
:下一个要发送的字节序列号。rcv_nxt
:下一个期望接收的字节序列号。
-
重传和拥塞控制:
snd_cwnd
:拥塞窗口,用于拥塞控制。snd_ssthresh
:慢启动阈值,用于拥塞控制。retransmit_timer
:重传定时器,用于控制何时触发重传。
-
连接状态和控制:
state
:TCP连接的状态(例如,ESTABLISHED
、SYN_SENT
、CLOSED
等)。fastopen_req
:快速打开请求的相关信息。
-
RTT(往返时间)和RTO(重传超时):
srtt_us
:平滑的往返时间,用来估算RTO。rttvar_us
:RTT变化,用于计算RTO。rto
:重传超时的值。
-
序列号和确认号:
snd_una
:已发送但未确认的最早字节序列号。snd_up
:紧急指针的序列号。rcv_up
:接收端的紧急指针。
-
TCP选项:
tcp_header_len
:TCP头的长度。tcp_mstamp
:TCP报文的时间戳。options
:TCP连接的选项,例如SACK(选择性确认)、TS(时间戳)等。
-
定时器:
delack_timer
:用于延迟确认ACK的定时器。retransmit_timer
:重传定时器。
tcp_sock
的连接生命周期
- 初始化:三次握手时分配并初始化。
- 状态更新:更新发送和接收窗口的序列号。
- 关闭连接:释放
tcp_sock
,完成连接关闭。
传输层的数据流向
- 发送方向:
- 数据通过
send()
进入传输层,存储到发送缓冲区。 - 在合适时机发送到网络。
- 数据通过
- 接收方向:
- 数据通过网卡驱动传递到
sk_buff
,存储到接收缓冲区。 - 应用层通过
recv()
获取数据。
- 数据通过网卡驱动传递到
tcp_sock
示例
struct tcp_sock {
__u32 snd_nxt;
__u32 rcv_nxt;
__u32 snd_una;
struct sk_buff_head write_queue;
struct sk_buff_head receive_queue;
// 其他状态变量
};
TCB的连接状态和半连接状态:
是的,在TCP协议中,TCB(Transmission Control Block)不仅维护与连接状态相关的信息,还负责管理全连接队列和半连接队列,这些队列在TCP三次握手过程中起着非常重要的作用。
在Linux内核中,全连接队列和半连接队列分别管理已经建立和正在建立的TCP连接。这些队列的核心与struct sock
以及相关的数据结构密切相关。
-
半连接队列(Syn Queue):
- 半连接队列中保存的是处于SYN_RECEIVED状态的连接,即那些客户端发起了SYN请求,服务端回复了SYN+ACK,但还未收到客户端的ACK确认的连接。
- 这些连接还没有完全建立,仍在等待客户端的最终ACK。
- 半连接队列是为了解决TCP三次握手中的中间状态,常用于抗击SYN Flood攻击(半连接攻击)等问题。
在Linux内核中,半连接队列由
struct request_sock
管理,它是inet_csk
模块的一部分。半连接队列使用inet_csk_reqsk_queue
来处理。 -
全连接队列(Accept Queue):
- 全连接队列中保存的是已经完成三次握手、处于ESTABLISHED状态的连接,即已经完全建立的TCP连接。
- 服务端已经准备好接受数据传输,等待应用程序调用
accept()
函数来从队列中取出连接。 - 全连接队列的大小可以通过
/proc/sys/net/ipv4/tcp_max_syn_backlog
参数进行调整,它定义了全连接队列的最大长度。
在Linux内核中,已建立的连接由
struct sock
(具体来说是struct tcp_sock
)管理,已完成的连接进入全连接队列等待应用层程序的处理。
相关结构体
- 半连接队列(
struct request_sock
):用于管理尚未完全建立的连接。 - 全连接队列(
struct tcp_sock
):用于管理已经建立的TCP连接。
三次握手过程中的队列变化
-
客户端发送SYN:
- 服务端收到客户端的SYN请求后,在半连接队列中创建一个新的
request_sock
条目,并进入SYN_RECEIVED状态。
- 服务端收到客户端的SYN请求后,在半连接队列中创建一个新的
-
服务端发送SYN+ACK:
- 该连接在半连接队列中继续等待客户端的ACK确认。
-
客户端发送ACK:
- 一旦服务端收到客户端的ACK,三次握手完成,服务端会将该连接从半连接队列中移出,放入全连接队列中。
- 之后,服务端等待应用程序通过
accept()
函数来处理这个连接。
总结
- 半连接队列:用于存放正在进行TCP三次握手、未完全建立的连接,服务端处于SYN_RECEIVED状态。
- 全连接队列:存放已完成三次握手、处于ESTABLISHED状态的连接,等待应用层处理。
这两个队列在TCP连接建立过程中起到了维护连接状态、管理连接流量的作用。
0voice · GitHub