前言
作为本系列的第一篇文章,让我们首先聊聊协议栈模型(stack layer model)。还记得在大学课堂里的时候学TCP/IP的时候,我其实并不能分清楚ISO/OSI分层模型和TCP/IP分层模型二者的区别. 就是下面这幅图
可以看出,虽然层次划分方式有区别,但大体上还是能对应上的。那么,为什么要这么分层?
为什么要分层
分层的好处在于,每一层可以各司其职,只关心自己这一层的功能,而不去关心上面下面需要做什么。每一层使用下面提供的接口,同时为上一层提供服务。每一层可能有多种实现,比如传输层(Transport)可以使用TCP
,也可以使用UDP
,它们都可以使用IP
作为下层网络层(Network)的实现。他们就像乐高积木一样, 你可以将不同的积木拼接起来.
报文在层间穿梭
这样说或许还比较抽象,我们来理一理报文在协议栈中的穿梭流程
以一个标准的使用TCP/IP
的HTTP
报文为例
它由多个Header
和一个Data
部分组成,这些Header
就是协议栈中各层对在报文上留下的痕迹,越上层Header
越靠近Data
。
发送方向
发送数据时,应用层在原始数据前面贴上HTTP Header
,结果作为整体下发给传输层(TCP
),TCP
才不管报文的组成,对它来说,现在的报文都将作为TCP
的Payload
。然后TCP
在现在的报文前面贴上TCP Header
,再作为payload
下发给下层(IP
). 以此类推,报文前方又贴上了IP Header
和MAC Header
,最终从实际设备发送出去了。
那么问题来了,为什么在上面的过程中,报文走的是传输层走的是
TCP
而不是UDP
,应用层走的是IP
而不是其他网络层实现 ,最后贴的以太头MAC
是贴的什么?
答案是,在应用层其实就指定好了。有过一点socket
编程经验应该知道,创建套接字的的接口是
int socket(int domain, int type, int protocol)
这里的domain
和protocol
就决定了网络层用哪个,比如IP
其他。
type
决定了传输层使用什么,比如是TCP
还是UDP
。
由此可见, 向外发送的报文通过传输层和网络层的路径在socket
创建的时候就已经决定好了.
而MAC Header
中的源MAC
是根据实际发送报文的出接口决定的,这会涉及一些路由相关的东西,比如对于一台有两个网卡的电脑, 每个网卡有自己的MAC
地址, 路由过程会根据目的地选择其中一个网卡作为报文的实际出接口,那么MAC Header
中的SMAC
就是该网卡的MAC
地址
至此, 我们就可以描绘出发送过程报文在内核协议栈中的穿梭路径了
接收方向
再来看接收方向, 当报文由网卡驱动程序递交给协议栈时, 就要开始它的协议栈旅程了. 和发送过程正好相反, 发送是一层一层贴Header
, 接收是一层一层脱Header
了.
还是上面的例子, 假设现在设备驱动程序收到完整的HTTP
以太帧了, 将报文上送给协议栈, 协议栈最下面的链路层(姑且这么称呼) 拿到报文了, 那么他是要将其上送给网络层 (比如IP
) ? 还是 ARP
或者 RARP
?
回想上面的报文图, 强调一下: 由于层与层是各司其职的, 所以这里链路层只能看到MAC Header
和后面的Payload of Ethernet Frame
, 而且, 它只能去理解前者, 对于后者, 它根本无法理解. 所以它必须从MAC Header
就能分析出该把这个报文给谁.
显然, Dest MAC
和Src MAC
是用来描述设备地址的, 那么答案很明显了, 只有type
字段。type
字段描述了该以太帧的类型, 比如IP
就是0x0800
, ARP
是0x0806
也就是说,当我解析到报文type
是0x0800
时, 就要将报文脱去MAC Header
后, 扔给IP
.
IP
收到后怎么办呢, 在进行自己的处理之后, 它需要把报文上送给上层(传输层) . 给传输层谁呢, 是TCP
还是UDP
还是其他 ? 一样的道理, 这个信息就在IP Header
里, 下面是IPv4
版本的Header
(IPv6
一样的道理)
看! 关键就是protocol
字段, 它指明了该使用的传输层协议. 比如TCP
是 6 , UDP
是 17. 在我们上面的例子中, 收到报文中这个字段一定是 6, 所以IP
将报文脱掉IP Header
后就会上送给TCP
.
TCP
收到后怎么办呢, 首先是自己的处理(这部分内容很多, 但我不打算在本文中谈), 最后, TCP
会将报文上送给应用层. 一台主机上,使用TCP
的应用可能有很多,那么TCP
如何正确分发呢。 还是一样,秘密就在TCP Header
中。
在一台主机上,每个应用程序使用的端口号是不同的,正是通过端口号的唯一性,TCP
就能知道该送给哪个应用层。从这里可以推断出, 内核一定是有一张类似表的结构,记录了使用的端口号和对应应用程序的信息。其实这个信息正是保存在内核的socket
结构里的。在发送过程中,我们没有关注TCP Header
中的src port
和dst port
,但其实 这两个端口号也是保存在内核socket
结构中,TCP
在填写TCP Header
时, 也正是根据它.
所以, TCP
根据Header
中的dst port
, 会去看哪个socket
结构上的src port
与它一致, 一致就说明应该这个报文脱掉TCP Header
后丢给这个socket
,而应用层程序就可以通过这个socket
读取到数据了
松耦合
内核协议栈层与层之间是松耦合的. 比如对于网络层IP
来说, 它的报文上送代码一定不是这样:
ip_local_deliver(skb)
{
protocol = get_protocol(skb)
switch (protocol):
case TCP:
tcp_rcv(skb);
break;
case UDP:
udp_rcv(skb);
break;
......
}
这样并不是不能工作, 只是如果这样, 当新添加一种传输层协议时, 还要在IP
的代码里新加一个case
, 这就不合理了. 所以, 内核采用的办法是, 各种传输层实现一组形式一样的接口, 注册到内核中, IP
根据Header
中的protocol
, 一个一个看这个protocol
是否已经注册了, 找到了就调用注册的接收函数. 像这样:
static struct net_protocol tcp_protocol {
.handler = tcp_v4_rcv,
}
static struct net_protocol udp_protocol {
.handler = udp_v4_rcv,
}
{
inet_add_protocol(&tcp_protocol, 6)
inet_add_protocol(&tcp_protocol, 17)
}
ip_local_deliver(skb)
{
protocol = get_protocol(skb) // TCP报文会返回 6 UDP报文会返回 17
transport = get(protocol); // TCP报文会返回 tcp_protocol UDP报文会返回 udp_protocol
transport->handler(skb);
}
这里的struct net_protocol
就是每种传输层实现需要实现的接口. 这种统一的形式(比如上面都实现了handler
函数)使得IP
可以不用管系统中究竟有实现了哪些传输层.后续即使添加新的传输层实现, IP
的逻辑也不用变. 这就是松耦合带来的好处.
上面只是网络层<->传输层的例子, 实际上, 协议栈的层与层之间都是以这种形式工作的. 这也是内核网络的设计美学.
总结
- 协议栈划分了不同的层次,每个层次都有多种实现. 无论是发送还是接收,报文都按顺序经过每一层的一种实现
- 发送过程穿梭路径在套接字创建的时候就已经确定了, 层层贴头
- 接收过程层层去头, 头中的信息指明了报文该送到上层哪个实现
- 协议栈层与层之间的松耦合体现了协议栈的设计美学