diff --git a/rt-thread-version/rt-thread-standard/_sidebar.md b/rt-thread-version/rt-thread-standard/_sidebar.md index f28d462b7f13790f0ecfb3dc259daea3f4159454..d848241b1f93480b38bf0b75648b51cc020fd382 100644 --- a/rt-thread-version/rt-thread-standard/_sidebar.md +++ b/rt-thread-version/rt-thread-standard/_sidebar.md @@ -58,9 +58,17 @@ - 网络组件 - [net 组件总概](/rt-thread-version/rt-thread-standard/programming-manual/net/net_introduce.md) - [AT 命令](/rt-thread-version/rt-thread-standard/programming-manual/at/at.md) + - [SAL 套接字抽象层](/rt-thread-version/rt-thread-standard/programming-manual/sal/sal.md) + - [TLS/SSL 加密](/rt-thread-version/rt-thread-standard/programming-manual/tls/tls.md) - [Lwip 协议栈](/rt-thread-version/rt-thread-standard/programming-manual/lwip/lwip.md) + - [Lwip ARP](/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/arp_basic.md) + - [Lwip ICMP](/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/icmp.md) + - [Lwip IP](/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/ip.md) + - [Lwip UDP](/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/udp.md) + - [Lwip TCP](/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/tcp.md) + - [Lwip MEMPOOL](/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/mempool.md) + - [Lwip DHCP](/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/dhcp.md) - [netdev 网卡](/rt-thread-version/rt-thread-standard/programming-manual/netdev/netdev.md) - - [SAL 套接字抽象层](/rt-thread-version/rt-thread-standard/programming-manual/sal/sal.md) - [FinSH 控制台](/rt-thread-version/rt-thread-standard/programming-manual/finsh/finsh.md) - [虚拟文件系统](/rt-thread-version/rt-thread-standard/programming-manual/filesystem/filesystem.md) - [ulog 日志](/rt-thread-version/rt-thread-standard/programming-manual/ulog/ulog.md) @@ -73,6 +81,8 @@ - 物联网 - [网络工具集 (NetUtils) 应用](/rt-thread-version/rt-thread-standard/application-note/packages/netutils/an0018-system-netutils.md) - [rw007 SPI WiFi 模块使用](/rt-thread-version/rt-thread-standard/application-note/packages/rw007_module_using/an0034-rw007-module-using.md) + - [MQTT-umqtt](/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/umqtt.md) + - [Telent](/rt-thread-version/rt-thread-standard/application-note/packages/netutils/telent.md) - 工具 - [SystemView 分析工具](/rt-thread-version/rt-thread-standard/application-note/debug/systemview/an0009-systemview.md) - [Segger RTT](/rt-thread-version/rt-thread-standard/application-note/debug/seggerRTT/segger_rtt.md) diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/code_01.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/code_01.png new file mode 100644 index 0000000000000000000000000000000000000000..b51b2aae4d885fa9258d03f0d79de869718095ed Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/code_01.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_01.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_01.png new file mode 100644 index 0000000000000000000000000000000000000000..92dce1033d59ab8bae6cc806cf7e337bf85a4c33 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_01.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_02.jpg b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7b69473b3227ebbb6a8c6e880bd25053cbc10dbb Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_02.jpg differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_03.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_03.png new file mode 100644 index 0000000000000000000000000000000000000000..4a64c0014469ec14d6ae128c0eb7db6166f24a5a Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/log_03.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_01.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_01.png new file mode 100644 index 0000000000000000000000000000000000000000..b0cd868313ff058f86213510aaf57bbf84ac3d5e Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_01.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_02.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_02.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2b65eb4173a744b1be55b3dd9c7d091c9d8152 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_02.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_03.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_03.png new file mode 100644 index 0000000000000000000000000000000000000000..8d6734ab9b5b9ed62da6866fc57315475c36f4e0 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_03.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_04.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_04.png new file mode 100644 index 0000000000000000000000000000000000000000..837019a511a78a85264bcc94822d3feb7cb17806 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_04.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_05.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_05.png new file mode 100644 index 0000000000000000000000000000000000000000..45e7896167a59a145b9c83847774a7ce884080c4 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_05.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_06.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_06.png new file mode 100644 index 0000000000000000000000000000000000000000..7126c216cf0bcc8c3a68436c55bcede864c85466 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/studio_06.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_01.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_01.png new file mode 100644 index 0000000000000000000000000000000000000000..ab39e3d92748ca41ba92a31a3a86d288d92258e8 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_01.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_02.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_02.png new file mode 100644 index 0000000000000000000000000000000000000000..afc617b95626af673de530ed1b46f28607bfa56b Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_02.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_03.png b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_03.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b51ad5234aba39edcb2f9d3e178c9006be380c Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/figure/table_03.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/netutils/telent.md b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/telent.md new file mode 100644 index 0000000000000000000000000000000000000000..9bdd5b4e77b7feff9ae800b2849fbb38bdad56a8 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/application-note/packages/netutils/telent.md @@ -0,0 +1,718 @@ +# Telnet 协议原理及使用体验 + +## 1\. 概述 + +Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议。Telnet 协议的目的是提供一个相对通用的,双向的,面向八位字节的通信方法,允许界面终端设备和面向终端的过程能通过一个标准过程进行互相交互。应用 Telnet 协议能够把本地用户所使用的计算机变成远程主机系统的一个终端。 + +Telnet 协议具有如下的特点: + +1. **适应异构**   + +为了使多个操作系统间的 Telnet 交互操作成为可能,就必须详细了解异构计算机和操作系统。比如,一些操作系统需要每行文本用 ASCII 回车控制符(CR)结束,另一些系统则需要使用 ASCII 换行符(LF),还有一些系统需要用两个字符的序列回车-换行(CR-LF);再比如,大多数操作系统为用户提供了一个中断程序运行的快捷键,但这个快捷键在各个系统中有可能不同(一些系统使用CTRL+C,而另一些系统使用ESCAPE)。如果不考虑系统间的异构性,那么在本地发出的字符或命令,传送到远地并被远地系统解释后很可能会不准确或者出现错误。因此,Telnet 协议必须解决这个问题。 + +为了适应异构环境,Telnet 协议定义了数据和命令在 Internet 上的传输方式,此定义被称作网络虚拟终端 NVT(Net Virtual Terminal)。它的应用过程如下: + +对于发送的数据:客户机软件把来自用户终端的按键和命令序列转换为 NVT 格式,并发送到服务器,服务器软件将收到的数据和命令,从 NVT 格式转换为远地系统需要的格式; +对于返回的数据:远地服务器将数据从远地机器的格式转换为 NVT 格式,而本地客户机将将接收到的 NVT 格式数据再转换为本地的格式。 + +2. **传送远地命令**   + +我们知道绝大多数操作系统都提供各种快捷键来实现相应的控制命令,当用户在本地终端键入这些快捷键的时候,本地系统将执行相应的控制命令,而不把这些快捷键作为输入。那么对于 Telnet 来说,它是用什么来实现控制命令的远地传送呢? + +Telnet 同样使用 NVT 来定义如何从客户机将控制功能传送到服务器。我们知道 USASCII 字符集包括 95 个可打印字符和 33 个控制码。当用户从本地键入普通字符时,NVT 将按照其原始含义传送;当用户键入快捷键(组合键)时,NVT 将把它转化为特殊的 ASCII 字符在网络上传送,并在其到达远地机器后转化为相应的控制命令。将正常 ASCII 字符集与控制命令区分主要有两个原因: + +这种区分意味着 Telnet 具有更大的灵活性:它可在客户机与服务器间传送所有可能的 ASCII 字符以及所有控制功能; +这种区分使得客户机可以无二义性的指定信令,而不会产生控制功能与普通字符的混乱。   + +3. **数据流向**   + +将 Telnet 设计为应用级软件有一个缺点,那就是:效率不高。这是为什么呢?下面给出 Telnet 中的数据流向: + +数据信息被用户从本地键盘键入并通过操作系统传到客户机程序,客户机程序将其处理后返回操作系统,并由操作系统经过网络传送到远地机器,远地操作系统将所接收数据传给服务器程序,并经服务器程序再次处理后返回到操作系统上的伪终端入口点,最后,远地操作系统将数据传送到用户正在运行的应用程序,这便是一次完整的输入过程;输出将按照同一通路从服务器传送到客户机。 + +因为每一次的输入和输出,计算机将切换进程环境好几次,这个开销是很昂贵的。还好用户的键入速率并不算高,这个缺点我们仍然能够接受。   + +4. **强制命令** + +我们应该考虑到这样一种情况:假设本地用户运行了远地机器的一个无休止循环的错误命令或程序,且此命令或程序已经停止读取输入,那么操作系统的缓冲区可能因此而被占满,如果这样,远地服务器也无法再将数据写入伪终端,并且最终导致停止从 TCP 连接读取数据,TCP 连接的缓冲区最终也会被占满,从而导致阻止数据流流入此连接。如果以上事情真的发生了,那么本地用户将失去对远地机器的控制。 + +为了解决此问题,Telnet 协议必须使用**外带信令以便强制服务器读取一个控制命令**。我们知道 TCP 用紧急数据机制实现外带数据信令,那么 Telnet 只要再附加一个被称为数据标记 (date mark) 的保留八位组,并通过让 TCP 发送已设置紧急数据比特的报文段通知服务器便可以了,携带紧急数据的报文段将绕过流量控制直接到达服务器。作为对紧急信令的相应,服务器将读取并抛弃所有数据,直到找到了一个数据标记。服务器在遇到了数据标记后将返回正常的处理过程。 + +5. **选项协商**   + +由于 Telnet 两端的机器和操作系统的异构性,使得 Telnet 不可能也不应该严格规定每一个 telnet 连接的详细配置,否则将大大影响 Telnet 的适应异构性。因此,Telnet 采用选项协商机制来解决这一问题。 + +Telnet选项的范围很广:一些选项扩充了大方向的功能,而一些选项制涉及一些微小细节。例如:有一个选项可以控制Telnet是在半双工还是全双工模式下工作(大方向);还有一个选项允许远地机器上的服务器决定用户终端类型(小细节)。 + +Telnet 选项的协商方式也很有意思,它对于每个选项的处理都是对称的,即任何一端都可以发出协商申请;任何一端都可以接受或拒绝这个申请。另外,如果一端试图协商另一端不了解的选项,接受请求的一端可简单的拒绝协商。因此,有可能将更新,更复杂的 Telnet 客户机服务器版本与较老的,不太复杂的版本进行交互操作。如果客户机和服务器都理解新的选项,可能会对交互有所改善。否则,它们将一起转到效率较低但可工作的方式下运行。所有的这些设计,都是为了增强适应异构性,可见 Telnet 的适应异构性对其的应用和发展是多么重要。  + +## 2\. 原理 + +Telnet 协议的主体由三个部分组成: + +网络虚拟终端(NVT,Network Virtual Terminal)的定义;操作协商定义;协商有限自动机; + +### 2.1. 网络虚拟终端(NVT) + +#### 2.1.1. NVT 工作原理 + +顾名思义,网络虚拟终端(NVT)是一种虚拟的终端设备,它被客户和服务器所采用,用来建立数据表示和解释的一致性。 + +#### 2.1.2. NVT的定义 + +1. NVT 的组成 + +网络虚拟终端 NVT 包括两个部分: + +输出设备:输出远程数据,一般为显示器 +输入设备:本地数据输入 + +2. 在 NVT 上传输的数据格式 + +在网络虚拟终端 NVT 上传输的数据采用 8 bit 字节数据,其中最高位为0的字节用于一般数据,最高位为 1 的字节用于 NVT 命令 + +3. NVT 在 TELNET 中的使用 + +TELNET 使用了一种对称的数据表示,当每个客户机发送数据时,把它的本地终端的字符表示影射到 NVT 的字符表示上,当接收数据时,又把 NVT 的表示映射到本地字符集合上。 + +在通信开始时,通信双方都支持一个基本的 NVT 终端特性子集(只能区分何为数据,何为命令),以便在最低层次上通信,在这个基础上,双方通过 NVT 命令协商确定 NVT 的更高层次上的特性,实现对 NVT 功能的扩展。 + +在 TELNET 中存在大量的子协议用于协商扩展基本的网络虚拟终端 NVT 的功能,由于终端类型的多样化,使得 TELNET 协议族变得庞大起来。 + +### 2.2. 操作协商 + +#### 2.2.1. 为什么要协商操作选项 + +当定义了网络虚拟终端设备后,通信的双方就可以在一个较低的层次上实现数据通信,但基本的 NVT 设备所具有的特性是十分有限的,它只能接收和显示 7 位的ASCII 码,没有最基本的编辑能力,所以简单的 NVT 设备是没有实际应用意义的;为此 TELNET 协议定义了一族协议用于扩展基本 NVT 的功能,目的是使 NVT 能够最大限度地达到用户终端所具有的功能。 + +为了实现对多种终端特性的支持,TELNET 协议规定在扩展 NVT 功能时采用协商的机制,只有通信双方通过协商后达成一致的特性才能使用,才能赋予 NVT 该项特性,这样就可以支持具有不同终端特性的终端设备可以互连,保证他们是工作在他们自己的能力以内。 + +#### 2.2.2. 操作协商命令格式 + +TELNET 的操作协商使用 NVT 命令,即最高位为 1 的字节流,每条 NVT 命令以字节 IAC(0xFF)开始。原理如下: + +只要客户机或服务器要发送命令序列而不是数据流,它就在数据流中插入一个特殊的保留字符,该保留字符叫做“解释为命令”(IAC ,Interpret As Command\) 字符。当接收方在一个入数据流中发现 IAC 字符时,它就把后继的字节处理为一个命令序列。下面列出了所有的 Telnet NVT 命令,其中很少用到。 + +表1 TELNET 命令 + +![table.png](./figure/table_01.png) + +其中常用的 TELNET选项协商如下: + +WILL \(option code\) 251 指示希望开始执行,或者确认现在正在操作指示的选项。 +WON'T \(option code\) 252 指出拒绝执行或继续招待所指示的选项。 +DO \(option code\) 253 指出要求对方执行,或者确认希望对方执行指示的选项。 +DON'T \(option code\) 254 指出要求对方停止执行,或者确诊要求对方停止执行指示的选项。 +那么对于接收方和发送方有以下几种组合: + +**表2 TELNET 选项协商的六种情况** + +![table.png](./figure/table_02.png) + +发送者希望对方使某选项无效,接受者必须接受该请求 + +选项协商需要 3 个字节:IAC,然后是 WILL、DO、WONT 或 DONT;最后一个标识字节用来指明操作的选项。常用的选项代码如下: + +**表3 TELNET 选项代码** + +![table.png](./figure/table_03.png) + +通常情况下,客户机向服务器发送字符而服务器将其回显到用户的终端上,但是,如果网络的时延回引起回显速度太慢,用户可能更愿意让本地系统回显字符。在客户机允许本地系统回显前,它要向服务器发送以下序列: + +IAC DONT ECHO + +服务器收到请求后,发出 3 个字符的响应: + +IAC WONT ECHO + +表示服务器已经按请求同意关闭回显。 + +### 2.3. 子选项协商 + +除了“打开”或“关闭”以外,有些选项还需要更多的信息,例如对于指明终端类型来说,客户必须发送一个字符串来标识终端类型,所以要定义子选项协商。 + +RFC 1091 定义了终端类型的子选项协商。举个例子: + +客户发送字节序列来请求打开选项: + +\< IAC,WILL,24> + +24 是终端类型的选项标识符。如果服务器同意该请求,响应为: + +\< IAC,DO,24 > + +接着服务器发送 + +\< IAC,SB,24,1,IAC,SE>请求客户给出其终端类型。 + +SB 是子选项开始命令,下一个字节 24 表示该子选项为终端类型选项。下一个字节 1 表示:发送你的终端类型。客户的响应为: + +\< IAC,SB,24,0,'I','B','M','P','C', IAC,SE> + +第四个字节 0 的含义是“我的终端类型为”。 + +### 3\. 实现 + +整个协议软件分为三个模块,各模块的功能如下: + +1. 与本地用户的输入/输出模块:处理用户输入/输出; +2. 与远地系统的输入/输出模块:处理与远程系统输入/输出; +3. TELNET协议模块:实现TELNET协议,维护协议状态机。 + +telnet客户机要做两件事: + +读取用户在键盘上键入的字符,并通过tcp连接把他们发送到远程服务器上。 +读取从tcp连接上收到的字符,并显示在用户的终端上。 + +### rt-thread中使用telnet功能,基于开发板Art-Pi + +1. 可以使示例工程 art\_pi\_wifi + ![studio.png](./figure/studio_01.png) + +2. 确保 “rt-thread setting” 三个组件安装 easyflash, netutils, fal + ![studio.png](./figure/studio_02.png) + +![studio.png](./figure/studio_03.png) + +![art-pi-0000D.png](https://oss-club.rt-thread.org/uploads/20210829/4803d371fc2c214bb5cd2076d7d4059d.png "art-pi-0000D.png") + +![studio.png](./figure/studio_04.png) + +![studio.png](./figure/studio_05.png) + +![studio.png](./figure/studio_06.png) + +3. 在 main.c 可以加上以下代码 + +```c +extern void wlan_autoconnect_init(void); + +rt_wlan_config_autoreconnect(RT_TURE); + +rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); +``` + +![code.png](./figure/code_01.png) + +4. 工程项目 packagesnetutils - v1.3.1 -> telnet -> telnet.c + +```c +/* + * File : telnet.c + * This file is part of RT-Thread RTOS + * COPYRIGHT (C) 2006-2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2012-04-01 Bernard first version + * 2018-01-25 armink Fix it on RT-Thread 3.0+ + */ +#include +#include + +#ifdef PKG_NETUTILS_TELNET +#if defined(RT_USING_DFS_NET) || defined(SAL_USING_POSIX) +#include +#else +#include +#endif /* SAL_USING_POSIX */ + +#if defined(RT_USING_POSIX) +#include +#include +#include +static int dev_old_flag; +#endif + +#include +#include +#include + +#define TELNET_PORT 23 +#define TELNET_BACKLOG 5 +#define RX_BUFFER_SIZE 256 +#define TX_BUFFER_SIZE 4096 + +#define ISO_nl 0x0a +#define ISO_cr 0x0d + +#define STATE_NORMAL 0 +#define STATE_IAC 1 +#define STATE_WILL 2 +#define STATE_WONT 3 +#define STATE_DO 4 +#define STATE_DONT 5 +#define STATE_CLOSE 6 + +#define TELNET_IAC 255 +#define TELNET_WILL 251 +#define TELNET_WONT 252 +#define TELNET_DO 253 +#define TELNET_DONT 254 + +struct telnet_session +{ + struct rt_ringbuffer rx_ringbuffer; + struct rt_ringbuffer tx_ringbuffer; + + rt_mutex_t rx_ringbuffer_lock; + rt_mutex_t tx_ringbuffer_lock; + + struct rt_device device; + rt_int32_t server_fd; + rt_int32_t client_fd; + + /* telnet protocol */ + rt_uint8_t state; + rt_uint8_t echo_mode; + + rt_sem_t read_notice; +}; + +static struct telnet_session* telnet; + +/* process tx data */ +static void send_to_client(struct telnet_session* telnet) +{ + rt_size_t length; + rt_uint8_t tx_buffer[32]; + + while (1) + { + rt_memset(tx_buffer, 0, sizeof(tx_buffer)); + rt_mutex_take(telnet->tx_ringbuffer_lock, RT_WAITING_FOREVER); + /* get buffer from ringbuffer */ + length = rt_ringbuffer_get(&(telnet->tx_ringbuffer), tx_buffer, sizeof(tx_buffer)); + rt_mutex_release(telnet->tx_ringbuffer_lock); + + /* do a tx procedure */ + if (length > 0) + { + send(telnet->client_fd, tx_buffer, length, 0); + } + else break; + } +} + +/* send telnet option to remote */ +static void send_option_to_client(struct telnet_session* telnet, rt_uint8_t option, rt_uint8_t value) +{ + rt_uint8_t optbuf[4]; + + optbuf[0] = TELNET_IAC; + optbuf[1] = option; + optbuf[2] = value; + optbuf[3] = 0; + + rt_mutex_take(telnet->tx_ringbuffer_lock, RT_WAITING_FOREVER); + rt_ringbuffer_put(&telnet->tx_ringbuffer, optbuf, 3); + rt_mutex_release(telnet->tx_ringbuffer_lock); + + send_to_client(telnet); +} + +/* process rx data */ +static void process_rx(struct telnet_session* telnet, rt_uint8_t *data, rt_size_t length) +{ + rt_size_t index; + + for (index = 0; index < length; index++) + { + switch (telnet->state) + { + case STATE_IAC: + if (*data == TELNET_IAC) + { + rt_mutex_take(telnet->rx_ringbuffer_lock, RT_WAITING_FOREVER); + /* put buffer to ringbuffer */ + rt_ringbuffer_putchar(&(telnet->rx_ringbuffer), *data); + rt_mutex_release(telnet->rx_ringbuffer_lock); + + telnet->state = STATE_NORMAL; + } + else + { + /* set telnet state according to received package */ + switch (*data) + { + case TELNET_WILL: + telnet->state = STATE_WILL; + break; + case TELNET_WONT: + telnet->state = STATE_WONT; + break; + case TELNET_DO: + telnet->state = STATE_DO; + break; + case TELNET_DONT: + telnet->state = STATE_DONT; + break; + default: + telnet->state = STATE_NORMAL; + break; + } + } + break; + + /* don't option */ + case STATE_WILL: + case STATE_WONT: + send_option_to_client(telnet, TELNET_DONT, *data); + telnet->state = STATE_NORMAL; + break; + + /* won't option */ + case STATE_DO: + case STATE_DONT: + send_option_to_client(telnet, TELNET_WONT, *data); + telnet->state = STATE_NORMAL; + break; + + case STATE_NORMAL: + if (*data == TELNET_IAC) + { + telnet->state = STATE_IAC; + } + else if (*data != '\r') /* ignore '\r' */ + { + rt_mutex_take(telnet->rx_ringbuffer_lock, RT_WAITING_FOREVER); + /* put buffer to ringbuffer */ + rt_ringbuffer_putchar(&(telnet->rx_ringbuffer), *data); + rt_mutex_release(telnet->rx_ringbuffer_lock); + rt_sem_release(telnet->read_notice); + } + break; + } + data++; + } + + +#if !defined(RT_USING_POSIX) + rt_size_t rx_length; + rt_mutex_take(telnet->rx_ringbuffer_lock, RT_WAITING_FOREVER); + /* get total size */ + rx_length = rt_ringbuffer_data_len(&telnet->rx_ringbuffer); + rt_mutex_release(telnet->rx_ringbuffer_lock); + + /* indicate there are reception data */ + if ((rx_length > 0) && (telnet->device.rx_indicate != RT_NULL)) + { + telnet->device.rx_indicate(&telnet->device, rx_length); + } +#endif + + return; +} + +/* client close */ +static void client_close(struct telnet_session* telnet) +{ + /* set console */ + rt_console_set_device(RT_CONSOLE_DEVICE_NAME); + /* set finsh device */ +#if defined(RT_USING_POSIX) + ioctl(libc_stdio_get_console(), F_SETFL, (void *) dev_old_flag); + libc_stdio_set_console(RT_CONSOLE_DEVICE_NAME, O_RDWR); +#else + finsh_set_device(RT_CONSOLE_DEVICE_NAME); +#endif /* RT_USING_POSIX */ + + rt_sem_release(telnet->read_notice); + + /* close connection */ + closesocket(telnet->client_fd); + + /* restore shell option */ + finsh_set_echo(telnet->echo_mode); + + rt_kprintf("telnet: resume console to %s\n", RT_CONSOLE_DEVICE_NAME); +} + +/* RT-Thread Device Driver Interface */ +static rt_err_t telnet_init(rt_device_t dev) +{ + return RT_EOK; +} + +static rt_err_t telnet_open(rt_device_t dev, rt_uint16_t oflag) +{ + return RT_EOK; +} + +static rt_err_t telnet_close(rt_device_t dev) +{ + return RT_EOK; +} + +static rt_size_t telnet_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size) +{ + rt_size_t result; + + rt_sem_take(telnet->read_notice, RT_WAITING_FOREVER); + + /* read from rx ring buffer */ + rt_mutex_take(telnet->rx_ringbuffer_lock, RT_WAITING_FOREVER); + result = rt_ringbuffer_get(&(telnet->rx_ringbuffer), buffer, size); + if (result == 0) + { + /** + * MUST return unless **1** byte for support sync read data. + * It will return empty string when read no data + */ + *(char *) buffer = '\0'; + result = 1; + } + rt_mutex_release(telnet->rx_ringbuffer_lock); + + return result; +} + +static rt_size_t telnet_write (rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size) +{ + const rt_uint8_t *ptr; + + ptr = (rt_uint8_t*) buffer; + + rt_mutex_take(telnet->tx_ringbuffer_lock, RT_WAITING_FOREVER); + while (size) + { + if (*ptr == '\n') + rt_ringbuffer_putchar(&telnet->tx_ringbuffer, '\r'); + + if (rt_ringbuffer_putchar(&telnet->tx_ringbuffer, *ptr) == 0) /* overflow */ + break; + ptr++; + size--; + } + rt_mutex_release(telnet->tx_ringbuffer_lock); + + /* send data to telnet client */ + send_to_client(telnet); + + return (rt_uint32_t) ptr - (rt_uint32_t) buffer; +} + +static rt_err_t telnet_control(rt_device_t dev, int cmd, void *args) +{ + return RT_EOK; +} + +#ifdef RT_USING_DEVICE_OPS + static struct rt_device_ops _ops = { + telnet_init, + telnet_open, + telnet_close, + telnet_read, + telnet_write, + telnet_control + }; +#endif +/* telnet server thread entry */ +static void telnet_thread(void* parameter) +{ +#define RECV_BUF_LEN 64 + + struct sockaddr_in addr; + socklen_t addr_size; + rt_uint8_t recv_buf[RECV_BUF_LEN]; + rt_int32_t recv_len = 0; + + if ((telnet->server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) + { + rt_kprintf("telnet: create socket failed\n"); + return; + } + + addr.sin_family = AF_INET; + addr.sin_port = htons(TELNET_PORT); + addr.sin_addr.s_addr = INADDR_ANY; + rt_memset(&(addr.sin_zero), 0, sizeof(addr.sin_zero)); + if (bind(telnet->server_fd, (struct sockaddr *) &addr, sizeof(struct sockaddr)) == -1) + { + rt_kprintf("telnet: bind socket failed\n"); + return; + } + + if (listen(telnet->server_fd, TELNET_BACKLOG) == -1) + { + rt_kprintf("telnet: listen socket failed\n"); + return; + } + + /* register telnet device */ + telnet->device.type = RT_Device_Class_Char; +#ifdef RT_USING_DEVICE_OPS + telnet->device.ops = &_ops; +#else + telnet->device.init = telnet_init; + telnet->device.open = telnet_open; + telnet->device.close = telnet_close; + telnet->device.read = telnet_read; + telnet->device.write = telnet_write; + telnet->device.control = telnet_control; +#endif + + /* no private */ + telnet->device.user_data = RT_NULL; + + /* register telnet device */ + rt_device_register(&telnet->device, "telnet", RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_STREAM); + + while (1) + { + rt_kprintf("telnet: waiting for connection\n"); + + /* grab new connection */ + if ((telnet->client_fd = accept(telnet->server_fd, (struct sockaddr *) &addr, &addr_size)) == -1) + { + continue; + } + + rt_kprintf("telnet: new telnet client(%s:%d) connection, switch console to telnet...\n", inet_ntoa(addr.sin_addr), addr.sin_port); + + /* process the new connection */ + /* set console */ + rt_console_set_device("telnet"); + /* set finsh device */ +#if defined(RT_USING_POSIX) + /* backup flag */ + dev_old_flag = ioctl(libc_stdio_get_console(), F_GETFL, (void *) RT_NULL); + /* add non-block flag */ + ioctl(libc_stdio_get_console(), F_SETFL, (void *) (dev_old_flag | O_NONBLOCK)); + /* set tcp shell device for console */ + libc_stdio_set_console("telnet", O_RDWR); + /* resume finsh thread, make sure it will unblock from last device receive */ + rt_thread_t tid = rt_thread_find(FINSH_THREAD_NAME); + if (tid) + { + rt_thread_resume(tid); + rt_schedule(); + } +#else + /* set finsh device */ + finsh_set_device("telnet"); +#endif /* RT_USING_POSIX */ + + /* set init state */ + telnet->state = STATE_NORMAL; + + telnet->echo_mode = finsh_get_echo(); + /* disable echo mode */ + finsh_set_echo(0); + /* output RT-Thread version and shell prompt */ +#ifdef FINSH_USING_MSH + msh_exec("version", strlen("version")); +#endif + rt_kprintf(FINSH_PROMPT); + + while (1) + { + /* try to send all data in tx ringbuffer */ + send_to_client(telnet); + + /* do a rx procedure */ + if ((recv_len = recv(telnet->client_fd, recv_buf, RECV_BUF_LEN, 0)) > 0) + { + process_rx(telnet, recv_buf, recv_len); + } + else + { + /* close connection */ + client_close(telnet); + break; + } + } + } +} + +/* telnet server */ +void telnet_server(void) +{ + rt_thread_t tid; + + if (telnet == RT_NULL) + { + rt_uint8_t *ptr; + + telnet = rt_malloc(sizeof(struct telnet_session)); + if (telnet == RT_NULL) + { + rt_kprintf("telnet: no memory\n"); + return; + } + /* init ringbuffer */ + ptr = rt_malloc(RX_BUFFER_SIZE); + if (ptr) + { + rt_ringbuffer_init(&telnet->rx_ringbuffer, ptr, RX_BUFFER_SIZE); + } + else + { + rt_kprintf("telnet: no memory\n"); + return; + } + ptr = rt_malloc(TX_BUFFER_SIZE); + if (ptr) + { + rt_ringbuffer_init(&telnet->tx_ringbuffer, ptr, TX_BUFFER_SIZE); + } + else + { + rt_kprintf("telnet: no memory\n"); + return; + } + /* create tx ringbuffer lock */ + telnet->tx_ringbuffer_lock = rt_mutex_create("telnet_tx", RT_IPC_FLAG_FIFO); + /* create rx ringbuffer lock */ + telnet->rx_ringbuffer_lock = rt_mutex_create("telnet_rx", RT_IPC_FLAG_FIFO); + + telnet->read_notice = rt_sem_create("telnet_rx", 0, RT_IPC_FLAG_FIFO); + + tid = rt_thread_create("telnet", telnet_thread, RT_NULL, 2048, 25, 5); + if (tid != RT_NULL) + { + rt_thread_startup(tid); + rt_kprintf("Telnet server start successfully\n"); + } + } + else + { + rt_kprintf("telnet: server already running\n"); + } + +} + +#ifdef RT_USING_FINSH +#include +FINSH_FUNCTION_EXPORT(telnet_server, startup telnet server); +#ifdef FINSH_USING_MSH +MSH_CMD_EXPORT(telnet_server, startup telnet server) +#endif /* FINSH_USING_MSH */ +#endif /* RT_USING_FINSH */ +#endif /* PKG_NETUTILS_TELNET */ +``` + +代码里主函数 void telnet\_server\(void\) + +5. 烧录到开发板里,开始在 msh> 下操作 + ![log.png](./figure/log_01.png) + +![log.png](./figure/log_02.jpg) + +![log.png](./figure/log_03.png) + +顺利与 telnet 服务器连接运行使用起来。 diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/frame.jpg b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/frame.jpg new file mode 100644 index 0000000000000000000000000000000000000000..07a5a541c1370fdf2a482a7a26e9a896570da4d7 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/frame.jpg differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/mqtt_flow_00.png b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/mqtt_flow_00.png new file mode 100644 index 0000000000000000000000000000000000000000..bf910eedd213e29354f04fec559fe34a63a70bb9 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/mqtt_flow_00.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/umqtt_flow_01.png b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/umqtt_flow_01.png new file mode 100644 index 0000000000000000000000000000000000000000..137406da192bedaa762084ef6ce266704cb89b9f Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/umqtt_flow_01.png differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/umqtt_level.jpg b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/umqtt_level.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af9fba86ff65b5ace39b44c70c644043df5b5115 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/figure/umqtt_level.jpg differ diff --git a/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/umqtt.md b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/umqtt.md new file mode 100644 index 0000000000000000000000000000000000000000..c9150e772f660f9002d73132e7a64170a3883f37 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/application-note/packages/umqtt/umqtt.md @@ -0,0 +1,1236 @@ +# **1. MQTT 背景应用** + +**MQTT**是机器对机器(`M2M`)/物联网(`IoT`)连接协议,英文全名为"**Message Queuing Telemetry Transport**",中文译名为“**消息队列遥测传输**”协议。它是专为受限设备和低带宽、高延迟或不可靠的网络而设计的,是一种基于`发布`/`订阅`(`publish`/`subscribe`)模式的"轻量级"通讯协议,该协议构建于`TCP/IP`协议之上,由IBM在1999年发布。目前,该协议的最新版本为V5.0,常用版本为V3.1.1。 + +举例说明MQTT的实际应用场景。如图1 - 1,很好地展示出了一个基于MQTT协议的通信网络系统案例: +![frame](./figure/frame.jpg) + +
图1 - 1 基于MQTT的通信案例
+ +名词释义: + +- **Publisher** - 发布者 + +- **Broker** - 代理(服务端) + +- **Subscriber** - 订阅者 + +- **Topic** - 发布/订阅的主题 + +流程概述:上图中,各类传感器的角色是发布者(Publisher)。譬如,湿度传感器和温度传感器分别向接入的MQTT Broker中(周期性)发布两个主题名为"Moisture"(湿度)和"Temp"(温度)的主题;当然,伴随着这两个主题共同发布的,还有湿度值和温度值,被称为“消息”。几个客户端的角色是订阅者Subscriber,如手机APP从Broker订阅了"Temp"主题,便能在手机上获取到温度传感器Publish在Broker中的温度值。 + +补充说明: +1. 发布者和订阅者的角色并非是固定的,而是相对的。发布者也可以同时从Broker订阅主题,同理,订阅者也可以向Broker发布主题;即发布者可以是订阅者,订阅者也可以是发布者。 +2. Broker可以是在线的云服务器,也可以是本地搭建的局域网客户端;按照需求,实际上Broker自身也会包含一些订阅/发布主题的功能。 + +更多参考资料,请前往[MQTT中文网](http://mqtt.p2hp.com/)或[MQTT官网](https://mqtt.org/)查阅。 + +------ + +学习目标: + +1. 了解MQTT协议及其报文格式 +2. 理解uMQTT和LWIP的关系 +3. 掌握uMQTT的实现原理 + +# **2. MQTT 报文结构** + +任何通用/私有协议都是由事先规定好的、按某种规则约束的各种报文数据包组成的,MQTT也不例外。在MQTT协议中,所有的数据包都是由最多三部分组成:`固定header` + `可变header` + `有效载荷`,如表2 - 1: + +| **[Fixed header](#2.1. Fixed header)(at least 2 Bytes)** | [Variable header](#2.2. Variable header) | [Payload](#2.3. Payload*) | +| ---------------------------------------------------------- | ---------------------------------------- | :------------------------- | +| **存在于所有MQTT控制数据包中** | 存在于部分MQTT控制数据包中 | 存在于部分MQTT控制数据包中 | +| Bytes[0] Bytes[1]... | ... ... | ...Bytes[N-1] Bytes[N] | +
表2 - 1 MQTT的数据包组成格式
+ +其中,固定Header是必需的,可变Header和有效载荷是非必需的。因此,理论上来说,MQTT协议数据包的最小长度为2个字节,造就了它本身占用的额外资源消耗最小化特色。接下来将以MQTT3.1.1为例,来介绍各部分报文的详细格式(详细用法见 [**4. uMQTT的实现**](#4. uMQTT的实现))。 + +## **2.1. Fixed header** + + + + + + + + + + + + + + + + + + + + + +
Bit76543210
Bytes[0]MQTT控制报文类型(MQTT Control Packet type)MQTT控制报文类型标志位(Flags specific to each MQTT Control Packet type)
Bytes[1]...剩余长度(Remaining Length)
+
表2 - 2 Fixed header format
+ +固定Header由至少两个字节组成,如表2 - 2。第一个字节的高4位描述了当前数据报文的类型([MQTT Control Packet type](#2.1.1 MQTT Control Packet type)),见下表2 - 3;低四位定义了与报文类型相关的标志位([Flags specific to each MQTT Control Packet type](#2.1.2 Flags specific to each MQTT Control Packet type)),见下文表2 - 4;第二个及之后的至多4个字节代表着剩余数据的字节长度([Remaining Length](#2.1.3 Remaining Length)),见下文表2 - 5。 + +### 2.1.1 MQTT Control Packet type + +| **名称** | **值** | **Bytes[0]** | **描述** | +| :---------: | :----: | :----------: | :--------------------------------: | +| Reserved | 0 | 0x0* | 预留位 | +| CONNECT | 1 | 0x1* | 客户端请求连接 | +| CONNACK | 2 | 0x2* | 服务端连接确认 | +| PUBLISH | 3 | 0x3* | 发布消息 | +| PUBACK | 4 | 0x4* | 发布确认(QoS1) | +| PUBREC | 5 | 0x5* | 发布收到(QoS2 - 保证交付第1部分) | +| PUBREL | 6 | 0x6* | 发布释放(QoS2 - 保证交付第2部分) | +| PUBCOMP | 7 | 0x7* | 发布完成(QoS2 - 保证交付第3部分) | +| SUBSCRIBE | 8 | 0x8* | 客户端订阅请求 | +| SUBACK | 9 | 0x9* | 服务端订阅确认 | +| UNSUBSCRIBE | 10 | 0xA* | 客户端取消订阅请求 | +| UNSUBACK | 11 | 0xB* | 服务端取消订阅确认 | +| PINGREQ | 12 | 0xC* | 客户端心跳(PING)请求 | +| PINGRESP | 13 | 0xD* | 服务端心跳(PING)响应 | +| DISCONNECT | 14 | 0xE* | 客户端即将断开连接 | +| Reserved | 15 | 0xF* | 预留位 | + +
表2 - 3 Control Packet type
+ +### 2.1.2 Flags specific to each MQTT Control Packet type + +| **控制报文类型** | **Fixed header flags** | **Bit 3** | **Bit 2** | **Bit 1** | **Bit 0** | +| ---------------- | ---------------------- | --------- | --------- | --------- | --------- | +| CONNECT | Reserved | 0 | 0 | 0 | 0 | +| CONNACK | Reserved | 0 | 0 | 0 | 0 | +| PUBLISH | Used in MQTT 3.1.1 | DUP | QoS | QoS | RETAIN | +| PUBACK | Reserved | 0 | 0 | 0 | 0 | +| PUBREC | Reserved | 0 | 0 | 0 | 0 | +| PUBREL | Reserved | 0 | 0 | 1 | 0 | +| PUBCOMP | Reserved | 0 | 0 | 0 | 0 | +| SUBSCRIBE | Reserved | 0 | 0 | 1 | 0 | +| SUBACK | Reserved | 0 | 0 | 0 | 0 | +| UNSUBSCRIBE | Reserved | 0 | 0 | 1 | 0 | +| UNSUBACK | Reserved | 0 | 0 | 0 | 0 | +| PINGREQ | Reserved | 0 | 0 | 0 | 0 | +| PINGRESP | Reserved | 0 | 0 | 0 | 0 | +| DISCONNECT | Reserved | 0 | 0 | 0 | 0 | + +
表2 - 4 Control Packet Flags
+ +Fixed header中**Bytes[0]**的**bit[3..0]**包含了每个MQTT控制报文类型的特殊标识,如上表2 - 4。表中flags为“Reserved”的各标志位虽然是预留给将来版本使用的,但是在数据传输过程中必须赋值为表中的值。如果识别到了与表中不符的非法flag,接收方必须关闭网络连接。 + +其中,已经用在3.1.1版本中的3种标志位: + +- DUP = 控制报文的重复分发标志[Duplicate] +- QoS = PUBLISH报文的服务质量等级[Quality of Service] +- RETAIN = PUBLISH报文的保留标志 + +### 2.1.3 Remaining Length + +| **字节数** | **起**(Bytes[1], Bytes[2], Bytes[3], Bytes[4]) | **止(Bytes[1], Bytes[2], Bytes[3], Bytes[4])** | +| ---------- | ---------------------------------------------- | ---------------------------------------------- | +| 1 | 0 (0x00) | 127 (0x7F) | +| 2 | 128 (0x80, 0x01) | 16 383 (0xFF, 0x7F) | +| 3 | 16 384 (0x80, 0x80, 0x01) | 2 097 151 (0xFF, 0xFF, 0x7F) | +| 4 | 2 097 152 (0x80, 0x80, 0x80, 0x01) | 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F) | + +
表2 - 5 Remaining Length
+ +剩余长度(Remaining Length)表示当前报文剩余部分的字节数,包括可变header和有效载荷的数据。剩余长度不包括用于编码剩余长度字段本身的字节数,即不包含Fixed header的长度。 + +剩余长度字段使用一个变长度编码方案,如上表2 - 5: + +- 小于128的值,使用单字节编码。 +- 更大的值按下面的方式处理——低7位有效位用于编码数据,最高有效位用于指示是否有更多的字节。因此每个字节可以编码128个数值和一个**延续位(continuation bit)**。延续位仅有指示进位功能,在计算长度时不能被计算在内。 +- 该字段最多4个字节。 + +## **2.2. Variable header** + +某些MQTT控制报文包含一个可变header部分,它位于固定header和有效载荷之间。可变header的内容根据控制报文类型([MQTT Control Packet type](#2.1.1 MQTT Control Packet type))的不同而不同;譬如**CONNECT**类型的报文,它的可变header含有“协议名称(Protocol Name)”、”协议等级(Protocol Level)“、”连接标志(Connect Flags)“和”保活间隔(Keep Alive)“四个字段域。其余可变header,详见[MQTT报文格式](#3. MQTT报文格式)。 + +虽然有各种不同的可变header内容,但是报文标识符(Packet Identifier)是一个重要的通用字段,且具有`唯一性`。见表2 - 6: + +| **字节** | **值** | +| -------- | -------------- | +| byte 1 | 报文标识符 MSB | +| byte 2 | 报文标识符 LSB | + +
表2 - 6 Packet Identifier
+ +这个通用字段存在于多种类型的报文里,见表2 - 7: + +| **报文类型** | **是否存在报文标识符** | +| ------------ | ---------------------- | +| CONNECT | NO | +| CONNACK | NO | +| PUBLISH | YES (If QoS > 0) | +| PUBACK | YES | +| PUBREC | YES | +| PUBREL | YES | +| PUBCOMP | YES | +| SUBSCRIBE | YES | +| SUBACK | YES | +| UNSUBSCRIBE | YES | +| UNSUBACK | YES | +| PINGREQ | NO | +| PINGRESP | NO | +| DISCONNECT | NO | + +
表2 - 7 Control Packets that contain a Packet Identifier
+ +报文标识符(Packet Identifier)的规则如下: + +- SUBSCRIBE、UNSUBSCRIBE、和PUBLISH(在QoS>0的情况下)控制数据包必须包含**非零16位**报文标识符。 +- 每次客户端发送这些类型之一的新数据包时,它必须为其分配一个当前未使用的报文标识符。 +- 如果一个客户端重新发送了某个特定的控制数据包,那么它必须在该数据包的后续重发中使用与之相同的报文标识符。在客户端处理了该特定控制数据包相对应的ACK数据包之后,该报文标识符得以重新使用。 + - QoS 1的PUBLISH包,PUBACK与之对应。 + - QoS 2的PUBLISH包,PUBCOMP与之对应。 + - 对于SUBSCRIBE 和 UNSUBSCRIBE,由SUBACK和UNSUBACK与之一一对应。 +- 当服务端发送QoS>0的PUBLISH报文时,也应遵守上述规则。 +- QoS=0的PUBLISH报文,**不允许**包含报文标识符。 +- PUBACK,PUBREC,PUBREL包的报文标识符,必须和PUBLISH包最初发送的值相同。 +- 类似地,SUBACK和UNSUBACK包,必须与SUBSCRIBE和UNSUBSCRIBE包中的报文标识符相同。 + +客户端和服务端相互独立地分配报文标识符。因此,客户端-服务端组合使用相同的报文标识符,可以实现并发信息交换。 + +## **2.3. Payload** + +某些MQTT控制报文在数据包的最后部分包含有效载荷,详情见[MQTT报文格式](#3. MQTT报文格式)。例如,对于PUBLISH报文来说,有效载荷就可以是应用消息。表2 - 8列出了各种需要有效载荷的控制报文。 + +| **控制报文** | **有效载荷** | +| :----------- | :----------- | +| CONNECT | Required | +| CONNACK | None | +| PUBLISH | Optional | +| PUBACK | None | +| PUBREC | None | +| PUBREL | None | +| PUBCOMP | None | +| SUBSCRIBE | Required | +| SUBACK | Required | +| UNSUBSCRIBE | Required | +| UNSUBACK | None | +| PINGREQ | None | +| PINGRESP | None | +| DISCONNECT | None | + +
表2 - 8 Control Packets that contain a Payload
+ +# **3. MQTT报文格式** + +由于Variable header和Payload的格式较多,本文不再赘述,详情可参考以下链接: + +- [**MQTT Version 3.1.1** | 3 MQTT Control Packets](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718027) +- [MQTT 3.1.1 协议 中文版 | MQTT中文网](http://mqtt.p2hp.com/mqtt311) +- [第三章 MQTT控制报文 MQTT Control Packets](https://github.com/mcxiaoke/mqtt/blob/master/mqtt/03-ControlPackets.md) +- [第三章 – MQTT控制报文](https://mcxiaoke.gitbook.io/mqtt/03-controlpackets) + +# **4. uMQTT的实现** + +uMQTT 软件包是 RT-Thread 自主研发的,基于 MQTT 3.1.1 协议的客户端实现,它提供了设备与 MQTT Broker 通讯的基本功能。GitHub主页[点击这里](https://github.com/RT-Thread-packages/umqtt),Gitee主页[点这里](https://gitee.com/RT-Thread-Mirror/umqtt)。 + +uMQTT 软件包功能如下: + +* 实现基础的连接、订阅、发布功能; +* 具备多重心跳保活,设备重连机制,保证 mqtt 在线状态,适应复杂情况; +* 支持 QoS=0, QoS=1, QoS=2 三种发送信息质量; +* 支持多客户端使用; +* 用户端接口简便,留有多种对外回调函数; +* 支持多种技术参数可配置,易上手,便于产品化开发; +* 功能强大,资源占用率低,支持功能可裁剪。 + +## **4.1. uMQTT的结构框架** + +uMQTT 软件包主要用于在嵌入式设备上实现 MQTT 协议,软件包的主要工作基于 MQTT 协议实现。软件包结构框图如图4 - 1: + +![umqtt_分层图](./figure/umqtt_level.jpg) + +
图4 - 1 uMQTT结构框图
+ +软件包实现过程中主要做了: + +1. 根据 MQTT 3.1.1 协议规定,进行软件包数据协议的封包解包; +2. 传输层函数适配对接 SAL (Socket Abstraction Layer)层; +3. uMQTT 客户端层,根据协议包层和传输层编写符合应用层的接口。实现基础连接、断连、订阅、取消订阅、发布消息等功能。支持 QoS0/1/2 三种发送信息质量。利用 uplink timer 定时器,实现多重心跳保活机制和设备重连机制,增加设备在线稳定性,适应复杂情况。 + +## **4.2. uMQTT客户端** + +由图1 - 1可知,想要连接Broker,嵌入式设备需要作为MQTT协议中的客户端来使用。 + +在uMQTT组件的`umqtt.h`文件中,抽象出了初始化客户端用到的MQTT配置信息,组成对应的数据结构体: + +```C +struct umqtt_info +{ + rt_size_t send_size, recv_size; /* 收发缓冲区大小 */ + const char *uri; /* 完整的URI (包含: URI + URN) */ + const char *client_id; /* 客户端ID */ + const char *lwt_topic; /* 遗嘱主题 */ + const char *lwt_message; /* 遗嘱消息 */ + const char *user_name; /* 用户名 */ + const char *password; /* 密码 */ + enum umqtt_qos lwt_qos; /* 遗嘱QoS */ + umqtt_subscribe_cb lwt_cb; /* 遗嘱回调函数 */ + rt_uint8_t reconnect_max_num; /* 最大重连次数 */ + rt_uint32_t reconnect_interval; /* 最大重连时间间隔 */ + rt_uint8_t keepalive_max_num; /* 最大保活次数 */ + rt_uint32_t keepalive_interval; /* 最大保活时间间隔 */ + rt_uint32_t recv_time_ms; /* 接收超时时间 */ + rt_uint32_t connect_time; /* 连接超时时间 */ + rt_uint32_t send_timeout; /* 上行(发布/订阅/取消订阅)超时时间 */ + rt_uint32_t thread_stack_size; /* 线程栈大小 */ + rt_uint8_t thread_priority; /* 线程优先级 */ +#ifdef PKG_UMQTT_TEST_SHORT_KEEPALIVE_TIME + rt_uint16_t connect_keepalive_sec; /* 连接信息,保活秒数 */ +#endif +}; +``` + +这些配置信息一般在创建uMQTT客户端之前需要自行填写指定,譬如Broker的”URI“、”用户名“或”密码“之类的关键信息。其它的非关键信息,如果没有指定,那么会在创建客户端函数`umqtt_create`中,调用`umqtt_check_def_info`函数来赋值为默认值: + +```C +static void umqtt_check_def_info(struct umqtt_info *info) +{ + if (info) + { + if (info->send_size == 0) { info->send_size = PKG_UMQTT_INFO_DEF_SENDSIZE; } + if (info->recv_size == 0) { info->recv_size = PKG_UMQTT_INFO_DEF_RECVSIZE; } + if (info->reconnect_max_num == 0) { info->reconnect_max_num = PKG_UMQTT_INFO_DEF_RECONNECT_MAX_NUM; } + if (info->reconnect_interval == 0) { info->reconnect_interval = PKG_UMQTT_INFO_DEF_RECONNECT_INTERVAL; } + if (info->keepalive_max_num == 0) { info->keepalive_max_num = PKG_UMQTT_INFO_DEF_KEEPALIVE_MAX_NUM; } + if (info->keepalive_interval == 0) { info->keepalive_interval = PKG_UMQTT_INFO_DEF_HEARTBEAT_INTERVAL; } + if (info->connect_time == 0) { info->connect_time = PKG_UMQTT_INFO_DEF_CONNECT_TIMEOUT; } + if (info->recv_time_ms == 0) { info->recv_time_ms = PKG_UMQTT_INFO_DEF_RECV_TIMEOUT_MS; } + if (info->send_timeout == 0) { info->send_timeout = PKG_UMQTT_INFO_DEF_SEND_TIMEOUT; } + if (info->thread_stack_size == 0) { info->thread_stack_size = PKG_UMQTT_INFO_DEF_THREAD_STACK_SIZE; } + if (info->thread_priority == 0) { info->thread_priority = PKG_UMQTT_INFO_DEF_THREAD_PRIORITY; } + } +} +``` + +然而只有上述信息,是无法运行起来一个MQTT客户端的。故在`umqtt.c`中,含有`umqtt_info`的`umqtt_client`结构体列出了初始化客户端用到的所有数据: + +```C +struct umqtt_client +{ + int sock; /* 套接字 */ + enum umqtt_client_state connect_state; /* mqtt客户端状态 */ + + struct umqtt_info mqtt_info; /* mqtt用户配置信息 */ + rt_uint8_t reconnect_count; /* mqtt客户端重连计数 */ + rt_uint8_t keepalive_count; /* mqtt保活计数 */ + rt_uint32_t pingreq_last_tick; /* mqtt的PING请求上一次滴答值 */ + rt_uint32_t uplink_next_tick; /* 上行连接的下一次滴答值 */ + rt_uint32_t uplink_last_tick; /* 上行连接的上一次滴答值 */ + rt_uint32_t reconnect_next_tick; /* 客户端断开重连时的下一次滴答值 */ + rt_uint32_t reconnect_last_tick; /* 客户端断开重连时的上一次滴答值 */ + + rt_uint8_t *send_buf, *recv_buf; /* 收发缓冲区指针 */ + rt_size_t send_len, recv_len; /* 收发数据的长度 */ + + rt_uint16_t packet_id; /* mqtt报文标识符 */ + + rt_mutex_t lock_client; /* mqtt客户端互斥锁 */ + rt_mq_t msg_queue; /* mqtt客户端消息队列 */ + + rt_timer_t uplink_timer; /* mqtt保活重连定时器 */ + + int sub_recv_list_len; /* 接收订阅信息的链表长度 */ + rt_list_t sub_recv_list; /* 订阅消息的链表头 */ + + rt_list_t qos2_msg_list; /* QoS2的消息链表 */ + struct umqtt_pubrec_msg pubrec_msg[PKG_UMQTT_QOS2_QUE_MAX]; /* 发布收到消息数组(QoS=2) */ + + umqtt_user_callback user_handler; /* 用户句柄 */ + + void *user_data; /* 用户数据 */ + rt_thread_t task_handle; /* umqtt任务线程 */ + + rt_list_t list; /* umqtt链表头 */ +}; +``` + +上述部分成员的结构体和枚举类型定义,可自行在`umqtt.h`文件中查看。该结构体会在创建客户端函数`umqtt_create`中,调用`umqtt_check_def_info`函数之后初始化: + +1. 初始化遗嘱数据结构(如果有的话) +2. 为收发缓冲区申请内存 +3. 创建互斥锁、消息队列和超时重连定时器(超时回调实现重连+保活) +4. 初始化各链表 +5. 创建`umqtt_thread`——mqtt数据收发线程 +6. 返回`mqtt_client`结构体地址 + +当第6步返回的值不为空时,即可调用`umqtt_start`函数来通过**LWIP**发送**CONNECT**报文连接Broker;连接成功后便会启动`umqtt_thread`线程,开启MQTT的通信之路了。 + +## **4.3. uMQTT与LWIP** + +在`umqtt_start`函数中,首先会将uMQTT客户端的状态置为`UMQTT_CS_LINKING`,表示`正在连接中`。接下来会调用`umqtt_connect`函数,将本地客户端连接到Broker。 + +连接到Broker的过程分两步: + +1. 创建**套接字**,与Broker建立链路连接 +2. 发送CONNECT报文,创建MQTT协议连接 + +在`umqtt_connect`函数中,通过调用`umqtt_trans_connect`函数,来完成第一步: + +```C +/** + * TCP/TLS Connection Complete for configured transport + * + * @param uri the input server URI address + * @param sock the output socket + * + * @return <0: failed or other error + * =0: success + */ +int umqtt_trans_connect(const char *uri, int *sock) +{ + int _ret = 0; + struct addrinfo *addr_res = RT_NULL; + + *sock = -1; + /* 域名解析 */ + _ret = umqtt_resolve_uri(uri, &addr_res); + if ((_ret < 0) || (addr_res == RT_NULL)) + { + LOG_E("resolve uri err"); + _ret = UMQTT_FAILED; + goto exit; + } + /* 创建套接字 */ + if ((*sock = socket(addr_res->ai_family, SOCK_STREAM, UMQTT_SOCKET_PROTOCOL)) < 0) + { + LOG_E("create socket error!"); + _ret = UMQTT_FAILED; + goto exit; + } + /* 设置套接字工作在非阻塞模式下 */ + _ret = ioctlsocket(*sock, FIONBIO, 0); + if (_ret < 0) + { + LOG_E(" iocontrol socket error!"); + _ret = UMQTT_FAILED; + goto exit; + } + /* 建立连接 */ + if ((_ret = connect(*sock, addr_res->ai_addr, addr_res->ai_addrlen)) < 0) + { + LOG_E(" connect err!"); + closesocket(*sock); + *sock = -1; + _ret = UMQTT_FAILED; + goto exit; + } + +exit: + if (addr_res) { + freeaddrinfo(addr_res); + addr_res = RT_NULL; + } + return _ret; +} +``` + +这个函数,就是uMQTT通过LWIP与Broker建立连接的核心函数。并且从[uMQTT的框架图](#**4.1. uMQTT的结构框架**)中我们可以知道,该函数是通过**SAL**即**套接字抽象层**组件,来调用相关接口访问LWIP的。用到的部分SAL组件封装的函数(`getaddrinfo`是在`umqtt_resolve_uri`函数中用来解析域名的)如下: + +```c +int getaddrinfo(const char *nodename, + const char *servname, + const struct addrinfo *hints, + struct addrinfo **res) +{ + return sal_getaddrinfo(nodename, servname, hints, res); +} +--------------------------------------------------------------------------------------------- +#define connect(s, name, namelen) sal_connect(s, name, namelen) +#define recvfrom(s, mem, len, flags, from, fromlen) sal_recvfrom(s, mem, len, flags, from, fromlen) +#define send(s, dataptr, size, flags) sal_sendto(s, dataptr, size, flags, NULL, NULL) +#define socket(domain, type, protocol) sal_socket(domain, type, protocol) +#define closesocket(s) sal_closesocket(s) +#define ioctlsocket(s, cmd, arg) sal_ioctlsocket(s, cmd, arg) +``` + +## **4.4. uMQTT发送组包** + +当uMQTT客户端与Broker成功建立链路层连接后,就会立刻发送CONNECT报文,建立MQTT的协议层连接。 + +uMQTT组件使用了巧妙的结构体+共用体来管理所有的收发报文: + +```C +union umqtt_pkgs_msg /* mqtt message packet type */ +{ + struct umqtt_pkgs_connect connect; /* connect */ + struct umqtt_pkgs_connack connack; /* connack */ + struct umqtt_pkgs_publish publish; /* publish */ + struct umqtt_pkgs_puback puback; /* puback */ + struct umqtt_pkgs_pubrec pubrec; /* publish receive (QoS 2, step_1st) */ + struct umqtt_pkgs_pubrel pubrel; /* publish release (QoS 2, step_2nd) */ + struct umqtt_pkgs_pubcomp pubcomp; /* publish complete (QoS 2, step_3rd) */ + struct umqtt_pkgs_subscribe subscribe; /* subscribe topic */ + struct umqtt_pkgs_suback suback; /* subscribe ack */ + struct umqtt_pkgs_unsubscribe unsubscribe; /* unsubscribe topic */ + struct umqtt_pkgs_unsuback unsuback; /* unsubscribe ack */ +}; + +struct umqtt_msg +{ + union umqtt_pkgs_fix_header header; /* fix header */ + rt_uint32_t msg_len; /* message length */ + union umqtt_pkgs_msg msg; /* retain payload message */ +}; +``` + +该结构体的各种报文类型正好对应[2.1.1章节](#2.1.1 MQTT Control Packet type)的各种控制报文类型(PINGREQ和PINGRESP的报文各自只需两个字节,[参考此处](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718081),因此没有必要使用结构体来管理)。通过`umqtt_encode`函数来调用不同的组包函数,填充对应格式的结构体,然后发送到Broker服务端: + +```C +/** + * packaging the data according to the format + * + * @param type the input packaging type + * @param send_buf the output send buf, result of the package + * @param send_len the output send buffer length + * @param message the input message + * + * @return <=0: failed or other error + * >0: package data length + */ +int umqtt_encode(enum umqtt_type type, rt_uint8_t *send_buf, size_t send_len, struct umqtt_msg *message) +{ + int _ret = 0; + switch (type) + { + case UMQTT_TYPE_CONNECT: + _ret = umqtt_connect_encode(send_buf, send_len, &(message->msg.connect)); + break; + case UMQTT_TYPE_PUBLISH: + _ret = umqtt_publish_encode(send_buf, send_len, message->header.bits.dup, message->header.bits.qos, &(message->msg.publish)); + break; + case UMQTT_TYPE_PUBACK: + _ret = umqtt_puback_encode(send_buf, send_len, message->msg.puback.packet_id); + break; + case UMQTT_TYPE_PUBREC: + // _ret = umqtt_pubrec_encode(); + break; + case UMQTT_TYPE_PUBREL: + _ret = umqtt_pubrel_encode(send_buf, send_len, message->header.bits.dup, message->msg.pubrel.packet_id); + break; + case UMQTT_TYPE_PUBCOMP: + _ret = umqtt_pubcomp_encode(send_buf, send_len, message->msg.pubcomp.packet_id); + break; + case UMQTT_TYPE_SUBSCRIBE: + _ret = umqtt_subscribe_encode(send_buf, send_len, &(message->msg.subscribe)); + break; + case UMQTT_TYPE_UNSUBSCRIBE: + _ret = umqtt_unsubscribe_encode(send_buf, send_len, &(message->msg.unsubscribe)); + break; + case UMQTT_TYPE_PINGREQ: + _ret = umqtt_pingreq_encode(send_buf, send_len); + break; + case UMQTT_TYPE_DISCONNECT: + _ret = umqtt_disconnect_encode(send_buf, send_len); + break; + default: + break; + } + return _ret; +} + +``` + +其中,MQTT控制报文类型的宏定义,与[2.1.1 MQTT Control Packet type](#2.1.1 MQTT Control Packet type)相对应: + +```C +enum umqtt_type +{ + UMQTT_TYPE_RESERVED = 0, + UMQTT_TYPE_CONNECT = 1, + UMQTT_TYPE_CONNACK = 2, + UMQTT_TYPE_PUBLISH = 3, + UMQTT_TYPE_PUBACK = 4, + UMQTT_TYPE_PUBREC = 5, + UMQTT_TYPE_PUBREL = 6, + UMQTT_TYPE_PUBCOMP = 7, + UMQTT_TYPE_SUBSCRIBE = 8, + UMQTT_TYPE_SUBACK = 9, + UMQTT_TYPE_UNSUBSCRIBE = 10, + UMQTT_TYPE_UNSUBACK = 11, + UMQTT_TYPE_PINGREQ = 12, + UMQTT_TYPE_PINGRESP = 13, + UMQTT_TYPE_DISCONNECT = 14, +}; +``` + +由于报文类型较多,接下来仅以**[CONNECT]( http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028 )**报文(可变header —— “协议名称”、“协议等级”、“连接标志”、“保活间隔(秒)”,有效载荷——“客户端标识符”、“遗嘱主题”、“遗嘱消息”、“用户名”、“密码”)为例,来简述uMQTT的组包过程: + +1. 填充MQTT客户端的默认配置信息 + + ```C + encode_msg.msg.connect.protocol_name_len = PKG_UMQTT_PROTOCOL_NAME_LEN; + encode_msg.msg.connect.protocol_name = PKG_UMQTT_PROTOCOL_NAME; + encode_msg.msg.connect.protocol_level = PKG_UMQTT_PROTOCOL_LEVEL; /* MQTT3.1.1 ver_lvl:4; MQTT3.1 ver_lvl:3 */ + encode_msg.msg.connect.connect_flags.connect_sign = UMQTT_DEF_CONNECT_FLAGS; + #ifdef PKG_UMQTT_TEST_SHORT_KEEPALIVE_TIME + encode_msg.msg.connect.keepalive_interval_sec = ((client->mqtt_info.connect_keepalive_sec == 0) ? PKG_UMQTT_CONNECT_KEEPALIVE_DEF_TIME : client->mqtt_info.connect_keepalive_sec); + #else + encode_msg.msg.connect.keepalive_interval_sec = PKG_UMQTT_CONNECT_KEEPALIVE_DEF_TIME; + #endif + encode_msg.msg.connect.client_id = client->mqtt_info.client_id; + encode_msg.msg.connect.will_topic = client->mqtt_info.lwt_topic; + encode_msg.msg.connect.will_message = client->mqtt_info.lwt_message; + encode_msg.msg.connect.user_name = client->mqtt_info.user_name; + if (client->mqtt_info.user_name) + { + encode_msg.msg.connect.connect_flags.bits.username_flag = 1; + } + encode_msg.msg.connect.password = client->mqtt_info.password; + if (client->mqtt_info.password) { + encode_msg.msg.connect.connect_flags.bits.password_flag = 1; + encode_msg.msg.connect.password_len = rt_strlen(client->mqtt_info.password); + } + ``` + +2. 调用`umqtt_encode`→`umqtt_connect_encode`编码函数(仅封装了`MQTTSerialize_connect`)组包: + + ```C + static int MQTTSerialize_connect(unsigned char* buf, int buflen, MQTTPacket_connectData* options) + { + unsigned char *ptr = buf; + MQTTHeader header = { 0 }; + int len = 0; + int rc = -1; + + if (umqtt_pkgs_len(len = MQTTSerialize_connectLength(options)) > buflen) + { + rc = UMQTT_BUFFER_TOO_SHORT; + goto exit; + } + + header.byte = 0; + header.bits.type = UMQTT_TYPE_CONNECT; + umqtt_writeChar(&ptr, header.byte); /* 写固定header的第一个字节 */ + + ptr += umqtt_pkgs_encode(ptr, len); /* 写剩余长度 */ + + if (options->protocol_level == 4) /* MQTT V3.1.1 */ + { + umqtt_writeCString(&ptr, "MQTT"); + umqtt_writeChar(&ptr, (char) 4); + } + else + { + umqtt_writeCString(&ptr, "MQIsdp"); /* MQTT V3.1 */ + umqtt_writeChar(&ptr, (char) 3); + } + + umqtt_writeChar(&ptr, options->connect_flags.connect_sign); + umqtt_writeInt(&ptr, options->keepalive_interval_sec); + // umqtt_writeInt(&ptr, PKG_UMQTT_CONNECT_KEEPALIVE_DEF_TIME); /* ping interval max, 0xffff */ + umqtt_writeMQTTString(&ptr, options->client_id); + if (options->connect_flags.bits.will_flag) + { + umqtt_writeMQTTString(&ptr, options->will_topic); + umqtt_writeMQTTString(&ptr, options->will_message); + } + + if (options->connect_flags.bits.username_flag) + umqtt_writeMQTTString(&ptr, options->user_name); + if (options->connect_flags.bits.password_flag) + umqtt_writeMQTTString(&ptr, options->password); + + rc = ptr - buf; + + exit: + return rc; + } + ``` + + 该函数首先调用`MQTTSerialize_connectLength`来计算**可变header**和**有效载荷**的长度,得到的len会被作为参数传递给`umqtt_pkgs_len`函数,它的作用是计算**固定header**中的`剩余长度`字段的字节数并加上**固定header**第一个字节长度即1,与buflen作比较,判断该包数据的有效性。 + + > 为什么这里使用了`if (umqtt_pkgs_len(len = MQTTSerialize_connectLength(options)) > buflen) `这种的复合语句呢? + > + > 因为我们希望得到的len长度就是**固定header**中的`剩余长度`值从而方便后面的组包过程,而有效的报文长度buflen = len + 1 + `剩余长度`字段的字节数;如果直接计算报文长度,当后面写入`剩余长度`值时,还需要减去自身字节长以及**固定header**第一个字节长度即1,更加复杂繁琐。 + + 这里拉出几个重要的结构体和共用体,来和MQTT协议对应一下: + + - 固定header + + 可参考[**2.1. Fixed header**](#**2.1. Fixed header**) + + ```c + union umqtt_pkgs_fix_header + { + rt_uint8_t byte; /* header */ + struct { + rt_uint8_t retain: 1; /* reserved bits */ + rt_uint8_t qos: 2; /* QoS, 0-Almost once; 1-Alteast once; 2-Exactly once */ + rt_uint8_t dup: 1; /* dup flag */ + rt_uint8_t type: 4; /* MQTT packet type */ + } bits; + }; + ``` + + - CONNECT标志位 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bit76543210
描述User Name FlagPassword FlagWill RetainWill QoSWill FlagClean SessionReserved
byte 8XXXXXXX0
+ + ```c + union umqtt_pkgs_connect_sign + { + rt_uint8_t connect_sign; + struct { + rt_uint8_t reserved: 1; /* reserved bits */ + rt_uint8_t clean_session: 1; /* clean session bit */ + rt_uint8_t will_flag: 1; /* will flag bit */ + rt_uint8_t will_Qos: 2; /* will Qos bit */ + rt_uint8_t will_retain: 1; /* will retain bit */ + rt_uint8_t password_flag: 1; /* password flag bit */ + rt_uint8_t username_flag: 1; /* user name flag bit */ + } bits; + }; + ------------------------------------------------------------------------------- + #define UMQTT_SET_CONNECT_FLAGS(user_name_flag, password_flag, will_retain, will_qos, will_flag, clean_session, reserved) \ + (((user_name_flag & 0x01) << 7) | \ + ((password_flag & 0x01) << 6) | \ + ((will_retain & 0x01) << 5) | \ + ((will_qos & 0x01) << 3) | \ + ((will_flag & 0x01) << 2) | \ + ((clean_session & 0x01) << 1) | \ + (reserved & 0x01)) + #define UMQTT_DEF_CONNECT_FLAGS (UMQTT_SET_CONNECT_FLAGS(0,0,0,0,0,1,0)) + ``` + +以上组包过程完成之后,会调用`umqtt_trans_send`函数,通过LWIP将发送缓冲区数据发送到socket连接的Broker: + +```c +/** + * TCP/TLS send datas on configured transport. + * + * @param sock the input socket + * @param send_buf the input, transport datas buffer + * @param buf_len the input, transport datas buffer length + * @param timeout the input, tcp/tls transport timeout + * + * @return <0: failed or other error + * =0: success + */ +int umqtt_trans_send(int sock, const rt_uint8_t *send_buf, rt_uint32_t buf_len, int timeout) +{ + int _ret = 0; + rt_uint32_t offset = 0U; + while (offset < buf_len) + { + _ret = send(sock, send_buf + offset, buf_len - offset, 0); + if (_ret < 0) + return -errno; + offset += _ret; + } + + return _ret; +} +``` + +## **4.5. uMQTT接收解包** + +当uMQTT将CONNECT报文发送完成后,就会调用`umqtt_handle_readpacket`函数(完成CONNECT过程后,该函数也会在`umqtt_thread`线程中被循环调用来收发数据)读取Broker的回复,对接收到的数据进行解包处理: + +```C +static int umqtt_handle_readpacket(struct umqtt_client *client) +{ + int _ret = 0, _onedata = 0, _cnt = 0, _loop_cnt = 0, _remain_len = 0; + int _temp_ret = 0; + int _pkt_len = 0; + int _multiplier = 1; + int _pkt_type = 0; + struct umqtt_msg decode_msg = { 0 }; + struct umqtt_msg_ack msg_ack = { 0 }; + struct umqtt_msg encode_msg = { 0 }; + RT_ASSERT(client); + + /* 1. 读Fixed header的第一个字节 */ + _temp_ret = umqtt_trans_recv(client->sock, client->recv_buf, 1); + if (_temp_ret <= 0) + { + _ret = UMQTT_FIN_ACK; + LOG_W(" server fin ack! connect failed! need to reconnect!"); + goto exit; + } + + /* 2. 读Fixed header的Remaining length字段并解析剩余长度 */ + do { + if (++_cnt > MAX_NO_OF_REMAINING_LENGTH_BYTES) + { + _ret = UMQTT_FAILED; + LOG_E(" umqtt packet length error!"); + goto exit; + } + _ret = umqtt_readpacket(client, (unsigned char *)&_onedata, 1, client->mqtt_info.recv_time_ms); + if (_ret == UMQTT_FIN_ACK) + { + LOG_W(" server fin ack! connect failed! need to reconnect!"); + goto exit; + } + else if (_ret != UMQTT_OK) + { + _ret = UMQTT_READ_FAILED; + goto exit; + } + *(client->recv_buf + _cnt) = _onedata; + _pkt_len += (_onedata & 0x7F) * _multiplier; + _multiplier *= 0x80; + } while ((_onedata & 0x80) != 0); + + /* 异常处理:如果当前报文的数据长度大于缓冲区长度,会将socket缓冲中的数据全部读出来丢掉,返回UMQTT_BUFFER_TOO_SHORT错误 */ + if ((_pkt_len + 1 + _cnt) > client->mqtt_info.recv_size) + { + LOG_W(" socket read buffer too short! will read and delete socket buff! "); + _loop_cnt = _pkt_len / client->mqtt_info.recv_size; + + do + { + if (_loop_cnt == 0) + { + umqtt_readpacket(client, client->recv_buf, _pkt_len, client->mqtt_info.recv_time_ms); + _ret = UMQTT_BUFFER_TOO_SHORT; + LOG_W(" finish read and delete socket buff!"); + goto exit; + } + else + { + _loop_cnt--; + umqtt_readpacket(client, client->recv_buf, client->mqtt_info.recv_size, client->mqtt_info.recv_time_ms); + _pkt_len -= client->mqtt_info.recv_size; + } + }while(1); + } + + /* 3. 读剩余数据——可变header+有效载荷 */ + _ret = umqtt_readpacket(client, client->recv_buf + _cnt + 1, _pkt_len, client->mqtt_info.recv_time_ms); + if (_ret == UMQTT_FIN_ACK) + { + LOG_W(" server fin ack! connect failed! need to reconnect!"); + goto exit; + } + else if (_ret != UMQTT_OK) + { + _ret = UMQTT_READ_FAILED; + LOG_E(" read remain datas error!"); + goto exit; + } + + /* 4. 解析数据包,并根据不同报文类型做相应处理 */ + rt_memset(&decode_msg, 0, sizeof(decode_msg)); + _ret = umqtt_decode(client->recv_buf, _pkt_len + _cnt + 1, &decode_msg); + if (_ret < 0) + { + _ret = UMQTT_DECODE_ERROR; + LOG_E(" decode error!"); + goto exit; + } + _pkt_type = decode_msg.header.bits.type; + switch (_pkt_type) + { + case UMQTT_TYPE_CONNACK: + { + LOG_D(" read connack cmd information!"); + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + set_connect_status(client, UMQTT_CS_LINKED); + } + break; + case UMQTT_TYPE_PUBLISH: + { + LOG_D(" read publish cmd information!"); + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + + if (decode_msg.header.bits.qos != UMQTT_QOS2) + { + LOG_D(" qos: %d, deliver message! topic nme: %s ", decode_msg.header.bits.qos, decode_msg.msg.publish.topic_name); + umqtt_deliver_message(client, decode_msg.msg.publish.topic_name, decode_msg.msg.publish.topic_name_len, + &(decode_msg.msg.publish)); + } + + if (decode_msg.header.bits.qos != UMQTT_QOS0) + { + rt_memset(&encode_msg, 0, sizeof(encode_msg)); + encode_msg.header.bits.qos = decode_msg.header.bits.qos; + encode_msg.header.bits.dup = decode_msg.header.bits.dup; + if (decode_msg.header.bits.qos == UMQTT_QOS1) + { + encode_msg.header.bits.type = UMQTT_TYPE_PUBACK; + encode_msg.msg.puback.packet_id = decode_msg.msg.publish.packet_id; + } + else if (decode_msg.header.bits.qos == UMQTT_QOS2) + { + encode_msg.header.bits.type = UMQTT_TYPE_PUBREC; + add_one_qos2_msg(client, &(decode_msg.msg.publish)); + encode_msg.msg.pubrel.packet_id = decode_msg.msg.publish.packet_id; + add_one_pubrec_msg(client, encode_msg.msg.pubrel.packet_id); /* add pubrec message */ + } + + _ret = umqtt_encode(encode_msg.header.bits.type, client->send_buf, client->mqtt_info.send_size, + &encode_msg); + if (_ret < 0) + { + _ret = UMQTT_ENCODE_ERROR; + LOG_E(" puback / pubrec failed!"); + goto exit; + } + client->send_len = _ret; + + _ret = umqtt_trans_send(client->sock, client->send_buf, client->send_len, + client->mqtt_info.send_timeout); + if (_ret < 0) + { + _ret = UMQTT_SEND_FAILED; + LOG_E(" trans send failed!"); + goto exit; + } + + } + } + break; + case UMQTT_TYPE_PUBACK: + { + LOG_D(" read puback cmd information!"); + rt_memset(&msg_ack, 0, sizeof(msg_ack)); + msg_ack.msg_type = UMQTT_TYPE_PUBACK; + msg_ack.packet_id = decode_msg.msg.puback.packet_id; + _ret = rt_mq_send(client->msg_queue, &msg_ack, sizeof(struct umqtt_msg_ack)); + if (_ret != RT_EOK) + { + _ret = UMQTT_SEND_FAILED; + LOG_E(" mq send failed!"); + goto exit; + } + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + } + break; + case UMQTT_TYPE_PUBREC: + { + LOG_D(" read pubrec cmd information!"); + rt_memset(&msg_ack, 0, sizeof(msg_ack)); + msg_ack.msg_type = UMQTT_TYPE_PUBREC; + msg_ack.packet_id = decode_msg.msg.puback.packet_id; + _ret = rt_mq_send(client->msg_queue, &msg_ack, sizeof(struct umqtt_msg_ack)); + if (_ret != RT_EOK) + { + _ret = UMQTT_SEND_FAILED; + LOG_E(" mq send failed!"); + goto exit; + } + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + } + break; + case UMQTT_TYPE_PUBREL: + { + LOG_D(" read pubrel cmd information!"); + + rt_memset(&encode_msg, 0, sizeof(encode_msg)); + encode_msg.header.bits.type = UMQTT_TYPE_PUBCOMP; + encode_msg.header.bits.qos = decode_msg.header.bits.qos; + encode_msg.header.bits.dup = decode_msg.header.bits.dup; + encode_msg.msg.pubrel.packet_id = decode_msg.msg.pubrec.packet_id; + + /* publish callback, and delete callback */ + qos2_publish_delete(client, encode_msg.msg.pubrel.packet_id); + + /* delete array numbers! */ + clear_one_pubrec_msg(client, encode_msg.msg.pubrel.packet_id); + + _ret = umqtt_encode(UMQTT_TYPE_PUBCOMP, client->send_buf, client->mqtt_info.send_size, &encode_msg); + if (_ret < 0) + { + _ret = UMQTT_ENCODE_ERROR; + LOG_E(" pubcomp failed!"); + goto exit; + } + client->send_len = _ret; + + _ret = umqtt_trans_send(client->sock, client->send_buf, client->send_len, + client->mqtt_info.send_timeout); + if (_ret < 0) + { + _ret = UMQTT_SEND_FAILED; + LOG_E(" trans send failed!"); + goto exit; + } + + } + break; + case UMQTT_TYPE_PUBCOMP: + { + LOG_D(" read pubcomp cmd information!"); + + rt_memset(&msg_ack, 0, sizeof(msg_ack)); + msg_ack.msg_type = UMQTT_TYPE_PUBCOMP; + msg_ack.packet_id = decode_msg.msg.pubcomp.packet_id; + _ret = rt_mq_send(client->msg_queue, &msg_ack, sizeof(struct umqtt_msg_ack)); + if (_ret != RT_EOK) + { + _ret = UMQTT_SEND_FAILED; + goto exit; + } + } + break; + case UMQTT_TYPE_SUBACK: + { + LOG_D(" read suback cmd information!"); + + rt_memset(&msg_ack, 0, sizeof(msg_ack)); + msg_ack.msg_type = UMQTT_TYPE_SUBACK; + msg_ack.packet_id = decode_msg.msg.suback.packet_id; + + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + + _ret = rt_mq_send(client->msg_queue, &msg_ack, sizeof(struct umqtt_msg_ack)); + if (_ret != RT_EOK) + { + _ret = UMQTT_SEND_FAILED; + goto exit; + } + } + break; + case UMQTT_TYPE_UNSUBACK: + { + LOG_D(" read unsuback cmd information!"); + + rt_memset(&msg_ack, 0, sizeof(msg_ack)); + msg_ack.msg_type = UMQTT_TYPE_UNSUBACK; + msg_ack.packet_id = decode_msg.msg.unsuback.packet_id; + + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + + _ret = rt_mq_send(client->msg_queue, &msg_ack, sizeof(struct umqtt_msg_ack)); + if (_ret != RT_EOK) + { + _ret = UMQTT_SEND_FAILED; + goto exit; + } + + } + break; + case UMQTT_TYPE_PINGRESP: + { + LOG_I(" ping resp! broker -> client! now tick: %d ", rt_tick_get()); + set_uplink_recon_tick(client, UPLINK_NEXT_TICK); + } + break; + default: + { + LOG_W(" not right type(0x%02x)!", _pkt_type); + } + break; + } + +exit: + return _ret; +} +``` + +简述上面几个关键步骤: + +1. 读Fixed header的第一个字节 + + 这里会调用`umqtt_trans_recv`函数读取socket数据: + + ```C + /** + * TCP/TLS receive datas on configured transport. + * + * @param sock the input socket + * @param recv_buf the output, receive datas buffer + * @param buf_len the input, receive datas buffer length + * + * @return <=0: failed or other error + * >0: receive datas length + */ + int umqtt_trans_recv(int sock, rt_uint8_t *recv_buf, rt_uint32_t buf_len) + { + return recv(sock, recv_buf, buf_len, 0); + // return read(sock, recv_buf, buf_len); + } + ------------------------------------------------------------------------- + #define recv(s, mem, len, flags) sal_recvfrom(s, mem, len, flags, NULL, NULL) + ``` + + 可以看到,该函数其实是对SAL层的`sal_recvfrom`函数做了一层封装,作用就是从对应的sock中读取buf_len长度的数据到recv_buf。 + +2. 读Fixed header的Remaining length字段并解析剩余长度 + + 这里就不赘述,该部分算法参照[2.1.3 Remaining Length](#2.1.3 Remaining Length)的规则。 + +3. 读剩余数据——可变header+有效载荷 + + 不再赘述。 + +4. 解析数据包,并根据不同报文类型做相应处理 + + 这里有一个关键的结构体和解包函数: + + - ```C + struct umqtt_msg decode_msg = { 0 }; + ``` + + 结构体成员可参考**[4.4. uMQTT发送组包](#4.4. uMQTT发送组包)** + + - ```C + /** + * parse the data according to the format + * + * @param recv_buf the input, the raw buffer data, of the correct length determined by the remaining length field + * @param recv_buf_len the input, the length in bytes of the data in the supplied buffer + * @param message the output datas + * + * @return <0: failed or other error + * =0: success + */ + int umqtt_decode(rt_uint8_t *recv_buf, size_t recv_buf_len, struct umqtt_msg *message) + { + int _ret = 0; + rt_uint8_t* curdata = recv_buf; + enum umqtt_type type; + if (message == RT_NULL) + { + _ret = UMQTT_INPARAMS_NULL; + LOG_E(" umqtt decode inparams null!"); + goto exit; + } + + message->header.byte = umqtt_readChar(&curdata); + type = message->header.bits.type; + + switch (type) + { + case UMQTT_TYPE_CONNACK: + _ret = umqtt_connack_decode(&(message->msg.connack), recv_buf, recv_buf_len); + break; + case UMQTT_TYPE_PUBLISH: + _ret = umqtt_publish_decode(message, recv_buf, recv_buf_len); + break; + case UMQTT_TYPE_PUBACK: + _ret = umqtt_puback_decode(message, recv_buf, recv_buf_len); + break; + case UMQTT_TYPE_PUBREC: + // _ret = umqtt_pubrec_decode(); + break; + case UMQTT_TYPE_PUBREL: + // _ret = umqtt_pubrel_decode(); + break; + case UMQTT_TYPE_PUBCOMP: + // _ret = umqtt_pubcomp_decode(); + break; + case UMQTT_TYPE_SUBACK: + _ret = umqtt_suback_decode(&(message->msg.suback), recv_buf, recv_buf_len); + break; + case UMQTT_TYPE_UNSUBACK: + _ret = umqtt_unsuback_decode(&(message->msg.unsuback), recv_buf, recv_buf_len); + break; + case UMQTT_TYPE_PINGRESP: + // _ret = umqtt_pingresp_encode(); + break; + default: + break; + } + exit: + return _ret; + } + ``` + + 报文类型比较多,依然只拿**CONNECT**报文举例: + + ```C + static int umqtt_connack_decode(struct umqtt_pkgs_connack *connack_msg, rt_uint8_t* buf, int buflen) + { + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = 0; + int mylen; + + header.byte = umqtt_readChar(&curdata); + if (header.bits.type != UMQTT_TYPE_CONNACK) + { + rc = UMQTT_FAILED; + LOG_E(" not connack type!"); + goto exit; + } + + curdata += (rc = umqtt_pkgs_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + if (enddata - curdata < 2) + { + LOG_D(" enddata:%d, curdata:%d, mylen:%d", enddata, curdata, mylen); + goto exit; + } + + connack_msg->connack_flags.connack_sign = umqtt_readChar(&curdata); + connack_msg->ret_code = umqtt_readChar(&curdata); + exit: + return rc; + } + ----------------------------------------------------------------------------------------- + union umqtt_pkgs_connack_sign + { + rt_uint8_t connack_sign; + struct { + rt_uint8_t sp: 1; /* current session bit */ + rt_uint8_t reserved: 7; /* retain bit */ + } bits; + }; + struct umqtt_pkgs_connack + { + /* variable header */ + union umqtt_pkgs_connack_sign connack_flags; /* connect flags */ + enum umqtt_connack_retcode ret_code; /* connect return code */ + /* payload = NULL */ + }; + ``` + + 仍旧是熟悉的套路:读Fixed header→读Remaining length→读Variable header解析相关flags + +5. UMQTT_TYPE_CONNACK: + + 调用`set_uplink_recon_tick(client, UPLINK_NEXT_TICK)`函数设置下一次重连滴答值,调用` set_connect_status(client, UMQTT_CS_LINKED)`函数设置uMQTT客户端状态为`已连接`。 + +至此,已经完成了CONNECT报文的收发过程,下一步就是启动`umqtt_thread`线程,调用`umqtt_handle_readpacket`函数来处理从Broker服务端收到的数据报文。报文处理流程与上文类似,不再赘述,具体内容相关流程可参考图4 - 2: + +![mqtt流程图](./figure/mqtt_flow_00.png) + +
图4 - 2 MQTT通信流程图
+ +## **4.6. uMQTT数据流向** + +综上所述,使用一张流程图简要描绘uMQTT一些重要的函数调用,见图4 - 3。由于很多细节不好展现出来,还需要从实际的代码中体会其功能流程。 + +![umqtt流程图](./figure/umqtt_flow_01.png) + +
图4 - 3 umqtt重要函数流程图
diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/arp_basic.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/arp_basic.md new file mode 100644 index 0000000000000000000000000000000000000000..295ca4ddbfda131e90b8cb8e923608cdab8a1d97 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/arp_basic.md @@ -0,0 +1,772 @@ +## 1. ARP 的背景 + +对于网络世界来说,有 IP 地址就代表了身份。不过在我们常用的网络拓扑类型中,IP 地址并不能准确表达我们的身份。在 ipv4 中牵扯到私有网络地址与子网划分,加上交换机,路由器设备的存在,这些极大扩展了可用的 IP 地址数量,在设备的 MAC 地址与 IP 地址的共同作用下使得更多的设备能连接到网络中。ipv6 拥有极大数量的 IP 地址,同时就没有 ARP 报文,但是会有一个其他类似的功能。 + +在实际使用中,**ARP (Address Resolution Protocol)地址解析协议** 就起到了沟通 IP 地址与 MAC 地址的作用。 + +ARP 的报文比较简单,就是两个功能:ARP request,ARP response;即一个 ARP 查询报文,一个 ARP 回复报文。 + +--- + +**学习目标:** + +1. 掌握 ARP 报文的作用。 +2. 掌握 lwip 中 ARP 的实现原理。 +3. 掌握 lwip 的 ARP 策略,能简单修改原生逻辑。 + +## 2. ARP 报文格式 + +ARP 报文与 IP 报文都是附着在 ETH 帧之上,可以看到 ARP 报文长度共有 28 字节;包含的内容包括【发送端】与【接收端】的【以太网地址】与【IP 地址】。 + +![](./figure/arp_protocol.png) + +* 以太网目的地址,要发送到的地址,对于不知道的地址,可以全部置为 1; +* 以太网源地址,也就是发送端的地址; +* 帧类型 0x0806,表示为 ARP 报文;0x0800 表示 IP 报文;对于 ARP 报文,"帧类型"的实际上与"协议类型"字段是同一个数据; +* 硬件类型,两个字节, 1 表示硬件地址;当然实际上,“ 0, 2, 3 “都没有太大意义; +* 协议类型,与帧类型是一致的;在数据填充时,可以把 “协议类型” 拷贝到 “帧类型” 字段; +* 硬件地址长度,6 个字节,再长也是 6 个字节; +* 协议地址长度,4个字节,别多想 ipv4 就是 4 个字节,ipv6 也不用这一套; +* **op,2 个字节; 1 表示 ARP_REQUEST,2 表示 ARP_REPLY;** +* 发送端以太网地址,意义如名称; +* 发送端 IP 地址,意义如名称; +* 目的以太网地址,意义如名称,不知道就置空; +* 目的 IP 地址,意义如名称; + +--- + +1. ARP 只有两个报文类型:request 与 response ( reply ) 。 +2. 是在知道 IP 地址的基础上,使用 request 查询该 IP 地址可以使用的 MAC 地址,而不是反过来。 +3. 一旦收到 ARP 请求,发现该 IP 与本机 IP 地址相符就回复 response ,如果不是就忽略。 +4. 为了能维护一个 MAC 与 IP 的映射表,会有一个 ARP 缓存表的存储结构;ETH 帧的收发都会尝试更新这个 ARP 缓存表。 +5. ARP 报文的收发解析的主要功能就是维护这个 ARP 缓存表,以便在发送 IP 报文时可以直接填充 【以太网目的地址】与【以太网源地址】。 + +## 3. lwip 对 ARP 功能的实现 + +根据上述的 ARP 报文,就该意识到有两个比较重要的结构体需要实现:ETH 报文的结构体,以及 ARP 报文的结构体。 + +```c +/** struct eth_addr and ip4_addr2 */ +#define ETH_HWADDR_LEN 6 + +#define PACK_STRUCT_FIELD(x) x +#define PACK_STRUCT_FLD_8(x) PACK_STRUCT_FIELD(x) +#define PACK_STRUCT_FLD_S(x) PACK_STRUCT_FIELD(x) + +struct eth_addr { + PACK_STRUCT_FLD_8(u8_t addr[ETH_HWADDR_LEN]); +} PACK_STRUCT_STRUCT; +struct ip4_addr2 { + PACK_STRUCT_FIELD(u16_t addrw[2]); +} PACK_STRUCT_STRUCT; +``` + +```c +/** Ethernet header */ +struct eth_hdr { +#if ETH_PAD_SIZE + PACK_STRUCT_FLD_8(u8_t padding[ETH_PAD_SIZE]); +#endif + PACK_STRUCT_FLD_S(struct eth_addr dest); + PACK_STRUCT_FLD_S(struct eth_addr src); + PACK_STRUCT_FIELD(u16_t type); +} PACK_STRUCT_STRUCT; +``` + +```c +/** the ARP message, see RFC 826 ("Packet format") */ +struct etharp_hdr { + PACK_STRUCT_FIELD(u16_t hwtype); + PACK_STRUCT_FIELD(u16_t proto); + PACK_STRUCT_FLD_8(u8_t hwlen); + PACK_STRUCT_FLD_8(u8_t protolen); + PACK_STRUCT_FIELD(u16_t opcode); + PACK_STRUCT_FLD_S(struct eth_addr shwaddr); + PACK_STRUCT_FLD_S(struct ip4_addr2 sipaddr); + PACK_STRUCT_FLD_S(struct eth_addr dhwaddr); + PACK_STRUCT_FLD_S(struct ip4_addr2 dipaddr); +} PACK_STRUCT_STRUCT; +``` + +以上的结构是为了能方便得按照报文格式来填充数据,还需要一个用来管理 ARP 映射表的结构; + +```c +/** struct for queueing outgoing packets for unknown address + * defined here to be accessed by memp.h + */ +struct etharp_q_entry { + struct etharp_q_entry *next; + struct pbuf *p; +}; + +struct etharp_entry { +#if ARP_QUEUEING + /** Pointer to queue of pending outgoing packets on this ARP entry. */ + struct etharp_q_entry *q; +#else /* ARP_QUEUEING */ + /** Pointer to a single pending outgoing packet on this ARP entry. */ + struct pbuf *q; +#endif /* ARP_QUEUEING */ + ip4_addr_t ipaddr; + struct netif *netif; + struct eth_addr ethaddr; + u16_t ctime; + u8_t state; +}; + +static struct etharp_entry arp_table[ARP_TABLE_SIZE]; +``` + +OK,描述到这里,lwip 对 ARP 报文的支持所需要的基础数据结构已经完备了;但是除了数据结构,也应当有对应的算法才能实现 TCP/IP 所要求的规范,即实现 ARP 程序。我们先不看具体实现,先通过我们上面对 ARP 功能的描述,先猜测一下应该需要什么功能: + +> 1. 发送数据时,根据 ARP 表填充 ETH 帧的地址。 +> 2. 如果 ARP 表没有对应的 IP 与 MAC 地址的条目,应当发送 ARP request 来查询。 +> 3. 在收到 ARP response 时,应当把报文中的 IP 与 MAC 地址添加到 ARP表中。 +> 4. 应当提供一个查询 ARP 表的功能,并能更新 ARP 映射关系。 + +#### 3.1 lwip 的 ARP 缓存表维护 + +上面的函数已经能完成对 ARP 数据包的发送,解析,查找功能;因为一个网络中,设备不可能是一直都不会变的。比如 DHCP 给你分配了一个 IP ,在你不使用后会收回这些资源,也许会分配给其他人;也就是说,IP 与 MAC 的对应关系,是会随着时间改变而改变的。因此,lwip 维护的 ARP 表是需要频繁更新的。 + +而为了表示 ARP 缓存表中,IP 与 ARP 的对应关系(后面称为一个表项),每个表项都会有个 state 来表示状态,也就是上面的 ```u8 state``` 字段。lwip 定义的表项状态共有 6 种,当然是在你支持静态 ARP 映射表的情况下。 + +```c +/** ARP states */ +enum etharp_state { + ETHARP_STATE_EMPTY = 0, /* 空表 */ + ETHARP_STATE_PENDING, /* 只记录了 IP ,而没有记录 MAC,一般是发送了 arp_request 的短暂状态 */ + ETHARP_STATE_STABLE, /* 记录了 IP 和 MAC */ + ETHARP_STATE_STABLE_REREQUESTING_1, /* stable 状态下,ctime 继续增大引发广播或者单播一个 arp_request ,并置为 REREQUESTING_1 */ + ETHARP_STATE_STABLE_REREQUESTING_2 /* 已经发送了 arp_request 但是没有回复,会从 REREQUESTING_1 置为 REREQUESTING_2 */ +#if ETHARP_SUPPORT_STATIC_ENTRIES + ,ETHARP_STATE_STATIC /* 静态表,不会别 ARP 管理修改的条目 */ +#endif /* ETHARP_SUPPORT_STATIC_ENTRIES */ +}; +``` + +这些表项的状态,都会影响 ARP 缓存表的更新;而更新策略与上面的一些函数的执行逻辑有关,比较重要的逻辑是: + +> * pending 状态下,对应的 pbuf 不会被发出,只有 >= stable 才会被发出。 +> * pending 状态下,ctime 超时会直接导致条目被删除,置为 empty。 +> * stable 状态下, ctime 超时会被字节置为 pending 。 +> * stable 状态下,会定时检查 ctime ,在达到一定阀值时会通过广播或者单播发送一个 arp_request,并被置为 rerequesting_1 状态。 +> * 在 arp_tmr 触发时,如果已经为 rerequesting_1 状态,会被置为 rerequesting_2。 +> * rerequesting_1 与 rerequesting_2 到底什么意义:避免连续(指在多次)发送 arp_request 而设计的,也许是怕网络环境干扰。 + +#### 3.2 lwip 提供的 ARP 功能函数 + +我们通过分析已经获悉,ARP 程序肯定要具备上述 4 种功能;不过,在 Lwip 的实际实现中,肯定不止这 4 种基础功能,肯定要有更多的函数来实现这个目的。关于 lwip 的 ARP 内容的代码,是存放在 ```lwip/src/core/ipv4/etharp.c``` 中。 + +为了梳理 ARP 的功能,我们选择其中的 5 个函数来学习 lwip 的 ARP 功能实现: + +```c +/* 查找 ARP 映射表中 ipaddr 条目,并返回相对位置 */ +static s8_t etharp_find_entry(const ip4_addr_t *ipaddr, u8_t flags, struct netif* netif); +/* 尝试发送 IP 数据包, 单播,多播或者广播数据 */ +err_t etharp_output(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr); +/* 查询 ARP 表,不存在则调用 etharp_request 获取,存在则发送数据 */ +err_t etharp_query(struct netif *netif, const ip4_addr_t *ipaddr, struct pbuf *q); +/* 尝试发送 ARP 请求数据包 */ +err_t etharp_request(struct netif *netif, const ip4_addr_t *ipaddr); +/* 解析 ARP 数据包 */ +void etharp_input(struct pbuf *p, struct netif *netif); +``` +其中,最难以理解得是 etharp_find_entry 与 etharp_query 函数,对于这两个函数我们主要分析其意义;剩下的 3 个函数,我们则具体看看代码的实现。通过这 5 个比较有代表性的函数,我们来理解一个表项中的 state ,ctime,p 这些值究竟怎样影响整个 ARP 报文的发送与解析策略。 + +##### 3.2.1 函数 etharp_find_entry + +该函数位于 ```lwip/src/core/ipv4/etharp.c``` 中,这个函数的作用可以通过注释来描述: + +```c +/** + * Search the ARP table for a matching or new entry. + * + * If an IP address is given, return a pending or stable ARP entry that matches + * the address. If no match is found, create a new entry with this address set, + * but in state ETHARP_EMPTY. The caller must check and possibly change the + * state of the returned entry. + * + * If ipaddr is NULL, return a initialized new entry in state ETHARP_EMPTY. + * + * In all cases, attempt to create new entries from an empty entry. If no + * empty entries are available and ETHARP_FLAG_TRY_HARD flag is set, recycle + * old entries. Heuristic choose the least important entry for recycling. + * + * @param ipaddr IP address to find in ARP cache, or to add if not found. + * @param flags See @ref etharp_state + * @param netif netif related to this address (used for NETIF_HWADDRHINT) + * + * @return The ARP entry index that matched or is created, ERR_MEM if no + * entry is found or could be recycled. + */ +``` + +通过上面的描述,我们能知道 ```etharp_find_entry``` 的策略如下: + +> 1. 如果参数中 IP 地址被给定,则返回一个 stable 或者 pending 状态的表项; +> 2. 如果 IP 地址被给定,但是没有处于 stable 或者 pending 状态的表项,则会找一个 empty 的表项并返回; +> 3. 如果 IP 地址为空,则会创建一个 empty 的表项并返回; +> 4. 如果很不巧,ARP 表中没有处于 empty 的表项;在 flag 为 ETHARP_FLAG_TRY_HARD 的情况下,找一个最不重要的表项置空并返回; +> 5. ETHARP_FLAG_FIND_ONLY 与 ETHARP_FLAG_TRY_HARD 的区别,就是前者尽量找到一个表项;后者必须找到一个表项(哪怕是清空之前已经存在的); + +上面的函数描述中有一个黑洞,ETHARP_FLAG_TRY_HARD 必须找到一个表项,哪怕是通过回收一个最不重要的表项的方式;那最不重要的表项是什么表项呢,我们可以一起看看代码 + +```c + /* b) choose the least destructive entry to recycle: // 1 -> 4, 优先级就是这样 + * 1) empty entry + * 2) oldest stable entry + * 3) oldest pending entry without queued packets + * 4) oldest pending entry with queued packets + * + * { ETHARP_FLAG_TRY_HARD is set at this point } + */ + + /* 1) empty entry available? */ + if (empty < ARP_TABLE_SIZE) { + i = empty; + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting empty entry %"U16_F"\n", (u16_t)i)); + } else { + /* 2) found recyclable stable entry? */ + if (old_stable < ARP_TABLE_SIZE) { + /* recycle oldest stable*/ + i = old_stable; + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting oldest stable entry %"U16_F"\n", (u16_t)i)); + /* no queued packets should exist on stable entries */ + LWIP_ASSERT("arp_table[i].q == NULL", arp_table[i].q == NULL); + /* 3) found recyclable pending entry without queued packets? */ + } else if (old_pending < ARP_TABLE_SIZE) { + /* recycle oldest pending */ + i = old_pending; + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting oldest pending entry %"U16_F" (without queue)\n", (u16_t)i)); + /* 4) found recyclable pending entry with queued packets? */ + } else if (old_queue < ARP_TABLE_SIZE) { + /* recycle oldest pending (queued packets are free in etharp_free_entry) */ + i = old_queue; + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: selecting oldest pending entry %"U16_F", freeing packet queue %p\n", (u16_t)i, (void *)(arp_table[i].q))); + /* no empty or recyclable entries found */ + } else { + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_find_entry: no empty or recyclable entries found\n")); + return (s8_t)ERR_MEM; + } + + /* { empty or recyclable entry found } */ + LWIP_ASSERT("i < ARP_TABLE_SIZE", i < ARP_TABLE_SIZE); + etharp_free_entry(i); + } +``` + +我在这里解释一下,为什么会是 ```1->4``` 这个顺序: + +1. 空表项与最老的 state 最先被使用的原因,就是因为 ARP 表是需要随时更新的,太老的表项可能本身就是错误的或者过时的,因此先删除是没有问题的。 +2. 处于 pending 状态且没有待发送数据的表项与处于 pending 状态但是有待发送数据的表项相比;有待发送数据的表项上挂载了一些数据,有可能是 PING 命令之类或者其他的数据,删除这些是比较危险的;而没有待发送数据的表项就没有这个顾虑。 + +##### 3.2.2 函数 etharp_query + +该函数位于 ```lwip/src/core/ipv4/etharp.c``` 中,这个函数其实是一些策略的合集: + +```c +/** + * Send an ARP request for the given IP address and/or queue a packet. + * + * If the IP address was not yet in the cache, a pending ARP cache entry + * is added and an ARP request is sent for the given address. The packet + * is queued on this entry. + * + * If the IP address was already pending in the cache, a new ARP request + * is sent for the given address. The packet is queued on this entry. + * + * If the IP address was already stable in the cache, and a packet is + * given, it is directly sent and no ARP request is sent out. + * + * If the IP address was already stable in the cache, and no packet is + * given, an ARP request is sent out. + * + * @param netif The lwIP network interface on which ipaddr + * must be queried for. + * @param ipaddr The IP address to be resolved. + * @param q If non-NULL, a pbuf that must be delivered to the IP address. + * q is not freed by this function. + * + * @note q must only be ONE packet, not a packet queue! + * + * @return + * - ERR_BUF Could not make room for Ethernet header. + * - ERR_MEM Hardware address unknown, and no more ARP entries available + * to query for address or queue the packet. + * - ERR_MEM Could not queue packet due to memory shortage. + * - ERR_RTE No route to destination (no gateway to external networks). + * - ERR_ARG Non-unicast address given, those will not appear in ARP cache. + * + */ +``` + +其实,顺着代码看下来,策略也很清晰: + +> 1. 如果不能找到表项,那没有办法,只能一个新的 empty 表项,发送一个 arp_request 并置为 pending;如果 pbuf 中有数据继续执行,没有直接返回。 +> 2. 如果找到表项,那最好,直接发送数据就行了(这里指真实的 IP 数据,而不是 ARP 数据),然后返回。 +> 3. 如果处于 pending 状态(其实走到这一步,empty 也已经是 pending 了),而且 pbuf 中有数据,那就挂载了对应的 pbuf 链表上,等待发送;这里有一些拷贝的动作,只要 pbuf 链表中有不是 PBUF_ROM 的的节点,整个 pbuf 数据都需要拷贝到新的数据链中,谨防被意外修改。 + +##### 3.2.3 函数 etharp_request + +```c +const struct eth_addr ethbroadcast = {{0xff,0xff,0xff,0xff,0xff,0xff}}; +const struct eth_addr ethzero = {{0,0,0,0,0,0}}; + +/** + * Send a raw ARP packet (opcode and all addresses can be modified) + * + * @param netif the lwip network interface on which to send the ARP packet + * @param ethsrc_addr the source MAC address for the ethernet header + * @param ethdst_addr the destination MAC address for the ethernet header + * @param hwsrc_addr the source MAC address for the ARP protocol header + * @param ipsrc_addr the source IP address for the ARP protocol header + * @param hwdst_addr the destination MAC address for the ARP protocol header + * @param ipdst_addr the destination IP address for the ARP protocol header + * @param opcode the type of the ARP packet + * @return ERR_OK if the ARP packet has been sent + * ERR_MEM if the ARP packet couldn't be allocated + * any other err_t on failure + */ +static err_t +etharp_raw(struct netif *netif, const struct eth_addr *ethsrc_addr, + const struct eth_addr *ethdst_addr, + const struct eth_addr *hwsrc_addr, const ip4_addr_t *ipsrc_addr, + const struct eth_addr *hwdst_addr, const ip4_addr_t *ipdst_addr, + const u16_t opcode); + +/** + * Send an ARP request packet asking for ipaddr to a specific eth address. + * Used to send unicast request to refresh the ARP table just before an entry + * times out + * + * @param netif the lwip network interface on which to send the request + * @param ipaddr the IP address for which to ask + * @param hw_dst_addr the ethernet address to send this packet to + * @return ERR_OK if the request has been sent + * ERR_MEM if the ARP packet couldn't be allocated + * any other err_t on failure + */ +static err_t +etharp_request_dst(struct netif *netif, const ip4_addr_t *ipaddr, const struct eth_addr* hw_dst_addr) +{ + return etharp_raw(netif, (struct eth_addr *)netif->hwaddr, hw_dst_addr, + (struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif), ðzero, + ipaddr, ARP_REQUEST); +} + +/** + * Send an ARP request packet asking for ipaddr. + * + * @param netif the lwip network interface on which to send the request + * @param ipaddr the IP address for which to ask + * @return ERR_OK if the request has been sent + * ERR_MEM if the ARP packet couldn't be allocated + * any other err_t on failure + */ +err_t +etharp_request(struct netif *netif, const ip4_addr_t *ipaddr) +{ + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_request: sending ARP request.\n")); + return etharp_request_dst(netif, ipaddr, ðbroadcast); +} +``` + +```etharp_request``` 已经是一个功能函数了,功能函数就表明很容易看懂。OK,我们看看到底干了什么。 + +> 首先 eth 帧的广播地址是全 1 的,再使用 etharp_raw 函数发送时,arp 协议中的 hw 字段,全部设置为 0;这里是协议要求,无可厚非。 +> +> 可以看到 lwip 这里确实是发送了一个 arp_request 的数据包出去。 + +##### 3.2.4 函数 etharp_input + +```C +/** + * Responds to ARP requests to us. Upon ARP replies to us, add entry to cache + * send out queued IP packets. Updates cache with snooped address pairs. + * + * Should be called for incoming ARP packets. The pbuf in the argument + * is freed by this function. + * + * @param p The ARP packet that arrived on netif. Is freed by this function. + * @param netif The lwIP network interface on which the ARP packet pbuf arrived. + * + * @see pbuf_free() + */ +void +etharp_input(struct pbuf *p, struct netif *netif) +{ + struct etharp_hdr *hdr; + /* these are aligned properly, whereas the ARP header fields might not be */ + ip4_addr_t sipaddr, dipaddr; + u8_t for_us; + + LWIP_ERROR("netif != NULL", (netif != NULL), return;); + + hdr = (struct etharp_hdr *)p->payload; + + /* RFC 826 "Packet Reception": */ + if ((hdr->hwtype != PP_HTONS(HWTYPE_ETHERNET)) || + (hdr->hwlen != ETH_HWADDR_LEN) || + (hdr->protolen != sizeof(ip4_addr_t)) || + (hdr->proto != PP_HTONS(ETHTYPE_IP))) { + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_WARNING, + ("etharp_input: packet dropped, wrong hw type, hwlen, proto, protolen or ethernet type (%"U16_F"/%"U16_F"/%"U16_F"/%"U16_F")\n", + hdr->hwtype, (u16_t)hdr->hwlen, hdr->proto, (u16_t)hdr->protolen)); + ETHARP_STATS_INC(etharp.proterr); + ETHARP_STATS_INC(etharp.drop); + pbuf_free(p); + return; + } + ETHARP_STATS_INC(etharp.recv); + +#if LWIP_AUTOIP + /* We have to check if a host already has configured our random + * created link local address and continuously check if there is + * a host with this IP-address so we can detect collisions */ + autoip_arp_reply(netif, hdr); +#endif /* LWIP_AUTOIP */ + + /* Copy struct ip4_addr2 to aligned ip4_addr, to support compilers without + * structure packing (not using structure copy which breaks strict-aliasing rules). */ + IPADDR2_COPY(&sipaddr, &hdr->sipaddr); + IPADDR2_COPY(&dipaddr, &hdr->dipaddr); + + /* this interface is not configured? */ + if (ip4_addr_isany_val(*netif_ip4_addr(netif))) { + for_us = 0; + } else { + /* ARP packet directed to us? */ + for_us = (u8_t)ip4_addr_cmp(&dipaddr, netif_ip4_addr(netif)); + } + + /* ARP message directed to us? + -> add IP address in ARP cache; assume requester wants to talk to us, + can result in directly sending the queued packets for this host. + ARP message not directed to us? + -> update the source IP address in the cache, if present */ + etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr), + for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY); + + /* now act on the message itself */ + switch (hdr->opcode) { + /* ARP request? */ + case PP_HTONS(ARP_REQUEST): + /* ARP request. If it asked for our address, we send out a + * reply. In any case, we time-stamp any existing ARP entry, + * and possibly send out an IP packet that was queued on it. */ + + LWIP_DEBUGF (ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: incoming ARP request\n")); + /* ARP request for our address? */ + if (for_us) { + /* send ARP response */ + etharp_raw(netif, + (struct eth_addr *)netif->hwaddr, &hdr->shwaddr, + (struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif), + &hdr->shwaddr, &sipaddr, + ARP_REPLY); + /* we are not configured? */ + } else if (ip4_addr_isany_val(*netif_ip4_addr(netif))) { + /* { for_us == 0 and netif->ip_addr.addr == 0 } */ + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: we are unconfigured, ARP request ignored.\n")); + /* request was not directed to us */ + } else { + /* { for_us == 0 and netif->ip_addr.addr != 0 } */ + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: ARP request was not for us.\n")); + } + break; + case PP_HTONS(ARP_REPLY): + /* ARP reply. We already updated the ARP cache earlier. */ + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: incoming ARP reply\n")); +#if (LWIP_DHCP && DHCP_DOES_ARP_CHECK) + /* DHCP wants to know about ARP replies from any host with an + * IP address also offered to us by the DHCP server. We do not + * want to take a duplicate IP address on a single network. + * @todo How should we handle redundant (fail-over) interfaces? */ + dhcp_arp_reply(netif, &sipaddr); +#endif /* (LWIP_DHCP && DHCP_DOES_ARP_CHECK) */ + break; + default: + LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_input: ARP unknown opcode type %"S16_F"\n", lwip_htons(hdr->opcode))); + ETHARP_STATS_INC(etharp.err); + break; + } + /* free ARP packet */ + pbuf_free(p); +} +``` + +> 可以看到,一旦 ARP 报文到达,都会被这个函数所判断执行;逻辑是这样的: +> +> 1. 如果收到的是给自己的 ARP request ,就回复一个 ARP reply 报文;如果不是给自己的,记录一些有用 ARP 的信息后,就忽略掉。 +> 2. 如果是给自己的 ARP reply 报文,会有一些逻辑与 DHCP 过程有关,可以在 DHCP 章节查看。 + +##### 3.2.5 函数 etharp_output + +```c +/** + * Resolve and fill-in Ethernet address header for outgoing IP packet. + * + * For IP multicast and broadcast, corresponding Ethernet addresses + * are selected and the packet is transmitted on the link. + * + * For unicast addresses, the packet is submitted to etharp_query(). In + * case the IP address is outside the local network, the IP address of + * the gateway is used. + * + * @param netif The lwIP network interface which the IP packet will be sent on. + * @param q The pbuf(s) containing the IP packet to be sent. + * @param ipaddr The IP address of the packet destination. + * + * @return + * - ERR_RTE No route to destination (no gateway to external networks), + * or the return type of either etharp_query() or ethernet_output(). + */ +err_t +etharp_output(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr) +{ + const struct eth_addr *dest; + struct eth_addr mcastaddr; + const ip4_addr_t *dst_addr = ipaddr; + + LWIP_ASSERT("netif != NULL", netif != NULL); + LWIP_ASSERT("q != NULL", q != NULL); + LWIP_ASSERT("ipaddr != NULL", ipaddr != NULL); + + /* Determine on destination hardware address. Broadcasts and multicasts + * are special, other IP addresses are looked up in the ARP table. */ + + /* broadcast destination IP address? */ + if (ip4_addr_isbroadcast(ipaddr, netif)) { + /* broadcast on Ethernet also */ + dest = (const struct eth_addr *)ðbroadcast; + /* multicast destination IP address? */ + } else if (ip4_addr_ismulticast(ipaddr)) { + /* Hash IP multicast address to MAC address.*/ + mcastaddr.addr[0] = LL_IP4_MULTICAST_ADDR_0; + mcastaddr.addr[1] = LL_IP4_MULTICAST_ADDR_1; + mcastaddr.addr[2] = LL_IP4_MULTICAST_ADDR_2; + mcastaddr.addr[3] = ip4_addr2(ipaddr) & 0x7f; + mcastaddr.addr[4] = ip4_addr3(ipaddr); + mcastaddr.addr[5] = ip4_addr4(ipaddr); + /* destination Ethernet address is multicast */ + dest = &mcastaddr; + /* unicast destination IP address? */ + } else { + s8_t i; + /* outside local network? if so, this can neither be a global broadcast nor + a subnet broadcast. */ + if (!ip4_addr_netcmp(ipaddr, netif_ip4_addr(netif), netif_ip4_netmask(netif)) && + !ip4_addr_islinklocal(ipaddr)) { +#if LWIP_AUTOIP + struct ip_hdr *iphdr = LWIP_ALIGNMENT_CAST(struct ip_hdr*, q->payload); + /* According to RFC 3297, chapter 2.6.2 (Forwarding Rules), a packet with + a link-local source address must always be "directly to its destination + on the same physical link. The host MUST NOT send the packet to any + router for forwarding". */ + if (!ip4_addr_islinklocal(&iphdr->src)) +#endif /* LWIP_AUTOIP */ + { +#ifdef LWIP_HOOK_ETHARP_GET_GW + /* For advanced routing, a single default gateway might not be enough, so get + the IP address of the gateway to handle the current destination address. */ + dst_addr = LWIP_HOOK_ETHARP_GET_GW(netif, ipaddr); + if (dst_addr == NULL) +#endif /* LWIP_HOOK_ETHARP_GET_GW */ + { + /* interface has default gateway? */ + if (!ip4_addr_isany_val(*netif_ip4_gw(netif))) { + /* send to hardware address of default gateway IP address */ + dst_addr = netif_ip4_gw(netif); + /* no default gateway available */ + } else { + /* no route to destination error (default gateway missing) */ + return ERR_RTE; + } + } + } + } +#if LWIP_NETIF_HWADDRHINT + if (netif->addr_hint != NULL) { + /* per-pcb cached entry was given */ + u8_t etharp_cached_entry = *(netif->addr_hint); + if (etharp_cached_entry < ARP_TABLE_SIZE) { +#endif /* LWIP_NETIF_HWADDRHINT */ + if ((arp_table[etharp_cached_entry].state >= ETHARP_STATE_STABLE) && +#if ETHARP_TABLE_MATCH_NETIF + (arp_table[etharp_cached_entry].netif == netif) && +#endif + (ip4_addr_cmp(dst_addr, &arp_table[etharp_cached_entry].ipaddr))) { + /* the per-pcb-cached entry is stable and the right one! */ + ETHARP_STATS_INC(etharp.cachehit); + return etharp_output_to_arp_index(netif, q, etharp_cached_entry); + } +#if LWIP_NETIF_HWADDRHINT + } + } +#endif /* LWIP_NETIF_HWADDRHINT */ + + /* find stable entry: do this here since this is a critical path for + throughput and etharp_find_entry() is kind of slow */ + for (i = 0; i < ARP_TABLE_SIZE; i++) { + if ((arp_table[i].state >= ETHARP_STATE_STABLE) && +#if ETHARP_TABLE_MATCH_NETIF + (arp_table[i].netif == netif) && +#endif + (ip4_addr_cmp(dst_addr, &arp_table[i].ipaddr))) { + /* found an existing, stable entry */ + ETHARP_SET_HINT(netif, i); + return etharp_output_to_arp_index(netif, q, i); + } + } + /* no stable entry found, use the (slower) query function: + queue on destination Ethernet address belonging to ipaddr */ + return etharp_query(netif, dst_addr, q); + } + + /* continuation for multicast/broadcast destinations */ + /* obtain source Ethernet address of the given interface */ + /* send packet directly on the link */ + return ethernet_output(netif, q, (struct eth_addr*)(netif->hwaddr), dest, ETHTYPE_IP); +} +``` + +> 这个函数会被 IP 层的输出函数直接调用以发送数据;IP 层的数据会有广播,多播,单播的数据,所以在该函数中,处理方式也是不一样的。 +> +> 1. 对于 IP 地址是广播地址的,eth 的地址也要设置为广播; +> 2. 对于 IP 地址是多播地址的,eth 的地址也要设置为多播; +> 3. 对于 IP 地址是单播地址且本网段内的,那直接继续下一步的操作; +> 4. **对于 IP 地址是单播地址但不是本网段的,也就是需要路由器的转发,eth 的地址要设置为网关的地址;** +> 5. etharp_cached_entry 的作用应该是加快处理速度,如果连续的数据发送,每次发送都要检索一边 arp 表会比较浪费;cached 的参与可以明显提供速度。 +> 6. 如果是 ARP 缓存表中找不到表项,那啥也别说了,调用 etharp_query 挂载起来吧,等待后面有机会再发送吧。 + + +#### 3.3 ARP / IP 报文在 lwip 中的流向 + +通过上面的函数,我们能大概了解具体的 ARP 功能的实现;对于 IP 数据包在整个 lwip 的流向,可以通过下面这个图来了解。在看图之前,还需要理解在 lwip 提供的三个接口: + +> IP 报文输入接口:tcpip_input +> +> IP 报文的输出接口:netif->output = etharp_output +> +> ETH 帧的输出接口:netif->linkoutput = ethernetif_linkoutput; + +![](./figure/ipv4_arp_data_flow.png) + + +## 4. 拓展 + +#### 4.1 ARP 攻击的原理 + +名字听起来很厉害,实际上就是通过 ARP 报文来达到干扰服务的目的。ARP 的报文可以使得设备不能正常发送 IP 数据。 + +> 1. 欺骗,劫持,基本是属于一类;如果你把该发往别人的 IP 地址的 ETH 帧拿走,就要在别人的机器里将对应的 ARP 缓存换为你的。这样的话,别人的设备可能就会有断网,丢包的现象;毕竟,你这都没有提供服务,别人不掉线才怪。 +> 2. DOS 攻击,也是一类;这基本是属于纯属的瘫痪手段,让设备只能处理 ARP 报文而忽略其他的报文来达到这个目的。 +> 3. 当然还有些更新型,更加巧妙的攻击方式;但是攻击的原理,都是依赖于 ARP 的 request 与 reply 功能。 + +#### 4.2 IPv6 如何处置 IPv4 的 APR 功能 + +ipv6 没有 ARP 协议,但是为了实现类似的效果;ipv6 拥有 NDP (Neighbor Discovery Protocol) 协议,这是 ARP 与 ICMP 的集合。 + +#### 4.3 ARP,IP,ICMP,IGMP,UDP,TCP,User Data 在 ETH 帧上的关系 + +ETH 是最底层的层级; + +ARP 和 IP 是同一个层级的; + +ICMP,IGMP,UDP,TCP 是同一个层级的; + +User Data 是最上层的层级,MQTT,HTTP 这些都属于这一层; + +![](./figure/eth_frame_information.png) + +从图片上可以看出来,每一层的具体范围;以太网帧指得就是 ETH 帧。 + +#### 4.4 Wireshark 抓取标准 ARP 报文 + +开发板使用 ETH 的方式接入交换机,选用 lwip 2.0.2 的协议栈,可以使用 RT-Thread 的自动配置功能;在虚拟机上搭一个 ubuntu 的平台,安装上 wireshark ,把网卡的驱动都安装好,就可以开始抓取 ARP 报文了。 + +> 开发板 : IP: 192.168.12.107 MAC:00 80 e1 0f 54 46 +> +> Ubuntu: IP: 192.168.12.200 MAC:00 0c 29 fa e3 4c + +ARP 的报文,在一个比较大的网络中,比如公司网络中可能会比较多;可以取个巧办法,不以 arp 的条件筛选报文,而使用 eth 的条件筛选。这样的话,无论是 ARP,DHCP 还是其他的什么报文,都可以抓上。如图所示: + +这是一个 request 报文,比如,在启动开发板使用 ping 命令来 ping 我们的 ubuntu 平台,抓出的信息被圈起来了。 + +![](./figure/arp_request.png) + +可以看到 ETH 帧的 dst 地址是广播地址全为1;而内部的 ARP 报文的 Target mac ( dst ) 的地址则全为0;这个抓包同我们上面的分析是一致的。 + +---- + +这是一个 reply 报文,开发板向 ubuntu 平台发送 ARP request 后的 ARP reply报文。 + +![](./figure/arp_reply.png) + +reply 报文的 ARP 报文部分,就是已经确定的 MAC 与 IP 地址了;而 ARP 的攻击的方式,一般都是从这里入手。毕竟一个广播帧任何设备都可以收到,在正常情况下,只有对应的才会回复;倘若是个 ARP 攻击设备,不管收到啥,都回复 reply 报文,并改为自己的 MAC 地址,其他设备上的 ARP 缓存表不一会儿就乱了。 + +#### 4.5 在 Ubuntu 上查看 ARP 缓存表的信息 + +![](./figure/linux_arp_command.png) + +在 root 的权限下,可以使用 ```arp -d xxxx``` 命令删除指定的表项,搭配 ping 与 wireshark 可以看到一个设备在没有 ARP 缓存表项时设备的策略。 + +1. 没有对应的表项,就先发送 ARP 报文以获取对应的 IP 与 MAC 关系,然后再发送 ping 命令。 +2. 如果有对应的表项,就直接发送 ping 命令,wireshark 就不能抓到 arp 的报文了。 + +**Attention:**我这里能看到 10 与 12 的原因是,我们这边的子网掩码是 16 位,也就是说,是属于同一个网段的,并不是分属 10 与 12 网段。一般不是本网段的 IP 包在数据发送时,直接把 ETH 的 dst 设置为网关地址了。 + +#### 4.6 VLAN 功能 + +在学习交换机的功能时,vlan 是一个可以划分不同组的一项功能;一旦在交换机中设置了 vlan 功能,只有相同的 vlan 的设备才能直接通信。而 vlan 标识的功能实现,就是在 ETH 帧上附着了 VLAN 字段,通过该字段来划分对应的网络。 + +既然是实现在 ETH 帧上,ARP ,IP 等报文也是符合这个标准的。 + +#### 4.7 lwip 的 ARP 缓存表策略探究 + +lwip 的 ARP 缓存表的更新,基本是放在 ```etharp_input``` 函数中,这也可以理解;毕竟收到 arp_reply 数据包后更新 ARP 缓存表就行了。 + +那我们如果在收到 arp_reply 的情况下,并不更新 ARP 缓存表,还会有数据发出吗?我们注释掉这个函数来试一下: + +```c +void etharp_input(struct pbuf *p, struct netif *netif) +{ + ··· + /* ARP message directed to us? + -> add IP address in ARP cache; assume requester wants to talk to us, + can result in directly sending the queued packets for this host. + ARP message not directed to us? + -> update the source IP address in the cache, if present */ +// etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr), +// for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY); + ··· +} +``` + +开发板上发送 ping 命令: + +![](./figure/arp_destory_ping_command.png) + +Ubuntu 上抓包: + +![](./figure/ubuntu_destory_ping.png) + +由于开发板上屏蔽了 arp 缓存表的更新,即使收到了 ARP reply 也不会更新 ARP 表,则永远也不会有 PING 包从开发板发出,取而代之的是 ARP request 数据包。 + +---- + +再上面代码的基础上,选择从 Ubuntu 上 PING 我们的开发板,在此之前先清空对应 ARP 的表项。 + +Ubuntu 删除表项并发送 ping 命令: + +![](./figure/arp_destory_ping_by_ubuntu_shell.png) + + + +Ubuntu 上抓包: + +![](./figure/arp_destory_ping_by_ubuntu_wireshark.png) + +可以看到 Ubuntu 发出了 ping 包,而发出的原因就是 Ubuntu 上已经由了 ARP 缓存,可以看到第 3174 帧就是 arp reply 。如果我们再注释掉,或者修改了 lwip 中的 arp reply 的代码,就可以实现比较初级的 ARP 攻击手段;怕挨打,就不做了,将来各位在实践时,哪怕是做出来也建议不要公开。 + +后面收到的 arp request 基本都是开发板发出的,因为开发板上没有对应的 ARP 表项,PING 的回复包一包也发不回来,导致Ubuntu 这边是 ping lost 状态。 \ No newline at end of file diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_by_ubuntu_shell.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_by_ubuntu_shell.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f726c859357ad01db374f7ef732e494f092bb3 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_by_ubuntu_shell.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_by_ubuntu_wireshark.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_by_ubuntu_wireshark.png new file mode 100644 index 0000000000000000000000000000000000000000..96bb4c6d94e51685850608265cbc8964d219fae4 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_by_ubuntu_wireshark.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_command.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_command.png new file mode 100644 index 0000000000000000000000000000000000000000..8bfb90f205801d9df64a3e0f0b856121b62af41c Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_destory_ping_command.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_protocol.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_protocol.png new file mode 100644 index 0000000000000000000000000000000000000000..eb1cb6a86b78944e757658515d747bbf03aa57b4 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_protocol.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_reply.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..3a922a245118e7328a142ce6062eeb56ae09e400 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_reply.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_request.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_request.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9d741164653a75a49d94762eee5e8c4ac87335 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/arp_request.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/eth_frame_information.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/eth_frame_information.png new file mode 100644 index 0000000000000000000000000000000000000000..f5036f79bafd1790945c7caa614bca548c50caa0 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/eth_frame_information.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/filter.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/filter.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c30767e96df1530265913c1186e1194439658f Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/filter.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/ipv4_arp_data_flow.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/ipv4_arp_data_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..67e3fd8f4fcf5a8d89708531619397e6bf683afb Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/ipv4_arp_data_flow.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/linux_arp_command.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/linux_arp_command.png new file mode 100644 index 0000000000000000000000000000000000000000..65dd60a8f6891bdd473ba0f22090805397c10af6 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/linux_arp_command.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/ubuntu_destory_ping.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/ubuntu_destory_ping.png new file mode 100644 index 0000000000000000000000000000000000000000..160f742c405ff87cce22418c07128af559fbe98f Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-arp/figure/ubuntu_destory_ping.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/dhcp.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/dhcp.md new file mode 100644 index 0000000000000000000000000000000000000000..34f635ec9298308c8c8cb234668df5042439e40d --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/dhcp.md @@ -0,0 +1,118 @@ +# 什么是 DHCP + +动态主机配置协议 DHCP(Dynamic Host Configuration Protocol)是一种对基于 TCP/IP 协议主机的网络参数进行动态配置和集中管理的协议,可以实现: + +* 为网络主机分配 IP 地址 + +DHCP 可以提供两种地址分配机制,网络管理员可以根据网络需求为不同的主机选择不同的分配策略。 + +**动态分配机制:** + +通过 DHCP 为主机分配一个有使用期限(这个使用期限通常叫做租期)的 IP 地址。这种分配机制适用于主机需要临时接入网络或者空闲地址数小于网络主机总数且主机不需要永久连接网络的场景。例如,企业出差员工的便携机、咖啡厅的移动终端为了临时接入网络需要获取IP地址。 + +**静态分配机制:** + +网络管理员通过 DHCP 为指定的主机分配固定的 IP 地址。这种分配机制适用于对 IP 地址有特殊要求的主机,例如企业的文件服务器由于需要对外网用户提供服务,需要使用固定的 IP 地址。相比手工静态配置 IP 地址,通过 DHCP 方式静态分配机制避免人工配置发生错误,方便管理员统一维护管理。 + +* 为网络主机提供除IP地址以外的网络参数 + +例如 DNS 服务器的 IP 地址、路由信息、网关地址等。 + +DHCP 也是一种基于客户端/服务器模型的协议。DHCP 服务器上不需要手工记录网络中所有主机 MAC 地址和 IP 地址的对应关系,而是通过地址池管理可供某网段主机使用的 IP 地址。 + +当主机成功向 DHCP 服务器申请到 IP 地址后,DHCP 服务器才会记录主机 MAC 地址和 IP 地址的对应关系,且此过程不需要人工参与。同时,DHCP 服务器还可以为某个网段内主机动态分配相同的网络参数,例如,缺省网关、DNS 服务器的 IP 地址等。 + +DHCP 可以把同一个地址在不同时间分配给不同的主机,当主机不需要使用地址时,可以释放此地址,供其他主机使用,从而实现了 IP 地址的重复利用。 + +* DHCP 的优点 + 1. 降低网络接入成本 + 2. 降低主机配置成本 + 3. 提高 IP 地址利用率 + 4. 方便统一管理 + +# DHCP报文格式及实现原理 + +## 报文格式 + +![lwip_dhcp_protocol.png](./figure/lwip_dhcp_protocol.png) + +括号里的数字表示字段的长度,单位是字节 + +| 报文名称 | 说明 | +| :----------- | :------------------: | +| DHCP DISCOVER | DHCP客户端首次登录网络时进行DHCP交互过程发送的第一个报文,用来寻找DHCP服务器。 | +| DHCP OFFER | DHCP服务器用来响应DHCP DISCOVER报文,此报文携带了各种配置信息。 | +| DHCP REQUEST | 此报文用于以下三种用途。1、客户端初始化后,发送广播的DHCP REQUEST报文来回应服务器的DHCP OFFER报文。2、客户端重启后,发送广播的DHCP REQUEST报文来确认先前被分配的IP地址等配置信息。3、当客户端已经和某个IP地址绑定后,发送DHCP REQUEST单播或广播报文来更新IP地址的租约。 | +| DHCP ACK | 服务器对客户端的DHCP REQUEST报文的确认响应报文,客户端收到此报文后,才真正获得了IP地址和相关的配置信息。 | +| DHCP NAK | 服务器对客户端的DHCP REQUEST报文的拒绝响应报文,例如DHCP服务器收到DHCP REQUEST报文后,没有找到相应的租约记录,则发送DHCP NAK报文作为应答,告知DHCP客户端无法分配合适IP地址。 | +| DHCP DECLINE | 当客户端发现服务器分配给它的IP地址发生冲突时会通过发送此报文来通知服务器,并且会重新向服务器申请地址。 | +| DHCP RELEASE | 客户端可通过发送此报文主动释放服务器分配给它的IP地址,当服务器收到此报文后,可将这个IP地址分配给其它的客户端。 | +| DHCP INFORM | DHCP客户端获取IP地址后,如果需要向DHCP服务器获取更为详细的配置信息(网关地址、DNS服务器地址),则向DHCP服务器发送DHCP INFORM请求报文。 | + +* DHCP 报文中的 Options 字段 + + DHCP 报文中的 Options 字段可以用来存放普通协议中没有定义的控制信息和参数。如果用户在 DHCP 服务器端配置了 Options 字段,DHCP 客户端在申请IP 地址的时候,会通过服务器端回应的 DHCP 报文获得 Options 字段中的配置信息。 + + Options 字段的格式如下图1所示。 + +![dhcp_protocol_style.png](./figure/dhcp_protocol_style.png) + +Options 字段由 Type、Length 和 Value 三部分组成。这三部分的表示含义如下所示: + +| 字段 | 长度 | 含义 | +| :----- | :------------: | ---------------: | +| Type | 1字节 | 该字段表示信息类型。 | +| Length | 1字节 | 该字段表示后面信息内容的长度。 | +| Value | 其长度为 Length 字段所指定 | 该字段表示信息内容。 | + +DHCP Options 选项的取值范围为 1~255,如下表 2 所示,介绍 DHCP Options 的部分知名选项 + +![dhcp_option_01.png](./figure/dhcp_option_01.png) + +![dhcp_option_02.png](./figure/dhcp_option_02.png)dhcp_work_01.png + +## 实现原理 + +![dhcp_work_01.png](./figure/dhcp_work_01.png) + +![dhcp_work_02.png](./figure/dhcp_work_02.png) + +* 发现阶段 + +DHCP 客户端在网络中广播发送 DHCP DISCOVER 请求报文,发现 DHCP 服务器,请求 IP 地址租约 + +* 提供阶段 + +DHCP 服务器通过 DHCP OFFER 报文向 DHCP 客户端提供 IP 地址预分配 + +* 选择阶段 + +DHCP 客户端通过 DHCP REQUEST 报文确认选择第一个 DHCP 服务器为它提供 IP 地址自动分配服务 + +* 确认阶段 + +被选择的 DHCP 服务器通过 DHCP ACK 报文把在 DHCP OFFER 报文中准备的 IP 地址租约给对应 DHCP 客户端 + +# RT-Thread 中使用 DHCP 功能 + +本次基于 env 环境搭建工程,基于 STM32F407 开发板,LAN8720 网口 + +light weight tcp/ip stack 这个里面内容比较多,根据自己的需要选择相应的功能即可,本次我们测试 DHCP 功能,选择 DHCP 功能 + +![dhcp_conf.png](./figure/dhcp_conf.png) + +退出保存即可 + +![lwip_code.png](./figure/lwip_code.png) + +需要注意的是,需要经过路由器连接,电脑连接路由器,开发板网口也连接路由器,输入 ifconfig,发现已经自动获取到 IP 地址啦 + +![ifconfig_log.png](./figure/ifconfig_log.png) + +接下来 ping 一下我们的主机 + +![ping_01.png](./figure/ping_01.png) + +礼尚往来,主机 ping 一下我们的开发板 + +![ping_02.png](./figure/ping_02.png) diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_conf.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_conf.png new file mode 100644 index 0000000000000000000000000000000000000000..8f45743ee7dacd7e6df1f0164b00333dd2dd813b Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_conf.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_option_01.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_option_01.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2884a7ec1cf7c0bf3fb109cf9725462c52c2a1 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_option_01.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_option_02.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_option_02.png new file mode 100644 index 0000000000000000000000000000000000000000..db06570ae7b69b817136538da77152a8dc54589b Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_option_02.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_protocol_style.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_protocol_style.png new file mode 100644 index 0000000000000000000000000000000000000000..2121a68a54773058b212b2a2035f9c964e22fb12 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_protocol_style.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_work_01.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_work_01.png new file mode 100644 index 0000000000000000000000000000000000000000..1ba13c43e105dfa7900b967d1bbd65792a259015 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_work_01.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_work_02.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_work_02.png new file mode 100644 index 0000000000000000000000000000000000000000..cd213b0507cc418968b5d45aa5c2da8ebc11282a Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/dhcp_work_02.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ifconfig_log.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ifconfig_log.png new file mode 100644 index 0000000000000000000000000000000000000000..135051a1fa663e73de275920d287a737c053177c Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ifconfig_log.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/lwip_code.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/lwip_code.png new file mode 100644 index 0000000000000000000000000000000000000000..75720ff0ea4624f72da274287e9e7449604fef77 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/lwip_code.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/lwip_dhcp_protocol.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/lwip_dhcp_protocol.png new file mode 100644 index 0000000000000000000000000000000000000000..6848d467dbaa07e61ed56ca76687b8b7203bc8ff Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/lwip_dhcp_protocol.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ping_01.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ping_01.png new file mode 100644 index 0000000000000000000000000000000000000000..0877cbae502d0c3a4aa9e5ca1996e3b4cdd1d723 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ping_01.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ping_02.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ping_02.png new file mode 100644 index 0000000000000000000000000000000000000000..f406d09e1a93d52b4667c409f4fcbe2e04f8f0f2 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-dhcp/figure/ping_02.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol.png new file mode 100644 index 0000000000000000000000000000000000000000..4dedc65ddb3befdcd546d3915e9f8d189abe34a3 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_01.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_01.png new file mode 100644 index 0000000000000000000000000000000000000000..51e806ac2fa38c080a57eb4d670d577901085847 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_01.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_detail.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..595c5e7d7e39b09d3dcb020858d79c45fd389671 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_detail.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_format.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_format.png new file mode 100644 index 0000000000000000000000000000000000000000..f80bc15258c13a6ac2fa607d9bebfb35740d3322 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/figure/icmp_protocol_format.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/icmp.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/icmp.md new file mode 100644 index 0000000000000000000000000000000000000000..1bd64a302030cd8093ded7d23f3aebca11f09382 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-icmp/icmp.md @@ -0,0 +1,66 @@ +ICMP +---- + +### ICMP 功能简介 + +IP是一种不可靠、无连接的协议,只在各个主机间交付数据,但是**对于数据的到达与否,IP 并不关心**。为了提高数据交付的准确性,ICMP ( Internet +Control Message Protocol ,因特网控制报文协议 ) 随之出现。在交付数据时,如果由于网络状况不佳、链路不通等情况导致数据报无法到达目标主机,ICMP 就会返回一个差错报文,让源主机知道数据没能正常到达目标主机,接着进行重发或者放弃发送都可以。 + +ICMP 通常被认为是 IP 层协议的一部分,但从体系结构上讲它是位于 IP 层之上的,因为 ICMP 报文是承载在 IP 数据报中的。这就是说:**ICMP 报文是作为 IP 数据报数据区域的,就像 TCP 与 UDP 报文作为 IP 数据报区域那样。**类似的,**当一台主机收到一个指明上层协议为 ICMP 的 IP 数据报时**,它将分解出该数据报的内容给 ICMP,就像分解出一个数据报的内容给 TCP 或 UDP 一样,但与 TCP 或 UDP 又有所不同,**ICMP 不是为上层应用程序提供服务,而只是在 IP 层传递差错信息的报文,依赖于 IP 进行传输。** + +### ICMP 报文结构 + +ICMP 报文是使用 IP 数据报来封装发送的,所以 ICMP 报文也没有额外的可靠性与优先级,它一样会被别的路由器丢弃。与此同时,ICMP 报文封装在 IP 数据报中,IP 数据报封装在以太网中,因此 ICMP 报文经过了两次封装,具体如下述所示: + +![protocol.png](./figure/icmp_protocol_format.png) + +ICMP 报文与 IP 报文一样,都由首部与数据区域组成,ICMP 首部是 8 个字节,对于不同类型的 ICMP 报文,ICMP 报文首部的格式也会略有不同,但首部的前四个字节是通用的。 + +**第一个字节:**占据 8 bit 空间,是类型 (type) 字段,表示产生这种类型 ICMP 报文的原因。 +**第二个字节:**占据 8 bit 空间,是代码 (code) 字段,进一步描述了产生这种类型 ICMP 报文的具体原因。每种类型的报文都可能有多个,比如目的不可达报文,产生这种报文的原因可能有主机不可达、协议不可达、端口不可达等。 +**第三个字节:**占据 16 bit 空间,是校验和字段,用于记录包括 ICMP 报文数据部分在内的整个 ICMP 数据报的校验和,以检验报文在传输过程中是否出现了差错,其计算方法与 IP 数据报中首部校验和的计算方法是一样的。 + +ICMP 首部剩下的四个字节会因为不同类型的报文而有不一样的定义,并且数据部分的长度也存在差异。ICMP 报文格式如下图所示: + +![protocol.png](./figure/icmp_protocol.png) + +### ICMP 报文类型 + + ICMP 报文可以划分为 **差错报告** 和 **报文查询** 报文两类。 + +- **差错报告报文**主要用来向 IP 数据报源主机返回一个差错报告信息,而产生这个差错报告信息的原因是路由器或者主机不能对当前数据进行正常的处理,简单来说,就是源主机发送的数据报无法到达目标主机,或者到达了目标主机而无法递交给上层协议。 + +- **查询报文**适用于一台主机向另一台主机发送一个请求情况,如果目标主机收到这个查询的请求后,就会按照查询报文的格式向源主机做出应答,比如我们使用的 ping 命令,它的本质就是一个 ICMP 查询报文。 + + 差错报告报文与查询报文的具体类型如下表所示: + +![icmp_protocol_detail.png](./figure/icmp_protocol_detail.png) + + +虽然 ICMP 报文有很多,但是它并不能纠正错误,只是借助IP简单报告差错,然后将差错报文返回源主机。因为如果出现差错,那么数据报中可用的就只有目标 IP地址和源 IP 地址,源主机收到 ICMP 差错报文后,传递给上层协议,至于要如何处理差错就不在 ICMP 的作用范围内。 + +#### ICMP 差错报告报文 + +- **目的不可达:**当路由器或主机不能交付数据报时就向源主机发送终点(目的)不可达的报文。 + +- **超时:**IP 数据报首部有一个 TTL 字段,当 IP 数据报每经过一个路由器转发,TTL 的值就会减 1,如果 TTL 的值被减到 0,那么路由或者主机就会丢弃该数据报,并返回一个 ICMP 超时报文到源主机中。此外,在数据报分片重装的时候也使用了 ICMP 报文,当所有的 IP 分片数据报无法在规定的时间内完成重装时,主机也会认为它超时了,那么这些数据报就会被删除,同时返回一个 ICMP 超时报文到源主机中。 + +- **参数问题:**IP 数据报在网络中传输时,都是根据其首部进行识别的,如果首部出现错误,那么就会产生严重的问题,因此**如果 IP 数据报首部出现错误就会丢弃数据报,并且向源主机返回一个 ICMP 参数错误表。**不过,**对于携带 ICMP 差错报文的数据报、非第一个分片的分片数据报、具有特殊目的地的数据报 ( 如环回、多播、广播 ) 等,即使出现了差错,也不会返回对应的差错报文。** + +- **重定向:**一般来说,某个主机在启动的时候只有一个路由表 ( 即:默认路由 ),所以它发送的数据都发给了默认路由,让其帮忙转发,而路由器发现数据应该是发送给另一个路由的,那么它会返回一个 ICMP 重定向报文给源主机,告诉源主机应该直接发给另一个路由器。**重定向一般用来让刚启动的主机逐渐建立更完善的路由表**,**重定向报文只能有路由器生成而不能有主机生成,但是使用重定向报文的只能是主机而非路由器**。在主机刚开始工作时,一般都在路由表中设置一个默认路由器的 IP 地址,不管数据报要发送到哪个目的地,都一律先把数据报传送给这个默认路由器,而这个默认路由器知道到每一个目的网络的最佳路由 ( 通过和其他路由器交换路由信息 )。如果路由器发现主机发往某个目的地址的数据报的最佳路由应该经过网络上的另一个路由器 R 时,就会发送重定向的 ICMP 报文将此情况告诉主机。于是主机就会在其路由表中增加一个项目:到某某目的地址应该经过路由器 R ( 而不是默认路由器 )。 + +**所有 ICMP 差错报文中的数据字段都具有同样的格式:把收到的需要进行差错报告的 IP 数据报的首部和数据字段的前 8 个字节提取出来,作为 ICMP 报文的数据字段,再加上相应的 ICMP 差错报文的前 8 个字节,就构成了 ICMP 差错报告报文。**如下图所示: + +![icmp_protocol_01.png](./figure/icmp_protocol_01.png) + +提取收到的数据报的数据字段前 8 个字节是为了得到运输层的端口号 ( 对于 TCP 和 UDP ) 以及运输层报文的发送序号 ( 对于 TCP ),这些信息对源主机通知高层协议是有用的,然后整个 ICMP 报文作为 IP 数据报的数据字段发送给源主机。 + +#### ICMP 查询报文 + +ICMP 的查询报文常见的只有两种: + +- **回送请求和回答:**ICMP 回送请求报文是由主机或路由器向一个特定的目的主机发出的询问。收到此报文的主机必须给源主机或路由器发送 ICMP 回送回答报文。这种询问报文用来测试目的站是否可达以及了解其有关状态。 + +- **时间戳请求和应答:**ICMP 时间戳请求报文是请某台主机或路由器回答当前的日期和时间。在 ICMP 时间戳回答报文中有一个 32 位的字段,其中写入的整数代表从 1900 年 1 月 1 日起到当前时刻一共多少秒。**时间戳请求和回答可用于时钟同步和时间测量。** + +ICMP 查询报文最通用的一个事例是 **ping 命令**,用来测试两台主机之间的连通性。ping 是应用层直接使用**网络层ICMP**的一个例子,**它没有通过传输层的 TCP 或 UDP**。Windows 操作系统的用户可以在接入互联网后,在 CMD 中输入 **ping hostname ( hostname 是要测试连通性的主机名或它的 IP 地址 ) **,按回车键后就可看到结果。 diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/figure/ip_protocol_format.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/figure/ip_protocol_format.png new file mode 100644 index 0000000000000000000000000000000000000000..6a04c8849aeaa757419635c45f8cd7d5281198a3 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/figure/ip_protocol_format.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/ip.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/ip.md new file mode 100644 index 0000000000000000000000000000000000000000..4f7b797125d52e81b7cc95c819b7fd83caffb52d --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-ip/ip.md @@ -0,0 +1,163 @@ +# IP报文基础及其在Lwip的实现 + +## 1\. IP的背景 + +  IP 协议是 TCP/IP 协议中最为核心的协议,所有的 TCP、UDP、ICMP 及 IGMP 数据都已 IP 数据报格式传输。IP协议在 TCP/IP 协议族的分层中属于网络层,不难理解,IP 的主要作用有两个:其一是对上层协议[^1^]( ## 4 参考资料)的数据进行封装( 增加 IP 首部 ),然后交给链路层协议进行发送;其二是对链路层接收到的数据进行解析,并根据解析结果将数据交给对应的上层协议进行处理。 + +  本文主要介绍 IP 数据报的格式,以及 IP 相关功能在 Lwip 中的实现方式,希望能对同样在学习 Lwip 的小伙伴们有所帮助。 + +## 2\. IP基础知识介绍 + +### 2.1 IP数据报的格式 + +  IP数据报的格式主要包含 IP 首部和数据,通常情况下,IP 首部的长度为 20 字节(含有选项字段的除外),具体如下图所示: + +![IP数据报格式](./figure/ip_protocol_format.png) + +#### 2.1.1 版本 + +  协议版本号,当前普遍应用的 IP 协议版本号是 4,因此也常称为 IPv4。 + +#### 2.1.2 首部长度 + +  首部长度即 IP 首部所占用的 32 bit 字的数目。因为首部长度是一个 4 bit 段,其最大值是 15,也就意味着 IP 首部的最大长度是 60 字节。 + +#### 2.1.3 服务类型(TOS) + +  服务类型字段长 8 位,最初的 TOS 字段中最高 3 位表示优先权,随后的 4 位表示服务类型,最后一位保留,恒设置为 0。 + +  当前的服务类型字段已经作为区分服务(Diffserv)架构的一部分被重新定义了,重定义的服务类型字段中前6位构成了区分代码点(DiffServ Code Point, DSCP),后 2 位用于显示拥塞通知(Explicit Congestion Notification, ECN)。这两个概念不是很易懂,在 Lwip 中,该字段由上层协议设定,经过在 windows系统下的随机抓包发现,该字段通常设置位 0x00,因此对该字段不再深究。 + +#### 2.1.4 总长度 + +  总长度即整个 IP 数据报的长度,单位是字节。因为总长度是一个 16 位字段,其最大值是 65535,所以 IP 数据报的最大长度位 65535 字节。利用总长度和首部长度两个字段就可以知道 IP 数据报中数据的起始位置和长度。 + +>     注意总长度和首部长度的单位不同,总长度的单位是字节,而首部长度的单位是 32 位字。 + +#### 2.1.5 标识 + +  标识字段唯一的标识发送的每一份数据报,通常每发送一份报文它的值就会加 1。在 Lwip 的源文件 "ip4.c" 中,定义了一个变量 `static u16_t ip_id`用于记录 IP 数据报的唯一标识,其初始值为 0,每次发送 IP 数据报时都将该变量的值填入 IP 首部的标识字段中,然后将该变量 +1。 + +#### 2.1.6 标志 + +  标志字段共有 3 位,其中第 1 位没有使用。第 2 位是不分段(DF)位,置 1 表示路由器不能对数据进行分段处理,如果数据包由于不能被分段而未能被转发,则路由器会丢弃该数据包并向源点发送错误消息。第三位是还有更多分段(MF)位,即表示数据还有其他的分段,当路由器对数据包进行分段时,除最后一个分段外,其他所有的分段 MF 位都应置 1,以便接收端直到接收到 MF 位为 0 的分段为止。 + +#### 2.1.7 生存时间(TTL) + +  生存时间反应了从源 IP 到目的 IP 经过的路由器数量即跳数。在最初创建 IP 数据报时,TTL 被设置为一个特定的值,通常是 32 或者 64,数据报每经过一个路由器,该路由器就将 TTL 减 1,因此可以通过 TTL 的值推算 IP 数据报经过的跳数。 + +#### 2.1.8 协议 + +  协议字段给出了 IP 协议上层协议的协议号,当前已分配的协议号有 100 多个,下表列出了一些常见的协议号,更多的协议号可以自百度。 + +| 协议号 | 协议 | +| ------ | ---------------------------- | +| 1 | Internet消息控制协议(ICMP) | +| 2 | Internet组管理协议(IGMP) | +| 4 | 被IP协议封装的IP | +| 6 | 传输控制协议(TCP) | +| 17 | 用户数据报协议(UDP) | + +#### 2.1.9 首部检验和 + +  首部检验和是针对 IP 首部的纠错字段,检验和不计算被封装的数据。检验和的计算方法和验证方法将在后文的代码分析部分进行介绍。 + +#### 2.1.10 源地址和目的地址 + +  顾名思义,指 IP 数据报的来源和目的 IP 地址,我们平时常见的 IP 地址如 “192.168.0.1” 是 IP 地址的点分十进制表示方法,是方便人读写和记忆的,IP 地址对于电子设备来说是一个 32 位的二进制数。 + +#### 2.1.11 可选项 + +  首先,可选项不是所有 IP 数据报都有的,可选项可以包含源点产生的信息和路由器产生的信息;其次,可选项的长度必须是 32 位的整数倍,最长长度是 40 个字节,这一点从首部长度的定义也可以看得出来,如果长度不是 32 位的整数倍,可以在结尾补 0 以满足要求。 + +  常用的可选项有安全和处理限制(常用于军事领域)、记录路径、时间戳、宽松的源站选路、严格的源站选路。因为使用频率很低而且并非所有的主机和路由器都支持这些可选项,此处不再详细叙述,感兴趣的小伙伴可以自行查阅资料进行了解。 + +### 2.2 IP地址基础知识 + +  前面的已经提到,IP 地址的是一个 32 位的二进制数,为了方便记忆和读写,常用点分十进制的方法来表示 IP 地址。IP 地址中包含了网络号和主机号,可能还包含有子网号,为了能正确的从 IP 地址中获取网络号、主机号、子网号,还需要有子网掩码的辅助。本小结简要介绍IP的点分十进制表示方法,网络号、主机号、子网号和子网掩码的含义。 + +#### 2.2.1 IP地址的点分十进制表示方法 + +  为了方便记忆和表述,将 32 位的 IP 地址分为 4 个字节,将每个字节都以十进制表示,在四个字节转换得到的十进制数中间分别加一个点用以区分,这就是我们常见的 IP 地址的形式。 + +  比如面这个 IP 地址 \(11000000101010000000000100000001\) 2,直接用二进制表示显得很长,也很难记忆,直接写成 10 进制数是(3,232,235,777)10,写成 16 进制数是(0xC0A8 0101),都比较难以记忆,而用点分十进制表示方法则可以写成 “192.160.1.1”,显然是一个我们经常会接触到的一个 IP 地址。由此可见,点分十进制表示方法,在日常使用中是非常方便的。 + +#### 2.2.2 网络号、主机号和子网号 + +  简单的讲,互联网是把一个个小的网络链接起来组成一个庞大的网络,因此要找到一个 IP 地址,首先要找到这个 IP 地址处于哪个小的网络中,然后再找到这个IP对应于这个网络中的哪一台主机。因此 IP 地址就分成了网络号和主机号两个部分。实际上,根据网络号找到一个网络后,这个网络可能会划分成几个更小的网络组成的,也就是说我们还需要再找到该 IP 属于这个网络的哪一个子网,然后才能找到这台主机。而要找到 IP 属于哪一个子网,自然也就需要一个子网号。因为不是所有的网络都会划分子网,因此并不是所有的IP地址都包含子网号的。 + +#### 2.2.3 IP地址的分类 + +  前文提到了 IP 地址包含了网络号、主机号,可能还有子网号,那么如果我们已知一个 IP 地址,如何确定这个 IP 是否含有子网号,它的网络号、子网号(如果有)、主机号分别是什么呢? + +  要解决这个问题,首先要了解IP地址的分类:A 类地址、B 类地址和 C 类地址。A类地址的前 8 位表示网络号,并且其最高位恒位 0,因此其高 8 位转换成十进制的范围是 1 \~ 126[^2^](## 4 参考资料),如果用点分十进制的方式表示 IP 地址,那么 A 类 IP 地址的第一段应该在 1 到 126 中间;B 类地址的前 16 位表示网络号,其最高两位恒为 \(10\)~2~,其高8位转换成 10 进制的范围是 128\~191;C类地址的前 24 位表示网络号,其最高三位恒为(110)~2~,高8位转换位10进制的范围是192\~223。 + +  根据上述规则,就可以确定一个给定IP地址的网络号。 + +#### 2.2.4 子网掩码 + +  确定一个 IP 地址的网络号后,还需要确定其子网号和主机号。前文叙述中,有时会把子网号放在主机号之后,这是因为子网号并不总是存在,但在IP地址中的顺序实际上是网络号、子网号(如果有)、主机号。确定网络号后,必须先确定该IP是否含有子网号,子网号是多少(如果有),然后才能获取主机号。 + +  要确定 IP 中的子网号,就需要子网掩码的辅助了。子网掩码也是一个 32 位的二进制数,并且通常也以点分十进制的方式表示,这一点与 IP 地址类似。其作用是告诉主机 IP 地址中有多少位是用来表示子网号的(实际上是告诉主机IP地址中网络号和子网号加起来有多少位),子网掩码中值位 1 的比特留给网络号和子网号,值为 0 的比特留给主机号。由于已知网络号的位数,通过子网掩码可以知道网络号和子网号的总位数,自然也就能区分出 IP 地址中的网络号、子网号和主机号了。 + +  以上文提到的 IP 地址 “192.168.1.1” 为例,假设其子网掩码为 “255.255.255.0”。首先根据第一段的值 “192” 可以确定该 IP 地址属于 C 类 IP,有 24 位表示网络号,其网络号位 “192.168.1”。子网掩码的高 24 位为 1,低 8 位为 0,说明网络号和子网号加起来共计 24 位,也就是说这里没有进行子网划分,那么其主机号就是 “1”。 + +  因为 IP 地址总是按网络号、子网号、主机号排列的,不难理解子网掩码的一个特征:高位是连续的 1,低位是连续的 0。在 Lwip 的实现中,“ip4\_aaddr.c” 中有一个检验子网掩码合法性的函数 `ip4_addr_netmask_valid`,就是利用这个特征,对于一个待检验的数,从最高位起,找到第一个 0,然后再检查第一个 0 后面还有没有 1,如果没有,说明待检验数是一个合法的子网掩码。 + +## 3\. IP相关功能在 Lwip 中的实现 + +### 3.1 ip\_hdr 结构体简介 + +  ip\_hdr 结构体就是 IP 首部的结构体,其定义如下: + +```c +struct ip_hdr { + PACK_STRUCT_FLD_8(u8_t _v_hl); + PACK_STRUCT_FLD_8(u8_t _tos); + PACK_STRUCT_FIELD(u16_t _len); + PACK_STRUCT_FIELD(u16_t _id); + PACK_STRUCT_FIELD(u16_t _offset); + PACK_STRUCT_FLD_8(u8_t _ttl); + PACK_STRUCT_FLD_8(u8_t _proto); + PACK_STRUCT_FIELD(u16_t _chksum); + PACK_STRUCT_FLD_S(ip4_addr_p_t src); + PACK_STRUCT_FLD_S(ip4_addr_p_t dest); +} PACK_STRUCT_STRUCT; +``` + +  在源代码中包含了对每个元素的注释,结合注释和 IP 首部的格式,不难理解各元素所代表的的意义。需要注意的一点是,IP 首部并不都是按字节定义数据的含义的,但是在 `ip_hdr` 结构体中,对部分字段进行了合并,方便了结构体定义。IP 首部的版本和首部长度两个字段,其长度都是 4 位,并且位置相邻,因此在 `ip_hdr `结构体总将这两个字段合并在一起,定义了一个 8 位的变量 `_v_hl`,其中的 "v" 便是 version,"hl" 即 head lenth。IP 首部的 3 位标志字段和 13 位的片偏移字段则合并成一个 16 位的变量 `_offset`。剩余变量则都与 IP 首部的字段一一对应,小伙伴们可自行翻阅源码,并对照 IP 数据报格式自行理解。 + +  从结构体定义来看,结构体中并没有可选项相关的元素,但这并不意味着 Lwip 没有支持可选项的能力。 + +### 3.2 Lwip 中 IP 层数据发送流程简介 + +  Lwip中,IP层对发送数据的处理依次经过了 `ip4_output`、`ip4_output_if`、`ip4_output_if_opt`、`ip4_output_if_opt_src `四个函数。`ip4_output `函数中,根据目的 IP 地址确定了该 IP 数据报的发送路径(即通过哪个网卡发送该数据报),并将传入的数据和网卡信息传递给函数 `ip4_output_if`。在当前定义中,函数 `ip4_output_if` 没有做任何操作,直接将数据传送给函数 `ip4_output_if_opt`,`ip4_output_if_opt` 比 `ip4_output_if` 多出的两个形参 `void *ip_options` 和 `u16_t optlen` 分别传入了实参空指针和 0。函数 `ip4_output_if_opt` 中根据选定的网卡确定了源地址,然后再次将所有数据传递给函数 `ip4_output_if_opt_src` 。函数 `ip4_output_if_opt_src` 根据传入的信息,完成 IP 报文首部的填充,然后调用网卡的发送函数,将 IP 数据报交由链路层处理。每个函数的具体实现,请小伙伴们自行阅读源码。 + +  接下来讨论下 Lwip 对 IP 首部可选项的处理。首先可以确定的是,Lwip 是有处理可选项的能力的,前面提到 `ip4_output_if_opt` 比 `ip4_output_if` 多出两个形参,这连个形参实际上就是指可选项的内容和长度,在传入实参时直接传入了空指针和 0,也就是说所有以函数 `ip4_output` 为处理起点的 IP 数据报,都是没有可选项的。如果想要发送一个首部含有可选项的 IP 数据报,则上层协议需要调用函数 `ip4_output_if_opt`,而以该函数位处理起点的 IP 数据报,就跳过了查找发送路径这一功能。在源码 "ip4.h" 中,有一句注释 “Currently, the function ip\_output\_if\_opt\(\) is only used with IGMP ”,据此推测,只有 IGMP 协议在发送IP数据报时调用的是函数 `ip4_output_if_opt`,而其他协议则调用 `ip4_output` 函数,至于该推测是否准确,需要分析上层协议的源码,本文不再进一步扩展。 + +  首部检验和的计算方法。关于首部检验和的计算方法,不同的参考资料中描述有所不同。也许是因为协议发展过程中有所改变,也许是有些书籍中出现了谬误,也许是这些不同的描述本质上是一样的(这设计到比较深奥的数学知识,已经超出了我的能力范畴),因此吧检验的介绍放到源码分析的部分来讲解,结合代码中的实际操作,确保给小伙伴们呈现除一个正确的计算方法。首部检验和的计算,属于 IP 首部填充的一部分,其源码实现包含在 `ip4_output_if_opt_src` 中。计算的方法是,定义一个 32 位的变量 `chk_sum` 并初始化为 0,然后将 IP 首部的内容按 16 位字相加,注意此处 IP 首部的检验和字段本身无需参与运算,然后将得到的结果进行如下处理: + +```c +chk_sum = (chk_sum >> 16) + (chk_sum & 0xFFFF); +chk_sum = (chk_sum >> 16) + chk_sum; +chk_sum = ~chk_sum; +``` + +最终得到的结果即为检验和,将其填入检验和字段即可。 + +### 3.3 Lwip 中接收数据处理简介 + +  Lwip 中 IP 层处理接收数据的函数是 `ip4_input`。该函数首先对 IP 数据报的合法性进行检查,包括 IP 版本、检验和、长度等信息的检查,然后判断 IP 数据报是否确实是却要本机接收的,如果满足以上条件,则根据 IP 首部中的协议字段,将 IP 数据报转发给对应的上层协议进行处理。 + +## 4 参考资料 + +1. RT-THREAD Lwip 组件 v2.0.2源码; +2. 《TCP-IP详解卷一:协议》; +3. 《TCP/IP路由技术(第一卷)(第二版)》。 + +* * * + +1. ICMP 和 IGMP 与IP同样属于网络层,但是因为其数据也需要以IP数据报的格式传输,所以也可以看做是IP的上层协议。 + +2. 网络号不能为全0或全1。 + diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/figure/mempool_style.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/figure/mempool_style.png new file mode 100644 index 0000000000000000000000000000000000000000..0ed5b58d45e28a80fc32fa43b2fe331b56ed1fd1 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/figure/mempool_style.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/mempool.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/mempool.md new file mode 100644 index 0000000000000000000000000000000000000000..7899016c12ea3e60aecb8845c61a4171dd415221 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-mempool/mempool.md @@ -0,0 +1,279 @@ +# Lwip 内存池内数据的初始化分配 + +## 1.简述 + +​ Lwip内存管理方式有三种:C库自带的内存分配策略,动态内存堆(HEAP)分配策略,动态内存池(POOL)分配策略。 + +​ 本篇文章只对内存池(POOL)数据的初始化分配做详细展开,内存池是一块连续的大内存,可看作数组。 + +​ 优点:内存的分配、释放效率高,可以有效防止内存碎片的产生。 + +​ 缺点:浪费部分内存。 + +​ 动态内存池(pool),pool有很多种,pool的个数由用户配置,根据协议栈实际使用状况进行规划。协议栈中所有pool是一片连续的内存区域。为什么LWIP需要有pool?因为协议栈里面有大量的协议首部,这些协议首部长度都是固定不变的,所以可以首先分配固定内存,给这些固定长度的协议首部,以后每次需要处理协议首部时,都直接使用这些已经分配的内存,不需要重新分配内存区域,这样子就达到一个地方分配,多个地方使用的方便与效率。 + + +## 2.数据结构 + +- `memp_t `:枚举类型,为每种pool定义编号。 + + ```c + typedef enum { + #define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name, + #include "lwip/memp_std.h" + MEMP_MAX + } memp_t; + ``` + + 简单看下这个枚举类型定义,##表示连接符,头文件做数组元素,表示在预处理阶段把头文件内容复制到此结构中。展开后,`memp_t`如下。 + + ```c + typedef enum { + MEMP_RAW_PCB, + MEMP_UDP_PCB, + MEMP_TCP_PCB, + MEMP_TCP_PCB_LISTEN, + MEMP_TCP_SEG, + ... + MEMP_MAX + } memp_t; + ``` + +- `memp_tab[] `:指针数组,分别指向每类pool的第一个元素。 + +- `memp_sizes[] `:数组,每种pool中的独立元素所占字节数。 + ```c + static const u16_t memp_sizes[MEMP_MAX] = { + #define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEM_ALIGN_SIZE(size), + #include "lwip/memp_std.h" + }; + + //展开如下 + static const u16_t memp_sizes[MEMP_MAX] = { + LWIP_MEM_ALIGN_SIZE(sizeof(struct raw_pcb)), + LWIP_MEM_ALIGN_SIZE(sizeof(struct udp_pcb)), + LWIP_MEM_ALIGN_SIZE(sizeof(struct tcp_pcb)), + LWIP_MEM_ALIGN_SIZE(sizeof(struct tcp_pcb_listen)), + LWIP_MEM_ALIGN_SIZE(sizeof(struct tcp_seg)), + ... + }; + ``` +- `memp_num[] `:数组,每种pool中所有元素的个数。 + ```c + static const u16_t memp_num[MEMP_MAX] = { + #define LWIP_MEMPOOL(name,num,size,desc) (num), + #include "lwip/memp_std.h" + }; + + //展开如下 + + static const u16_t memp_num[MEMP_MAX] = { + MEMP_NUM_RAW_PCB, + MEMP_NUM_UDP_PCB, + MEMP_NUM_TCP_PCB, + MEMP_NUM_TCP_PCB_LISTEN, + MEMP_NUM_TCP_SEG, + ... + }; + ``` +- `memp_desc[] `:指针数组,指向每个pool的描述字符串。 + ```c + static const char *memp_desc[MEMP_MAX] = { + #define LWIP_MEMPOOL(name,num,size,desc) (desc), + #include "lwip/memp_std.h" + }; + + //展开如下 + + static const char *memp_desc[MEMP_MAX] = { + ("RAW_PCB"), + ("UDP_PCB"), + ("TCP_PCB"), + ("TCP_PCB_LISTEN"), + ... + }; + ``` +- `memp_memory[] `:数组,所有pool所占内存总和。 + ```c + static u8_t memp_memory[MEM_ALIGNMENT - 1 + #define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) ) + #include "lwip/memp_std.h" + ]; + + //展开如下 + + static u8_t memp_memory[MEM_ALIGNMENT - 1 + + ( (MEMP_NUM_RAW_PCB) * (MEMP_SIZE + MEMP_ALIGN_SIZE(sizeof(struct raw_pcb)) ) ) + + ( (MEMP_NUM_UDP_PCB) * (MEMP_SIZE + MEMP_ALIGN_SIZE(sizeof(struct udp_pcb)) ) ) + + ...]; + ``` +## 3.函数接口 + +- `memp_init`:对`memp_pools`数组进行初始化,包括对齐内存池首地址、清空内存池空间、构建内存池链表、初始化内存统计结构体。 +- `memp_malloc`:根据传入的内存池类型进行实际内存分配。 +- `memp_free`:内存释放。 + +## 4.内存池初始化分配 + +```c + void + memp_init(void) + { + u16_t i; + + /* 对每个类型的内存池进行初始化 */ + for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) { + memp_init_pool(memp_pools[i]); + + #if LWIP_STATS && MEMP_STATS + lwip_stats.memp[i] = memp_pools[i]->stats; + #endif + } + + #if MEMP_OVERFLOW_CHECK >= 2 + /* 第一次检查一切,看看它是否有效 */ + memp_overflow_check_all(); + #endif /* MEMP_OVERFLOW_CHECK >= 2 */ + } +``` + +```c + void + memp_init_pool(const struct memp_desc *desc) + { + #if MEMP_MEM_MALLOC + LWIP_UNUSED_ARG(desc); + #else + int i; + struct memp *memp; + + *desc->tab = NULL; + /* 内存池起始空间对齐 */ + memp = (struct memp*)LWIP_MEM_ALIGN(desc->base); + /* create a linked list of memp elements */ + for (i = 0; i < desc->num; ++i) { + /* 将当前种类所有pool以链表方式链接 */ + memp->next = *desc->tab; + *desc->tab = memp; + #if MEMP_OVERFLOW_CHECK + memp_overflow_init_element(memp, desc); + #endif /* MEMP_OVERFLOW_CHECK */ + /* 地址偏移 */ + memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size + #if MEMP_OVERFLOW_CHECK + + MEMP_SANITY_REGION_AFTER_ALIGNED + #endif + ); + } + #if MEMP_STATS + desc->stats->avail = desc->num; + #endif /* MEMP_STATS */ + #endif /* !MEMP_MEM_MALLOC */ + + #if MEMP_STATS && (defined(LWIP_DEBUG) || LWIP_STATS_DISPLAY) + desc->stats->name = desc->desc; + #endif /* MEMP_STATS && (defined(LWIP_DEBUG) || LWIP_STATS_DISPLAY) */ + } +``` + +![mempool.png](./figure/mempool_style.png) + +## 5.内存分配 +```c + #define memp_malloc(t) memp_malloc_fn((t), __FILE__, __LINE__) + + void * + memp_malloc_fn(memp_t type, const char* file, const int line) + { + void *memp; + LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;); + + memp = do_memp_malloc_pool_fn(memp_pools[type], file, line); + + return memp; + } + + static void* + do_memp_malloc_pool_fn(const struct memp_desc *desc, const char* file, const int line) + { + struct memp *memp; + SYS_ARCH_DECL_PROTECT(old_level); + + SYS_ARCH_PROTECT(old_level); + + memp = *desc->tab; + + if (memp != NULL) { + #if !MEMP_MEM_MALLOC + #if MEMP_OVERFLOW_CHECK == 1 + memp_overflow_check_element_overflow(memp, desc); + memp_overflow_check_element_underflow(memp, desc); + #endif /* MEMP_OVERFLOW_CHECK */ + + *desc->tab = memp->next; + #if MEMP_OVERFLOW_CHECK + memp->next = NULL; + #endif /* MEMP_OVERFLOW_CHECK */ + #endif /* !MEMP_MEM_MALLOC */ + #if MEMP_OVERFLOW_CHECK + memp->file = file; + memp->line = line; + #endif /* MEMP_OVERFLOW_CHECK */ + LWIP_ASSERT("memp_malloc: memp properly aligned", + ((mem_ptr_t)memp % MEM_ALIGNMENT) == 0); + + SYS_ARCH_UNPROTECT(old_level); + /* cast through u8_t* to get rid of alignment warnings */ + return ((u8_t*)memp + MEMP_SIZE); + } else { + LWIP_DEBUGF(MEMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, \ + ("memp_malloc: out of memory in pool %s\n", desc->desc)); + } + + SYS_ARCH_UNPROTECT(old_level); + return NULL; + } +``` +​ 内存池申请函数的核心代码`memp = *desc->tab;`执行后,能直接得到对应内存块中的第一个空闲内存块,如果该内存块不是NULL,则将其取出,并且移动 `*desc->tab`指针,指向下一个空闲内存块,然后将`((u8_t *)memp + MEMP_SIZE)`返回。 +## 6.内存释放 +```c + void + memp_free(memp_t type, void *mem) + { + LWIP_ERROR("memp_free: type < MEMP_MAX", (type < MEMP_MAX), return;); + + if (mem == NULL) { + return; + } + do_memp_free_pool(memp_pools[type], mem); + } +``` + +```c + static void + do_memp_free_pool(const struct memp_desc* desc, void *mem) + { + struct memp *memp; + SYS_ARCH_DECL_PROTECT(old_level); + + LWIP_ASSERT("memp_free: mem properly aligned", + ((mem_ptr_t)mem % MEM_ALIGNMENT) == 0); + + /* cast through void* to get rid of alignment warnings */ + memp = (struct memp *)(void *)((u8_t*)mem - MEMP_SIZE); + + SYS_ARCH_PROTECT(old_level); + + #if MEMP_OVERFLOW_CHECK == 1 + memp_overflow_check_element_overflow(memp, desc); + memp_overflow_check_element_underflow(memp, desc); + #endif /* MEMP_OVERFLOW_CHECK */ + + memp->next = *desc->tab; + *desc->tab = memp; + + SYS_ARCH_UNPROTECT(old_level); + } +``` + +​ 内存释放函数需要把使用完毕的内存添加到对应内存池中的空闲内存块链表,释放内存有两个参数,一POOL的类型,二内存块的起始地址。 \ No newline at end of file diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/rto_rtt_calc.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/rto_rtt_calc.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf86d7c4794407bc2eb9b804645746d4a4f56b2 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/rto_rtt_calc.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_client_connect.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_client_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..3aef605baae2c2d34b5eaf0a0c679231cc7b3b39 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_client_connect.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_connect.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..78be37428ca747b7e003883afd0d5ec8d5b9ade2 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_connect.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_connect_log.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_connect_log.png new file mode 100644 index 0000000000000000000000000000000000000000..37f4e34a91e88f19630b9e1b9b4b792bd3c5dfbe Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_connect_log.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..31ce0c1e57c1c1d1612d613847573d21bbf5edf0 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_client.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_client.png new file mode 100644 index 0000000000000000000000000000000000000000..5d7ee7df821c5002764b871809361b7bffd6377a Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_client.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_log.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_log.png new file mode 100644 index 0000000000000000000000000000000000000000..72fe3bc142d9dc82ac6238256573e0dc86e0ce6a Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_log.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_server.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_server.png new file mode 100644 index 0000000000000000000000000000000000000000..4de554187f0d1c570cf72f389cff274b34333a60 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_disconnect_server.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_protocol.jpg b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_protocol.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aedc93f45cf4c000ef72cfa8d75a8b45c6fcd9fd Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_protocol.jpg differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_server_connect.png b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_server_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..80aec03132bb18b69101ef6a468680ca70049c8b Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/figure/tcp_server_connect.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/tcp.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/tcp.md new file mode 100644 index 0000000000000000000000000000000000000000..ad4abb9c4327a9218811d23dad08b5d222851185 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-tcp/tcp.md @@ -0,0 +1,378 @@ +# LWIP TCP 报文基础 + +TCP协议(Transmission Control Protocol)传输控制协议在LWIP协议栈中占据了大半的代码,它是最常见的传输层协议,也是最稳定的传输层协议,很多上层应用都是依赖 TCP 协议进程传输数据,如 SMTP,FTP 等等。 + +## TCP 协议简介 + +TCP 与 UDP 一样,都是传输层的协议,但是提供的服务却不相同,UDP 为上层应用提供的是一种不可靠的,无连接的服务,而 TCP 提供一种面向连接,可靠的字节传输服务,TCP 让两个主机建立连接的关系,应用数据以数据流的形式进行传输,先后发出的数据在网络中虽然也是互不干扰的传输,但是这些数据本身携带的信息确实紧密联系的,TCP 协议会给每个传输的字号进行编号,当然了,两个主机方向上的数据编号是彼此独立的,在传输的过程中,发送方把数据的起始编号与长度放在 TCP 报文中,接收方将所有数据按照编号组装起来,然后返回一个确认,当所有的数据接收完成后才将数据递交到应用层。 + +## TCP 报文段结构 + +TCP 报文段依赖 IP 协议进行发送,因此 TCP 报文段与 ICMP 报文 一样,都是封装在 IP 数据报文中,IP 数据报封装在以太网帧中,因此 TCP 报文段也是经过 了两次的封装,然后发送出去,报文结构如下: + +![tcp_protocol](./figure/tcp_protocol.jpg) + +1. 源端口(Source Port),16 位。 + +2. 目的端口(Destination Port),16 位。 + +3. 序列号 \(Sequence Numbe\) 发送数据包中的第一个字节的,32 位。TCP 协议是基于流发送数据的,所有使用序号给发送的所有数据字节都编上号,并按照序号进行顺序发送。 + +4. 确认序列号 \(Acknowledgment Number\),32 位。TCP 的通信是全双工的(两端同时发送和接受),当连接建立成功后,每一方都能同时发送和接收数据。每一个方向的序号代表这个报文段携带的第一个字节数据的编号。每一方还需要使用确认号对它已经收到的字节进行确认。即:本端把收到的最后一个字节的编号加一,这个值就是确认号,然后发送给对端。 + +5. 数据偏移 \(Data Offset\),4 位,该字段的值是 TCP 首部(包括选项)长度除以 4。 + +6. 标志位:6 位,URG 表示紧急指针(Urgent Pointer)字段有意义,ACK 表示确认序(Acknowledgment Number)字段有意义;TCP 规定,只有 ACK=1 时有效,也规定连接建立后所有发送的报文的 ACK 必须为1 。PSH 表示 Push 功能,推送数据。RST 表示复位 TCP 连接 SYN 表示 SYN 报文,在建立 TCP 连接的时候使用,用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使 SYN=1 和 ACK=1。因此, SYN 置 1 就表示这是一个连接请求或连接接受报文。FIN 表示没有数据需要发送了,终止连接。在关闭 TCP 连接的时候使用,用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。 + +7. 窗口(Window)表示接收缓冲区的空闲空间,16 位,用来告诉 TCP 连接对端自己能够接收的最大数据长度。 + +8. 校验和(Checksum),16 位。每一个报文段都包括有校验和字段,若报文有损伤,则由终点 TCP 将其丢弃,并被认为是丢失。TCP 的校验和是强制的。 + +9. 紧急指针(Urgent Pointers),16 位,只有 URG 标志位被设值时该字段才有意义,表示紧急数据相对序列号(Sequence Number字段的值)的偏移。用URG 值 + 序号得到最后一个紧急字节。 + +# LWIP 中 TCP 协议的实现 + +## TCP 控制块 + +TCP 报文段如 APR 报文、IP 数据报一样,也是由首部 + 数据区域组成,TCP 报文段的首部我们称之为 TCP 首部,其首部内容很丰富,各个字段都有不一样的含义,如果不计算 选项字段,一般来说 TCP 首部只有 20 个字节,具体见上图。在 LwIP 中,报文段首部采用一个名字叫 tcp\_hdr 的结构体进行描述。 + +```c +PACK_STRUCT_BEGIN +struct tcp_hdr { + PACK_STRUCT_FIELD(u16_t src); // 源端口 + PACK_STRUCT_FIELD(u16_t dest); // 目标端口 + PACK_STRUCT_FIELD(u32_t seqno); // 序号 + PACK_STRUCT_FIELD(u32_t ackno); // 确认序号 + PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); // 首部长度+保留位+标志位 + PACK_STRUCT_FIELD(u16_t wnd); // 窗口大小 + PACK_STRUCT_FIELD(u16_t chksum); // 校验和 + PACK_STRUCT_FIELD(u16_t urgp); // 紧急指针 +} PACK_STRUCT_STRUCT; +PACK_STRUCT_END +``` + +每个 TCP 报文段都包含源主机和目标主机的端口号,用于寻找发送端和接收端应用线程,这两个值加上 IP 首部中的源 IP 地址和目标 IP 地址就能确定唯一一个 TCP 连接。 + +序号字段用来标识从 TCP 发送端向 TCP 接收端发送的数据字节流,它的值表示在这 个报文段中的第一个数据字节所处位置吗,根据接收到的数据区域长度,就能计算出报文 最后一个数据所处的序号,因为 TCP 协议会对发送或者接收的数据进行编号(按字节的形式),那么使用序号对每个字节进行计数,就能很轻易管理这些数据。序号是 32 bit 的无 符号整数。 + +当建立一个新的连接时,TCP 报文段首部的 SYN 标志变 1,序号字段包含由这个主机随机选择的初始序号 ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序 号为 ISN+1,因为 SYN 标志会占用一个序号。 + +既然 TCP 协议给每个传输的字节都了编号,那么确认序号就包含接收端所期望收到的 下一个序号,因此,确认序号应当是上次已成功收到数据的最后一个字节序号加 1。当然, 只有 ACK 标志为 1 时确认序号字段才有效,TCP 为应用层提供全双工服务,这意味数据能 在两个方向上独立地进行传输,因此确认序号通常会与反向数据(即接收端传输给发送端 的数据)封装在同一个报文中(即捎带),所以连接的每一端都必须保持每个方向上的传输数据序号准确性。 + +首部长度字段占据 4 bit 空间,它指出了 TCP 报文段首部长度,以字节为单位,最大能记录 15\*4=60 字节的首部长度,因此,TCP 报文段首部最大长度为 60 字节。在字段后接下 来有 6 bit 空间是保留未用的。 + +此外还有 6 bit 空间,是 TCP 报文段首部的标志字段,用于标志一些信息: + +- URG:首部中的紧急指针字段标志,如果是 1 表示紧急指针字段有效。 + +- ACK:首部中的确认序列字段标志,如果是 1 表示确认序列字段有效。 + +- PSH:该字段置一表示接收方应该尽快将这个报文段交给应用层。 + +- RST:重新建立 TCP 连接。 + +- SYN:用同步序列发起连接。 + +- FIN:终止连接。 + +TCP 的流量控制由连接的每一端通过声明的窗口大小来提供,窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的数据序号,发送发根据窗口的大小调整发送数据,以实现流量控制。窗口大小是一个占据 16 bit 空间的字段,因而窗口最大为 65535 字节,当接收方告诉发送发一个大小为 0 的窗口时,将完全阻止发送方的数据发送。 + +检验和覆盖了整个 TCP 报文段:TCP 首部和 TCP 数据区域,由发送端计算填写,并由接收端进行验证。 + +## TCP 连接 + +#### TCP 建立连接 + +每个 TCP 连接都需要一个 TCP 控制块结构体 tcp\_pcb (宏展开后), 结构体内包含了 TCP 连接所需的各种信息, 如 ip 地址和端口号, 连接状态和标志, 报文参数和数据指针等: + +```c +/** the TCP protocol control block */ +struct tcp_pcb { +/** common PCB members */ + /* ip addresses in network byte order * + ip_addr_t local_ip; + ip_addr_t remote_ip; + /* Socket options */ + u8_t so_options; + /* Type Of Service */ + u8_t tos; + /* Time To Live */ + u8_t ttl + /* link layer address resolution hint */ \ + IP_PCB_ADDRHINT + +/** protocol specific PCB members */ + type *next; /* for the linked list */ + void *callback_arg; + enum tcp_state state; /* TCP state */ //记录TCP连接所处的状态 + u8_t prio; + /* ports are in host byte order */ + u16_t local_port + + /* ports are in host byte order */ + u16_t remote_port; + + tcpflags_t flags; + + /* the rest of the fields are in host byte order + as we have to do some math with them */ + + /* Timers */ + u8_t polltmr, pollinterval; + u8_t last_timer; + u32_t tmr; + + /* receiver variables */ + u32_t rcv_nxt; /* next seqno expected */ + tcpwnd_size_t rcv_wnd; /* receiver window available */ + tcpwnd_size_t rcv_ann_wnd; /* receiver window to announce */ + u32_t rcv_ann_right_edge; /* announced right edge of window */ + + /* Retransmission timer. */ + s16_t rtime; + + u16_t mss; /* maximum segment size */ + + /* RTT (round trip time) estimation variables */ + u32_t rttest; /* RTT estimate in 500ms ticks */ + u32_t rtseq; /* sequence number being timed */ + s16_t sa, sv; /* @todo document this */ + + s16_t rto; /* retransmission time-out */ + u8_t nrtx; /* number of retransmissions */ + + /* fast retransmit/recovery */ + u8_t dupacks; + u32_t lastack; /* Highest acknowledged seqno. */ + + /* congestion avoidance/control variables */ + tcpwnd_size_t cwnd; + tcpwnd_size_t ssthresh; + + /* sender variables */ + u32_t snd_nxt; /* next new seqno to be sent */ + u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last + window update. */ + u32_t snd_lbb; /* Sequence number of next byte to be buffered. */ + tcpwnd_size_t snd_wnd; /* sender window */ + tcpwnd_size_t snd_wnd_max; /* the maximum sender window announced by the remote host */ + + tcpwnd_size_t snd_buf; /* Available buffer space for sending (in bytes). */ +#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3) + u16_t snd_queuelen; /* Number of pbufs currently in the send buffer. */ + +#if TCP_OVERSIZE + /* Extra bytes available at the end of the last pbuf in unsent. */ + u16_t unsent_oversize; +#endif /* TCP_OVERSIZE */ + + /* These are ordered by sequence number: */ + struct tcp_seg *unsent; /* Unsent (queued) segments. */ + struct tcp_seg *unacked; /* Sent but unacknowledged segments. */ +#if TCP_QUEUE_OOSEQ + struct tcp_seg *ooseq; /* Received out of sequence segments. */ +#endif /* TCP_QUEUE_OOSEQ */ + + struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */ + +#if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG + struct tcp_pcb_listen* listener; +#endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG */ + +#if LWIP_CALLBACK_API + /* Function to be called when more send buffer space is available. */ + tcp_sent_fn sent; + /* Function to be called when (in-sequence) data has arrived. */ + tcp_recv_fn recv; + /* Function to be called when a connection has been set up. */ + tcp_connected_fn connected; + /* Function which is called periodically. */ + tcp_poll_fn poll; + /* Function to be called whenever a fatal error occurs. */ + tcp_err_fn errf; +#endif /* LWIP_CALLBACK_API */ + +#if LWIP_TCP_TIMESTAMPS + u32_t ts_lastacksent; + u32_t ts_recent; +#endif /* LWIP_TCP_TIMESTAMPS */ + + /* idle time before KEEPALIVE is sent */ + u32_t keep_idle; +#if LWIP_TCP_KEEPALIVE + u32_t keep_intvl; + u32_t keep_cnt; +#endif /* LWIP_TCP_KEEPALIVE */ + + /* Persist timer counter */ + u8_t persist_cnt; + /* Persist timer back-off */ + u8_t persist_backoff; + + /* KEEPALIVE counter */ + u8_t keep_cnt_sent; + +#if LWIP_WND_SCALE + u8_t snd_scale; + u8_t rcv_scale; +#endif +}; +``` + +state 是一个 tcp\_state 型枚举变量, 它表示 TCP 所处的状态, 具体状态有: + +```c +enum tcp_state { + CLOSED = 0, //关闭状态 + LISTEN = 1, //监听状态, server端状态 + SYN_SENT = 2, //建立TCP连接时, client发送SYN后的状态 + SYN_RCVD = 3, //server端收到client的SYN报文后, 由listen状态转移过来 + ESTABLISHED = 4, //创建TCP成功, 可以是client, 也可以时server + FIN_WAIT_1 = 5, //主动断开者发送完FIN后的状态 + FIN_WAIT_2 = 6, //主动断开者收到ACK后的状态 + CLOSE_WAIT = 7, //被动断开者收到FIN之后, 自己发送FIN之前的状态 + CLOSING = 8, //应该是主动断开者FIN_WAIT_1状态时,收到ACK前,先收到了对方FIN报文后的状态 + LAST_ACK = 9, //被断开者发送FIN报文后的状态 + TIME_WAIT = 10 //主动断开者收到被动断开者的FIN报文并回复ACK后的状态 +}; +``` + +TCP 建立连接时需要完成 3 个步骤,常称作三次握手: + +1. 客户端发送含 SYN 标志的报文,表示要建立一个连接, 初始化连接的同步序号 seq; +2. 服务器端接收到含 SYN 的报文后, 也将一条含 SYN 标志报文, 作为 ACK 回复给客户端, 一则表示收到客户端创建连接的请求,二则表示服务端也同时创建与客户端的连接; +3. 客户端收到回复后再发送一条 ACK 报文, 表示收到服务端的创建连接请求。 + +不论是客户端还是服务端, 创建连接发送数据的时候数据包中都包含一个初始化序列号 seq, 对方收到数据并回复时会将收到的 seq 加 1 作为 ACK 的数据。 这样发送端就可以判断是不是对本次数据的回复。 以后每次发数据都会在前一次 seq 的基础上 +1 作为本次的 seq (注意, 建立连接和断开连接时 seq 是加 1, 传输应用数据时 seq 加的是上次发送的数据长度 )。 + +![tcp_connect.png](./figure/tcp_connect.png) + +下图为三次握手抓包数据: + +![conncet.png](./figure/tcp_connect_log.png) + +客户端要建立网络连接时, 首先要发一个SYN标志的数据包。就是tcp\_connect实现的,大概流程如下: +![conncet.png](./figure/tcp_client_connect.png) + +服务端连接流程: + +![conncet.png](./figure/tcp_server_connect.png) + +#### TCP断开连接 + +TCP断开连接时需要 4 个步骤来完成断开, 俗称四次挥手: + +1. 已经建立连接的其中一段想要断开连接,称为主动关闭方。其发送一个含 FIN 标志、seq 值(假设为 Q)和 ACK 值(假设为 K, 来自上次收到的 seq)的报文到连接对方,称为被关闭方; +2. 被关闭方收到含 FIN 标志的报文后,将 K 作为 seq 值,Q 加 1 后作为 ACK 值回复给主动关闭方。 +3. 被关闭方同时检查自身是否还有处理完的数据包,若没有则开始启动关闭操作,身份转变为主动关闭方,同样发送一个含 FIN、seq(值仍为 K)和 ACK 值(值仍为 Q+1)初始化的报文到连接对方。 +4. 原主动关闭方转变为被关闭方,收到报文后,回复一条 seq 为 Q+1,ACK 为 K+1 的报文。至此,完成 TCP 连接的断开。 + +![tcp_disconncet](./figure/tcp_disconnect.png) + +下图为四次挥手抓包数据: + +![tcp_disconncet](./figure/tcp_disconnect_log.png) + +正常的TCP断开连接一般是从ESTABLISHED状态开始, 就是说两个网络端已经建立了连接, 断开流程如下: + +以下是主动关闭方流程: + +![tcp_disconncet](./figure/tcp_disconnect_client.png) + +以下是被动关闭方流程: + +![tcp_disconncet](./figure/tcp_disconnect_server.png) + +## TCP 超时重传 + +TCP 在发起一次传输时会开启一个定时器,如果在定时器超时时未收到应答就会进行重传。一个 TCP 连接的往返时间为 RTT(Round-Trip Time),然后根据 RTT 来设置重传时间就是 RTO(Retransmission TimeOut)。 如果 RTO 较大,由于等待时间过长会影响 TCP 的效率;较小则可能使系统来不及响应而认为数据传输失败而重传。所以 RTO 是随着网络的状态动态调整的,而网络状态可以通过**测量报文段**的**往返时间**来反应。 + +jacobson 的 RTO 估计算法: + +``` +Err = M-A +A = A+g*Err +D = D+h*(|Err|-D) +RTO = A+4*D +M:某次测量的RTT值 A:RTT平均值 D:RTT方差 g:常数,一般取1/8 h:常数,一般取1/4 +``` + +从 jacobson 的 RTT 平滑算法可以知道,RTO 估计是基于 RTT 的**均值**和**方差**来计算的,是一种可以平滑较大变化的计算方法。 + +#### lwip 中 的 RTO 估计 + +```c +/* RTT估计计算。这是通过传入的确认报文段来得到的报文段的往返时间。*/ +if (pcb->rttest && TCP_SEQ_LT(pcb->rtseq, ackno)) { + /* diff between this shouldn't exceed 32K since this are tcp timer ticks + and a round-trip shouldn't be that long... */ + m = (s16_t)(tcp_ticks - pcb->rttest); + + LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_receive: experienced rtt %"U16_F" ticks (%"U16_F" msec).\n", + m, (u16_t)(m * TCP_SLOW_INTERVAL))); + + /* This is taken directly from VJs original code in his paper */ + m = (s16_t)(m - (pcb->sa >> 3)); + pcb->sa = (s16_t)(pcb->sa + m); + if (m < 0) { + m = (s16_t) - m; + } + m = (s16_t)(m - (pcb->sv >> 2)); + pcb->sv = (s16_t)(pcb->sv + m); + pcb->rto = (s16_t)((pcb->sa >> 3) + pcb->sv); + + LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_receive: RTO %"U16_F" (%"U16_F" milliseconds)\n", + pcb->rto, (u16_t)(pcb->rto * TCP_SLOW_INTERVAL))); + + pcb->rttest = 0; +} +``` + +在输出一个报文段使,将全局变量时钟滴答 tcp\_ticks 赋值给 tcp 控制块的 rttest 字段表示 RTT 测量开始的时间,如下图中选自 tcp\_out.c 的 tcp\_output\_segment 的源码,源码在估算 RTO 时首先用当前的 tcp\_ticks \- pcb->rttest 表示 RTT 的测量值 m,然后基于 jacobson 算法计算 rto 的值。 + +![rto_rtt_calc.png](./figure/rto_rtt_calc.png) + +#### 超时重传 + +超时重传是在 tcp\_slowtmr\(\) 函数里完成的,内核会每 500ms 执行一次该函数,在函数中判断 pcb->rtime 是否大于 pcb->rto 来判断是否超时,如果超时则执行tcp\_rexmit\_rto\_commit\(\) 函数来重传报文段。 + +```c +if (pcb->rtime >= pcb->rto) { + /* Time for a retransmission. */ + LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_slowtmr: rtime %"S16_F + " pcb->rto %"S16_F"\n", + pcb->rtime, pcb->rto)); + /* If prepare phase fails but we have unsent data but no unacked data, + still execute the backoff calculations below, as this means we somehow + failed to send segment. */ + if ((tcp_rexmit_rto_prepare(pcb) == ERR_OK) || ((pcb->unacked == NULL) && (pcb->unsent != NULL))) { + /* Double retransmission time-out unless we are trying to + * connect to somebody (i.e., we are in SYN_SENT). */ + if (pcb->state != SYN_SENT) { + u8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1); + int calc_rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[backoff_idx]; + pcb->rto = (s16_t)LWIP_MIN(calc_rto, 0x7FFF); + } + + /* Reset the retransmission timer. */ + pcb->rtime = 0; + + /* Reduce congestion window and ssthresh. */ + eff_wnd = LWIP_MIN(pcb->cwnd, pcb->snd_wnd); + pcb->ssthresh = eff_wnd >> 1; + if (pcb->ssthresh < (tcpwnd_size_t)(pcb->mss << 1)) { + pcb->ssthresh = (tcpwnd_size_t)(pcb->mss << 1); + } + pcb->cwnd = pcb->mss; + LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_slowtmr: cwnd %"TCPWNDSIZE_F + " ssthresh %"TCPWNDSIZE_F"\n", + pcb->cwnd, pcb->ssthresh)); + pcb->bytes_acked = 0; + + /* The following needs to be called AFTER cwnd is set to one + mss - STJ */ + tcp_rexmit_rto_commit(pcb); + } +} +``` + diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/figure/udp_protocol.jpg b/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/figure/udp_protocol.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c229cc4cdfd65c96b3abe729bd2f86a0dff6a79 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/figure/udp_protocol.jpg differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/udp.md b/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/udp.md new file mode 100644 index 0000000000000000000000000000000000000000..46f2d8735e626cd0ff17a6e97a702443db6f0ffd --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/lwip-udp/udp.md @@ -0,0 +1,307 @@ +# 1\. UDP说明 + +## 1.1 协议简介 + +UDP (User Datagram Protocol):用户数据报协议,是一种简单、无连接、不可靠的传输协议。 + +无需建立连接、没有提供任何流量控制、拥塞控制机制,收到的报文也没有确认,因此 UDP 的传输速度快,但不能保证数据到达目的地。 + +与我们熟知的 TCP 协议一样,都属于 OSI 模型中的传输层协议。 + +## 1.2 UDP 特点 + +1. 无连接性 + UDP 可以提供无连接的数据传输服务,无需在通讯前建立连接,也无需在通讯结束后断开连接,节省了维护连接的开销。 +2. 不可靠性 + 受自身协议的限制,UDP 的传输是一种不靠传输方式,无法保证数据一定能完整有效的传输到目标。 +3. 以报文为边界 + 因为没有 TCP 协议的数据编号和接收确认机制,UDP 对于应用层交付的数据直接进行封装传输,不会对报文进行拆分合并,在多包数据传输时可能出现乱序的现象。 +4. 无流量和拥塞控制功能 + UDP 协议没有流量控制和拥塞控制的机制,因此更适用于对数据连续性比完整性要求更高、对轻微的数据差错不敏感的场景,如语音、视频通话等。 +5. 支持广播、组播 + 不同于 TCP 协议只能实现一对一的单播通讯,UDP 协议支持单播、广播、组播通讯,实现一对一、一对多、多对多的数据传输。因此所有以广播、组播方式通信的协议都是在 UDP 协议上实现的,如我们常见的 DHCP、SNMP 协议。 + +# 1.3 报文格式 + +![udp_protocol.jpg](./figure/udp_protocol.jpg) + +本篇文章重点是 UDP 在 LwIP 中的实现,报文格式就不再展开介绍了,但还是可以直观地看出 UDP 首部只有 8 字节的长度 ( 伪首部只参与校验和的计算,不实际发送 ),贯彻了 UDP 的简洁易用的特点。 + +# 2\. UDP 在 LWIP 上的实现 + +## 2.1. 数据结构 + +### 2.1.1 UDP 控制块 + +```c +struct udp_pcb { + IP_PCB; //通用IP控制块 + + struct udp_pcb *next; //下一节点的指针,用于构成控制块链表 + + u8_t flags; //控制块状态 + + u16_t local_port, remote_port; //本地端口号、远程端口号 + + udp_recv_fn recv; //处理网络接收数据的回调 + + void *recv_arg; //用户自定义参数,接收回调入参 +}; +``` + +同时,lwip 在 udp.c 中创建了全局的 udp 控制块指针,作为管理所有 UDP 控制块的链表头。 + +```c +struct udp_pcb *udp_pcbs; +``` + +### 2.1.2 UDP 首部 + +```c +PACK_STRUCT_BEGIN +struct udp_hdr { + PACK_STRUCT_FIELD(u16_t src); //源端口 + PACK_STRUCT_FIELD(u16_t dest); //目的端口 + PACK_STRUCT_FIELD(u16_t len); //此次发送的数据报的长度 + PACK_STRUCT_FIELD(u16_t chksum);//校验和 +} PACK_STRUCT_STRUCT; +PACK_STRUCT_END +``` + +报文格式中提到了 UDP 伪首部,但数据结构中没有出现,那计算伪首部的功能是在哪里实现的呢? + +找到计算 UDP 首部中的校验和计算函数: + +```c +/** + * 计算首部校验和 + * @param p 待计算数据的pbuf指针 + * @param proto 协议类型 + * @param proto_len ip数据部分的长度 + * @param src 源ip地址 (这里的IP是网络字节序) + * @param dst 目标ip地址 + * @return 创建的UDP控制块结构体指针,创建失败返回NULL + */ +u16_t ip_chksum_pseudo(struct pbuf *p, u8_t proto, u16_t proto_len,const ip_addr_t *src, const ip_addr_t *dest) +``` + +例如在 udp\_sendto\_if\_src\(\) 中,数据发送之前需要计算出首部校验和,可以看到源 IP、目的 IP 等参数是现算现传的,没有再使用额外的数据结构来维护伪首部。 + +```c +if (IP_IS_V6(dst_ip) || (pcb->flags & UDP_FLAGS_NOCHKSUM) == 0) { + u16_t udpchksum = ip_chksum_pseudo(q, IP_PROTO_UDP, q->tot_len,src_ip, dst_ip); + + /*0表示“无校验和,因此计算为0时需要改为0xffff*/ + if (udpchksum == 0x0000) { + udpchksum = 0xffff; + } + udphdr->chksum = udpchksum; +} +``` + +## 2.2 接口函数 + +### 2.2.1. 创建/删除 UDP 控制块 + +```c +/** + * 创建UDP控制块 + * @return 创建的UDP控制块结构体指针,创建失败返回NULL + */ +struct udp_pcb* udp_new(void); +``` + +udp\_new\(\) 为创建的 UDP 控制块申请内存空间、初始化控制块,返回创建的控制块指针供后续操作。 + + + +```c +/** + * 创建UDP控制块 + * @param type 控制块的IP类型 + * @return 创建的UDP控制块结构体指针 + */ +struct udp_pcb * udp_new_ip_type(u8_t type); +``` + +与 udp\_new\(\) 相似,都是创建 UDP 控制块,区别是 udp\_new\_ip\_type\(\) 可以指定创建的 UDP 控制块为 IPV4 / IPV6 / IPV4+IPV6 类型,而 udp\_new\(\) 默认创建 IPV4 的 UDP 控制块。 + +以上两个函数都很简单,就不把函数体展开讨论了,这里需要注意的是两个创建函数都只创建了控制块的内存空间,进行了简单的初始化,并未将控制块挂载到udp\_pcbs 链表中。 + +```c +/** + * 删除UDP控制块 + * @param pcb UDP控制块指针 + */ +void udp_remove(struct udp_pcb *pcb); +{ + struct udp_pcb *pcb2; + + LWIP_ASSERT_CORE_LOCKED(); + + LWIP_ERROR("udp_remove: invalid pcb", pcb != NULL, return); + + mib2_udp_unbind(pcb); + /* 判断待删除的控制块在链表开头 */ + if (udp_pcbs == pcb) { + /* 从将第二个控制块作为链表头 */ + udp_pcbs = udp_pcbs->next; + + } else { + /* 遍历udp 控制块链表 */ + for (pcb2 = udp_pcbs; pcb2 != NULL; pcb2 = pcb2->next) { + /* 在链表中找到了该控制块 */ + if (pcb2->next != NULL && pcb2->next == pcb) { + /* 将该控制块在链表中删除 */ + pcb2->next = pcb->next; + break; + } + } + } + /* 释放该控制块的内存空间 */ + memp_free(MEMP_UDP_PCB, pcb); +} +``` + +删除 UDP 控制块,并将该控制块从 UDP 控制块链表中删除,最后释放控制块的内存空间。 + +通过 udp\_remove\(\) 以及后面的 udp\_connect\(\) 可以看到 lwip 对 udp 控制块链表的管理方式:单向链表,每次添加新节点插到链表开头,尾节点的 next 为 NULL。简单方便,但个人认为控制块链表头作为全局变量存放,使用时也没有加锁或者关中断保护,在 RT-Thread 这类抢占式的操作系统中,是存在临界区问题的,应用开发时应避免频繁的对控制块链表有操作。 + +### 2.2.2. 绑定 + +```c +/** + * 将UDP控制块绑定到一个本地IP和端口号上 + * @param pcb UDP控制块指针 + * @param ipaddr 要绑定的本地IP + * @param port 要绑定的本地端口号,输入0时会绑定一个随机端口 + * @return 错误码 + */ +err_t udp_bind(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port); +``` + +udp\_bind\(\) 除了将控制块与指定的 IP 和端口号绑定,还会检查 UDP 控制块是否挂载到了上文提到的全局 UDP 控制块链表中、待绑定的 IP - 端口号是否与链表中的其他控制块重复,未挂载、未重复的话会执行挂载: + +```c +{ + /* code... */ + + rebind = 0; + /* 遍历udp控制块链表 */ + for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb->next) { + /* 如果当前控制块已在控制块链表中 */ + if (pcb == ipcb) { + /* 已挂载标志位置位 */ + rebind = 1; + break; + } + } + + /* code... */ + + /* 未挂载? */ + if (rebind == 0) { + /* 将当前控制块插入到链表头 */ + pcb->next = udp_pcbs; + udp_pcbs = pcb; + } + /* code... */ +} +``` + +绑定本地端口不是 UDP 通讯的必要步骤,因为如果没有绑定本地端口,调用 sendto\(\) 时会分配一个随机端口。 +该接口一般是设备作 UDPS 时使用,在此场景下,存在 UDPC 先向 UDPS 发送数据的情况,因此需要预先知道 UDPS 的端口号,即 UDPS 需要绑定某个端口而不能是随机端口。 + +### 2.2.3. 连接/断连 + +```c +/** + * 将UDP与指定IP、端口“建立连接” + * @param pcb UDP控制块指针 + * @param ipaddr 要连接的目的IP + * @param port 要连接的目的端口 + * @return 错误码 + */ +err_t udp_connect(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port); +``` + +1. UDP 是无连接的,因此 udp\_connect\(\) 并不会真的像 TCP 的 connect() 一样去执行建立连接的网络交互,而只是在内部把目标 IP 和端口号与 UDP 控制块绑定。ip 和端口号绑定成功后,函数内部会将该 PCB 的 flag 置位为已连接: + +```c + pcb->flags |= UDP_FLAGS_CONNECTED; +``` + +1. udp\_connect\(\) 会检查控制块是否绑定了本地 ip 端口,如果未绑定,会执行一次 udp\_bind\(\),绑定到随机端口。 +2. udp\_connect\(\) 还会检查该控制块是否挂载到了 udp 控制块链表中,未挂载的话执行挂载。 +3. udp\_connect\(\) 同样也不是 UDP 通讯的必要步骤,绑定的优点在于绑定后可以直接调用 udp\_send\(\),直接向绑定的目标IP和端口发送数据,无需像调用udp\_sendto\(\) 接口一样每次指定目标 IP 和端口,同时提高了执行效率,recv\(\) 时防止受到其他 IP 数据。 +4. 该接口一般是设备作 UDPC 时使用,绑定了目标 IP 后直接调用 udp\_send\(\) 发送,UDPS 这类需要频繁向不同目标 IP、端口发送数据的应用显然不适合使用该接口。 + +```c +/** + * “断开”UDP控制块已经建立的连接 + * @param pcb UDP控制块指针 + */ +void udp_disconnect(struct udp_pcb *pcb); +``` + +与 udp\_connect\(\) 同理,udp\_disconnect\(\) 也不会真的执行断开连接的交互,只是将控制块中绑定的远程 IP、端口号清零,并将 flag 的连接标志复位。也没有将控制块从链表中删除的操作。 + +```c + pcb->remote_port = 0; + pcb->netif_idx = NETIF_NO_INDEX; + udp_clear_flags(pcb, UDP_FLAGS_CONNECTED); +``` + +### 2.2.4. 发送 + +```c +err_t udp_send(struct udp_pcb *pcb, struct pbuf *p); + +err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p, + const ip_addr_t *dst_ip, u16_t dst_port); + +err_t udp_sendto_if(struct udp_pcb *pcb, struct pbuf *p, + const ip_addr_t *dst_ip, u16_t dst_port, + struct netif *netif); + +err_t udp_sendto_if_src(struct udp_pcb *pcb, struct pbuf *p, + const ip_addr_t *dst_ip, u16_t dst_port, + struct netif *netif, const ip_addr_t *src_ip); +``` + +1. 四个函数是一层一层调用的,udp\_send\(\) -> udp\_sendto\(\) -> udp\_sendto\_if\(\) -> udp\_sendto\_if\_src\(\),从 udp\_send\(\) 函数开始,只需要传入 UDP 控制块和pbuf 指针,在每层的调用过程中根据控制块中的信息将目的 ip、端口号、netif、源 IP 信息逐步补全,最后通过 ip\_output\_if\_src\(\) 函数将数据传输到 IP 层继续处理。 +2. 实际开发中常用的两个接口是 send\(\) 和 sendto\(\),如上文介绍,执行过 connect\(\) 的 UDP 可以直接调用send\(\)发送到固定 IP 端口,代码设计上更加简洁高效。而调用 sendto\(\) 可以每次向不同目标 IP 端口发送,使用上更加灵活。 + +### 2.2.5. 接收 + +```c +/** + * 为控制块注册接收回调 + * @param pcb UDP控制块指针 + * @param recv 处理网络数据的接收回调 + * @param recv_arg 触发时传入回调的用户自定义参数 + */ +void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv,void *recv_arg); +``` + +udp 层提供的方法是通过注册接收回调的方式实现接收网络数据。 +回调类型: + +```c +/** + * udp接收回调 + * @param arg 回调注册时设置的用户自定义参数 + * @param pcb UDP控制块 + * @param p pbuf指针(payload在这里) + * @param addr 数据来源IP + * @param port 数据来源端口号 + */ +typedef void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port); +``` + +注册的回调在 udp\_input\(\) 中被执行,而 udp\_input\(\) 由 IP 层的 ip4\_input\(\) / ip6\_input\(\) 触发。网络端收到数据后,IP 层会判断数据协议是否为 UDP 协议,若是则将数据、发送方的信息、用户自定义数据传入 udp\_input\(\),最终到达用户设置的回调中供使用。 + +# 3\. 参考文献: + +1. LwIP-2.1.0源码 +2. 《深入理解计算机网络》 \ No newline at end of file diff --git a/rt-thread-version/rt-thread-standard/programming-manual/posix/posix.md b/rt-thread-version/rt-thread-standard/programming-manual/posix/posix.md index fbbe56c7b8c24e95c9f74808703426999d541679..bdc65608eb2ad65bb751b6381c830cbf1ae5d5fe 100644 --- a/rt-thread-version/rt-thread-standard/programming-manual/posix/posix.md +++ b/rt-thread-version/rt-thread-standard/programming-manual/posix/posix.md @@ -2630,3 +2630,83 @@ int mq_getattr(mqd_t mqdes, struct mq_attr *mqstat); |**返回**| —— | | 0 | 成功 | |-1 | 参数无效 | + +## select 函数 + +### select 函数原型: + +```c +int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout); +``` + +#### 【参数说明】 + +* **nfds**:select监视的文件句柄数,一般设为要监视各文件中的最大文件描述符值加1。 +* **readfds**:文件描述符集合监视文件集中的任何文件是否有数据可读,当select函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符。 +* **writefds**:文件描述符集合监视文件集中的任何文件是否有数据可写,当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。 +* **exceptfds**:文件集将监视文件集中的任何文件是否发生错误,可用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG\_OOB标志发送到套接字上。当select函数返回的时候,exceptfds将清除其中的其他文件描述符,只留下标记有OOB数据的文件描述符。 +* **timeout** 参数是一个指向 struct timeval 类型的指针,它可以使 select\(\)在等待 timeout 时间后若没有文件描述符准备好则返回。其timeval结构用于指定这段时间的秒数和微秒数。它可以使select处于三种状态: + +> \(1\) 若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止; +> \(2\) 若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值; +> \(3\) timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。 + +timeval 结构体定义 + +```c +struct timeval +{ + int tv_sec; /* 秒 */ + int tv_usec; /* 微妙 */ +}; +``` + +#### 【返回值】 + +* **int**:若有就绪描述符返回其数目,若超时则为0,若出错则为-1 + +下列操作用来设置、清除、判断文件描述符集合。 + +```c +FD_ZERO(fd_set *set); // 清除一个文件描述符集。 +FD_SET(int fd,fd_set *set); // 将一个文件描述符加入文件描述符集中。 +FD_CLR(int fd,fd_set *set); // 将一个文件描述符从文件描述符集中清除。 +FD_ISSET(int fd,fd_set *set); // 判断文件描述符是否被置位 +``` + +fd\_set可以理解为一个集合,这个集合中存放的是文件描述符\(file descriptor\),即文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。 + +**select\(\)的机制中提供一种fd\_set的数据结构**,实际上是一个long类型的数组,每一个数组元素都能与打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select\(\)时,由内核根据IO状态修改fd\_set的内容,由此来通知执行了select\(\)的进程哪一Socket或文件可读。 + +## poll 函数 + +### poll 的函数原型: + +```c +int poll(struct pollfd *fds, nfds_t nfds, int timeout); +``` + +#### 【参数说明】 + +* **fds**:fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll\(\) 监视多个文件描述符。 + +struct pollfd原型如下: + +```c +typedef struct pollfd { + int fd; // 需要被检测或选择的文件描述符 + short events; // 对文件描述符fd上感兴趣的事件 + short revents; // 文件描述符fd上当前实际发生的事件 +} pollfd_t; +``` + +其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。 + +* **nfds**:记录数组fds中描述符的总数量。 +* **timeout**:指定等待的毫秒数,无论 I/O 是否准备好,poll\(\) 都会返回,和select函数是类似的。 + +#### 【返回值】 + +* **int**:函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错; + +poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd\_set结构,使得poll支持的文件描述符集合限制远大于select的1024。这也是和select不同的地方。 diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/at_socket.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/at_socket.png new file mode 100644 index 0000000000000000000000000000000000000000..6b05e69ef178657cc22a8286f56db26651d2fd0d Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/at_socket.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/netdev_add_at.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/netdev_add_at.png new file mode 100644 index 0000000000000000000000000000000000000000..13929ae801e5a4c518a8d7ecfc4a2b3a296a7eca Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/netdev_add_at.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/netdev_add_netif_add.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/netdev_add_netif_add.png new file mode 100644 index 0000000000000000000000000000000000000000..e981f097b21b65ae4656ce15fb8068ddb44dd333 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/netdev_add_netif_add.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_at_lwip.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_at_lwip.png new file mode 100644 index 0000000000000000000000000000000000000000..a85bc9b6bdb852797d96e43e5f4df8afc328303f Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_at_lwip.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_lwip_struct_detail.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_lwip_struct_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..add0d901b511598e69020222a90c663918db8b01 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_lwip_struct_detail.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_struct_detail.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_struct_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..1501e773aa4192497b3595bc5424bffabf34426c Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/sal_struct_detail.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/wiz_socket.png b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/wiz_socket.png new file mode 100644 index 0000000000000000000000000000000000000000..aa20ec497b81b8a1df9d4cce4855ecf645e67053 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/sal/figures/wiz_socket.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/sal/sal_at_wiznet_lwip.md b/rt-thread-version/rt-thread-standard/programming-manual/sal/sal_at_wiznet_lwip.md new file mode 100644 index 0000000000000000000000000000000000000000..7fb8748f71e8737a19322253acf056376828177a --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/sal/sal_at_wiznet_lwip.md @@ -0,0 +1,1545 @@ +# lwip socket, at socket, sal socket 接口对比 + +# sal scoket + +## sal socket 与 其他socket 关系 + +![sal_at_lwip.png](./figures/sal_at_lwip.png) + +结合上面结构体之间的引用关系图,与下图结合看; + +![sal_struct_detail.png](./figures/sal_struct_detail.png) + +从 uml 对象关系图中可以得到下面信息: + +1. 从 sal\_scoket 中的 user\_data, 获取对应的 socket; +2. 从 sal\_socket 中的 netdev, 获取对应的协议操作接口; + +## sal\_socket 接口 + +```c +// rt-thread\components\net\sal_socket\include\sal.h +struct sal_socket +{ + uint32_t magic; /* SAL socket magic word */ + + int socket; /* SAL socket descriptor */ + int domain; + int type; + int protocol; + + struct netdev *netdev; /* SAL network interface device */ + + void *user_data; /* user-specific data */ +#ifdef SAL_USING_TLS + void *user_data_tls; /* user-specific TLS data */ +#endif +}; + +/* network interface socket opreations */ +struct sal_socket_ops +{ + int (*socket) (int domain, int type, int protocol); + int (*closesocket)(int s); + int (*bind) (int s, const struct sockaddr *name, socklen_t namelen); + int (*listen) (int s, int backlog); + int (*connect) (int s, const struct sockaddr *name, socklen_t namelen); + int (*accept) (int s, struct sockaddr *addr, socklen_t *addrlen); + int (*sendto) (int s, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen); + int (*recvfrom) (int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen); + int (*getsockopt) (int s, int level, int optname, void *optval, socklen_t *optlen); + int (*setsockopt) (int s, int level, int optname, const void *optval, socklen_t optlen); + int (*shutdown) (int s, int how); + int (*getpeername)(int s, struct sockaddr *name, socklen_t *namelen); + int (*getsockname)(int s, struct sockaddr *name, socklen_t *namelen); + int (*ioctlsocket)(int s, long cmd, void *arg); +#ifdef SAL_USING_POSIX + int (*poll) (struct dfs_fd *file, struct rt_pollreq *req); +#endif +}; + +/* sal network database name resolving */ +struct sal_netdb_ops +{ + struct hostent* (*gethostbyname) (const char *name); + int (*gethostbyname_r)(const char *name, struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop); + int (*getaddrinfo) (const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res); + void (*freeaddrinfo) (struct addrinfo *ai); +}; + +struct sal_proto_family +{ + int family; /* primary protocol families type */ + int sec_family; /* secondary protocol families type */ + const struct sal_socket_ops *skt_ops; /* socket opreations */ + const struct sal_netdb_ops *netdb_ops; /* network database opreations */ +}; +``` + +`rt-thread\components\net\sal_socket\include\socket\sys_socket\sys\socket.h` + +```c + +#ifdef SAL_USING_POSIX +int accept(int s, struct sockaddr *addr, socklen_t *addrlen); +int bind(int s, const struct sockaddr *name, socklen_t namelen); +int shutdown(int s, int how); +int getpeername(int s, struct sockaddr *name, socklen_t *namelen); +int getsockname(int s, struct sockaddr *name, socklen_t *namelen); +int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen); +int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen); +int connect(int s, const struct sockaddr *name, socklen_t namelen); +int listen(int s, int backlog); +int recv(int s, void *mem, size_t len, int flags); +int recvfrom(int s, void *mem, size_t len, int flags, + struct sockaddr *from, socklen_t *fromlen); +int send(int s, const void *dataptr, size_t size, int flags); +int sendto(int s, const void *dataptr, size_t size, int flags, + const struct sockaddr *to, socklen_t tolen); +int socket(int domain, int type, int protocol); +int closesocket(int s); +int ioctlsocket(int s, long cmd, void *arg); +#else +#define accept(s, addr, addrlen) sal_accept(s, addr, addrlen) +#define bind(s, name, namelen) sal_bind(s, name, namelen) +#define shutdown(s, how) sal_shutdown(s, how) +#define getpeername(s, name, namelen) sal_getpeername(s, name, namelen) +#define getsockname(s, name, namelen) sal_getsockname(s, name, namelen) +#define getsockopt(s, level, optname, optval, optlen) sal_getsockopt(s, level, optname, optval, optlen) +#define setsockopt(s, level, optname, optval, optlen) sal_setsockopt(s, level, optname, optval, optlen) +#define connect(s, name, namelen) sal_connect(s, name, namelen) +#define listen(s, backlog) sal_listen(s, backlog) +#define recv(s, mem, len, flags) sal_recvfrom(s, mem, len, flags, NULL, NULL) +#define recvfrom(s, mem, len, flags, from, fromlen) sal_recvfrom(s, mem, len, flags, from, fromlen) +#define send(s, dataptr, size, flags) sal_sendto(s, dataptr, size, flags, NULL, NULL) +#define sendto(s, dataptr, size, flags, to, tolen) sal_sendto(s, dataptr, size, flags, to, tolen) +#define socket(domain, type, protocol) sal_socket(domain, type, protocol) +#define closesocket(s) sal_closesocket(s) +#define ioctlsocket(s, cmd, arg) sal_ioctlsocket(s, cmd, arg) +#endif /* SAL_USING_POSIX */ +``` + +`rt-thread\components\net\sal_socket\include\sal_socket.h` + +```c +int sal_accept(int socket, struct sockaddr *addr, socklen_t *addrlen); +int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen); +int sal_shutdown(int socket, int how); +int sal_getpeername (int socket, struct sockaddr *name, socklen_t *namelen); +int sal_getsockname (int socket, struct sockaddr *name, socklen_t *namelen); +int sal_getsockopt (int socket, int level, int optname, void *optval, socklen_t *optlen); +int sal_setsockopt (int socket, int level, int optname, const void *optval, socklen_t optlen); +int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen); +int sal_listen(int socket, int backlog); +int sal_recvfrom(int socket, void *mem, size_t len, int flags, + struct sockaddr *from, socklen_t *fromlen); +int sal_sendto(int socket, const void *dataptr, size_t size, int flags, + const struct sockaddr *to, socklen_t tolen); +int sal_socket(int domain, int type, int protocol); +int sal_closesocket(int socket); +int sal_ioctlsocket(int socket, long cmd, void *arg); +``` + +## sal\_init sal初始化 + +```c +// rt-thread\components\net\sal_socket\src\sal_socket.c +/* the socket table used to dynamic allocate sockets */ +struct sal_socket_table +{ + uint32_t max_socket; + struct sal_socket **sockets; +}; +``` + +系统中最多有可以创建多少个 socket, step形式增减 ; + +```c +/** + * SAL (Socket Abstraction Layer) initialize. + * + * @return result 0: initialize success + * -1: initialize failed + */ +int sal_init(void) +{ + int cn; + + if (init_ok) + { + LOG_D("Socket Abstraction Layer is already initialized."); + return 0; + } + + /* init sal socket table */ + cn = SOCKET_TABLE_STEP_LEN < SAL_SOCKETS_NUM ? SOCKET_TABLE_STEP_LEN : SAL_SOCKETS_NUM; + socket_table.max_socket = cn; + socket_table.sockets = rt_calloc(1, cn * sizeof(struct sal_socket *)); + if (socket_table.sockets == RT_NULL) + { + LOG_E("No memory for socket table.\n"); + return -1; + } + + /* create sal socket lock */ + rt_mutex_init(&sal_core_lock, "sal_lock", RT_IPC_FLAG_FIFO); + + LOG_I("Socket Abstraction Layer initialize success."); + init_ok = RT_TRUE; + + return 0; +} +INIT_COMPONENT_EXPORT(sal_init); +``` + +## sal\_socket 与协议栈具体的socket 关联 + +`rt-thread\components\net\sal_socket\src\sal_socket.c` + +```c +/** + * This function will initialize sal socket object and set socket options + * + * @param family protocol family + * @param type socket type + * @param protocol transfer Protocol + * @param res sal socket object address + * + * @return 0 : socket initialize success + * -1 : input the wrong family + * -2 : input the wrong socket type + * -3 : get network interface failed + */ +static int socket_init(int family, int type, int protocol, struct sal_socket **res) +{ + + struct sal_socket *sock; + struct sal_proto_family *pf; + struct netdev *netdv_def = netdev_default; + struct netdev *netdev = RT_NULL; + rt_bool_t flag = RT_FALSE; + + if (family < 0 || family > AF_MAX) + { + return -1; + } + + if (type < 0 || type > SOCK_MAX) + { + return -2; + } + + sock = *res; + sock->domain = family; + sock->type = type; + sock->protocol = protocol; + + if (netdv_def && netdev_is_up(netdv_def)) + { + /* check default network interface device protocol family */ + pf = (struct sal_proto_family *) netdv_def->sal_user_data; + if (pf != RT_NULL && pf->skt_ops && (pf->family == family || pf->sec_family == family)) + { + sock->netdev = netdv_def; + flag = RT_TRUE; + } + } + + if (flag == RT_FALSE) + { + /* get network interface device by protocol family */ + netdev = netdev_get_by_family(family); + if (netdev == RT_NULL) + { + LOG_E("not find network interface device by protocol family(%d).", family); + return -3; + } + + sock->netdev = netdev; + } + + return 0; +} +``` + +`rt-thread\components\net\sal_socket\src\sal_socket.c` + +1. **sal\_socket 调用 socket\_init 初始化一个 sal\_socket 结构体,在 socket\_init 中 socket 使用了默认的网络设备 `struct netdev *netdv_def = netdev_default;`; 当默认 netdev 没有 linkup 时,才会在查找同 family 下的其他 netdev;** +2. `struct sal_socket *sock;` 中的 user\_data 在 sal\_socket 函数中 `sock->user_data = (void *) proto_socket;`,与具体的 protocol socket 进行关联了; + +```c +int sal_socket(int domain, int type, int protocol) +{ + int retval; + int socket, proto_socket; + struct sal_socket *sock; + struct sal_proto_family *pf; + + /* allocate a new socket and registered socket options */ + socket = socket_new(); + if (socket < 0) + { + return -1; + } + + /* get sal socket object by socket descriptor */ + sock = sal_get_socket(socket); + if (sock == RT_NULL) + { + socket_delete(socket); + return -1; + } + + /* Initialize sal socket object */ + retval = socket_init(domain, type, protocol, &sock); + if (retval < 0) + { + LOG_E("SAL socket protocol family input failed, return error %d.", retval); + socket_delete(socket); + return -1; + } + + /* valid the network interface socket opreation */ + SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, socket); + + proto_socket = pf->skt_ops->socket(domain, type, protocol); + if (proto_socket >= 0) + { +#ifdef SAL_USING_TLS + if (SAL_SOCKOPS_PROTO_TLS_VALID(sock, socket)) + { + sock->user_data_tls = proto_tls->ops->socket(socket); + if (sock->user_data_tls == RT_NULL) + { + socket_delete(socket); + return -1; + } + } +#endif + sock->user_data = (void *) proto_socket; + return sock->socket; + } + socket_delete(socket); + return -1; +} +``` + +`rt-thread\components\net\sal_socket\socket\net_sockets.c (line 147)` + +```c +int connect(int s, const struct sockaddr *name, socklen_t namelen) +{ + int socket = dfs_net_getsocket(s); + + return sal_connect(socket, name, namelen); +} +RTM_EXPORT(connect); +``` + +`rt-thread\components\net\sal_socket\src\sal_socket.c (line 801)` + +```c +int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen) +{ + struct sal_socket *sock; + struct sal_proto_family *pf; + int ret; + + /* get the socket object by socket descriptor */ + SAL_SOCKET_OBJ_GET(sock, socket); + + /* check the network interface is up status */ + SAL_NETDEV_IS_UP(sock->netdev); + /* check the network interface socket opreation */ + SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, connect); + + ret = pf->skt_ops->connect((int) sock->user_data, name, namelen); +#ifdef SAL_USING_TLS + if (ret >= 0 && SAL_SOCKOPS_PROTO_TLS_VALID(sock, connect)) + { + if (proto_tls->ops->connect(sock->user_data_tls) < 0) + { + return -1; + } + + return ret; + } +#endif + + return ret; +} +``` + +# lwip socket + +## 创建 LwIP socket + +![sal_lwip_struct_detail.png](./figures/sal_lwip_struct_detail.png) + +`rt-thread\components\net\lwip-2.0.2\src\include\lwip\sockets.h` + +```c +int lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen); +int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen); +int lwip_shutdown(int s, int how); +int lwip_getpeername (int s, struct sockaddr *name, socklen_t *namelen); +int lwip_getsockname (int s, struct sockaddr *name, socklen_t *namelen); +int lwip_getsockopt (int s, int level, int optname, void *optval, socklen_t *optlen); +int lwip_setsockopt (int s, int level, int optname, const void *optval, socklen_t optlen); +int lwip_close(int s); +int lwip_connect(int s, const struct sockaddr *name, socklen_t namelen); +int lwip_listen(int s, int backlog); +int lwip_recv(int s, void *mem, size_t len, int flags); +int lwip_read(int s, void *mem, size_t len); +int lwip_recvfrom(int s, void *mem, size_t len, int flags, + struct sockaddr *from, socklen_t *fromlen); +int lwip_send(int s, const void *dataptr, size_t size, int flags); +int lwip_sendmsg(int s, const struct msghdr *message, int flags); +int lwip_sendto(int s, const void *dataptr, size_t size, int flags, + const struct sockaddr *to, socklen_t tolen); +int lwip_socket(int domain, int type, int protocol); +int lwip_write(int s, const void *dataptr, size_t size); +int lwip_writev(int s, const struct iovec *iov, int iovcnt); +int lwip_select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, + struct timeval *timeout); +int lwip_ioctl(int s, long cmd, void *argp); +int lwip_fcntl(int s, int cmd, int val); +``` + +## LWiP socket 接口 + +`rt-thread\components\net\sal_socket\impl\af_inet_lwip.c (line 286)` + +```c +static const struct sal_socket_ops lwip_socket_ops = +{ + inet_socket, + lwip_close, + lwip_bind, + lwip_listen, + lwip_connect, + inet_accept, + (int (*)(int, const void *, size_t, int, const struct sockaddr *, socklen_t))lwip_sendto, + (int (*)(int, void *, size_t, int, struct sockaddr *, socklen_t *))lwip_recvfrom, + lwip_getsockopt, + //TODO fix on 1.4.1 + lwip_setsockopt, + lwip_shutdown, + lwip_getpeername, + inet_getsockname, + inet_ioctlsocket, +#ifdef SAL_USING_POSIX + inet_poll, +#endif +}; + +static const struct sal_netdb_ops lwip_netdb_ops = +{ + lwip_gethostbyname, + lwip_gethostbyname_r, + lwip_getaddrinfo, + lwip_freeaddrinfo, +}; + +static const struct sal_proto_family lwip_inet_family = +{ + AF_INET, +#if LWIP_VERSION > 0x2000000 + AF_INET6, +#else + AF_INET, +#endif + &lwip_socket_ops, + &lwip_netdb_ops, +}; + +/* Set lwIP network interface device protocol family information */ +int sal_lwip_netdev_set_pf_info(struct netdev *netdev) +{ + RT_ASSERT(netdev); + + netdev->sal_user_data = (void *) &lwip_inet_family; + return 0; +} +``` + +LWiP 中的socket +`rt-thread\components\net\sal_socket\impl\af_inet_lwip.c` + +```c +static int inet_socket(int domain, int type, int protocol) +{ +#ifdef SAL_USING_POSIX + int socket; + + socket = lwip_socket(domain, type, protocol); + if (socket >= 0) + { + struct lwip_sock *lwsock; + + lwsock = lwip_tryget_socket(socket); + lwsock->conn->callback = event_callback; + + rt_wqueue_init(&lwsock->wait_head); + } + + return socket; +#else + return lwip_socket(domain, type, protocol); +#endif /* SAL_USING_POSIX */ +} +``` + +`rt-thread\components\net\lwip-2.0.2\src\api\sockets.c` + +```c +/** Contains all internal pointers and states used for a socket */ +struct lwip_sock { + /** sockets currently are built on netconns, each socket has one netconn */ + struct netconn *conn; + /** data that was left from the previous read */ + void *lastdata; + /** offset in the data that was left from the previous read */ + u16_t lastoffset; + /** number of times data was received, set by event_callback(), + tested by the receive and select functions */ + s16_t rcvevent; + /** number of times data was ACKed (free send buffer), set by event_callback(), + tested by select */ + u16_t sendevent; + /** error happened for this socket, set by event_callback(), tested by select */ + u16_t errevent; + /** last error that occurred on this socket (in fact, all our errnos fit into an u8_t) */ + u8_t err; + /** counter of how many threads are waiting for this socket using select */ + SELWAIT_T select_waiting; + +#ifdef SAL_USING_POSIX + rt_wqueue_t wait_head; +#endif +}; +``` + +## LWIP 创建 socket 过程 + +net\_socket.c 中,返回文件描述符索引; + +```c +int socket(int domain, int type, int protocol) +{ + /* create a BSD socket */ + int fd; + int socket; + struct dfs_fd *d; + + /* allocate a fd */ + fd = fd_new(); + if (fd < 0) + { + rt_set_errno(-ENOMEM); + + return -1; + } + d = fd_get(fd); + + /* create socket and then put it to the dfs_fd */ + socket = sal_socket(domain, type, protocol); + if (socket >= 0) + { + /* this is a socket fd */ + d->type = FT_SOCKET; + d->path = NULL; + + d->fops = dfs_net_get_fops(); + + d->flags = O_RDWR; /* set flags as read and write */ + d->size = 0; + d->pos = 0; + + /* set socket to the data of dfs_fd */ + d->data = (void *) socket; + } + else + { + /* release fd */ + fd_put(d); + fd_put(d); + + rt_set_errno(-ENOMEM); + + return -1; + } + + /* release the ref-count of fd */ + fd_put(d); + + return fd; +} +``` + +socket 是通过 dfs 管理的, socket 创建的过程分为以下几步: + +1. allocate a file descriptor, **返回文件描述符的索引**; +2. 通过文件描述符索引,获取文件描述符; +3. 创建一个 sal\_socket; 成功后,修改文件描述符; + + * 修改文件描述符 type; + * 修改文件描述符中的文件操作函数; + * 将创建成功的 socket 作为文件操作符的私有数据; + +4. 通过fd\_put将文件描述符引用归0,表示当前文件描述符未使用; + +创建sal\_socket,返回 **socket descriptor 索引**; + +```c +int sal_socket(int domain, int type, int protocol) +{ + int retval; + int socket, proto_socket; + struct sal_socket *sock; + struct sal_proto_family *pf; + + /* allocate a new socket and registered socket options */ + socket = socket_new(); + if (socket < 0) + { + return -1; + } + + /* get sal socket object by socket descriptor */ + sock = sal_get_socket(socket); + if (sock == RT_NULL) + { + return -1; + } + + /* Initialize sal socket object */ + retval = socket_init(domain, type, protocol, &sock); + if (retval < 0) + { + LOG_E("SAL socket protocol family input failed, return error %d.", retval); + socket_delete(socket); + return -1; + } + + /* valid the network interface socket opreation */ + SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, socket); + + proto_socket = pf->skt_ops->socket(domain, type, protocol); + if (proto_socket >= 0) + { +#ifdef SAL_USING_TLS + if (SAL_SOCKOPS_PROTO_TLS_VALID(sock, socket)) + { + sock->user_data_tls = proto_tls->ops->socket(socket); + if (sock->user_data_tls == RT_NULL) + { + socket_delete(socket); + return -1; + } + } +#endif + sock->user_data = (void *) proto_socket; + return sock->socket; + } + + return -1; +} +``` + +1. allocate a scoket, **返回scoket的索引**; +2. 通过 scoket的索引获取 socket object; +3. 初始化 socket object; +4. 通过 SAL\_NETDEV\_SOCKETOPS\_VALID 获取 pf(sal\_proto\_family); +5. 通过 pf->skt\_ops->socket 引用到 const struct sal\_socket\_ops lwip\_socket\_ops 中的 lwip\_socket 创建 socket;(lwip\_socket 返回 (/\*The global array of available sockets\*/ (static struct lwip\_sock sockets\[NUM\_SOCKETS\];))的索引) +6. 将创建的 socket 挂在 sal\_socket 上; + +## 从 lwip\_connect 到 BSD connect + +### 从 lwip\_connect 到 sal\_connect + +sal\_lwip\_netdev\_set\_pf\_info 在进行 netdev\_add 时挂在 sal 框架上; +![netdev_add_netif_add.png](./figures/netdev_add_netif_add.png) + +然后在 sal\_socket.c 中,通过 SAL\_NETDEV\_SOCKETOPS\_VALID 将 `sal_user_data` 转换为需要的协议族, sal\_connect 就可以利用对应的协议族进行工作了; + +```c +#define SAL_NETDEV_SOCKETOPS_VALID(netdev, pf, ops) \ +do { \ + (pf) = (struct sal_proto_family *) netdev->sal_user_data; \ + if ((pf)->skt_ops->ops == RT_NULL){ \ + return -1; \ + } \ +}while(0) +``` + +```c +int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen) +{ + struct sal_socket *sock; + struct sal_proto_family *pf; + int ret; + + /* get the socket object by socket descriptor */ + SAL_SOCKET_OBJ_GET(sock, socket); + + /* check the network interface is up status */ + SAL_NETDEV_IS_UP(sock->netdev); + /* check the network interface socket opreation */ + SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, connect); + + ret = pf->skt_ops->connect((int) sock->user_data, name, namelen); +#ifdef SAL_USING_TLS + if (ret >= 0 && SAL_SOCKOPS_PROTO_TLS_VALID(sock, connect)) + { + if (proto_tls->ops->connect(sock->user_data_tls) < 0) + { + return -1; + } + + return ret; + } +#endif + + return ret; +} +``` + +### 从 sal\_connect 到 connect + +net socket.c 中,通过 `connect` 对 `sal_connect` 进行封装;与 The BSD sockets application programming interface \(API\) 函数名称进行统一; + +```c +int connect(int s, const struct sockaddr *name, socklen_t namelen) +{ + int socket = dfs_net_getsocket(s); + + return sal_connect(socket, name, namelen); +} +``` + +# wiznet socket + +## 创建 wiz socket + +![wiz_socket.png](./figures/wiz_socket.png) + +## wiznet socket 接口 + +packageswiznet-latestsrcwiz\_af\_inet.c + +```c +static const struct sal_socket_ops wiz_socket_ops = +{ + wiz_socket, + wiz_closesocket, + wiz_bind, + wiz_listen, + wiz_connect, + wiz_accept, + wiz_sendto, + wiz_recvfrom, + wiz_getsockopt, + wiz_setsockopt, + wiz_shutdown, + NULL, + NULL, + NULL, +#ifdef SAL_USING_POSIX + wiz_poll, +#endif /* SAL_USING_POSIX */ +}; + +static const struct sal_netdb_ops wiz_netdb_ops = +{ + wiz_gethostbyname, + NULL, + wiz_getaddrinfo, + wiz_freeaddrinfo, +}; + + +static const struct sal_proto_family wiz_inet_family = +{ + AF_WIZ, + AF_INET, + &wiz_socket_ops, + &wiz_netdb_ops, +}; +``` + +# at socket + +## 创建 at socket + +![at_socket.png](./figures/at_socket.png) + +`rt-thread\components\net\at\at_socket\at_socket.h` + +```c +int at_socket(int domain, int type, int protocol); +int at_closesocket(int socket); +int at_shutdown(int socket, int how); +int at_bind(int socket, const struct sockaddr *name, socklen_t namelen); +int at_connect(int socket, const struct sockaddr *name, socklen_t namelen); +int at_sendto(int socket, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen); +int at_send(int socket, const void *data, size_t size, int flags); +int at_recvfrom(int socket, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen); +int at_recv(int socket, void *mem, size_t len, int flags); +int at_getsockopt(int socket, int level, int optname, void *optval, socklen_t *optlen); +int at_setsockopt(int socket, int level, int optname, const void *optval, socklen_t optlen); +struct hostent *at_gethostbyname(const char *name); +int at_getaddrinfo(const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res); +void at_freeaddrinfo(struct addrinfo *ai); + +struct at_socket *at_get_socket(int socket); +``` + +## at socket 接口 + +`rt-thread\components\net\sal_socket\impl\af_inet_at.c (line 67)` + +```c +static const struct sal_socket_ops at_socket_ops = +{ + at_socket, + at_closesocket, + at_bind, + NULL, + at_connect, + NULL, + at_sendto, + at_recvfrom, + at_getsockopt, + at_setsockopt, + at_shutdown, + NULL, + NULL, + NULL, +#ifdef SAL_USING_POSIX + at_poll, +#endif /* SAL_USING_POSIX */ +}; + +static const struct sal_netdb_ops at_netdb_ops = +{ + at_gethostbyname, + NULL, + at_getaddrinfo, + at_freeaddrinfo, +}; + +static const struct sal_proto_family at_inet_family = +{ + AF_AT, + AF_INET, + &at_socket_ops, + &at_netdb_ops, +}; + + +/* Set AT network interface device protocol family information */ +int sal_at_netdev_set_pf_info(struct netdev *netdev) +{ + RT_ASSERT(netdev); + + netdev->sal_user_data = (void *) &at_inet_family; + return 0; +} +``` + +`rt-thread\components\net\at\at_socket\at_socket.h` + +```c +/* AT socket operations function */ +struct at_socket_ops +{ + int (*at_connect)(struct at_socket *socket, char *ip, int32_t port, enum at_socket_type type, rt_bool_t is_client); + int (*at_closesocket)(struct at_socket *socket); + int (*at_send)(struct at_socket *socket, const char *buff, size_t bfsz, enum at_socket_type type); + int (*at_domain_resolve)(const char *name, char ip[16]); + void (*at_set_event_cb)(at_socket_evt_t event, at_evt_cb_t cb); +}; + + +/* AT receive package list structure */ +struct at_recv_pkt +{ + rt_slist_t list; + size_t bfsz_totle; + size_t bfsz_index; + char *buff; +}; +typedef struct at_recv_pkt *at_recv_pkt_t; + +struct at_socket +{ + /* AT socket magic word */ + uint32_t magic; + + int socket; + /* device releated information for the socket */ + void *device; + /* type of the AT socket (TCP, UDP or RAW) */ + enum at_socket_type type; + /* current state of the AT socket */ + enum at_socket_state state; + /* sockets operations */ + const struct at_socket_ops *ops; + /* receive semaphore, received data release semaphore */ + rt_sem_t recv_notice; + rt_mutex_t recv_lock; + rt_slist_t recvpkt_list; + + /* timeout to wait for send or received data in milliseconds */ + int32_t recv_timeout; + int32_t send_timeout; + /* A callback function that is informed about events for this AT socket */ + at_socket_callback callback; + + /* number of times data was received, set by event_callback() */ + uint16_t rcvevent; + /* number of times data was ACKed (free send buffer), set by event_callback() */ + uint16_t sendevent; + /* error happened for this socket, set by event_callback() */ + uint16_t errevent; + +#ifdef SAL_USING_POSIX + rt_wqueue_t wait_head; +#endif + rt_slist_t list; + + /* user-specific data */ + void *user_data; +}; + +int at_socket(int domain, int type, int protocol); +int at_closesocket(int socket); +int at_shutdown(int socket, int how); +int at_bind(int socket, const struct sockaddr *name, socklen_t namelen); +int at_connect(int socket, const struct sockaddr *name, socklen_t namelen); +int at_sendto(int socket, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen); +int at_send(int socket, const void *data, size_t size, int flags); +int at_recvfrom(int socket, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen); +int at_recv(int socket, void *mem, size_t len, int flags); +int at_getsockopt(int socket, int level, int optname, void *optval, socklen_t *optlen); +int at_setsockopt(int socket, int level, int optname, const void *optval, socklen_t optlen); +struct hostent *at_gethostbyname(const char *name); +int at_getaddrinfo(const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res); +void at_freeaddrinfo(struct addrinfo *ai); + +struct at_socket *at_get_socket(int socket); + +#ifndef RT_USING_SAL + +#define socket(domain, type, protocol) at_socket(domain, type, protocol) +#define closesocket(socket) at_closesocket(socket) +#define shutdown(socket, how) at_shutdown(socket, how) +#define bind(socket, name, namelen) at_bind(socket, name, namelen) +#define connect(socket, name, namelen) at_connect(socket, name, namelen) +#define sendto(socket, data, size, flags, to, tolen) at_sendto(socket, data, size, flags, to, tolen) +#define send(socket, data, size, flags) at_send(socket, data, size, flags) +#define recvfrom(socket, mem, len, flags, from, fromlen) at_recvfrom(socket, mem, len, flags, from, fromlen) +#define getsockopt(socket, level, optname, optval, optlen) at_getsockopt(socket, level, optname, optval, optlen) +#define setsockopt(socket, level, optname, optval, optlen) at_setsockopt(socket, level, optname, optval, optlen) + +#define gethostbyname(name) at_gethostbyname(name) +#define getaddrinfo(nodename, servname, hints, res) at_getaddrinfo(nodename, servname, hints, res) +#define freeaddrinfo(ai) at_freeaddrinfo(ai) + +#endif /* RT_USING_SAL */ +``` + +```c +int ec20_socket_class_register(struct at_device_class *class) +{ + RT_ASSERT(class); + + class->socket_num = AT_DEVICE_EC20_SOCKETS_NUM; + class->socket_ops = &ec20_socket_ops; + + return RT_EOK; +} +``` + +创建的socket 为 at\_socket 结构体;通过内部的 int socket 与 `struct at_socket *at_get_socket(int socket)` 中的参数做比对; + +```txt +ec20_device_class_register --> ec20_socket_class_register --> class->socket_ops = &ec20_socket_ops +``` + +## at device class ec20 与 at socket 关联 + +`packages\at_device-v2.0.3\class\ec20\at_device_ec20.c` + +```c +static const struct at_socket_ops ec20_socket_ops = +{ + ec20_socket_connect, + ec20_socket_close, + ec20_socket_send, + ec20_domain_resolve, + ec20_socket_set_event_cb, +}; +``` + +`rt-thread\components\net\sal_socket\impl\af_inet_at.c (line67)` +通过下面 socket\_ops 将操作函数挂接到 sal\_socket 上;内部的操作函数如 `at_socket` 调用 通过 `socket_ops` 调用底层的驱动; + +```c +static const struct sal_socket_ops at_socket_ops = +{ + at_socket, + at_closesocket, + at_bind, + NULL, + at_connect, + NULL, + at_sendto, + at_recvfrom, + at_getsockopt, + at_setsockopt, + at_shutdown, + NULL, + NULL, + NULL, +#ifdef SAL_USING_POSIX + at_poll, +#endif /* SAL_USING_POSIX */ +}; + +static const struct sal_netdb_ops at_netdb_ops = +{ + at_gethostbyname, + NULL, + at_getaddrinfo, + at_freeaddrinfo, +}; + +static const struct sal_proto_family at_inet_family = +{ + AF_AT, + AF_INET, + &at_socket_ops, + &at_netdb_ops, +}; + +/* Set AT network interface device protocol family information */ +int sal_at_netdev_set_pf_info(struct netdev *netdev) +{ + RT_ASSERT(netdev); + + netdev->sal_user_data = (void *) &at_inet_family; + return 0; +} +``` + +这里将 at\_inet\_family 的相关操作给了 netdev->sal\_user\_data; + +![netdev_add_at.png](./figures/netdev_add_at.png) + +此后,便可通过 netdev->sal\_user\_data 获取该 at 设备的相关操作了; + +## 从 BSD API connect 到 at\_connect + +```c +// rt-thread\components\net\sal_socket\include\socket\sys_socket\sys\socket.h +#define connect(s, name, namelen) sal_connect(s, name, namelen) +``` + +```c +// rt-thread\components\net\sal_socket\src\sal_socket.c +int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen) +{ + struct sal_socket *sock; + struct sal_proto_family *pf; + int ret; + + /* get the socket object by socket descriptor */ + SAL_SOCKET_OBJ_GET(sock, socket); + + /* check the network interface is up status */ + SAL_NETDEV_IS_UP(sock->netdev); + /* check the network interface socket opreation */ + SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, connect); + + ret = pf->skt_ops->connect((int) sock->user_data, name, namelen); +#ifdef SAL_USING_TLS + if (ret >= 0 && SAL_SOCKOPS_PROTO_TLS_VALID(sock, connect)) + { + if (proto_tls->ops->connect(sock->user_data_tls) < 0) + { + return -1; + } + + return ret; + } +#endif + + return ret; +} +``` + +sal\_connect 首先获取 sal\_socket 结构体所在内存,通过 sock->netdev 获取到 netdev, 通过 SAL\_NETDEV\_SOCKETOPS\_VALID 获取通过 sal\_at\_netdev\_set\_pf\_info 函数注册到`netdev->sal_user_data`上的借口及数据,将其值赋给 pf,然后通过 pf->skt\_ops 调用相关接口; + +```c +// rt-thread\components\net\sal_socket\src\sal_socket.c +#define SAL_SOCKET_OBJ_GET(sock, socket) \ +do { \ + (sock) = sal_get_socket(socket); \ + if ((sock) == RT_NULL) { \ + return -1; \ + } \ +}while(0) +``` + +```c +#define SAL_NETDEV_SOCKETOPS_VALID(netdev, pf, ops) \ +do { \ + (pf) = (struct sal_proto_family *) netdev->sal_user_data; \ + if ((pf)->skt_ops->ops == RT_NULL){ \ + return -1; \ + } \ +}while(0) \ +``` + +```c +struct at_device_class +{ + uint16_t class_id; /* AT device class ID */ + const struct at_device_ops *device_ops; /* AT device operaiotns */ +#ifdef AT_USING_SOCKET + uint32_t socket_num; /* The maximum number of sockets support */ + const struct at_socket_ops *socket_ops; /* AT device socket operations */ +#endif + rt_slist_t list; /* AT device class list */ +}; +``` + +```c +// rt-thread\components\net\sal_socket\src\sal_socket.c +/** + * This function will get sal socket object by sal socket descriptor. + * + * @param socket sal socket index + * + * @return sal socket object of the current sal socket index + */ +struct sal_socket *sal_get_socket(int socket) +{ + struct sal_socket_table *st = &socket_table; + + socket = socket - SAL_SOCKET_OFFSET; + + if (socket < 0 || socket >= (int) st->max_socket) + { + return RT_NULL; + } + + /* check socket structure valid or not */ + RT_ASSERT(st->sockets[socket]->magic == SAL_SOCKET_MAGIC); + + return st->sockets[socket]; +} +``` + +## at device 与 at socket + +at\_device 通过内部的 user\_data 与具体的 at\_device,如 at\_device\_ec20 关联; +at\_device\_ec20 通过内部的 user\_data 与 at\_socket 关联; ec20\_socket\_send 中 `ec20->user_data = (void *) device_socket;`, 将对应at socket 的事件发送出去; + +## at socket 数据接收 + +`ec20_socket_event_send(device, SET_EVENT(device_socket, EC20_EVENT_SEND_OK));`,这样在socket 中就知道此时用哪个 device 中的 socket进行网络通信了;**事件的发送使用的是 ec20->user\_data 中存储的socket id**; + +```c +static void urc_send_func(struct at_client *client, const char *data, rt_size_t size) +{ + int device_socket = 0; + struct at_device *device = RT_NULL; + struct at_device_ec20 *ec20 = RT_NULL; + char *client_name = client->device->parent.name; + + RT_ASSERT(data && size); + + device = at_device_get_by_name(AT_DEVICE_NAMETYPE_CLIENT, client_name); + if (device == RT_NULL) + { + LOG_E("get device(%s) failed.", client_name); + return; + } + ec20 = (struct at_device_ec20 *) device->user_data; + device_socket = (int) ec20->user_data; + + if (rt_strstr(data, "SEND OK")) + { + ec20_socket_event_send(device, SET_EVENT(device_socket, EC20_EVENT_SEND_OK)); + } + else if (rt_strstr(data, "SEND FAIL")) + { + ec20_socket_event_send(device, SET_EVENT(device_socket, EC20_EVENT_SEND_FAIL)); + } +} +``` + +**事件的接收使用的是 at\_socket socket->user\_data 中存储的socket id;** + +```c +/** + * send data to server or client by AT commands. + * + * @param socket current socket + * @param buff send buffer + * @param bfsz send buffer size + * @param type connect socket type(tcp, udp) + * + * @return >=0: the size of send success + * -1: send AT commands error or send data error + * -2: waited socket event timeout + * -5: no memory + */ +static int ec20_socket_send(struct at_socket *socket, const char *buff, size_t bfsz, enum at_socket_type type) +{ + uint32_t event = 0; + int result = 0, event_result = 0; + size_t cur_pkt_size = 0, sent_size = 0; + at_response_t resp = RT_NULL; + int device_socket = (int) socket->user_data; + struct at_device *device = (struct at_device *) socket->device; + struct at_device_ec20 *ec20 = (struct at_device_ec20 *) device->user_data; + rt_mutex_t lock = device->client->lock; + + RT_ASSERT(buff); + + resp = at_create_resp(128, 2, 5 * RT_TICK_PER_SECOND); + if (resp == RT_NULL) + { + LOG_E("no memory for resp create."); + return -RT_ENOMEM; + } + + rt_mutex_take(lock, RT_WAITING_FOREVER); + + /* set current socket for send URC event */ + ec20->user_data = (void *) device_socket; + + /* clear socket send event */ + event = SET_EVENT(device_socket, EC20_EVENT_SEND_OK | EC20_EVENT_SEND_FAIL); + ec20_socket_event_recv(device, event, 0, RT_EVENT_FLAG_OR); + + /* set AT client end sign to deal with '>' sign.*/ + at_obj_set_end_sign(device->client, '>'); + + while (sent_size < bfsz) + { + if (bfsz - sent_size < EC20_MODULE_SEND_MAX_SIZE) + { + cur_pkt_size = bfsz - sent_size; + } + else + { + cur_pkt_size = EC20_MODULE_SEND_MAX_SIZE; + } + + /* send the "AT+QISEND" commands to AT server than receive the '>' response on the first line. */ + if (at_obj_exec_cmd(device->client, resp, "AT+QISEND=%d,%d", device_socket, cur_pkt_size) < 0) + { + result = -RT_ERROR; + goto __exit; + } + + /* send the real data to server or client */ + result = (int) at_client_send(buff + sent_size, cur_pkt_size); + if (result == 0) + { + result = -RT_ERROR; + goto __exit; + } + + /* waiting result event from AT URC */ + if (ec20_socket_event_recv(device, SET_EVENT(device_socket, 0), 10 * RT_TICK_PER_SECOND, RT_EVENT_FLAG_OR) < 0) + { + result = -RT_ETIMEOUT; + goto __exit; + } + /* waiting OK or failed result */ + event_result = ec20_socket_event_recv(device, + EC20_EVENT_SEND_OK | EC20_EVENT_SEND_FAIL, 1 * RT_TICK_PER_SECOND, RT_EVENT_FLAG_OR); + if (event_result < 0) + { + LOG_E("%s device socket(%d) wait sned OK|FAIL timeout.", device->name, device_socket); + result = -RT_ETIMEOUT; + goto __exit; + } + /* check result */ + if (event_result & EC20_EVENT_SEND_FAIL) + { + LOG_E("%s device socket(%d) send failed.", device->name, device_socket); + result = -RT_ERROR; + goto __exit; + } + + if (type == AT_SOCKET_TCP) + { + // at_wait_send_finish(socket, cur_pkt_size); + rt_thread_mdelay(10); + } + + sent_size += cur_pkt_size; + } + +__exit: + /* reset the end sign for data conflict */ + at_obj_set_end_sign(device->client, 0); + + rt_mutex_release(lock); + + if (resp) + { + at_delete_resp(resp); + } + + return result > 0 ? sent_size : result; +} +``` + +### at 命令解析的线程 + +`rt-thread\components\net\at\src\at_client.c`中 850 有一个执行 at 命令解析的线程 +at\_client\_para\_init,执行相关的 urc 解析注册函数,或将数据存储在 resp->buf ; + +```c + client->parser = rt_thread_create(name, + (void (*)(void *parameter))client_parser, + client, + 1024 + 512, + RT_THREAD_PRIORITY_MAX / 3 - 1, + 5); +``` + +```c +static void client_parser(at_client_t client) +{ + const struct at_urc *urc; + + while(1) + { + if (at_recv_readline(client) > 0) + { + // 判断是哪个 urc 命令 + if ((urc = get_urc_obj(client)) != RT_NULL) + { + /* current receive is request, try to execute related operations */ + if (urc->func != RT_NULL) + { + urc->func(client, client->recv_line_buf, client->recv_line_len); + } + } + else if (client->resp != RT_NULL) + { + at_response_t resp = client->resp; + + /* current receive is response */ + client->recv_line_buf[client->recv_line_len - 1] = '\0'; + if (resp->buf_len + client->recv_line_len < resp->buf_size) + { + /* copy response lines, separated by '\0' */ + rt_memcpy(resp->buf + resp->buf_len, client->recv_line_buf, client->recv_line_len); + + /* update the current response information */ + resp->buf_len += client->recv_line_len; + resp->line_counts++; + } + else + { + client->resp_status = AT_RESP_BUFF_FULL; + LOG_E("Read response buffer failed. The Response buffer size is out of buffer size(%d)!", resp->buf_size); + } + /* check response result */ + if (rt_memcmp(client->recv_line_buf, AT_RESP_END_OK, rt_strlen(AT_RESP_END_OK)) == 0 + && resp->line_num == 0) + { + /* get the end data by response result, return response state END_OK. */ + client->resp_status = AT_RESP_OK; + } + else if (rt_strstr(client->recv_line_buf, AT_RESP_END_ERROR) + || (rt_memcmp(client->recv_line_buf, AT_RESP_END_FAIL, rt_strlen(AT_RESP_END_FAIL)) == 0)) + { + client->resp_status = AT_RESP_ERROR; + } + else if (resp->line_counts == resp->line_num && resp->line_num) + { + /* get the end data by response line, return response state END_OK.*/ + client->resp_status = AT_RESP_OK; + } + else + { + continue; + } + + client->resp = RT_NULL; + rt_sem_release(client->resp_notice); + } + else + { +// log_d("unrecognized line: %.*s", client->recv_line_len, client->recv_line_buf); + } + } + } +} +``` + +client\_parser 中通过 `urc->func(client, client->recv_line_buf, client->recv_line_len);` 发送事件 `ec20_socket_event_send(device, SET_EVENT(device_socket, EC20_EVENT_SEND_OK));`;这样 ec20\_socket\_send 中通过`ec20_socket_event_recv(device, event, 0, RT_EVENT_FLAG_OR);` 被挂起的线程就会到达就绪态; + +### at\_socket 注册回调函数 + +创建 at\_socket 时,会注册两个回调函数 + +```c + /* set AT socket receive data callback function */ + sock->ops->at_set_event_cb(AT_SOCKET_EVT_RECV, at_recv_notice_cb); + sock->ops->at_set_event_cb(AT_SOCKET_EVT_CLOSED, at_closed_notice_cb); +``` + +```c +// rt-thread\components\net\at\at_socket\at_socket.c +int at_socket(int domain, int type, int protocol) +{ + struct at_socket *sock = RT_NULL; + enum at_socket_type socket_type; + + /* check socket family protocol */ + RT_ASSERT(domain == AF_AT || domain == AF_INET); + + //TODO check protocol + + switch(type) + { + case SOCK_STREAM: + socket_type = AT_SOCKET_TCP; + break; + + case SOCK_DGRAM: + socket_type = AT_SOCKET_UDP; + break; + + default : + LOG_E("Don't support socket type (%d)!", type); + return -1; + } + + /* allocate and initialize a new AT socket */ + sock = alloc_socket(); + if (sock == RT_NULL) + { + return -1; + } + sock->type = socket_type; + sock->state = AT_SOCKET_OPEN; + + /* set AT socket receive data callback function */ + sock->ops->at_set_event_cb(AT_SOCKET_EVT_RECV, at_recv_notice_cb); + sock->ops->at_set_event_cb(AT_SOCKET_EVT_CLOSED, at_closed_notice_cb); + + return sock->socket; +} +``` + +at\_recv\_notice\_cb 接收到的数据,并将接收到的数据块加入到recvpkt\_list链表;修改 at socket 接收到数据的事件统计; + +```c +static void at_recv_notice_cb(struct at_socket *sock, at_socket_evt_t event, const char *buff, size_t bfsz) +{ + RT_ASSERT(buff); + RT_ASSERT(event == AT_SOCKET_EVT_RECV); + + /* check the socket object status */ + if (sock->magic != AT_SOCKET_MAGIC) + { + return; + } + + /* put receive buffer to receiver packet list */ + rt_mutex_take(sock->recv_lock, RT_WAITING_FOREVER); + at_recvpkt_put(&(sock->recvpkt_list), buff, bfsz); + rt_mutex_release(sock->recv_lock); + + rt_sem_release(sock->recv_notice); + + at_do_event_changes(sock, AT_EVENT_RECV, RT_TRUE); +} +``` + +device 实例中借用创建 at\_socket 时注册的回调函数,`at_evt_cb_set[AT_SOCKET_EVT_RECV](socket, AT_SOCKET_EVT_RECV, recv_buf, bfsz);` 执行数据接收,并递交给 socket 应用; +urc\_recv\_func 为注册的 at\_urc 接收命令; + +```c +// packages\at_device-v2.0.3\class\ec20\at_socket_ec20.c +static void urc_recv_func(struct at_client *client, const char *data, rt_size_t size) +{ + int device_socket = 0; + rt_int32_t timeout; + rt_size_t bfsz = 0, temp_size = 0; + char *recv_buf = RT_NULL, temp[8] = {0}; + struct at_socket *socket = RT_NULL; + struct at_device *device = RT_NULL; + char *client_name = client->device->parent.name; + + RT_ASSERT(data && size); + + device = at_device_get_by_name(AT_DEVICE_NAMETYPE_CLIENT, client_name); + if (device == RT_NULL) + { + LOG_E("get device(%s) failed.", client_name); + return; + } + + /* get the current socket and receive buffer size by receive data */ + sscanf(data, "+QIURC: \"recv\",%d,%d", &device_socket, (int *) &bfsz); + /* set receive timeout by receive buffer length, not less than 10 ms */ + timeout = bfsz > 10 ? bfsz : 10; + + if (device_socket < 0 || bfsz == 0) + { + return; + } + + recv_buf = (char *) rt_calloc(1, bfsz); + if (recv_buf == RT_NULL) + { + LOG_E("no memory for URC receive buffer(%d).", bfsz); + /* read and clean the coming data */ + while (temp_size < bfsz) + { + if (bfsz - temp_size > sizeof(temp)) + { + at_client_obj_recv(client, temp, sizeof(temp), timeout); + } + else + { + at_client_obj_recv(client, temp, bfsz - temp_size, timeout); + } + temp_size += sizeof(temp); + } + return; + } + + /* sync receive data */ + if (at_client_obj_recv(client, recv_buf, bfsz, timeout) != bfsz) + { + LOG_E("%s device receive size(%d) data failed.", device->name, bfsz); + rt_free(recv_buf); + return; + } + + /* get at socket object by device socket descriptor */ + socket = &(device->sockets[device_socket]); + + /* notice the receive buffer and buffer size */ + if (at_evt_cb_set[AT_SOCKET_EVT_RECV]) + { + at_evt_cb_set[AT_SOCKET_EVT_RECV](socket, AT_SOCKET_EVT_RECV, recv_buf, bfsz); + } +} +``` + diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/SG105_Pro.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/SG105_Pro.png new file mode 100644 index 0000000000000000000000000000000000000000..37c3a5d6c0da244d5f3f444d13b1c55870d7f796 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/SG105_Pro.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http.png new file mode 100644 index 0000000000000000000000000000000000000000..e18227ab70995ef73b41dffc4d439671d9d0407a Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_https.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_https.png new file mode 100644 index 0000000000000000000000000000000000000000..378086fccff058455026712a24748ca636d0a179 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_https.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_protocol_message.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_protocol_message.png new file mode 100644 index 0000000000000000000000000000000000000000..e6059ab12c03f8d75ab2837ace1ed895d91e78ed Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_protocol_message.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_wireshark.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_wireshark.png new file mode 100644 index 0000000000000000000000000000000000000000..b33cb57e1ee5c144db07e706834f8e3d814ecf4f Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/http_wireshark.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ecf76f9da76eb55d4fa50c5d53b144cbf5b201 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https_protocol_message.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https_protocol_message.png new file mode 100644 index 0000000000000000000000000000000000000000..b1548a7c0f9561e2f325d1c0ada3dcefcd391224 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https_protocol_message.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https_wireshark.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https_wireshark.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcee124ccdb32a23fc47c55e42c9ea89abe8450 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/https_wireshark.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_lwip_conf.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_lwip_conf.png new file mode 100644 index 0000000000000000000000000000000000000000..aedc428cd1eebb5f17eb9ef390a8214bb9e503de Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_lwip_conf.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_mbedtls_conf.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_mbedtls_conf.png new file mode 100644 index 0000000000000000000000000000000000000000..3ee39ce1da70adb20991633a159247e56394e3ca Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_mbedtls_conf.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_sal_tls_conf.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_sal_tls_conf.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b60e8b97d714f2a80af2041b59174f9729c2b1 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/menuconfig_sal_tls_conf.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_http_layer.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_http_layer.png new file mode 100644 index 0000000000000000000000000000000000000000..421d4f2dc6a5445554b944c5e1d4571e8c4fb048 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_http_layer.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_protocol.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_protocol.png new file mode 100644 index 0000000000000000000000000000000000000000..ad1991123f92bbba0731cd7e06a8885ccb3e449a Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_protocol.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_ssl_version.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_ssl_version.png new file mode 100644 index 0000000000000000000000000000000000000000..d7035fc39716445e8d0d4c69ff7f34d393037511 Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/tls_ssl_version.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/wireshark.png b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/wireshark.png new file mode 100644 index 0000000000000000000000000000000000000000..18962f3b8c6388a248868143486a1004252afe4d Binary files /dev/null and b/rt-thread-version/rt-thread-standard/programming-manual/tls/figure/wireshark.png differ diff --git a/rt-thread-version/rt-thread-standard/programming-manual/tls/tls.md b/rt-thread-version/rt-thread-standard/programming-manual/tls/tls.md new file mode 100644 index 0000000000000000000000000000000000000000..8fa729966d54cc47f4b746e5fafae8916140ee23 --- /dev/null +++ b/rt-thread-version/rt-thread-standard/programming-manual/tls/tls.md @@ -0,0 +1,387 @@ +# 1. TLS 的背景 + +在 SSL/TLS 出现之前,很多应用层协议(http、ftp、smtp 等)都存在着网络安全问题,例如大家所熟知的 http 协议,在传输过程中使用的是明文信息,传输报文一旦被截获便会泄露传输内容;传输过程中报文如果被篡改,无法轻易发现;无法保证消息交换的对端身份的可靠性。为了解决此类问题,人们在应用层和传输层之间加入了 SSL/TLS 协议。 +如下图所示,HTTP/1.1 协议默认是以明文方式传输数据的,这就带来三个风险:窃听风险、伪装风险、篡改风险。HTTP 协议自身没有加密机制,但可以通过和 TLS (Transport Layer Security) / SSL (Secure Socket Layer) 的组合使用,加密 HTTP 的通信内容,借助 TLS / SSL 提供的信息加密功能、完整性校验功能、身份验证功能保障网络通信的安全,与 TLS / SSL 组合使用的 HTTP 被称为 HTTPS(HTTPSecure),可以说 HTTPS 相当于身披 SSL / TLS 外壳的 HTTP。 + +**HTTPS协议分层模型** + +![http_https.png](./figure/http_https.png) + +# 2. 什么是 TLS + +TLS(Transport Layer Security,安全传输层),TLS 是建立在传输层 TCP 协议之上的协议,服务于应用层,它的前身是 SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由 TCP 进行传输的功能。 + +## 2.1 TLS 的作用 + +TLS 协议主要解决如下三个网络安全问题。 + +保密\(message privacy\),保密通过加密 encryption 实现,所有信息都加密传输,第三方无法嗅探; +完整性\(message integrity\),通过MAC校验机制,一旦被篡改,通信双方会立刻发现; +认证\(mutual authentication\),双方认证,双方都可以配备证书,防止身份被冒充; + +## 2.2 TLS的发展过程 + +1995: SSL 2.0, 由Netscape提出,这个版本由于设计缺陷,并不安全,很快被发现有严重漏洞,已经废弃 +1996: SSL 3.0. 写成RFC,开始流行。目前\(2015年\)已经不安全,必须禁用 +1999: TLS 1.0. 互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版 +2006: TLS 1.1. 作为 RFC 4346 发布。主要修复了CBC模式相关的如BEAST攻击等漏洞 +2008: TLS 1.2. 作为 RFC 5246 发布 。增进安全性,目前应该主要部署的版本 +2018: TLS 1.3 作为 RFC 8446 发布 ,大幅增进安全性 + +**SSL/TLS协议发布时间与网站支持率对比** + +![tls_ssl_version.png](./figure/tls_ssl_version.png) + + +## 2.3 TLS 报文结构 + +TLS \(Transport Layer Security\)协议是由TLS 记录协议(TLS Record Protocol)和TLS 握手协议(TLS Handshake Protocol)这两层协议叠加而成的,位于底层的TLS 记录协议负责进行信息传输和认证加密,位于上层的TLS 握手协议则负责除加密以外的其它各种操作,比如密钥协商交换等。上层的TLS 握手协议又可以分为4个子协议,TLS 协议的层次结构如下图所示: +![tls_http_layer.png](./figure/tls_http_layer.png) +前面已经介绍了 TLS 协议的认证加密算法,这里先介绍 TLS 记录协议的报文结构,每一条 TLS 记录以一个短标头起始,标头包含记录内容的类型(或子协议)、协议版本和长度,消息数据紧跟在标头之后,如下图所示: + +![tls_protocol.png](./figure/tls_protocol.png) +了解了 TLS 记录报文结构,前面也介绍了认证加密过程,下面以 AES-GCM 为例,看看密文(消息明文 AES-CNT 加密而来,当不需要对消息加密时,此处保持明文即可)和 MAC(将序列号、标头等附加信息与密文一起通过 Galois-MAC 计算而来)是如何添加进 TLS 记录报文的: + +上图中的几个字段作用如下: + +- 序列号:任一端都有自身的序列号并跟踪来自另一端 TLS 记录的数量,能确保消息不被重放攻击; +- 标头:将标头作为计算 GMAC 输入的一部分,能确保未进行加密的标头不会遭受篡改; +- nonce:在加密通信中仅使用一次的密钥,比如前面介绍过的 IV\(Initialization Vector\) 或 CTR 初始值。 + +# 3. RT-Thread使用TLS + +目前常用的 TLS 方式:MbedTLS、OpenSSL、s2n 等,但是对于不同的加密方式,需要使用其指定的加密接口和流程进行加密,对于部分应用层协议的移植较为复杂。因此 SAL TLS 功能产生,主要作用是提供 Socket 层面的 TLS 加密传输特性,抽象多种 TLS 处理方式,提供统一的接口用于完成 TLS 数据交互。 + +## 3.1 RT-Thread SAL TLS 功能使用方式 + +使用流程如下: +配置开启任意网络协议栈支持(如 lwIP 协议栈); + +![menuconfig_lwip_conf.png](./figure/menuconfig_lwip_conf.png) +配置开启 MbedTLS 软件包(目前RT-Thread只支持 MbedTLS 类型加密方式); +![menuconfig_mbedtls_conf.png](./figure/menuconfig_mbedtls_conf.png) +配置开启 SAL\_TLS 功能支持(如下配置选项章节所示); +![menuconfig_sal_tls_conf.png](./figure/menuconfig_sal_tls_conf.png) +配置完成之后,只要在 socket 创建时传入的 protocol 类型使用 PROTOCOL\_TLS 或 **PROTOCOL\_DTLS** ,即可使用标准 BSD Socket API 接口,完成 TLS 连接的建立和数据的收发。示例代码如下所示: + +```c +#include +#include + +#include +#include +#include + +/* RT-Thread 官网,支持 TLS 功能 */ +#define SAL_TLS_HOST "www.rt-thread.org" +#define SAL_TLS_PORT 443 +#define SAL_TLS_BUFSZ 1024 + +static const char *send_data = "GET /download/rt-thread.txt HTTP/1.1\r\n" + "Host: www.rt-thread.org\r\n" + "User-Agent: rtthread/4.0.1 rtt\r\n\r\n"; + +void sal_tls_test(void) +{ + int ret, i; + char *recv_data; + struct hostent *host; + int sock = -1, bytes_received; + struct sockaddr_in server_addr; + + /* 通过函数入口参数url获得host地址(如果是域名,会做域名解析) */ + host = gethostbyname(SAL_TLS_HOST); + + recv_data = rt_calloc(1, SAL_TLS_BUFSZ); + if (recv_data == RT_NULL) + { + rt_kprintf("No memory\n"); + return; + } + + /* 创建一个socket,类型是SOCKET_STREAM,TCP 协议, TLS 类型 */ + if ((sock = socket(AF_INET, SOCK_STREAM, PROTOCOL_TLS)) < 0) + { + rt_kprintf("Socket error\n"); + goto __exit; + } + + /* 初始化预连接的服务端地址 */ + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(SAL_TLS_PORT); + server_addr.sin_addr = *((struct in_addr *)host->h_addr); + rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero)); + + if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0) + { + rt_kprintf("Connect fail!\n"); + goto __exit; + } + + /* 发送数据到 socket 连接 */ + ret = send(sock, send_data, strlen(send_data), 0); + if (ret <= 0) + { + rt_kprintf("send error,close the socket.\n"); + goto __exit; + } + + /* 接收并打印响应的数据,使用加密数据传输 */ + bytes_received = recv(sock, recv_data, SAL_TLS_BUFSZ - 1, 0); + if (bytes_received <= 0) + { + rt_kprintf("received error,close the socket.\n"); + goto __exit; + } + + rt_kprintf("recv data:\n"); + for (i = 0; i < bytes_received; i++) + { + rt_kprintf("%c", recv_data[i]); + } + +__exit: + if (recv_data) + rt_free(recv_data); + + if (sock >= 0) + closesocket(sock); +} + +#ifdef FINSH_USING_MSH +#include +MSH_CMD_EXPORT(sal_tls_test, SAL TLS function test); +#endif /* FINSH_USING_MSH */ +``` + +# 4. RT-Thread 基于 SAL 接口的 TLS 实现 + +## 4.1 TLS 接口注册 + +TLS \(Transport Layer Security\) 安全传输层协议主要用于通信数据的加密,并不影响 SAL 向上提供的接口。RT-Thread 使用的 TLS 组件时 mbedtls(一个由 ARM 公司使用 C 语言实现和维护的 SSL/TLS 算法库),如果启用了 TLS 组件,SAL 层的实现函数中会自动调用 mbedtls 的接口函数,实现数据的加密传输。 + +RT-Thread 的 SAL 接口的 TLS 协议的数据结构描述与需要向其注册的接口函数集合如下: + +```c +// .\rt-thread\components\net\sal_socket\include\sal_tls.h + +struct sal_proto_tls +{ + char name[RT_NAME_MAX]; /* TLS protocol name */ + const struct sal_proto_tls_ops *ops; /* SAL TLS protocol options */ +}; + +struct sal_proto_tls_ops +{ + int (*init)(void); + void* (*socket)(int socket); + int (*connect)(void *sock); + int (*send)(void *sock, const void *data, size_t size); + int (*recv)(void *sock, void *mem, size_t len); + int (*closesocket)(void *sock); + + int (*set_cret_list)(void *sock, const void *cert, size_t size); /* Set TLS credentials */ + int (*set_ciphersurite)(void *sock, const void* ciphersurite, size_t size); /* Set select ciphersuites */ + int (*set_peer_verify)(void *sock, const void* peer_verify, size_t size); /* Set peer verification */ + int (*set_dtls_role)(void *sock, const void *dtls_role, size_t size); /* Set role for DTLS */ +}; +``` + +sal\_proto\_tls 对象的初始化和注册过程如下,(注册相关函数代码位于 rt-thread/components/ne/tsal\_socket/impl/proto\_mbedtls.c ): + +```c +// .\rt-thread\components\net\sal_socket\impl\proto_mbedtls.c + +static const struct sal_proto_tls_ops mbedtls_proto_ops= +{ + RT_NULL, + mebdtls_socket, + mbedtls_connect, + (int (*)(void *sock, const void *data, size_t size)) mbedtls_client_write, + (int (*)(void *sock, void *mem, size_t len)) mbedtls_client_read, + mbedtls_closesocket, +}; + +static const struct sal_proto_tls mbedtls_proto = +{ + "mbedtls", + &mbedtls_proto_ops, +}; + +int sal_mbedtls_proto_init(void) +{ + /* register MbedTLS protocol options to SAL */ + sal_proto_tls_register(&mbedtls_proto); + + return 0; +} +INIT_COMPONENT_EXPORT(sal_mbedtls_proto_init); + +// .\rt-thread\components\net\sal_socket\src\sal_socket.c + +#ifdef SAL_USING_TLS +/* The global TLS protocol options */ +static struct sal_proto_tls *proto_tls; +#endif + +#ifdef SAL_USING_TLS +int sal_proto_tls_register(const struct sal_proto_tls *pt) +{ + RT_ASSERT(pt); + proto_tls = (struct sal_proto_tls *) pt; + + return 0; +} +#endif +``` + +变量 proto\_tls 在文件 sal\_socket.c 中属于全局变量(被 static 修饰,仅限于本文件内),该文件是 SAL 组件对外访问接口函数的实现文件。也就是说,我们启用 TLS 协议后,SAL 组件对外提供的访问接口函数实现代码中就会调用 TLS 接口,完成数据的加密传输,而且该组件的注册是被自动初始化的,比较省心。 + +## 4.2 SAL 对外提供的访问接口 + +SAL 组件初始化并注册成功后,我们就可以使用 SAL 提供的接口进行应用开发了,先看看 SAL 组件提供了哪些访问接口: + +```c +// .\rt-thread\components\net\sal_socket\include\sal_socket.h + +int sal_accept(int socket, struct sockaddr *addr, socklen_t *addrlen); +int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen); +int sal_shutdown(int socket, int how); +int sal_getpeername (int socket, struct sockaddr *name, socklen_t *namelen); +int sal_getsockname (int socket, struct sockaddr *name, socklen_t *namelen); +int sal_getsockopt (int socket, int level, int optname, void *optval, socklen_t *optlen); +int sal_setsockopt (int socket, int level, int optname, const void *optval, socklen_t optlen); +int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen); +int sal_listen(int socket, int backlog); +int sal_recvfrom(int socket, void *mem, size_t len, int flags, + struct sockaddr *from, socklen_t *fromlen); +int sal_sendto(int socket, const void *dataptr, size_t size, int flags, + const struct sockaddr *to, socklen_t tolen); +int sal_socket(int domain, int type, int protocol); +int sal_closesocket(int socket); +int sal_ioctlsocket(int socket, long cmd, void *arg); +``` + +```c +// .\rt-thread\components\net\sal_socket\include\sal_netdb.h + +struct hostent *sal_gethostbyname(const char *name); + +int sal_gethostbyname_r(const char *name, struct hostent *ret, char *buf, + size_t buflen, struct hostent **result, int *h_errnop); +void sal_freeaddrinfo(struct addrinfo *ai); +int sal_getaddrinfo(const char *nodename, + const char *servname, + const struct addrinfo *hints, + struct addrinfo **res); +``` + +如果不习惯使用 SAL 层提供的 sal\_xxx 形式的接口,SAL 还为我们进行了再次封装,将其封装为比较通用的 BSD Socket API,封装后的接口如下: + +```c +// .\rt-thread\components\net\sal_socket\include\socket\sys_socket\sys\socket.h + +#ifdef SAL_USING_POSIX +int accept(int s, struct sockaddr *addr, socklen_t *addrlen); +int bind(int s, const struct sockaddr *name, socklen_t namelen); +int shutdown(int s, int how); +int getpeername(int s, struct sockaddr *name, socklen_t *namelen); +int getsockname(int s, struct sockaddr *name, socklen_t *namelen); +int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen); +int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen); +int connect(int s, const struct sockaddr *name, socklen_t namelen); +int listen(int s, int backlog); +int recv(int s, void *mem, size_t len, int flags); +int recvfrom(int s, void *mem, size_t len, int flags, + struct sockaddr *from, socklen_t *fromlen); +int send(int s, const void *dataptr, size_t size, int flags); +int sendto(int s, const void *dataptr, size_t size, int flags, + const struct sockaddr *to, socklen_t tolen); +int socket(int domain, int type, int protocol); +int closesocket(int s); +int ioctlsocket(int s, long cmd, void *arg); +#else +#define accept(s, addr, addrlen) sal_accept(s, addr, addrlen) +#define bind(s, name, namelen) sal_bind(s, name, namelen) +#define shutdown(s, how) sal_shutdown(s, how) +#define getpeername(s, name, namelen) sal_getpeername(s, name, namelen) +#define getsockname(s, name, namelen) sal_getsockname(s, name, namelen) +#define getsockopt(s, level, optname, optval, optlen) sal_getsockopt(s, level, optname, optval, optlen) +#define setsockopt(s, level, optname, optval, optlen) sal_setsockopt(s, level, optname, optval, optlen) +#define connect(s, name, namelen) sal_connect(s, name, namelen) +#define listen(s, backlog) sal_listen(s, backlog) +#define recv(s, mem, len, flags) sal_recvfrom(s, mem, len, flags, NULL, NULL) +#define recvfrom(s, mem, len, flags, from, fromlen) sal_recvfrom(s, mem, len, flags, from, fromlen) +#define send(s, dataptr, size, flags) sal_sendto(s, dataptr, size, flags, NULL, NULL) +#define sendto(s, dataptr, size, flags, to, tolen) sal_sendto(s, dataptr, size, flags, to, tolen) +#define socket(domain, type, protocol) sal_socket(domain, type, protocol) +#define closesocket(s) sal_closesocket(s) +#define ioctlsocket(s, cmd, arg) sal_ioctlsocket(s, cmd, arg) +#endif /* SAL_USING_POSIX */ +``` + +```c +// .\rt-thread\components\net\sal_socket\include\socket\netdb.h + +struct hostent *gethostbyname(const char *name); + +int gethostbyname_r(const char *name, struct hostent *ret, char *buf, + size_t buflen, struct hostent **result, int *h_errnop); +void freeaddrinfo(struct addrinfo *ai); +int getaddrinfo(const char *nodename, + const char *servname, + const struct addrinfo *hints, + struct addrinfo **res); +``` + +BSD Socket API 也是我们进行网络应用开发时最常使用的接口,如果要使用这些接口,除了启用相应的宏定义,还需要包含这些接口所在的两个头文件。 + +# 5. RT-Thread TLS拓展 + +## 5.1 Wireshark 抓取 TLS 报文 + +### 5.1.1 配置抓包环境 + +1. SG105 Pro 网关交换机配置 + ![SG105_Pro.png](./figure/SG105_Pro.png) + 按照图片所示配置配置网关交换机的端口镜像功能,这样端口 1 连接 PC,端口 2 连接开发板,开发板所有数据包都可以通过 PC 端的 wireshark 软件抓取。 +2. PC 端 Wireshark 配置 + ![wireshark.png](./figure/wireshark.png) + 设置 IP 地址过滤 ( ip.addr == xxx.xxx.xxx.xxx, 其中 ip 地址为开发板 ip 地址) +3. webclient 抓包 http 协议,所有数据都是明文,没有加密 + +```c +#include +#include + +#define GET_HEADER_BUFSZ 1024 +#define GET_RESP_BUFSZ 1024 + +#define GET_LOCAL_URI "http://www.rt-thread.com/service/rt-thread.txt" + +/* send HTTP GET request by common request interface, it used to receive longer data */ +static int webclient_get_comm(const char *uri) +``` + +![http.png](./figure/http.png) +![http.png](./figure/http_wireshark.png) +![http.png](./figure/http_protocol_message.png) + +4. webclient 抓包 https 协议,密文数据 + +```c +#include +#include + +#define GET_HEADER_BUFSZ 1024 +#define GET_RESP_BUFSZ 1024 + +#define GET_LOCAL_URI "https://www.rt-thread.com/service/rt-thread.txt" + +/* send HTTP GET request by common request interface, it used to receive longer data */ +static int webclient_get_comm(const char *uri) +``` + +![http.png](./figure/https.png) +![http.png](./figure/https_wireshark.png) +![http.png](./figure/https_protocol_message.png) \ No newline at end of file