文章目录
一、TCP/IP分层思想
直接看理论书籍不够直观高效,这个系列依然主要从如何实现的角度介绍TCP/IP协议。就像前面介绍操作系统,主要从比较简单的UCOS源码介绍操作系统的实现,对RTOS的实现有了深入的了解,再去阅读Linux的源码会更容易理解。这里介绍的TCP/IP系列也从相对轻量的LwIP协议栈源码的实现来介绍TCP/IP协议的原理和应用,LwIP协议源码如下:http://git.savannah.gnu.org/cgit/lwip.git
这里选择相对简单的LwIP 1.4.1版本作为示例代码,下面先看下TCP/IP经典的分层模型:
- 网络接口层:定义数据帧(对电信号0/1进行的特定分组)、确认主机的物理地址(MAC地址),通过传输介质在网络上传输数据帧。网络接口有不同的实现方式,比如可以通过有线或无线的方式收发数据帧,不同的实现方式意味着不同的帧结构、传输速率等。
- 网络层:定义网络地址(IP地址)、区分网段、对于子网内的数据包进行MAC寻址、对于不同子网的数据包进行路由,实现网络中主机到主机的通信。
- 传输层:定义端口(Port)、标识应用程序身份、实现端口到端口的通信,TCP协议可以保证数据传输的可靠性。
- 应用层:定义数据格式并按照对应的格式解读数据(下层传送过来的是字节流,不能很好的被程序识别)。应用层定义了各种各样的协议来规范数据格式,常见的有 HTTP、FTP、SMTP 等。
下面以访问一个网址为例,把每层模型的职责串联起来:
当你输入一个网址并按下回车键的时候,首先,应用层协议对该请求包做了格式定义;紧接着传输层协议加上了双方的端口号,确认了双方通信的应用程序;然后网络层协议加上了双方的IP地址,确认了双方的网络位置;最后网络接口层协议加上了双方的MAC地址,确认了双方的物理位置,同时将数据进行分组,形成数据帧,采用广播方式,通过传输介质发送给对方主机。而对于不同网段,该数据包首先会转发给网关路由器,经过多次转发后,最终被发送到目标主机。目标机接收到数据包后,采用对应的协议,对帧数据进行组装,然后再通过一层一层的协议进行解析,最终被应用层的协议解析并交给服务器处理。
LwIP协议栈实现的TCP/IP主要功能如下:
协议实现文件 | 功能描述 |
---|---|
.\src\netif\ppp | Point to Point Protocol,支持PPPoE(Point-to-Point Protocol Over Ethernet) ,比如宽带拨号上网就使用了PPPoE |
slipif.c | Serial Line Internet Protocol,在串行链路上传输IP数据包 |
ethernetif.c | Ethernet Protocol,通过以太网卡传输IP数据包,但移植时需要实现以太网卡驱动函数 |
etharp.c | Address Resolution Protocol,实现主机以太网物理地址到IP地址的映射 |
.\src\core\ip | 包括IPv4和IPv6,支持IP分片重组,支持多网络接口下数据报的转发;为数据包在网络主机间传输提供支持,是TCP/IP协议簇中最重要的协议 |
autoip.c | IP地址自动配置,若主机从DHCP服务器获取IP地址失败,则可选择启用该功能来配置自身IP地址 |
icmp.c | Internet Control Message Protocol,为IP数据包传递过程中的差错报告、差错纠正、目的可达性提供支持,常见的ping命令就属于该协议应用的一种 |
igmp.c | Internet Group Management Protocol,为网络中的多播数据传输提供支持,主机加入某多播组后,可以接收该组的UDP多播数据包 |
udp.c | User Datagram Protocol,无连接非可靠高速率的传输协议,本身不支持重传应答机制,但可以在上层实现简单的重传应答机制来保证一定的传输可靠性 |
tcp.c | Transmission Control Protocol,面向连接的可靠的传输协议,支持TCP拥塞控制、RTT估计、快速恢复与重传等,保证传输的可靠势必会降低一定的传输速率 |
.\src\core\snmp | Simple Network Management Protocol,基于UDP实现,为互联网上设备的管理提供了框架 |
dhcp.c | Dynamic Host Configuration Protocol,可以从DHCP服务器处获得一个有效的IP地址,使计算机使用者不必为主机IP地址的分配而烦恼 |
dns.c | Domain Name System,可以通过主机名从DNS服务器处获得与该主机名对应的IP地址,使计算机使用者访问某主机时只需记住该主机名,而不用记住该主机的IP地址 |
raw.c | 为应用层提供了一种直接与IP数据包交互的方式,与UDP、TCP处于同一等级,类似于Socket编程中原始套接字的概念 |
.\src\api | 提供了Sequential API与BSD Socket API两种上层接口,这两种API实现原理都是通过引进邮箱和信号量等通信与同步机制,实现对内核中Raw/Callback API函数的封装与调用,要使用这两套API需要底层操作系统的支持 |
二、网络数据包管理
TCP/IP是一种数据通信机制,协议栈的实现本质上就是对数据包进行处理。例如,底层网络接口层判断收到的数据包类型,提取数据包中的数据字段,记录主机物理地址信息;IP层根据数据包中的IP地址实现数据的存储、转发,根据数据包编号实现数据包的重装,提取数据包中关于传输层的信息,向上层递交数据包并记录递交结果;TCP层使用数据包中的信息更新TCP状态机,并向应用程序递交数据等。上述所有过程都与数据包操作密切相关,因此,数据包管理是整个协议栈中很重要的部分。
在标准TCP/IP协议结构中,各层都被描述为一个独立的模块形式,每层负责完成一个独立的通信问题,因为每层协议相互独立,它们都可以被单独实现,只要保证它们之间的接口不变就可以了。但如果按照这种严格的分层模式来实现TCP/IP协议,会使数据包在各层间的递交变得非常慢,因涉及到一系列的内存拷贝问题,使系统总体性能受到影响。考虑到嵌入式系统资源受限的特点,LwIP内部并没有采用完整的分层结构,它会假设各层间的部分数据结构和实现原理在其他层可见,在数据包递交过程中,各层协议可以直接对数据包中属于其他层协议的字段进行操作,这就避免了数据包在各层间拷贝时的时间开销与内存开销。
下面展示下数据包在各层间传递时的封包与拆包过程:
数据包传递到协议栈某层时,或添加或甄别该层的数据报头信息,应用程序数据及各层协议数据报头都是数据包的一部分。数据包就像协议栈的血液,在各层间存储并传输各种类型的数据。
2.1 数据包的描述
数据包管理机构采用数据结构pbuf来描述协议栈中使用的数据包,这个结构同BSD中的mbuf结构类似。在LwIP中,文件pbuf.h和pbuf.c实现了协议栈数据包管理相关的所有数据结构及函数,结构pbuf的定义如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\pbuf.h
struct pbuf {
/** next pbuf in singly linked pbuf chain */
struct pbuf *next;
/** pointer to the actual data in the buffer */
void *payload;
/**
* total length of this buffer and all next buffers in chain
* belonging to the same packet.
*
* For non-queue packet chains this is the invariant:
* p->tot_len == p->len + (p->next? p->next->tot_len: 0)
*/
u16_t tot_len;
/** length of this buffer */
u16_t len;
/** pbuf_type as u8_t instead of enum to save space */
u8_t /*pbuf_type*/ type;
/** misc flags */
u8_t flags;
/**
* the reference count always equals the number of pointers
* that refer to this pbuf. This can be pointers from an application,
* the stack itself, or pbuf->next pointers from a chain.
*/
u16_t ref;
};
typedef enum {
PBUF_RAM, /* pbuf data is stored in RAM */
PBUF_ROM, /* pbuf data is stored in ROM */
PBUF_REF, /* pbuf comes from the pbuf pool */
PBUF_POOL /* pbuf payload refers to RAM */
} pbuf_type;
从上面的pbuf_type可以看出,pbuf有四种类型:PBUF_RAM、PBUF_POOL、PBUF_ROM与PBUF_REF,下面先看看四者的主要区别:
- PBUF_RAM类型:通过内存堆分配得到,该类型pbuf在协议栈中最常用,协议栈的待发送数据和应用程序的待发送数据一般都采用这种形式的pbuf;
- PBUF_POOL类型:通过内存池分配得到,该类型pbuf可以在极短时间内得到分配(内存池的优点),在网卡接收数据包时常使用这种方式包装数据;
- PBUF_ROM类型:在内存池中分配一个pbuf结构但不申请数据区空间,payload指向ROM空间内的某段数据,在发送某些静态数据时常采用该类型pbuf;
- PBUF_REF类型:在内存池中分配一个pbuf结构但不申请数据区空间,payload指向RAM空间内的某段数据,在发送某些静态数据时常采用该类型pbuf。
下面先看PBUF_RAM类型的pbuf结构图示:
payload指针指向该pbuf所记录的数据区域,但从上图可以看出payload并不是直接指向整个数据区的起始处,而是间隔了一定区域,这段间隔的数据区域offset用来存储数据包的各种首部字段,比如TCP报文首部、IP首部、以太网帧首部等。源码中申请PBUF_RAM类型的pbuf代码如下:
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));
//调用内存堆分配函数,SIZEOF_STRUCT_PBUF为pbuf结构大小,offset为各首部字段大小,length为数据存储空间大小
接下来看PBUF_POOL类型的pbuf结构图示:
pbuf通过next构成单向链表,只有第一个pbuf的payload有一个offset用于保存各首部字段。前面说PBUF_POOL得益于内存池分配的优点,分配速度很快,主要是由于内存池每个存储单位都是固定长度且使用前已经过初始化,常用的内存池类型有两种:MEMP_PBUF与MEMP_PBUF_POOL,前者专门用来存放pbuf结构体(下面将要介绍的PBUF_ROM与PBUF_REF类型pbuf的分配便使用此类型),后者MEMP_PBUF_POOL的空间不仅包含了pbuf结构,还包含了LwIP认为协议栈中可能使用的最大TCP数据包空间(所有各层首部字段和 + 最大TCP数据段),默认长度为590字节(14 + 20 + 20 + 536),这个长度小于某些大的以太网数据包(比如大的ping包,长度可以达到MTU也即1500字节),此时可能需要多个MEMP_PBUF_POOL空间才能放得下这么大的数据包。源码中申请PBUF_POOL类型的pbuf代码如下:
p = memp_malloc(MEMP_PBUF_POOL); //调用内存池分配函数,分配MEMP_PBUF_POOL大小的空间
最后看PBUF_ROM与PBUF_REF类型的pbuf结构图示:
PBUF_ROM与PBUF_REF类型的pbuf基本相同,它们申请的都是内存池中一个MEMP_PBUF类型的POOL,而不申请数据区空间,二者的区别在于前者payload指向ROM空间内的某段数据,而后者指向RAM空间内的某段数据。源码中申请PBUF_ROM或PBUF_REF类型的pbuf代码如下:
p = memp_malloc(MEMP_PBUF); //调用内存池分配函数,分配MEMP_PBUF大小的空间
对于一个数据包,它可以使用上述任意的pbuf类型来描述,还可能一大串不同类型的pbuf连在一起,共同保存一个数据包的数据。
2.2 数据包的操作
前面描述了数据包pbuf的结构与类型,也简单介绍了数据包的分配,四种类型的数据包的分配都是通过调用内存堆或内存池分配函数实现的。一个数据包可能由多个pbuf链接而成,每个pbuf是一段连续的内存空间,对内存空间的操作最基础的就是分配和释放,下面先介绍数据包申请与释放函数。
struct pbuf *pbuf_alloc(pbuf_layer l, u16_t length, pbuf_type type); //pbuf分配函数
u8_t pbuf_free(struct pbuf *p); //pbuf释放函数
数据包分配函数有三个参数:pbuf_layer指定该pbuf数据所处的协议层级,分配函数根据该值在pbuf数据区预留出首部空间offset;length表示需要申请的数据区长度;pbuf_type指出需要申请的pbuf类型。其中pbuf_type一共有四种类型,前面已经介绍过了,下面介绍下pbuf_layer:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\pbuf.h
#define ETH_PAD_SIZE 0
#define PBUF_LINK_HLEN (14 + ETH_PAD_SIZE)
#define PBUF_TRANSPORT_HLEN 20 //TCP报文首部长度
#define PBUF_IP_HLEN 20 //IP数据报首部长度
typedef enum {
PBUF_TRANSPORT, //传输层,预留PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN
PBUF_IP, //网络层,预留PBUF_LINK_HLEN + PBUF_IP_HLEN
PBUF_LINK, //链路层,预留PBUF_LINK_HLEN
PBUF_RAW //原始层,不预留任何空间
} pbuf_layer;
下面给出数据包申请函数的源码如下:
// rt-thread\components\net\lwip-1.4.1\src\core\pbuf.c
struct pbuf *pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
struct pbuf *p, *q, *r;
u16_t offset;
s32_t rem_len; /* remaining length */
/* determine header offset */
switch (layer) {
case PBUF_TRANSPORT:
/* add room for transport (often TCP) layer header */
offset = PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN;
break;
case PBUF_IP:
/* add room for IP layer header */
offset = PBUF_LINK_HLEN + PBUF_IP_HLEN;
break;
case PBUF_LINK:
/* add room for link layer header */
offset = PBUF_LINK_HLEN;
break;
case PBUF_RAW:
offset = 0;
break;
default:
return NULL;
}
switch (type) {
case PBUF_POOL:
/* allocate head of pbuf chain into p */
p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
if (p == NULL) {
PBUF_POOL_IS_EMPTY();
return NULL;
}
p->type = type;
p->next = NULL;
/* make the payload pointer point 'offset' bytes into pbuf data memory */
p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + (SIZEOF_STRUCT_PBUF + offset)));
/* the total length of the pbuf chain is the requested size */
p->tot_len = length;
/* set the length of the first pbuf in the chain */
p->len = LWIP_MIN(length, PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));
/* set reference count (needed here in case we fail) */
p->ref = 1;
/* now allocate the tail of the pbuf chain */
/* remember first pbuf for linkage in next iteration */
r = p;
/* remaining length to be allocated */
rem_len