跳转至

LuatOS socket

Hello,大家好,我是朱天华。

欢迎大家来到合宙LuatOS直播课堂,一起学习LuatOS课程。

第一部分:LuatOS课程背景

因为今天是我们LuatOS系列课程的第二讲,所以在这里我就不重复讲解整个LuatOS课程的背景了;

如果您还不清楚LuatOS课程背景,可以访问:LuatOS课程背景 这个链接,进行了解;

第二部分:LuatOS socket课程讲哪些内容

今天是LuatOS课程的第002讲,LuatOS socket;

LuatOS socket是LuatOS开发中最常用到的网络应用之一;

LuatOS socket课程主要包含以下几个部分:

  1. TCP/IP总体介绍;
  2. LuatOS上的 4G/WiFi/以太网 三种网络环境下的TCP/IP协议栈总体介绍;
  3. TCP/UDP/SSL/证书以及socket概念;
  4. LuatOS socket核心库和libnet扩展库功能介绍;
  5. LuatOS socket client应用开发框架(TCP,UDP,SSL,网络环境检测看门狗);
  6. 如何分析LuatOS socket日志;

第三部分:TCP/IP总体介绍

今天我们讲的LuatOS socket是LuatOS开发中最常用到的网络应用之一,用户使用LuatOS socket开发网络应用,会直接接触到TCP/UDP/SSL/认证等一些网络核心概念;

而说到网络应用,就绕不开TCP/IP;

3.1 TCP/IP网络模型

有网络应用开发经验的人,应该都听说过OSI七层模型、TCP/IP协议四层模型和TCP/IP协议五层模型;

这三种网络协议模型的说明,参考下表:

OSI七层模型 TCP/IP四层模型 TCP/IP五层模型
应用层 应用层 应用层
表示层
会话层
传输层 传输层 传输层
网络层 网络层 网络层
数据链路层 网络接口层 数据链路层
物理层 物理层

看了这张表之后,我们应该有以下几点认识:

  1. 都采用了分层的思想,将复杂的通信过程分解为更小、更易于管理的部分;
  2. 每一层都为其上层提供服务,并使用其下层提供的服务;
  3. 这三种网络模型,只是分层的颗粒度不一样,实际上,这三种网络模型的本质内容都是一样的;

既然这三种网络模型的本质内容是一样的,为什么还要存在三种网络模型呢?每种网络模型又有什么作用呢?

我们简单地看一下这三种网络模型的历史:

最终TCP/IP模型在实践中得到广泛应用;

至于TCP/IP四层模型和TCP/IP五层模型,二者的差别不大,主要体现在对最底层的划分不同:

  1. 四层模型隐藏了底层细节,将网络接入视为一个黑盒,更关注对软件层面的设计,不关心具体的硬件;
  2. 五层模型明确包含了物理硬件层;

四层模型和五层模型本质上是同一个东西的两种不同表述方式,所以我们接下来不纠结四层还是五层模型,而是统称为TCP/IP模型,下文中会根据不同的应用场景来决定选择四层模型还是五层模型来讲解。

3.2 TCP/IP协议

在了解了TCP/IP网络模型之后,接下来我们看一下TCP/IP协议这个概念;

TCP/IP网络模型是理论上的框架和蓝图,而TCP/IP协议是这个框架的具体实现;

我们来看下面这张表格

TCP/IP五层模型 协议实现
应用层 HTTP,MQTT,FTP,WebSocket,DNS,NTP,SMTP等
传输层 TCP,UDP等
网络层 IP等
数据链路层 以太网,WiFi,4G 等网络各自的数据链路层协议以太网的IEEE 802.3系列WiFi网络的IEEE 802.11系列4G网络的PDCP、RLC、MAC等协议
物理层 光纤,双绞线,无线电波等传输介质

可以看到,每一层都有多个协议去实现,在所有的这些协议中,TCP和IP两种协议是其中的核心协议,所以用TCP/IP协议代指网络模型中的所有协议;

所以说,TCP/IP协议 并不是两个单一的协议,而是一个协议家族,包含TCP/IP网络模型中的所有协议;

所有协议协同工作,从软件上完成了互联网上的数据传输任务;

3.3 TCP/IP协议的核心工作原理

TCP/IP协议通过功能分层以及数据封装/解封装,来协同工作完成网络数据传输任务;

3.3.1 功能分层

下层为上层提供服务,上层使用下层提供的服务;

每一层都只专注于自己的任务;

TCP/IP五层模型,从上到下划分为应用层,传输层,网络层,数据链路层,物理层;

每一层的主要功能如下:

1. 应用层:应用层的主要工作是定义数据格式并按照对应的格式解读数据,应用层定义了各种各样的协议来规范数据格式,常见的有 HTTP、MQTT、FTP、WebSocket等;

例如常见的HTTP协议,服务端收到客户端的请求以后,就能按照HTTP报文的格式,正确地解析客户端发来的数据,当请求处理完以后,再按照HTTP报文的格式返回结果给客户端,客户端收到结果后,按照HTTP报文的格式进行解析;

2. 传输层:传输层的主要工作是定义端口,标识应用程序身份,实现端到端的通信,为应用层的应用程序提供可靠的或不可靠的数据传输服务;

数据包是从发送端设备的某个应用程序发出,然后由接收端设备的某个应用程序接收。而每台设备都有可能同时运行着很多个应用程序,所以要通过传输层定义的端口来确认使用的是设备上的哪一个应用程序;

例如HTTP应用程序的默认端口就是80,也可以自定义端口;

3. 网络层:网络层的主要工作是定义网络IP地址,区分网段,逻辑寻址和在不同的公共网络之间进行路由选择

网络层的通信视野是全局的,它从源IP地址出发,最终要到达目的IP地址,中间可能经过无数个不同的网络(例如以太网、Wi-Fi、4G LTE等),它管的是“全局”的事情;

例如下图中红框所标注的网络单元中的网络路由选择,就是靠网络层实现的:

img

4. 数据链路层:数据链路层的主要工作是在本地局域网内将数据发送给接收方;

可以是带交换机功能的WiFi路由器和这个WiFi下的设备相互发送数据,或者这个WiFi路由器下的两个设备相互发送数据;

也可以是以太网交换机和这个交换机下的设备相互发送数据,或者这个交换机下的两个设备相互发送数据;

也可以是4G网络中,某个运营商基站和这个基站下的设备(例如手机)相互发送数据;

本地局域网的数据链路层也需要寻址,以太网和WiFi网络环境中,需要用到设备的MAC地址进行寻址;4G网络中,需要用到RNTI(无线网络临时标识符)进行寻址;

数据链路层的通信视野是局部的,只负责局域网内将数据发送给接收方,它管的是“局部”的事情;

例如下图中红框所标注的网络单元中的局域网内的寻址,就是靠数据链路层实现的:

img

5. 物理层:物理层的主要工作是传输0和1的电信号,这些电信号通过物理传输介质进行传输

常见的物理层传输介质有:

(1) 光纤(我们日常生活中可以接触到的就是,运营商将光纤直接从运营商机房铺设到用户家中,提供百兆、千兆的高速互联网接入。你家办理的中国电信、联通或移动的“千兆光网”。你家的“光猫”就是接收光信号并将其转换为电信号的设备。)

(2) 双绞线(例如在以太网网络环境中常用的网线就是双绞线的一种)

(3) 无线电波(例如Wi-Fi、4G、5G网络都必须依赖无线电波作为载体才能进行通信。它们本质上都是通过调制解调技术,将数字信号(0和1)加载到无线电波上发射出去,接收端再解调回来)

不同的物理层传输介质,决定了电信号(0和1)的传输方式,物理介质的不同决定了电信号的传输带宽、速率、传输距离以及抗干扰性等等。

例如下图中红框所标注的网络单元间的数据传输,就是靠物理层实现的:

img

TCP/IP协议的上面四层(应用层,传输层,网络层,数据链路层),可以理解为是软件层面的实现,最终封装成数据包;

而TCP/IP协议的最下面一层(物理层)将数据包转化为 0 和 1 的电信号,通过物理介质进行传输才能到达下一个网络单元,因此物理介质可以认为是网络通信的基石。

大家现在对每一层的功能先有一个总体认识即可,不用关注细节;

后续会有各种网络环境下的实例来进一步说明如何工作;

3.3.2 数据封装/解封装

理解了功能分层的概念之后,我们再来看下面这张图片,了解一下数据的封装和解封装;

img

img

  1. 在数据发送端,应用层的网络协议生成应用数据
  2. 应用数据向下传,交给传输层,增加了传输层的头部信息,在头部信息中,有很多字段,其中有两个字段分别是源端口号目标端口号,这两个端口号的作用是:定义了发送端和接收端的应用程序,例如http应用的默认端口号是80;可以提供两个主机之上端到端的通信;
  3. 传输层头部+应用数据向下传,交给网络层,增加了网络层的头部信息,在头部信息中,有很多字段,其中有两个字段分别是源IP地址目标IP地址,这两个IP地址的作用是:定义了发送端和接收端的网络设备地址;可以提供主机到主机的通信;
  4. 网络层头部+传输层头部+应用数据向下传,交给数据链路层,增加了数据链路层的头部信息;
  5. 最终,数据链路层头部+网络层头部+传输层头部+应用数据的数据包,通过物理层转换为0和1的电信号,然后通过物理传输介质发给了接收端;
  6. 接收端收到数据后,从下到上,依次解析并且剥离出数据链路层头部、网络层头部、传输层头部,最终将应用数据交给应用程序去处理;
  7. 接收端处理完应用数据后,如果需要返回应用数据给发送端;此时发送端和接收端的角色互换,再走一遍数据分层的封装和解封装过程;

了解了功能分层和数据封装/解封装的核心工作原理之后,大家可能对TCP/IP网络数据传输的理解还是比较模糊,因为刚才说的这些都是偏理论的东西,理解起来不是那么直观;

接下来我使用电脑的网络浏览器访问http://airtest.openluat.com/,抓取一个完整的数据包封装实例,再来加深一下对功能分层和数据封装/解封装的理解:

img

接下来我们打开下面这张图片,实际分析一下http请求数据包

img

通过上面这个http请求的网络数据包分析,大家对 TCP/IP的功能分层以及数据封装/解封装 应该有了进一步的认识;

3.4 不同网络环境下的TCP/IP工作实例

讲到这里,不知道大家有没有发现,之前讲述的网络模型,网络数据包分析,我使用的都是极度简化后的网络拓扑结构,只有一个网络硬件单元或者两个网络硬件单元;

但是在现实世界中,网络拓扑结构不可能如此简单,从源设备到目标设备之间,可能要经过几十个网络硬件单元;

接下来,我分别以4G网络环境、WiFi网络环境和以太网网络环境为例,来说明一个实例:

当使用手机或者电脑访问https://docs.openluat.com/这个网站时,在整个TCP/IP网络环境中,有哪些重要的网络硬件单元在配合工作?这些网络硬件单元之上各自分别实现了TCP/IP协议的哪几层?这些网络硬件单元的核心作用分别是什么?

为了方便理解,接下来的每个实例中都会给出一个总体的网络拓扑结构图,此处给出的网络拓扑结构图和之前的网络拓扑结构图相比,会更加详细,但是所给出的仍然是是简化后的网络拓扑结构图;

虽然也是简化后的拓扑图,但是这里所描述的网络拓扑结构图,对于我们理解整个网络系统的工作流程,也会提供很大的帮助;可以让我们从一个全局的视野,去理解TCP/IP是如何在整个网络系统中配合工作的;

3.4.1 4G网络环境下TCP/IP协议的工作实例

第一种实例:使用中国移动4G手机卡网络的手机访问托管在中国电信的服务器https://docs.openluat.com/

我们打开下面这张图总体了解一下这个过程

img

第二种实例:使用中国移动4G手机卡网络的手机访问托管在中国移动的服务器https://docs.openluat.com/

和 中国移动4G手机卡的手机访问托管在中国电信的服务器 相比,中国移动4G手机卡的手机访问托管在中国移动的服务器,不需要经过中国移动和边界路由器和中国电信边界路由器,在中国移动的地市、省干、国干核心路由器之间进行路由转发,如下图所示(差异部分已通过浅绿色背景进行区分):

img

3.4.2 WiFi网络环境下TCP/IP协议的工作实例

第一种实例:使用中国移动WIFI网络的手机访问托管在中国电信的服务器https://docs.openluat.com/

img

第二种实例:使用中国移动WiFi网络的手机访问托管在中国移动的服务器https://docs.openluat.com/

和 中国移动WiFi网络的手机访问托管在中国电信的服务器 相比,中国移动4G手机卡的手机访问托管在中国移动的服务器,不需要经过中国移动和边界路由器和中国电信边界路由器,在中国移动的地市、省干、国干核心路由器之间进行路由转发,如下图所示(差异部分已通过浅绿色背景进行区分):

img

3.4.3 以太网网络环境下TCP/IP协议的工作实例

第一种实例:使用中国移动以太网网络的电脑访问托管在中国电信的服务器https://docs.openluat.com/

img

第二种实例:使用中国移动以太网网络的电脑访问托管在中国移动的服务器https://docs.openluat.com/

和 中国移动WiFi网络的电脑访问托管在中国电信的服务器 相比,中国移动以太网的电脑访问托管在中国移动的服务器,不需要经过中国移动和边界路由器和中国电信边界路由器,在中国移动的地市、省干、国干核心路由器之间进行路由转发,如下图所示(差异部分已通过浅绿色背景进行区分):

img

第四部分:LuatOS上的TCP/IP协议栈总体介绍

在总体了解了TCP/IP的核心理论知识,以及不同网络环境下的TCP/IP工作实例之后,大家对TCP/IP应该具备了一个总体的认识;

接下来我们一起看下LuatOS上对TCP/IP协议栈的支持情况,以及提供了哪些编程接口给LuatOS项目应用脚本来使用,参考下表:

TCP/IP五层模型 LuatOS支持的TCP/IP协议 LuatOS上的编程接口 备注
应用层 HTTP,MQTT,FTP,WebSocket,DNS,NTP,DHCP SSL/TLS:从OSI七层模型来看,和表示层最接近,所以此处把SSL/TLS放到TCP/IP模型中应用层
socket核心库/libnet扩展库
http核心库/httpplus扩展库/httpsrv核心库
mqtt核心库
ftp核心库
websocket核心库
httpdns扩展库
dhcpsrv扩展库
udpsrv扩展库
应用层提供的这些编程接口和LuatOS项目应用开发关系最为密切;
这些核心库和扩展库的API文档参考:
LuatOS API手册
在后续直播课程中,我们会逐一进行讲解
传输层 TCP,UDP socket核心库/libnet扩展库
网络层 IP,ICMP socket核心库exnetif扩展库IP_READY、IP_LOSE 在这里重点说一下exnetif扩展库,exnetif扩展库有两项核心功能:
1、LuatOS产品做为设备模式来使用,可以配置使用4G网卡,以太网网卡,WiFi网卡中的一种或者多种来上网,使用多种网卡上网时,可以配置使用的多种网卡优先级;
2、LuatOS产品做为路由器模式来使用,支持以下三大类网络路由场景:
(1) 4G网卡连接外网,WiFi设备通过WiFi AP热点接入上网,以太网设备通过LAN口接入上网;
(2) WiFi网卡连接外网,WiFi设备通过WiFi AP热点接入上网,以太网设备通过LAN口接入上网;
(3) 以太网WAN口连接外网,WiFi设备通过WiFi AP热点接入上网,以太网设备通过LAN口接入上网;
数据链路层 以太网,WiFi,4G 等网络各自的数据链路层协议
以太网的IEEE 802.3系列
WiFi网络的IEEE 802.11系列
4G网络的PDCP、RLC、MAC等协议
物理层 双绞线,无线电波等传输介质

第五部分:TCP/UDP/SSL/证书以及socket概念

经过第四部分的学习,大家对LuatOS上TCP/IP网络应用的总体支持情况,应该有了一个总体的认识;

因为我们本讲课程是要讲解LuatOS socket,所以从本节开始,我们重点学习一下和socket有关的理论知识和实践方法;

先简单地看一下socket的概念:

socket是TCP/IP提供的网络应用开发的编程接口,包含地址、端口、传输层/安全层协议 三部分;

地址比较好理解,可以是域名地址,也可以是ip地址,用来标识网络上的一台设备;

端口也比较好理解,在socket应用中,服务器端的端口一般都是服务器自定义的,用来标识自定义的应用;

传输层/安全层协议包括TCP/UDP/SSL/证书的知识,这一部分知识有点儿复杂,接下来我们重点看下TCP/UDP/SSL/证书的一些理论知识;

5.1 TCP/UDP

先来看一张TCP/UDP的对比表格:

是否面向连接 是否可靠 是否支持流量控制
TCP
UDP

TCP 提供了面向连接的、可靠的、支持流量控制的传输层服务;

UDP 提供了无连接的、不可靠的、不支持流量控制的传输层服务;

5.1.1 是否面向连接

TCP的“面向连接”不仅仅是一个形式,而是实现其所有可靠性保障的基础。它的作用可以概括为:为后续可靠、有序的数据传输做好必要的准备工作和管理工作。

具体来说,它的作用体现在以下两个关键阶段:

1. 建立连接(三次握手)

在发送任何实际应用数据之前,客户端和服务器必须通过三次握手建立一个双向的通信通道;

过程:

  • SYN (Synchronize Sequence Numbers): 客户端发送一个SYN包(一个特殊的TCP报文)到服务器,并带上自己的初始序列号(Seq = x)。意思是:“你好,我想和你建立连接。我这边数据的起始编号是x。”

  • SYN-ACK: 服务器收到后,如果同意连接,会发送一个SYN-ACK包。其中包含:

  • ACK = x + 1: “你刚才发的序列号x我收到了,我期待你下一个数据包的序列号是x+1。”(这是对客户端SYN的确认)
  • 同时,服务器也发送自己的初始序列号(Seq = y)。意思是:“我也同意建立连接。我这边数据的起始编号是y。”

  • ACK: 客户端收到服务器的SYN-ACK后,再发送一个ACK包(ACK = y + 1)给服务器。意思是:“你发的序列号y我也收到了,我期待你下一个数据包的序列号是y+1。”

抓包分析:

  • 合宙TCP/UDP web测试工具:使用说明,创建TCP Server;

  • SSCOM:使用SSCOM提供的TCP Client工具; 或者 LLCOM:使用LLCOM提供的socket客户端工具(支持TLS)

  • 网络抓包工具:wireshark,这个工具的用法,在这里就不讲了,大家如果有不懂的,上网自行学习;

  • Client连接Server之后,在wireshark中过滤搜索 ip.addr == XXX && tcp.port == YYY,XXX为服务器ip地址,YYY为服务器端口,例如ip.addr == 112.125.89.8 && tcp.port == 47082

下面我们对一个TCP连接成功的网络包进行分析:

img

img

img

下面我们对一个TCP连接失败的网络包进行分析,客户端去连接一个不存在的服务器:

img

作用:

  • 交换初始序列号 (ISN): 这是最核心的作用。序列号是TCP实现可靠性(确认、重传、排序)的根基。双方通过握手告知对方自己将从哪个号码开始编号字节数据。

  • 确认双方通信能力: 证明客户端和服务器之间的网络路径在两个方向上(Client->Server 和 Server->Client)都是通的。如果握手失败,说明根本没法通信,应用层会立刻得到错误信息,而不用等待数据传输超时。

  • 分配资源: 在握手成功后,双方的操作系统都会为这个连接分配资源,如创建socket、分配发送和接收缓冲区等。如果连接无法建立,就避免了资源的浪费。

2. 终止连接(四次挥手)

过程:

  • 一方发送FIN包,表示“我数据发完了,要关闭我到你方向的连接”。

  • 另一方ACK这个FIN包,表示“我知道你要关了”。

  • 可能另一方向也还有数据要发送,等发完后,它再发送一个FIN包。

  • 最初的一方再ACK这个FIN包。

抓包分析:

img

作用:

  • 可靠地释放资源: 确保双方都知道通信即将结束,可以安全地释放为这个连接分配的所有内核资源(TCB、缓冲区等)。

  • 保证数据完整性: 确保在连接关闭前,所有在途的数据包都已经被正确处理完毕。防止一方以为连接已断而另一方还在发送数据的情况。

而UDP呢?提供的是一个无连接的传输层服务,意思就是在正式发送应用数据之前,客户端和服务端没有三次握手连接,客户端和服务器端之间没有网络数据交互,如下图所示:

img

同理,因为就没有连接动作,所以UDP断开时,也没有四次挥手的断开连接动作;

5.1.2 是否可靠

TCP 提供的是一种可靠的传输层服务;TCP 通过多种机制确保数据准确无误地送达:

  1. 确认应答机制 (ACK): 接收方收到数据后,会发送一个确认报文(ACK)给发送方。
  2. 超时重传机制: 发送方在发送一个数据段后启动一个定时器。如果在规定时间内没有收到对应的ACK,它会重新发送该数据段。
  3. 序列号和确认号机制: 每个字节的数据都被分配一个序列号(Seq),ACK号则告诉发送方“我期望收到的下一个字节的序列号是多少”。这解决了数据包乱序和重复的问题。
  4. 校验和机制: 每个数据包都包含一个校验和。接收方会验证它,如果校验失败则丢弃该包,从而触发发送方的重传机制。

img

img

而UDP没有这些可靠的机制,我们看一下UDP发送数据的网络包:

img

img

和TCP相比,UDP数据发送出去之后,没有机制确认数据是否发送成功;在网络不稳定的情况下,会出现丢失数据的问题;

5.1.3 是否支持流量控制

TCP 提供的是一种支持流量控制的传输层服务;TCP 通过滑动窗口协议确保数据准确无误地送达:

  • 使用滑动窗口协议来防止发送方发送数据过快,导致接收方缓冲区溢出。
  • 接收方在ACK中会告知发送方自己当前剩余的缓冲区大小(窗口大小),发送方根据这个窗口大小调整发送速率。

img

img

下面的例子是一个出现0窗口的网络包,因为我这边没有环境复现这个问题,所以从网上找了一张图片,给大家看一下:

img

而UDP是没有ACK机制的,所以也没有窗口控制协议;

5.2 数字证书

5.2.1 数字证书是什么?

数字证书,是一种数字文件,其核心作用类似于现实世界中的身份证或营业执照。

它的根本目的是:将一个数字证书中的公钥与一个实体(个人、设备、组织、域名)的身份信息进行强绑定,并由一个受信任的第三方进行担保。

简单来说,数字证书就是证明“这个公钥确实属于某某某” 的电子凭证。

5.2.2 为什么需要数字证书?

要理解证书的价值,必须先理解它要解决的问题:中间人攻击。

  1. 当客户端(如浏览器)访问一个服务器时,如果一个攻击者拦截了通信,他可以将自己的数字证书(包含公钥)发送给客户端,并冒充服务器。
  2. 客户端用攻击者的公钥加密数据。
  3. 攻击者截获数据,用自己的私钥解密,窃取信息。
  4. 攻击者再用服务器的真实公钥加密数据,转发给服务器。整个过程神不知鬼不觉。

核心问题:如何确保你拿到的服务器的证书公钥,确实来自你真正想通信的一方,而不是攻击者?

数字证书就是答案。它由一个受信任的第三方机构(Certificate Authority, CA)对公钥和身份信息进行签名,任何信任该CA的人都可以验证这张证书的真伪,从而信任其中的公钥。

5.2.3 数字证书里包含什么?(X.509标准)

数字证书遵循X.509标准格式。它包含以下核心信息:

  1. 基本身份信息(证书主体)

  2. 版本号:使用的X.509版本(如v3)。

  3. 序列号:由CA分配的唯一标识符,用于吊销查询。

  4. 签名算法:CA签发此证书时使用的算法(如SHA256withRSA)。

  5. 颁发者:签发此证书的证书颁发机构(CA) 的名称。

  6. 有效期:证书有效的起止日期和时间。证书在此时间窗口外无效。

  7. 主题:证书持有者的身份信息。最重要的是通用名称(Common Name, CN),通常是域名(如 docs.openluat.com)。

  8. 核心密码学材料
  9. 公钥本身(一串很长的数字)。

  10. 该公钥所使用的算法(如RSA 2048位、ECC)。

  11. 信任的证明(CA的签名)
  12. 数字签名:这是最关键的部分。首先对上述所有信息(除了签名本身)计算一个哈希摘要,然后对这个摘要使用CA自己的私钥进行加密,得到的结果就是数字签名。

我们现在访问docs.openluat.com,看下docs网站的数字证书:

img

img

img

点击docs.openluat.com的证书文件的每个字段,我们来对照说明一下;

5.2.4 数字证书的工作原理:信任链(Chain of Trust)

数字证书的信任并非凭空产生,它依赖于一个层级化的信任模型,称为公钥基础设施(Public Key Infrastructure, PKI)。

5.2.4.1 信任链的组成:
  1. 根证书(Root CA Certificate)

  2. 位于信任链顶端的、自签名的证书。

  3. 它们的公钥被预先内置在操作系统(如Windows、macOS)和浏览器(如Chrome、Firefox)的信任根证书存储区中。这是所有信任的起点。

  4. 根CA的私钥被极其严格地离线保管,几乎不直接用于签发服务器证书。

  5. 中间证书
  6. 由根CA签发,用于代表根CA去实际签发最终的用户证书。

  7. 这种层级结构增加了安全性:即使中间CA的私钥被泄露,可以迅速吊销该中间证书,而根CA依然安全,无需重建整个信任体系。

  8. 一个服务器证书通常伴随着一个或多个中间证书。

  9. 终端实体证书
  10. 也就是我们通常为网站、服务申请和使用的证书。

如下图所示,docs.openluat.com服务器使用就是一个三层证书结构:

img

5.2.4.2 证书验证过程(以浏览器访问https://docs.openluat.com网站为例):

当浏览器收到网站的证书时,它会执行以下验证步骤:

1. 验证基本信息:检查证书的有效期是否当前时间在内,检查证书的“主题”或“SAN”字段是否包含正img

2. 验证签名(构建信任链):

  • 浏览器使用中间证书A里的公钥,去解密网站证书的数字签名,得到一个哈希值H1。

  • 浏览器对网站证书的内容(除签名外)用相同的哈希算法计算,得到另一个哈希值H2。

  • 如果H1 == H2,则说明:

  • 网站证书的内容完整,未被篡改。
  • 该证书确实是由中间证书A对应的CA签发的(因为只有用中间CA的私钥才能生成能用其公钥解开的签名)。

img

3. 递归验证:

  • 浏览器接着发现,它可能不直接信任中间证书A。于是它继续向上验证:使用根证书的公钥,去验证中间证书A的签名。

  • 因为根证书的公钥已经内置在浏览器的信任存储中,且是自签名的,验证到此结束。

img

4. 吊销检查:浏览器还会通过证书吊销列表(CRL) 或在线证书状态协议(OCSP) 查询该证书是否已被CA提前吊销(例如因为私钥泄露)。

5. 最终信任:所有步骤都通过后,浏览器才最终信任该网站证书,并使用证书中的公钥与服务器进行加密通信;

数字证书的知识,理解起来可能有点儿复杂,即使大家不了解关系也不大,最终在LuatOS socket编程中,如果用到证书,使用起来会非常简单,在下文中我们会重点说明如何使用;

5.3 SSL/TLS

5.3.1 什么是 SSL/TLS?

SSL(Secure Sockets Layer,安全套接层) 和 TLS(Transport Layer Security,传输层安全) 是加密协议,旨在为计算机网络通信提供安全性和数据完整性。它们的核心目标是建立一个安全的通信通道,防止窃听、篡改和消息伪造。

关系: TLS 是 SSL 的继任者。由于历史原因,两者名称经常混用,但现在所有现代应用都使用 TLS。SSL 3.0 及其早期版本已被发现存在严重漏洞(如 POODLE 攻击)并被完全废弃。现在当我们说“SSL”时,通常指的是 TLS。

主要目标:

  1. 加密(Encryption): 混淆传输的数据,防止第三方窃听。
  2. 认证(Authentication): 验证通信方的身份(通常是验证服务器的身份,也可选择验证客户端身份),防止中间人攻击。
  3. 完整性(Integrity): 检测数据在传输过程中是否被篡改或损坏。

5.3.2 为什么需要 SSL/TLS?

在没有 SSL/TLS 的情况下,网络通信(如 HTTP、FTP、SMTP)以纯文本形式传输。这意味着在你和服务器之间的任何节点(如你的路由器、ISP、公共Wi-Fi热点)都可以看到你发送的所有内容,包括密码、信用卡号、私人消息等。SSL/TLS 通过在传输层之上建立一个加密层来解决这个问题,从而保护上层应用协议(如 HTTP→HTTPS)。

例如,如果一个TCP Client连接上一个TCP Server之后,如果TCP Client给Server发送数据LuatOS,在整个网络传输过程中的任何节点(局域网的交换机和路由器,基站,服务网关,数据网关,核心路由器,边界路由器)都能看到明文数据LuatOS,如下图所示:

img

如果在TCP之上支持了TLS,在传输数据之前,Client和Server会动态协商一个密钥,密钥协商成功后,后续发送方传输的应用数据都会通过密钥加密,然后再传输到接收方,接收方使用密钥进行解密;只有发送端和接收端的应用程序才能加密数据以及解密为原始明文数据,在网络传输过程中,网络中间节点因为不知道密钥,即使截获了数据,因为没有密钥,也无法解密数据,如下图所示:

img

img

5.3.3 SSL/TLS 如何工作?—— 握手过程详解

SSL/TLS 的核心是其握手过程。这个过程在双方开始传输实际应用数据之前进行,目的是协商连接参数、验证身份并生成会话密钥。以下是TLS 1.2 握手流程:

整个过程的目标是在客户端和服务器之间安全地协商出两样东西:

  1. 会话密钥(Session Keys):用于后续通信使用的对称加密密钥。
  2. 加密参数:使用哪种对称加密算法(如 AES)、哪种哈希算法(如 SHA256)等。

整个握手过程在 TCP 连接建立之后进行,可以概括为以下四个主要阶段和 10 个步骤:

img

第一阶段:Hello 消息交换(协商安全参数)
步骤 1: Client Hello

客户端向服务器发起握手,发送一个 Client Hello 消息,包含以下关键信息:

  • 客户端随机数(Client Random):一个由客户端生成的 32 字节随机数,用于后续生成主密钥。
  • 支持的协议版本:例如 TLS 1.2
  • 支持的密码套件列表(Cipher Suites):客户端支持的所有加密算法组合的列表,按优先级排序。例如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

img

步骤 2: Server Hello

服务器响应客户端的 Hello 消息,发送一个 Server Hello 消息,包含以下关键信息:

  • 服务器随机数(Server Random):一个由服务器生成的 32 字节随机数,同样用于生成主密钥。
  • 选择的协议版本:通常是双方都支持的最高版本,例如 TLS 1.2
  • 选择的密码套件:从客户端提供的列表中选出一个服务器也支持的套件,例如 TLS_RSA_WITH_AES_128_CBC_SHA
  • 会话ID(Session ID):服务器为本次会话生成的一个唯一ID,用于后续会话恢复。
  • 选择的扩展。

至此,双方就使用哪种加密算法进行通信达成了一致。

img

第二阶段:服务器认证与密钥交换(Server Key Exchange)
步骤 3: Server Certificate

服务器将自己的数字证书发送给客户端。这个证书包含了服务器的公钥、域名、颁发机构(CA)等信息,并由CA的私钥签名。客户端会验证该证书的有效性(是否过期、域名是否匹配、签发者是否可信等),以确认正在通信的确实是目标服务器,而不是中间人。

步骤 4: Server Key Exchange Message(可选)

注意:此步骤仅在选择的密码套件不是 RSA 密钥交换时才需要(例如使用 DHEECDHE 时)。如果使用 RSA 套件(如本例),服务器会跳过这一步。 对于 DHE/ECDHE 套件,服务器会在此消息中发送其临时密钥交换参数(如迪菲-赫尔曼参数),并用证书对应的私钥签名,以供客户端验证。

步骤 5: Server Hello Done

服务器发送一个 Server Hello Done 消息,表示它已经将所有初始协商信息发送完毕。

img

第三阶段:客户端认证与密钥交换(Client Key Exchange)
步骤 6: Client Certificate(可选)

注意:此步骤仅在服务器要求对客户端进行认证(例如银行系统、API网关等场景)时才发生。绝大多数 HTTPS 网站访问不需要这一步。 如果需要,服务器会在之前发送 Certificate Request 消息(本例未列出),然后客户端在此步骤发送自己的证书。

步骤 7: Client Key Exchange Message

这是客户端密钥交换的核心步骤。客户端会生成一个 Pre-Master Secret(预主密钥),这是一个 48 字节的随机数。

  • 对于 RSA 套件:客户端会用步骤 3 中收到的服务器证书里的公钥加密这个 Pre-Master Secret,然后将加密后的结果发送给服务器。
  • 对于 DHE/ECDHE 套件:客户端会基于服务器的参数,计算并发送自己的临时密钥交换参数。

至此,只有拥有对应私钥的服务器才能解密出 Pre-Master Secret

img

步骤 8: Certificate Verify(可选)

如果客户端发送了证书(步骤6),它还需要用其私钥对之前所有握手消息的哈希值进行签名,并发送给服务器。服务器用客户端证书中的公钥验证这个签名,以证明客户端确实拥有该证书对应的私钥,完成了客户端认证。

第四阶段:完成握手(Switch to Encryption)

此时,客户端和服务器已经拥有了生成会话密钥的所有材料:

  • Client Random
  • Server Random
  • Pre-Master Secret

双方使用相同的伪随机函数(PRF),根据这三个参数独立计算出相同的:

  1. Master Secret(主密钥)
  2. 最终用于加密和完整性验证的会话密钥块(Key Block),其中包含:

  3. 客户端到服务器的对称加密密钥

  4. 服务器到客户端的对称加密密钥

  5. 客户端到服务器的MAC验证密钥

  6. 服务器到客户端的MAC验证密钥

步骤 9: Change Cipher Spec

客户端发送一个 Change Cipher Spec 消息。这是一个简单的信号,通知服务器:“从现在开始,我发送的所有消息都将使用我们刚刚协商好的加密算法和密钥进行加密。”

紧接着,客户端发送 Finished 消息。这条消息本身已经是加密状态,它包含之前所有握手消息的哈希值的MAC(消息认证码)。服务器收到后,会解密并验证这个MAC。如果验证成功,说明:

  1. 客户端计算出的密钥与服务器的一致。
  2. 握手过程没有被篡改。

img

步骤 10: Change Cipher Spec & Finished

服务器同样发送 Change Cipher Spec 消息,通知客户端:“我也准备好了,后续消息将加密。”

紧接着,服务器发送加密后的 Finished 消息。客户端同样进行解密和验证。

img

握手完成

至此,TLS 1.2 握手全部完成。一个安全的加密通道已经建立。之后所有的应用数据(HTTP请求/响应等)都将使用协商出的对称会话密钥进行加密和完整性保护。

5.3.4 TLS和TCP/UDP是什么关系?

TLS 运行在 TCP 之上,为基于 TCP 的应用提供安全;

而 TLS 本身不能直接运行在 UDP 之上,但有其对应的安全版本 DTLS;DTLS应用没有TLS广泛,所以本讲课程不再讲述DTLS,大家有兴趣可以自行上网学习了解;

我们再来看一下这张OSI七层模型 和 TCP/IP五层模型表格:

OSI七层模型 TCP/IP五层模型 该层的协议举例 功能描述
应用层 应用层 HTTP, FTP, SMTP, IMAP 最终为用户提供应用服务的协议
表示层 SSL / TLS 加密、解密、压缩、解压缩
会话层 建立、管理、终止会话
传输层 传输层 TCP, UDP 提供端到端的可靠或不可靠传输
网络层 网络层 IP 寻址和路由
数据链路层 数据链路层 Ethernet, WiFi,4G 在局域网内传输数据帧
物理层 物理层 传输原始比特流

从这张表可以清晰地看到:

  • TCP/UDP 属于传输层。它们的主要职责是建立主机到主机的连接,解决数据如何可靠(TCP)或高效(UDP)地送达的问题。
  • TLS 大致对应于表示层。它不关心数据如何送达,它的职责是解决数据在送达后内容是否安全的问题,即加密、身份认证和完整性验证。

一个非常恰当的比喻:

  • TCP 像是一个可靠的邮政服务。它确保你的信件(数据包)不丢失、不损坏、按顺序投递。
  • TLS 则像是给这封信件加上了一个防弹、防窥视、防篡改的保险箱。邮政服务(TCP)只负责运送保险箱,并不关心也不知晓保险箱里装的是什么。只有拥有密钥的收件人才能打开保险箱读取里面的原始信件。

因此,TLS 强烈依赖于 TCP 的可靠性。TLS 握手本身包含多条消息交换,需要严格按顺序到达,任何丢包或乱序都会导致握手失败,而这正是 TCP 所擅长的。

5.4 socket概念

在了解了TCP/UDP/TLS/证书知识之后,大家可能觉得这些内容太复杂了,如果在编程过程中,直接操作这么复杂的接口,简直是一个灾难!

这时socket就出现了,socket的中文名称是套接字;

socket是TCP/IP提供的网络应用开发的编程接口,包含地址、端口、传输层/安全层协议 三部分;

socket 是网络应用程序与网络协议栈(TCP/IP栈)之间的桥梁,用户可以直接使用socket api,而socket内部设计会根据用户的api配置选择对应的传输层和安全层协议,可以大大简化网络编程工作量。

LuatOS上也支持socket编程接口,提供了socket核心库和libnet扩展库两个库文件;

接下来我们重点看下这两个库的使用方法;

第六部分:LuatOS上的socket核心库(待细化)

6.1 常量详解

核心库常量,顾名思义是由合宙 LuatOS 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;

6.1.1 网络适配器编号:

注意:

1. 网络适配器,它的一个更广为人知的名字是——网卡,后续我们会使用网卡和网络适配器这两种名称,大家只要知道这两种名称表示同一个概念就行了;

2. LuatOS上的网络适配器和电脑上的网络适配器的作用是完全一样的,我们先来看一张电脑上的网络适配器图片

img在上面这张图片中,有WiFi网卡、标准的物理以太网卡、虚拟的USB RNDIS以太网卡三种网卡;

3. 从下面这张TCP/IP五层模型图中,我们再进一步理解下网卡这个概念

TCP/IP五层模型 协议实现
应用层 HTTP,MQTT,FTP,WebSocket,DNS,NTP,SMTP等
传输层 TCP,UDP等
网络层 IP等
数据链路层 以太网,WiFi,4G 等网络各自的数据链路层协议以太网的IEEE 802.3系列WiFi网络的IEEE 802.11系列4G网络的PDCP、RLC、MAC等协议
物理层 光纤,双绞线,无线电波等传输介质

网卡主要实现的是 物理层 和 数据链路层 的功能;

物理层的主要功能包括:

  • 信号转换:将数字比特流(0和1)和 能够在物理传输介质(网线、光纤、无线电波)上传输的物理信号(如电信号、光信号、无线电波) 之间转换;

  • 信号发送与接收:通过接口(RJ-45水晶头、天线)向传输介质上发送和接收这些物理信号;

数据链路层的主要功能包括

  • MAC地址控制:使用唯一的MAC地址或者RNTI标识(4G网络中使用)来标识网卡,并处理帧的寻址问题。网卡会检查所有流过网络的帧,只接收目标MAC地址与自己地址匹配或为广播地址的帧,丢弃其他所有帧;

  • 数据帧的封装与解封装:

    发送时,将从上层(网络层)接收到的数据包,加上帧头(包含目标MAC地址、源MAC地址等信息)和帧尾封装成帧;

    接收时,检查接收到的帧,去掉帧头和帧尾,将里面的数据包提取出来交给上层的网络层处理;

4. 可以这样理解,从本质上说,网卡建立了一条支持发送和接收的双向通信的数据链路网络承载,网络承载类型可以是以太网,WiFi网络,4G网络等;

对于应用层,传输层,网络层来说,不需要关心下面的网络承载类型,一旦网络承载建立之后,可以使用DHCP应用层协议动态获取ip地址或者直接配置静态ip地址,ip地址成功获取之后,整个网络环境就算准备就绪,就可以使用应用层协议的接口或者使用socket接口开发具体的网络应用了;

5. LuatOS上支持4G,WiFi,以太网,自定义虚拟等多种类型的网卡;目前Air6101/Air8101系列产品默认使用的是WiFi网卡,默认编号为 socket.LWIP_STA;其他Air8000/Air780系列等产品默认使用的是4G网卡,默认编号为socket.LWIP_GP;

6. LuatOS上只需要直接使用默认的网卡,或者根据自己的需求调用API配置使用的某一种或者多种网卡即可,至于数据链路网络承载的建立,ip地址的分配,完全由核心库或者扩展库自动实现,使用起来非常简单;

7. 下面所列举的网卡编号常量,仅仅是一个编号,关于这个编号的理解,分为以下两种情况:

  • 第一种是,合宙AirXXXX设备(Air8000/Air780/Air6101系列)内自带的网卡,这种网卡的编号是固定的,不允许配置修改;例如socket.LWIP_GP的编号为1,从LWIP_GP的字面意思来看,这个是蜂窝数据网络网卡,所以使用蜂窝数据网络(例如4G网络)上网时,LuatOS内核固件中默认就使用了这个编号,而且LuatOS内核固件没有开放接口允许把蜂窝数据网络的网卡配置为其他编号;这种类型的网卡编号有socket.LWIP_GP,socket.LWIP_STA,socket.LWIP_AP;

  • 第二种是,合宙AirXXXX设备(Air8000/Air780/Air6101系列)需要外挂的网卡,这种网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH,也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从socket.LWIP_ETH、socket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;

socket.LWIP_GP
常量含义:4G网卡
         类似于我们平常手机使用运营商的手机卡上网时使用的网卡;
         LWIP是指:传输层和网络层使用的是LuatOS内核固件中的LwIP协议栈
         GP是GPRS的缩写GPRS是2G网络时代的分组数据网络,此处用来代指移动蜂窝数据网络,例如4G网络
数据类型:number
取值范围:1
示例代码:socket.localIP(socket.LWIP_GP)
         表示:基于WiFi设备模式网卡上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_STA
常量含义:WiFi设备模式网卡
         类似于我们平常手机或者电脑WiFi上网时使用的网卡
         LWIP是指:传输层和网络层使用的是LuatOS内核固件中的LwIP协议栈
         STA是STATION的缩写,表示WiFi设备模式,需要连接WiFi热点才能上网
数据类型:number
取值范围:2
示例代码:socket.localIP(socket.LWIP_STA),基于WiFi设备模式网卡上网时,获取本地的ip地址
socket.LWIP_AP
常量含义:WiFi热点模式网卡
         类似于我们平常使用的WiFi路由器或者手机开WiFi热点时使用的网卡
         LWIP是指:传输层和网络层使用的是LuatOS内核固件中的LwIP协议栈
         AP是Access Ponit的缩写,意思是WiFi热点,供其他WiFi设备接入上网
数据类型:number
取值范围:3
示例代码:socket.localIP(socket.LWIP_AP)
         表示:开启WiFi热点模式时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_ETH
常量含义:使用LwIP协议栈的以太网卡
         例如通过SPI外挂CH390实现以太网上网时的网卡
         例如通过RMII接口外挂PHY芯片实现以太网上网时的网卡
         LWIP是指:传输层和网络层使用的是LuatOS内核固件中的LwIP协议栈
         ETH是Ethernet的缩写,意思是以太网;
数据类型:number
取值范围:4
示例代码:socket.localIP(socket.LWIP_ETH)
         表示:基于LwIP协议栈的以太网卡上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.ETH0
常量含义:使用硬件协议栈的以太网卡;
         socket.LWIP_ETH相比,区别在于传输层和网络层的协议由谁来实现;
         如果由LuatOS内核固件使用LwIP实现,就是socket.LWIP_ETH
         如果是网卡内部实现,不需要LuatOS内核固件实现,就是socket.ETH0
         也就是说,socket.ETH0这种网卡,传输层,网络层,数据链路层,物理层都是由以太网卡去实现;
         例如通过SPI外挂W5500实现以太网上网时的网卡(目前已经不推荐使用这种方式了);
         例如LuatOS的PC模拟器上网使用的电脑网卡
         ETH是Ethernet的缩写,意思是以太网,ETH0表示编号为0的硬件协议栈以太网卡
数据类型:number
取值范围:16
示例代码:socket.localIP(socket.ETH0)
         表示:基于硬件协议栈的以太网卡上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.USB
常量含义:USB接口的以太网卡

         USB接口的以太网卡是一种通过USB线虚拟出有线网卡功能的技术
         以电脑为例,它允许电脑通过一根USB数据线,将手机或者合宙AirXXX硬件板连接到电脑
         并让电脑像使用普通有线网卡一样通过该USB线,使用手机或者合宙AirXXX硬件板来上网
         手机或者合宙AirXXX硬件板就可以看做电脑的USB接口的以太网卡

         常见的USB以太网卡又可以分为 USB RNDIS以太网卡  USB ECM以太网卡两种


         主机(例如电脑)发送数据时,主机的以太网数据帧,经过RNDIS协议或者ECM协议封包
         然后经过USB传输给RNDIS设备 或者 ECM设备,设备经过RNDIS协议或者ECM协议解包
         提取出原始的以太网数据帧,然后利用自己的网络将数据发送出去;         
         主机接收数据的过程和发送数据的过程相反;
数据类型:number
取值范围:17
示例代码:socket.localIP(socket.USB)
         表示:基于USB接口的以太网卡上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER0
常量含义:使用LWIP协议栈的自定义网卡0

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:7
示例代码:socket.localIP(socket.LWIP_USER0)
         表示:基于自定义网卡0上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER1
常量含义:使用LWIP协议栈的自定义网卡1

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:8
示例代码:socket.localIP(socket.LWIP_USER1)
         表示:基于自定义网卡1上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER2
常量含义:使用LWIP协议栈的自定义网卡2

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:9
示例代码:socket.localIP(socket.LWIP_USER2)
         表示:基于自定义网卡2上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER3
常量含义:使用LWIP协议栈的自定义网卡3

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:10
示例代码:socket.localIP(socket.LWIP_USER3)
         表示:基于自定义网卡3上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER4
常量含义:使用LWIP协议栈的自定义网卡4

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:11
示例代码:socket.localIP(socket.LWIP_USER4)
         表示:基于自定义网卡4上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER5
常量含义:使用LWIP协议栈的自定义网卡5

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:12
示例代码:socket.localIP(socket.LWIP_USER5)
         表示:基于自定义网卡5上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER6
常量含义:使用LWIP协议栈的自定义网卡6

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:13
示例代码:socket.localIP(socket.LWIP_USER6)
         表示:基于自定义网卡6上网时,获取本地的ip地址、网络掩码和网关ip地址
socket.LWIP_USER7
常量含义:使用LWIP协议栈的自定义网卡7

         这种网卡编号的使用场景是:
         当合宙AirXXXX设备Air8000/Air780/Air6101系列)需要外挂网卡来上网时:
         这种外挂网卡的编号,可以使用我们推荐的编号值,也可以使用自定义类型的编号值;
         例如通过SPI外挂CH390以太网卡时,这种网卡的编号可以使用推荐的socket.LWIP_ETH
         也可以使用自定义的socket.LWIP_USER0/1/2/3/4/5/6/7中的任何一个;
         如果通过SPI外挂了5块CH390以太网卡,这5块以太网卡的编号可以从
         socket.LWIP_ETHsocket.LWIP_USER0/1/2/3/4/5/6/7,这9个网卡编号中选择任意5个;
数据类型:number
取值范围:14
示例代码:socket.localIP(socket.LWIP_USER7)
         表示:基于自定义网卡7上网时,获取本地的ip地址、网络掩码和网关ip地址

6.1.2 网络事件:

"IP_RAEDY"

常量含义

表示某一个网络适配器编号对应的网卡准备就绪,string类型; 此处的“准备就绪”是指:AirXXX硬件(例如Air780EPM)的某一个网卡在狭义或者广义的内网内,和路由设备之间双向通信的数据链路已经建立,并且已经获取到了本地的ip地址; 4G网络环境下,是指AirXXX硬件和4G运营商的分组数据网关之间的数据链路已经建立,并且分组数据网关已经给AirXXX硬件分配了运营商内部的私有ip地址;

如下图红框所示(手机相当于AirXXX硬件,例如Air780EPM):

img

一般来说,只要使用的sim卡没有欠费,AirXXX硬件开机之后,3秒左右就可以产生4G网卡的"IP_READY"消息;

​ 以太网网络环境下,是指AirXXX硬件(例如Air8000)通过网线和内网内的路由器之间的数据链路已经建立,并且路由器已经给AirXXX硬件分配了内网内部的内网ip地址或者AirXXX硬件主动设置了内网静态ip地址;

如下图红框所示(电脑相当于AirXXX硬件,例如Air8000):

img

一般来说,只要通过网线和路由器之间的连接正常,2秒左右就可以产生以太网网卡的"IP_READY"消息; WiFi网络环境下,是指AirXXX硬件(例如Air6101)通过WiFi连接上了内网的无线路由器,并且路由器已经给AirXXX硬件分配了内网的ip地址或者AirXXX硬件主动设置了内网静态ip地址;

如下图红框所示(手机相当于AirXXX硬件,例如Air6101);

img

一般来说,只要输入正确的密码去连接WiFi路由器,2秒左右就可以产生WiFi网卡的"IP_READY"消息;

img

发布并且处理"IP_RAEDY"消息的完整周期如下:

1、初始化配置某一种网卡(4G网卡不需要主动配置,WiFi和以太网需要调用exnetif的接口主动配置);

2、配置成功之后,LuatOS内核固件会自动执行数据链路的建立以及ip地址的获取动作;

3、准备就绪后,LuatOS内核固件中会执行sys.publish("IP_RAEDY", ip, adapter)发布全局消息; "IP_RAEDY"为消息名称,string类型; ip为本地ip地址,string类型; adapter为网卡编号,number类型,和本文6.1.1章节所描述的网络适配器编号常量对应;

4、LuatOS应用脚本中通过sys.subscribe或者sys.waitUntil订阅处理"IP_RAEDY"这个消息; 如果使用sys.subscribe,要在第1步之前就要订阅,否则会有遗漏"IP_RAEDY"消息的风险; 如果使用sys.waitUntil,在sys.waitUntil之前使用socket.adapter接口主动查询下网卡状态, 如果查询结果是已经准备就绪,则不再需要sys.waitUntil,否则再使用sys.waitUntil;

对某一种网卡来说,以下两种情况会产生"IP_READY"消息:

1、初始化配置网卡之后,网卡准备就绪后,会产生"IP_READY"消息;

2、如果网卡发生掉线产生了"IP_LOSE"消息,再次准备就绪后,会产生"IP_READY"消息;

再来看另外一个问题,如果某个网卡产生了"IP_READY"消息,是不是意味着使用这个网卡,肯定可以正常访问外网了?这个是不一定的,以下图中的WiFi网络为例来说明一下:

img

如果无线路由器已经正常上电,处于运行状态,但是路由器并没有连接外网(例如和ISP接入网的CGANT设备之间的光纤没有连接);

这种状态下,红框内的数据链路可以正常建立,并且无线路由器也可以给手机(类似于Air8000)分配内网ip地址,Air8000就能产生"IP_READY"消息,但是Air8000并不能访问外网,因为无线路由器无法访问外网;

同理,4G网络环境下,以太网环境下的"IP_READY"消息都是表示一个广义或者狭义的内网网络环境准备就绪,能否访问外网,取决于内网到外网以及整个外网的网络环境是否正常;

也就是说:

没有"IP_READY"是不能访问外网的;

有"IP_READY",大概率是可以访问外网的,但是也存在一些极低极低的概率不能访问外网;

即使如此,用户在编程时,还是要以"IP_READY"来作为判断可以连接外网的第一个条件,然后在应用层,使用socket,http,mqtt等网络应用接口去实现自己的业务逻辑,如果socket,http,mqtt连接不成功,就有可能是外网不通,遇到这种情况,在socket,http,mqtt执行重连就行了,用户不用关系链路层的异常的原因,只关于自己的应用层异常应该怎么处理就行了;

应用层的网络业务的核心代码逻辑如下所示:

while true do
    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
    while not socket.adapter(socket.dft()) do
        log.warn("tcp_client_main_task_func", "wait IP_READY", socket.dft())
        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
        -- 或者等待1秒超时退出阻塞等待状态;
        -- 注意:此处的1000毫秒超时不要修改的更长;
        -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
        -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
        sys.waitUntil("IP_READY", 1000)
    end

    -- 检测到了IP_READY消息
    log.info("tcp_client_main_task_func", "recv IP_READY", socket.dft())

    -- 创建socket client对象
    socket_client = socket.create(nil, TASK_NAME)
    -- 如果创建socket client对象失败
    if not socket_client then
        log.error("tcp_client_main_task_func", "socket.create error")
        goto EXCEPTION_PROC
    end

    -- 配置socket client对象为tcp client
    result = socket.config(socket_client)
    -- 如果配置失败
    if not result then
        log.error("tcp_client_main_task_func", "socket.config error")
        goto EXCEPTION_PROC
    end

    -- 连接server
    result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
    -- 如果连接server失败
    if not result then
        log.error("tcp_client_main_task_func", "libnet.connect error")
        goto EXCEPTION_PROC
    end

    log.info("tcp_client_main_task_func", "libnet.connect success")

    -- 此处省略了很多业务逻辑代码

    -- 出现异常    
    ::EXCEPTION_PROC::        
    -- 如果存在socket client对象
    if socket_client then
        -- 关闭socket client连接
        libnet.close(TASK_NAME, 5000, socket_client)

        -- 释放socket client对象
        socket.release(socket_client)
        socket_client = nil
    end

    -- 5秒后跳转到循环体开始位置,自动发起重连
    sys.wait(5000)
end

使用示例

使用sys.subscribe处理的方式:

 local function ip_ready_cbfunc(ip, adapter)
     log.info("ip_ready_cbfunc", "IP_READY", ip, adapter)
 end

 sys.subscribe("IP_READY", ip_ready_cbfunc)

 exnetif.set_priority_order(
     {
         {
             ETHERNET = 
             { -- 以太网配置
                 pwrpin = 140, -- 供电使能引脚(number)
                 tp = netdrv.CH390, -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
                 opts = {spi = 1, cs = 12}, -- 外挂方式,需要额外的参数(选填参数),仅spi方式外挂以太网时需要填写。
         }
     })

​ 使用sys.waitUntil处理的方式:

exnetif.set_priority_order(
     {
         {
             ETHERNET = 
             { -- 以太网配置
                 pwrpin = 140, -- 供电使能引脚(number)
                 tp = netdrv.CH390, -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
                 opts = {spi = 1, cs = 12}, -- 外挂方式,需要额外的参数(选填参数),仅spi方式外挂以太网时需要填写。
         }
     })

local function tcp_client_main_task_func() 
    while not socket.adapter(socket.dft()) do
        sys.waitUntil("IP_READY", 1000)
    end

    --此处省略了其他业务逻辑代码
end

sys.taskInitEx(tcp_client_main_task_func, "tcp_client")
"IP_LOSE"

常量含义

表示某一个网络适配器编号对应的网卡从正常状态下发生掉线,string类型;

发生掉线的原因包含但不仅仅包含以下几种:

1、4G网络环境下,sim卡不识别、sim卡欠费、没有4G网络信号等;

2、WiFi网络环境下,WiFi路由器关闭、WiFi路由器做了限制、离开WiFi路由器有效信号范围等;

3、以太网网络环境下,以太网路由器关闭、以太网路由器做了限制、网线断开等;

发生掉线后,LuatOS内核固件中会执行sys.publish("IP_LOSE", adapter)发布全局消息; "IP_LOSE"为消息名称,string类型; adapter为网卡编号,number类型,和本文6.1.1章节所描述的网络适配器编号常量对应;

使用示例

一般情况下,推荐使用sys.subscribe处理的方式来监控"IP_LOSE"消息,监控到这个消息之后,可以根据自己的项目需求走一些具体项目业务逻辑的显示,例如通知已断网,但是不需要手动写代码再去控制网卡重试连网,因为在内核固件中,会自动重试连网动作;

 local function ip_lose_cbfunc(adapter)
     log.info("ip_lose_cbfunc", "IP_LOSE", adapter)
 end

 sys.subscribe("IP_LOSE", ip_lose_cbfunc)

关于这个常量,了解即可,LuatOS项目应用脚本程序中不推荐使用

常量含义:表示某一个网络适配器编号对应的网卡是否准备就绪;
         和网络事件常量"IP_READY"消息所表示的含义有一定重复;

         但是和"IP_READY"的具体含义,发布时机和使用场景不一样,我们看下面这张图片,
         回顾一下系统全局消息和系统定向消息两种类型的消息;

         "IP_READY"消息是内核固件发布的系统全局消息,表示某一个网卡准备就绪;
         任何一个网卡编号对应的网卡准备就绪后,都会发布这个消息,例如4G网卡WiFi网卡,以太网网卡等;
         任何应用都可以订阅接收处理这个消息,例如sockethttpmqttftpwebsocket等等

         socket.LINK消息是内核固件发布的系统定向消息,表示某一个网卡是否准备就绪;
         只有成功调用socket.linkup接口之后,并且当前时刻,socket对象创建时绑定的网卡尚未准备就绪时
         内核固件中自动监测网卡是否准备就绪的结果,有结果时,才会发送这个消息;

         例如,在一个高级task的任务处理函数中
         使用socket.create(adapter, task_name)创建了一个socket client对象
         创建这个对象时,使用的第一个参数adapter就是网卡编号,可以是一个具体的网卡编号,例如4G网卡
         也可以是nil(表示当前这个时刻系统中默认使用的网卡), 
         只有成功调用socket.linkup接口之后,并且当前时刻,socket对象创建时绑定的网卡尚未准备就绪时        
         内核固件中自动监测网卡是否准备就绪的结果,有结果时,才会发送这个消息,
         携带的第一个参数如果为0,则表示准备就绪,其余值表示为未准备就绪;

         而且发送的socket.LINK消息只能由特定的高级task接收处理
         例如,如果在一个高级task的任务处理函数中
         使用socket.create(adapter, task_name)创建了一个socket client对象
         创建这个对象时,使用的第二个参数task_name就是这个高级task的任务名称
         发送的socket.LINK消息只能被这个高级task的任务处理函数使用sys.waitMsg接收处理        
数据类型:number
取值范围:在不同种类的的内核固件中或者同一种内核固件的不同版本中,这个常量的具体数值可能会发生变化;
         在编程时,如果用到这个常量,直接使用socket.LINK,不要使用它具体的number数值
注意事项:用户在自己的应用脚本中用不到此消息,因为此消息的处理逻辑已经被封装到libnet扩展库中
         用户在自己的应用脚本中使用libnet扩展库提供的接口libnet.waitLink,使用起来会更简洁;
         此处对这个消息进行说明,是为了方便有些用户深入理解学习libnet扩展库源码

         另外,我们刚才说过,socket.LINK消息和"IP_READY"消息所表示的含义相同,
         但是"IP_READY"适用于所有网络应用,例如sockethttpmqttftpwebsocket等等
         socket.LINK消息,只能用于socket网络应用
         所以我们后续所有网络应用的demo,都会统一使用"IP_READY"消息来判断网卡是否准备就绪,
         这样所有网络应用开发的框架可以最大化的保持一致;
         socket.LINK消息,或者libnet扩展库封装的接口libnet.waitLink,都不会使用;
示例代码:如下示例,是在一个高级task的任务处理函数中运行的代码片段
         -- 创建socket client对象
         socket_client = socket.create(nil, "tcp_client")

         -- 判断socket_client绑定的网卡是否准备就绪
         local succ, result = socket.linkup(socket_client)

         -- 调用接口socket.linkup失败
         if not succ then
             return false
         end

         -- 调用接口socket.linkup接口成功,此时通过第二个返回值result来判断网卡是否准备就绪
         -- result为true,表示网卡准备就绪
         if result then
             return true
         end

         -- result为false,表示网卡还没有准备就绪,此时需要通过异步消息socket.LINK来监测结果
         if not result then
             result = sys.waitMsg("tcp_client", socket.LINK, timeout)
         end

         -- 收到了socket.LINK消息,并且携带的第一个参数为0表示成功
         if type(result) == 'table' and result[2] == 0 then
             return true
         -- 超时没有收到socket.LINK消息,或者收到了socket.LINK消息但是携带的第一个参数非0表示失败
         else
             return false
         end

img

socket.ON_LINE(了解即可)

关于这个常量,了解即可,LuatOS项目应用脚本程序中不推荐使用

常量含义:做client使用时,表示某一个socket client是否成功连接server
         server使用时,表示某一个socket server是否成功连接client


         socket.ON_LINE消息是内核固件发布的系统定向消息

         client使用时
         只有成功调用socket.connect接口之后,并且当前时刻,socket client还没有成功连接server时
         内核固件中自动监测连接结果,有结果时,才会发送这个消息;

         例如,在一个高级task的任务处理函数中
         使用socket.create(adapter, task_name)创建了一个socket client对象
         只有成功调用socket.connect接口之后,并且当前时刻,socket client还没有成功连接server时        
         内核固件中自动监测是否连接成功的结果,有结果时,才会发送这个消息,
         携带的第一个参数如果为0,则表示连接成功,其余值表示连接失败;

         而且发送的socket.ON_LINE消息只能由task_name这个高级task处理
         在这个task的任务处理函数使用sys.waitMsg接收处理 

         server使用时
         只有成功调用socket.listen接口之后,并且当前时刻,socket server还没有成功连接client时
         内核固件中自动监测连接结果,有结果时,才会发送这个消息;

         例如,在一个高级task的任务处理函数中
         使用socket.create(adapter, task_name)创建了一个socket server对象
         只有成功调用socket.listen接口之后,并且当前时刻,socket server还没有成功连接client时        
         内核固件中自动监测是否连接成功的结果,有结果时,才会发送这个消息,
         携带的第一个参数如果为0,则表示连接成功,其余值表示连接失败;

         而且发送的socket.ON_LINE消息只能由task_name这个高级task处理
         在这个task的任务处理函数使用sys.waitMsg接收处理

数据类型:number
取值范围:在不同种类的的内核固件中或者同一种内核固件的不同版本中,这个常量的具体数值可能会发生变化;
         在编程时,如果用到这个常量,直接使用socket.ON_LINE,不要使用它具体的number数值
注意事项:用户在自己的应用脚本中用不到此消息,因为此消息的处理逻辑已经被封装到libnet扩展库中
         用户在自己的应用脚本中使用libnet扩展库提供的接口libnet.connect或者libnet.listen,使用起来会更简洁;
         此处对这个消息进行说明,是为了方便有些用户深入理解学习libnet扩展库源码
示例代码:如下示例,是基于socket client应用场景,在一个高级task的任务处理函数中运行的代码片段
         -- 创建socket client对象
         socket_client = socket.create(nil, "tcp_client")

         -- 判断socket_client绑定的网卡是否准备就绪
         local succ, result = socket.connect(socket_client, "112.125.89.8", 46946)

         -- 调用接口socket.connect失败
         if not succ then
             return false
         end

         -- 调用接口socket.connect接口成功,此时通过第二个返回值result来判断是否连接成功
         -- result为true,表示连接成功
         if result then
             return true
         end

         -- result为false,表示连接中,还没有连接成功,此时需要通过异步消息socket.ON_LINE来监测结果
         if not result then
             result = sys.waitMsg("tcp_client", socket.ON_LINE, timeout)
         end

         -- 收到了socket.ON_LINE消息,并且携带的第一个参数为0表示成功
         if type(result) == 'table' and result[2] == 0 then
             return true
         -- 超时没有收到socket.ON_LINE消息,或者收到了socket.ON_LINE消息但是携带的第一个参数非0表示失败
         else
             return false
         end
socket.EVENT
常量含义:表示socket对象和对端连接成功之后,出现的通用事件消息,以下几种业务逻辑会产生此消息:
         1socket对象和对端之间的连接出现异常(例如对端主动断开,网络环境出现异常等),
            此时在内核固件中会发送消息socket.EVENT
         2socket对象接收到对端发送过来的数据,此时在内核固件中会发送消息socket.EVENT
         3、在socket应用脚本程序中,根据自己的项目需求,发送消息socket.EVENT
            例如socket client需要发送数据到server,可以通过消息socket.EVENT通知socket client主task处理

         socket.EVENT消息可以由内核固件发送,也可以在项目应用脚本中使用sys.sendMsg接口发送

         这种消息的处理,是整个socket应用开发框架的核心内容,我们直接参考下面的示例代码来看一下如何使用;

数据类型:number
取值范围:在不同种类的的内核固件中或者同一种内核固件的不同版本中,这个常量的具体数值可能会发生变化;
         在编程时,如果用到这个常量,直接使用socket.EVENT,不要使用它具体的number数值
注意事项:暂无;
示例代码:如下示例,是基于socket client应用场景,在一个高级task的任务处理函数中运行的代码片段
         -- 数据收发以及网络连接异常事件总处理逻辑
         while true do
             -- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
             -- 如果处理失败,则退出循环
             if not tcp_client_receiver.proc(socket_client) then
                 log.error("tcp_client_main_task_func", "tcp_client_receiver.proc error")
                 break
             end

             -- 数据发送处理
             -- 如果处理失败,则退出循环
             if not tcp_client_sender.proc(TASK_NAME, socket_client) then
                 log.error("tcp_client_main_task_func", "tcp_client_sender.proc error")
                 break
             end

             -- 阻塞等待socket.EVENT事件或者15秒钟超时
             -- 以下三种业务逻辑会发布事件:
             -- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
             -- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
             -- 3、socket client需要发送数据到server, 在tcp_client_sender.lua中会发布事件socket.EVENT
             result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
             log.info("tcp_client_main_task_func", "libnet.wait", result, para1, para2)

             -- 如果连接异常,则退出循环
             if not result then
                 log.warn("tcp_client_main_task_func", "connection exception")
                 break
             end
         end
socket.TX_OK(了解即可)

关于这个常量,了解即可,LuatOS项目应用脚本程序中不推荐使用

常量含义:表示socket对象的应用数据发送结果消息

         socket.TX_OK消息是内核固件发布的系统定向消息

         当应用脚本中调用socket.tx接口发送数据时
         如果接口调用成功,并且内核固件中的发送缓冲区没有满,数据就会被丢到协议栈中去发送,
         然后内核固件中自动监测发送结果,有结果时,才会产生这个消息;

         例如,在一个高级task的任务处理函数中
         使用socket.create(adapter, task_name)创建了一个socket client对象
         调用libnet.connect接口成功连接对端之后,成功调用socket.tx接口向对端发送数据        
         内核固件中自动监测是否发送成功的结果,有结果时,才会产生这个消息,
         携带的第一个参数如果为0,则表示发送成功,其余值表示发送失败;

         而且发送的socket.TX_OK消息只能由task_name这个高级task处理
         在这个task的任务处理函数使用sys.waitMsg接收处理 
数据类型:number
取值范围:在不同种类的的内核固件中或者同一种内核固件的不同版本中,这个常量的具体数值可能会发生变化;
         在编程时,如果用到这个常量,直接使用socket.TX_OK,不要使用它具体的number数值
注意事项:暂无;
示例代码:如下示例,是基于socket client应用场景,在一个高级task的任务处理函数中运行的代码片段
         -- 创建socket client对象
         socket_client = socket.create(nil, TASK_NAME)
         -- 如果创建socket client对象失败
         if not socket_client then
             log.error("tcp_client_main_task_func", "socket.create error")
             goto EXCEPTION_PROC
         end

         -- 配置socket client对象为tcp client
         result = socket.config(socket_client)
         -- 如果配置失败
         if not result then
             log.error("tcp_client_main_task_func", "socket.config error")
             goto EXCEPTION_PROC
         end

         -- 连接server
         result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
         -- 如果连接server失败
         if not result then
             log.error("tcp_client_main_task_func", "libnet.connect error")
             goto EXCEPTION_PROC
         end

         log.info("tcp_client_main_task_func", "libnet.connect success")

         -- 给server发送数据"123456"
         local succ, is_full, result = socket.tx(socket_client, "123456")

         -- succ为false,表示socket.tx接口调用失败
         if not succ then
             log.error("tcp_client_main_task_func", "socket.tx error")
             goto EXCEPTION_PROC
         end

         -- succ为true,表示socket.tx接口调用成功
         -- is_full为true,表示内核固件中的发送缓冲区满了,本次数据发送没有执行
         -- 需要等待
         if is_full then
             log.warn("tcp_client_main_task_func", "socket.tx send buff is full")
             goto EXCEPTION_PROC
         end

         -- succ为true,表示socket.tx接口调用成功
         -- is_full为false,表示内核固件中的发送缓冲区正常,本次数据已经交给协议栈去发送
         -- result为false,表示正在发送中,此时需要通过异步消息socket.TX_OK来监测发送结果
         if not result then
             result = sys.waitMsg(TASK_NAME, socket.TX_OK, timeout)
         -- succ为true,表示socket.tx接口调用成功
         -- is_full为false,表示内核固件中的发送缓冲区正常,本次数据已经交给协议栈去发送
         -- result为true,表示数据发送成功
         else
             log.info("tcp_client_main_task_func", "send success")
             goto EXCEPTION_PROC
         end

         -- 收到了socket.TX_OK消息,并且携带的第一个参数为0表示成功
         if type(result) == 'table' and result[2] == 0 then
             log.info("tcp_client_main_task_func", "send success")
             goto EXCEPTION_PROC
         -- 超时没有收到socket.TX_OK消息,或者收到了socket.TX_OK消息但是携带的第一个参数非0表示失败
         else
             log.error("tcp_client_main_task_func", "send error")
             goto EXCEPTION_PROC
         end
socket.CLOSED(了解即可)

关于这个常量,了解即可,LuatOS项目应用脚本程序中不推荐使用

常量含义:表示socket连接已关闭的消息

         socket.CLOSED消息是内核固件发布的系统定向消息

         当应用脚本中调用socket.discon接口主动关闭socket时
         如果接口调用成功,内核固件中自动监测关闭结果,有结果时,才会产生这个消息;

         例如,在一个高级task的任务处理函数中
         使用socket.create(adapter, task_name)创建了一个socket client对象
         调用libnet.connect接口成功连接对端之后,成功调用socket.discon接口关闭socket        
         内核固件中自动监测关闭结果,有结果时,才会产生这个消息,
         一旦产生这个消息,不携带任何参数,就表示关闭成功;

         而且发送的socket.CLOSED消息只能由task_name这个高级task处理
         在这个task的任务处理函数使用sys.waitMsg接收处理 
数据类型:number
取值范围:在不同种类的的内核固件中或者同一种内核固件的不同版本中,这个常量的具体数值可能会发生变化;
         在编程时,如果用到这个常量,直接使用socket.TX_OK,不要使用它具体的number数值
注意事项:暂无;
示例代码:如下示例,是基于socket client应用场景,在一个高级task的任务处理函数中运行的代码片段
         -- 创建socket client对象
         socket_client = socket.create(nil, TASK_NAME)
         -- 如果创建socket client对象失败
         if not socket_client then
             log.error("tcp_client_main_task_func", "socket.create error")
             goto EXCEPTION_PROC
         end

         -- 配置socket client对象为tcp client
         result = socket.config(socket_client)
         -- 如果配置失败
         if not result then
             log.error("tcp_client_main_task_func", "socket.config error")
             goto EXCEPTION_PROC
         end

         -- 连接server
         result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
         -- 如果连接server失败
         if not result then
             log.error("tcp_client_main_task_func", "libnet.connect error")
             goto EXCEPTION_PROC
         end

         log.info("tcp_client_main_task_func", "libnet.connect success")

         -- 主动关闭socket 
         socket.discon(socket_client)

         -- 阻塞等待关闭结果或者超时退出
         sys.waitMsg(TASK_NAME, socket.CLOSED, timeout)

6.2 函数详解

socket核心库提供的api比较多,有一部分api是异步操作,使用起来比较复杂,已经不推荐使用,在此处就不再讲解;如果大家还需要了解这些已经不推荐的异步api的使用方法,可以访问:socket核心库API文档 自行学习;

这些不推荐的异步api,在libnet扩展库中已经被封装成了同步api,使用起来更加方便,在第七部分,我们会详细讲解这些libnet扩展库中的同步api接口;

除此之外,socket核心库还有一些同步api接口,这些api主要涉及到网卡操作,socket的创建、配置、读取、销毁等功能,本章节,我们先详细学习一下这些同步api接口;

socket.adapter(adapter_id)

功能

查看网卡的联网状态

参数

adapter_id

参数含义:表示网卡编号;
         adapter_id是number类型时,表示某一种网卡;
         adapter_id是nil类型时,表示:从第一个网卡(socket.LWIP_GPnumber值为1)开始,
         遍历全部网卡,遍历过程中如果发现IP_READY的网卡,则终止遍历;
数据类型:number或者nil
取值范围:number类型时,本文常量详解章节中的所有网络适配器编号常量;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:例如socket.LWIP_STA表示内置的WiFi设备模式网卡

返回值

local is_ready, index = socket.adapter(adapter_id)

is_ready

含义说明:被查看的某一种网卡是否IP READY
         或者遍历网卡时,中途遍历结束,或者全部遍历结束,遍历的最后一个网卡是否IP_READY;
         true表示IP_READYfalse表示还没有准备就绪
数据类型:boolean
取值范围:true或者false
注意事项:暂无;
返回示例:true或者false

index

含义说明:最后一个被查看的网卡编号;
数据类型:number
取值范围:本文常量详解章节中的所有网络适配器编号常量;
注意事项:暂无;
返回示例:例如socket.LWIP_GP表示内置的4G网卡

示例

-- 以Air8000为例;
-- 如果Air8000当前状态下,4G网卡socket.LWIP_GP和WiFi设备模式网卡socket.LWIP_STA都已经准备就绪
-- 则下面这一行代码
-- 返回值is_ready为true
-- 返回值index为socket.LWIP_STA
local is_ready, index = socket.adapter(socket.LWIP_STA)


-- 以Air8000为例;
-- 如果Air8000当前状态下,4G网卡socket.LWIP_GP和WiFi设备模式网卡socket.LWIP_STA都已经准备就绪
-- 则下面这一行代码
-- 返回值is_ready为true
-- 返回值index为socket.LWIP_GP
-- 因为socket.LWIP_GP是遍历的第一个网卡,而且这个网卡已经准备就绪,所有遍历到socket.LWIP_GP提前结束
local is_ready, index = socket.adapter()

socket.dft(adapter_id)

功能

读取或者设置当前使用的默认网卡;

默认网卡的作用如下:

创建socket,mqtt,http等网络应用对象时,可以通过参数主动指定要使用的网卡;

如果没有制定,就会使用系统中的默认网卡;

例如:

socket.create(socket.LWIP_GP, "tcp_task") 表示使用socket.LWIP_GP网卡创建一个socket对象,后续这个socket对象的所有数据通信都会基于socket.LWIP_GP网卡进行;

socket.create(nil, "tcp_task") 表示使用当前时刻的默认网卡创建一个socket对象,后续这个socket对象的所有数据通信都会基于当前时刻的默认网卡进行;

socket.create(nil, "tcp_task")等价于socket.create(socket.dft(), "tcp_task");

默认网卡的设置有以下三种方式:

1、LuatOS内核固件运行起来之后,不同的产品已经初始化设置了默认网卡;目前Air8000/Air780系列等产品默认使用的是4G网卡,编号为socket.LWIP_GP;Air6101/Air8101系列产品默认使用的是WiFi设备模式网卡,编号为 socket.LWIP_STA;

2、使用扩展库exnetif中的接口exnetif.set_priority_order配置单网卡或者多网卡时,set_priority_order接口内部会根据配置自动设置默认网卡;

3、LuatOS项目的用户应用脚本运行起来之后,在脚本中可以调用接口socket.dft(adapter_id)主动设置新的默认网卡adapter_id;

在这三种设置默认网卡的方式中:

第1种设置方式,用户无法参与设置;

第2种设置方式,用户可以参与设置,如果需要更改设置默认网卡,推荐使用这一种方式;这样设置有两个好处:

1、exnetif扩展库已经高度集成,使用这个扩展库提供的接口,无论是单网卡设置还是多网卡设置,使用起来非常方便;

2、默认网卡设置和具体的socket,mqtt,http网络应用解耦,用户只需要在初始化时配置一次,所有的网络应用中不要再指定具体的网卡,就可以使用初始化配置的优先级使用默认网卡;

我们结合实际代码看一下exnetif扩展库接口的使用方式;

第3种设置方式,用户可以参与设置,如果需要更改设置默认网卡,不推荐使用这一种方式;因为和第2种方式相比,使用起来会复杂;

参数

adapter_id

参数含义:表示网卡编号;
         adapter_id是number类型时,表示设置默认网卡为adapter_id所表示的网卡
         adapter_id是nil类型时,表示读取当前使用的默认网卡;
数据类型:number或者nil
取值范围:number类型时,本文常量详解章节中的所有网络适配器编号常量;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:例如socket.LWIP_STA表示内置的WiFi设备模式网卡

返回值

查询默认网卡时:

local adapter_id, last_reg_adapter_id = socket.dft()

adapter_id

参数含义:表示默认网卡对应的编号;
数据类型:number
取值范围:本文常量详解章节中的所有网络适配器编号常量;
注意事项:暂无;
参数示例:例如socket.LWIP_STA表示内置的WiFi设备模式网卡

last_reg_adapter_id

参数含义:表示最后一个注册的网卡对应的编号;
数据类型:number
取值范围:本文常量详解章节中的所有网络适配器编号常量;
注意事项:这个返回值,对用户开发来说,基本上没什么用,不用过多关注;
参数示例:例如socket.LWIP_AP表示内置的WiFi路由模式网卡

设置默认网卡时:

local new_adapter_id, last_reg_adapter_id = socket.dft(adapter_id)

new_adapter_id

参数含义:表示设置之后的新的默认网卡对应的编号;
数据类型:number
取值范围:本文常量详解章节中的所有网络适配器编号常量;
注意事项:暂无;
参数示例:例如socket.LWIP_STA表示内置的WiFi设备模式网卡

last_reg_adapter_id

参数含义:表示最后一个注册的网卡对应的编号;
数据类型:number
取值范围:本文常量详解章节中的所有网络适配器编号常量;
注意事项:这个返回值,对用户开发来说,基本上没什么用,不用过多关注;
参数示例:例如socket.LWIP_AP表示内置的WiFi路由模式网卡

示例

不推荐直接使用socket.dft来设置默认网卡,所有此处不再列举设置功能的示例,仅列举查询功能的示例

-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
    -- log.warn("tcp_client_main_task_func", "wait IP_READY", socket.dft())
    -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
    -- 或者等待1秒超时退出阻塞等待状态;
    -- 注意:此处的1000毫秒超时不要修改的更长;
    -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
    -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
    -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
    sys.waitUntil("IP_READY", 1000)
end

socket.localIP(adapter_id)

功能

获取某个网卡上的本地 ip地址、网络掩码和网关地址;

只有网卡准备就绪后,才能读取到这三个值;

参数

adapter_id

参数含义:表示网卡编号,用来标识某一种网卡;
数据类型:number或者nil
取值范围:本文常量详解章节中的所有网络适配器编号常量,例如:socket.LWIP_GP
是否必选:可选传入此参数;如果没有传入此参数,会使用当前时刻的默认网卡;
注意事项:暂无;
参数示例:socket.LWIP_GP

返回值

local ip, netmask, gateway = socket.localIP(adapter_id)

有三个返回值 ip, netmask, gateway

192.168.31.156 255.255.255.0 192.168.31.1 nil

ip

含义说明:本地IP地址
数据类型:string
取值范围:暂无;
注意事项:只有网卡准备就绪后,才能读取到这个值;
返回示例:例如WiFi设备模式网卡准备就绪后,读取到的本地ip为"192.168.31.156"
         例如4G网卡准备就绪后,读取到的本地ip为"10.80.235.107"

netmask

含义说明:网络掩码;
数据类型:string
取值范围:暂无;
注意事项:只有网卡准备就绪后,才能读取到这个值;
返回示例:"255.255.255.255"

gateway

含义说明:网关IP
数据类型:string
取值范围:暂无;
注意事项:只有网卡准备就绪后,才能读取到这个值;
返回示例:例如WiFi设备模式网卡准备就绪后,读取到的网关ip为"192.168.31.1"
         例如4G网卡准备就绪后,读取到的网关ip为" 0.0.0.0"

示例

local function get_4g_local_ip()
    log.info("socket.localIP(socket.LWIP_GP)", socket.localIP(socket.LWIP_GP))
end

local function get_wifi_local_ip()
    log.info("socket.localIP(socket.LWIP_STA)", socket.localIP(socket.LWIP_STA))
end

sys.timerLoopStart(get_4g_local_ip, 1000)
sys.timerLoopStart(get_wifi_local_ip, 1000)

-- 4G网卡如果还没有准备就绪,4G网卡的ip地址信息打印日志如下:
-- socket.localIP(socket.LWIP_GP)

-- 4G网卡如果准备就绪,4G网卡的ip地址信息打印日志如下:
-- socket.localIP(socket.LWIP_GP) 10.80.235.107 255.255.255.255 0.0.0.0

-- WiFi设备模式网卡如果还没有准备就绪,WiFi设备模式网卡的ip地址信息打印日志如下:
-- socket.localIP(socket.LWIP_STA) 0.0.0.0 0.0.0.0 0.0.0.0

-- WiFi设备模式网卡如果准备就绪,WiFi设备模式网卡的ip地址信息打印日志如下:
-- socket.localIP(socket.LWIP_STA) 192.168.31.156 255.255.255.0 192.168.31.1

socket.setDNS(adapter_id, dns_index, ip)

功能

设置某个网卡使用的DNS服务器IP地址;

内核固件中有4个位置可以存储DNS服务器IP地址;

对应的位置索引为本接口的dns_index参数,取值范围为1到4;

所以最多同时存在4个DNS服务器IP地址;

DNS服务器IP地址有两大类:

1、第一类是:网卡初始化过程中,自动从网络运营商获取到默认的DNS服务器IP地址

​ 这一类DNS服务器IP地址通常由2个或者1个,这一类DNS服务器IP地址存储在dns_index为3和4的位置;

2、第二类是:在应用脚本程序中调用socket.setDNS接口手动设置的自定义的DNS服务器IP地址;例如:

  • socket.setDNS(socket.LWIP_GP, 1, "223.5.5.5")表示为4G网卡在位置1设置自定义的DNS服务器IP地址"223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
  • socket.setDNS(socket.LWIP_GP, 2, "114.114.114.114")表示为4G网卡在位置2设置自定义的DNS服务器IP地址"114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;

DNS解析服务的逻辑是:

遍历位置1到4的DNS服务器IP地址;

如果当前位置存在DNS服务器IP地址,则解析域名,每个DNS服务器IP地址尝试3次,每次超时分别是1秒钟、2秒钟、3秒钟;

如果当前位置不存在DNS服务器IP地址,则直接跳过;

也就是说,如果4个位置上都有DNS服务器IP地址,则最多尝试12次,最长超时24秒可以返回解析结果;

为了防止出现默认的DNS服务器IP地址 解析服务器不稳定的问题,把位置3和4用来存储默认的DNS服务器IP地址,同时,可以在位置1和2设置为自定义的DNS服务器IP地址,如下所示,为了方便用户参考使用,我们在每个网络应用demo中,已经为所有使用到的网卡增加了如下配置:

-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码
-- 如果使用国外的网络,不要使用这两行代码
socket.setDNS(adapter, 1 "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")

参数

adapter_id

参数含义:表示网卡编号,用来标识某一种网卡;
数据类型:number或者nil
取值范围:本文常量详解章节中的所有网络适配器编号常量,例如:socket.LWIP_GP
是否必选:可选传入此参数;如果没有传入此参数,会使用当前时刻的默认网卡;
注意事项:暂无;
参数示例:socket.LWIP_GP

dns_index

参数含义:dns服务器ip地址序号
数据类型:number
取值范围:14
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:1表示存储dns服务器ip地址的第一个位置

ip

参数含义:dns服务器ip地址
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"114.114.114.114"

返回值

local result = socket.setDNS(adapter_id, dns_index, ip)

result

含义说明:成功返回true,失败返回false
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:暂无;

示例

local function ip_ready_func(ip, adapter)    
    -- 在位置1和2设置自定义的DNS服务器ip地址:
    -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
    -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
    -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
    -- 如果使用专网卡,不要使用这两行代码;
    -- 如果使用国外的网络,不要使用这两行代码;
    socket.setDNS(adapter, 1, "223.5.5.5")
    socket.setDNS(adapter, 2, "114.114.114.114")

    if adapter == socket.LWIP_GP then
        log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
    end
end

sys.subscribe("IP_READY", ip_ready_func)

socket.sslLog(log_level)

功能

设置内核固件中ssl功能的log等级;

一般来说,当需要抓取更多的日志给合宙技术人员分析时,可以使用此接口,设置输出更多日志信息的等级;

如果打开debug开关,使用Luatools抓日志时,在主界面窗口会出现类似于下面的日志信息(出现很多mbedtls开头的日志):

[2025-09-22 16:45:34.347][000000073.662] mbedtls_ssl_handshake_wrapup 3474:=> handshake wrapup
[2025-09-22 16:45:34.368][000000073.662] mbedtls_ssl_handshake_wrapup_free_hs_transform 3447:=> handshake wrapup: final free
[2025-09-22 16:45:34.389][000000073.662] mbedtls_ssl_handshake_wrapup_free_hs_transform 3467:<= handshake wrapup: final free
[2025-09-22 16:45:34.410][000000073.662] mbedtls_ssl_handshake_wrapup 3529:<= handshake wrapup
[2025-09-22 16:45:34.432][000000073.663] network_default_socket_callback 1107:after process socket 2,state:5(在线),wait:0(无等待)
[2025-09-22 16:45:34.458][000000073.667] I/user.tcp_ssl_ca_main_task_func libnet.connect success
[2025-09-22 16:45:34.479][000000073.667] mbedtls_ssl_read 5394:=> read
[2025-09-22 16:45:34.498][000000073.668] mbedtls_ssl_read_record 3936:=> read record
[2025-09-22 16:45:34.524][000000073.668] mbedtls_ssl_fetch_input 1883:=> fetch input
[2025-09-22 16:45:34.543][000000073.668] mbedtls_ssl_fetch_input 2037:in_left: 0, nb_want: 5
[2025-09-22 16:45:34.562][000000073.669] mbedtls_ssl_fetch_input 2062:in_left: 0, nb_want: 5
[2025-09-22 16:45:34.586][000000073.669] network_rx 2536:socket 2 ssl data need more
[2025-09-22 16:45:47.173][000000088.670] I/user.tcp_ssl_ca_main_task_func libnet.wait true false nil
[2025-09-22 16:45:47.193][000000088.670] mbedtls_ssl_read 5394:=> read
[2025-09-22 16:45:47.211][000000088.671] mbedtls_ssl_read_record 3936:=> read record
[2025-09-22 16:45:47.228][000000088.671] mbedtls_ssl_fetch_input 1883:=> fetch input
[2025-09-22 16:45:47.253][000000088.671] mbedtls_ssl_fetch_input 2037:in_left: 0, nb_want: 5
[2025-09-22 16:45:47.275][000000088.671] mbedtls_ssl_fetch_input 2062:in_left: 0, nb_want: 5
[2025-09-22 16:45:47.295][000000088.671] network_rx 2536:socket 2 ssl data need more
[2025-09-22 16:46:02.176][000000103.673] I/user.tcp_ssl_ca_main_task_func libnet.wait true false nil
[2025-09-22 16:46:02.182][000000103.673] mbedtls_ssl_read 5394:=> read
[2025-09-22 16:46:02.189][000000103.673] mbedtls_ssl_read_record 3936:=> read record
[2025-09-22 16:46:02.200][000000103.674] mbedtls_ssl_fetch_input 1883:=> fetch input
[2025-09-22 16:46:02.216][000000103.674] mbedtls_ssl_fetch_input 2037:in_left: 0, nb_want: 5
[2025-09-22 16:46:02.219][000000103.674] mbedtls_ssl_fetch_input 2062:in_left: 0, nb_want: 5
[2025-09-22 16:46:02.227][000000103.674] network_rx 2536:socket 2 ssl data need more
[2025-09-22 16:46:17.192][000000118.675] I/user.tcp_ssl_ca_main_task_func libnet.wait true false nil
[2025-09-22 16:46:17.227][000000118.676] mbedtls_ssl_read 5394:=> read
[2025-09-22 16:46:17.249][000000118.676] mbedtls_ssl_read_record 3936:=> read record
[2025-09-22 16:46:17.270][000000118.676] mbedtls_ssl_fetch_input 1883:=> fetch input
[2025-09-22 16:46:17.298][000000118.676] mbedtls_ssl_fetch_input 2037:in_left: 0, nb_want: 5
[2025-09-22 16:46:17.325][000000118.677] mbedtls_ssl_fetch_input 2062:in_left: 0, nb_want: 5

参数

log_level

参数含义:内核固件中的ssl功能的log等级
         0:不打印
         1:只打印错误和警告
         2:打印大部分日志信息
         33以上:打印最详细的日志信息
数据类型:number
取值范围:大于等于0的整数;
是否必选:必须传入此参数;
注意事项:使用位置无特别要求,实时生效,如果要抓取完整的日志,可以在使用socket网络应用之前进行设置
         打印过多信息会造成内存碎片化,仅在调试时有需要再打开,量产软件中不要打开;
参数示例:3

返回值

nil

示例

-- 打印内核固件中ssl功能详细的日志信息
socket.sslLog(3)

socket.create(adapter_id, task_name)

功能

在指定网卡上创建一个socket对象;

在ram资源足够的情况下:

1、Air780系列/Air8000系列的模组,允许创建的同时存在的socket对象数量为64个;

2、Air6101系列/Air8101系列的模组,允许创建的同时存在的socket对象数量为32个;

参数

adapter_id

参数含义:表示socket对象绑定的网卡编号
         adapter_id是number类型时,表示具体的某一种网卡;
         adapter_id是nil类型时,表示当前时刻的默认网卡;
数据类型:number或者nil
取值范围:number类型时,本文常量详解章节中的所有网络适配器编号常量;
是否必选:可选传入此参数;
注意事项:如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡;
         除非你socket请求时,一定要使用某一种网卡,才设置此参数;
         如果没什么特别要求,不要设置此参数,使用系统中设置的默认网卡即可;

         一般来说,LuatOS的网络应用demo中都会有netdrv_device功能模块
         在这个功能模块中会使用exnetif扩展库的接口去配置使用的网卡参数
         所以建议不要设置此参数,直接使用exnetif扩展库的接口自动设置的默认网卡就行
         这样做,有以下两项好处:
         1exnetif扩展库已经高度集成,使用这个扩展库提供的接口,无论是单网卡设置还是多网卡设置,
            使用起来非常方便;
         2、默认网卡设置和具体的socketmqtthttp网络应用解耦,用户只需要在初始化时配置一次,
            所有的网络应用中不要再指定具体的网卡,就可以使用初始化配置的优先级使用默认网卡;
参数示例:例如socket.LWIP_GP表示使用4G网卡

task_name

参数含义:表示socket对象绑定的task的名称
         socket的所有异步消息,都是内核固件通过定向消息发送,所以要指定一个task名称,才能处理定向消息;
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task"

返回值

local socket_ctrl = socket.create(nil, "socket_client_task")

socket_ctrl

含义说明:创建的socket对象;如果是userdata类型表示创建成功,如果是nil表示创建失败
数据类型:userdata或者nil
取值范围:暂无;
注意事项:在ram资源足够的情况下
         1Air780系列/Air8000系列的模组,允许创建的同时存在的socket对象数量为64个
         2Air6101系列/Air8101系列的模组,允许创建的同时存在的socket对象数量为32个
         如果同时存在的socket对象数量超过了最大限制,再次创建就会返回失败;
         这种失败会在日志中打印:adapter no more ctrl!
返回示例:非nil值的一个userdata类型,可以直接使用if语句做逻辑判断

-- 创建socket client对象
socket_client = socket.create(nil, "socket_client_task")
-- 如果创建socket client对象失败
if not socket_client then
    log.error("socket_client_task", "socket.create error")
end

socket.debug(socket_ctrl, onoff)

功能

配置是否打开内核固件中network的debug日志信息;

一般来说,当需要抓取更多的日志给合宙技术人员分析时,可以打开此开关;

如果打开debug开关,使用Luatools抓日志时,在主界面窗口会出现类似于下面的日志信息(出现很多network开头的日志):

[2025-09-22 14:50:48.598][000000006.364] network_default_socket_callback 1103:before process socket -1,event:0xf2000003(DNS结果),state:2(等待DNS),wait:2(等待连接完成)
[2025-09-22 14:50:48.611][000000006.364] network_state_wait_dns 624:dns ip0, ttl 36, 36.152.44.93
[2025-09-22 14:50:48.628][000000006.364] network_state_wait_dns 624:dns ip1, ttl 36, 36.152.44.132
[2025-09-22 14:50:48.632][000000006.364] network_socket_connect 1578:network 1 local port auto select 50652
[2025-09-22 14:50:48.642][000000006.365] network_default_socket_callback 1107:after process socket 1,state:3(正在连接),wait:2(等待连接完成)
[2025-09-22 14:50:48.650][000000006.365] dns_run 691:dns all done ,now stop
[2025-09-22 14:50:48.660][000000006.391] network_default_socket_callback 1103:before process socket 1,event:0xf2000009(连接成功),state:3(正在连接),wait:2(等待连接完成)
[2025-09-22 14:50:48.668][000000006.410] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.678][000000006.411] D/sntp Unix timestamp: 1758523848
[2025-09-22 14:50:48.690][000000006.444] network_default_socket_callback 1103:before process socket 1,event:0xf2000004(发送成功),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.698][000000006.444] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.928][000000006.451] network_default_socket_callback 1103:before process socket 1,event:0xf2000005(有新的数据),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.934][000000006.453] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.941][000000006.453] network_default_socket_callback 1103:before process socket 1,event:0xf2000005(有新的数据),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.950][000000006.454] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.961][000000006.455] network_default_socket_callback 1103:before process socket 1,event:0xf2000005(有新的数据),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.979][000000006.456] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.984][000000006.457] network_default_socket_callback 1103:before process socket 1,event:0xf2000005(有新的数据),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:48.995][000000006.480] tls_verify 1799:1, 00000000
[2025-09-22 14:50:49.006][000000006.481] tls_verify 1799:0, 00000000
[2025-09-22 14:50:49.015][000000006.484] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:49.025][000000006.484] network_default_socket_callback 1103:before process socket 1,event:0xf2000005(有新的数据),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:49.035][000000006.877] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:49.045][000000006.924] network_default_socket_callback 1103:before process socket 1,event:0xf2000004(发送成功),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:49.062][000000006.924] network_default_socket_callback 1107:after process socket 1,state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:49.067][000000006.925] network_default_socket_callback 1103:before process socket 1,event:0xf2000005(有新的数据),state:4(正在TLS握手),wait:2(等待连接完成)
[2025-09-22 14:50:49.078][000000006.927] network_default_socket_callback 1107:after process socket 1,state:5(在线),wait:0(无等待)

参数

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口控制debug信息开关
参数示例:暂无;

onoff

参数含义:表示是否打开内核固件中network的debug信息的开关
数据类型:boolean
取值范围:true表示打开debug开关false表示关闭debug开关
是否必选:可选传入此参数,若不填此参数,默认为false
注意事项:暂无;
参数示例:truefalse

返回值

nil

示例

-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
    log.error("tcp_ssl_ca_main_task_func", "socket.create error")
    return
end

-- 在socket_client上打开内核固件的debug信息开关
socket.debug(socket_client, true)

socket.config(socket_ctrl, local_port, is_udp, is_tls, keep_idle, keep_interval, keep_cnt, server_cert, client_cert, client_key, client_password)

功能

配置socket对象的参数(本地端口号,TCP/UDP/TLS协议,TCP keep alive心跳参数,证书认证信息)

本地端口号,TCP/UDP/TLS协议,证书认证信息,这三部分的知识,我们在理论知识章节,已经介绍过,此处就不再重复介绍了,如果不清楚,可以看一下本文第五部分的内容;

TCP keep alive心跳参数,之前还没有介绍过,我们先看一下TCP keep alive心跳;

TCP Keepalive 是一种用于检测 TCP 连接另一端是否仍然存活和可达的机制。它通过在一条空闲的连接上定期发送特殊的探测报文(Keepalive 探针)来实现;

例如客户端和服务器建立了 TCP 连接之后,双方再也没有应用数据要发送。此时,连接处于空闲状态;

但是客户端又想一直维持这个连接,否则有可能空闲超时被网络服务商给断开(例如4G网络服务商发现某个连接长时间没有数据通信,可能是几分钟,也能更长时间,时间不确定,就会主动的断开这个连接);

TCP Keepalive机制可以定时发送Keepalive 探针报文,防止连接空闲时间太长,导致连接被异常断开;

在TCP协议的规范中,TCP Keepalive机制的数据交互过程受三个核心参数控制:

  1. keep_idle(默认值7200秒,也就是2小时):连接空闲多长时间后,开始发送第一个 Keepalive 探针报文;
  2. keep_interval(默认值75秒):发送第一个探针后,如果没收到ACK回复,间隔多久再发送下一个探针;
  3. keep_cnt(默认值9次):总共发送多少次探针后,如果依然没有回复,则判定连接已断开;

假设这三个参数都使用默认值,TCP Keepalive机制的工作流程如下:

  1. 连接建立后,空闲了 7200 秒(2小时);
  2. 客户端 发送第一个 Keepalive 探针报文到服务器;
  3. 可能出现三种结果:
  4. 成功:服务器回复 ACK,客户端知道和服务器之间的连接还存在;定时器重置,再等待2小时的空闲时间;
  5. 超时:客户端等待 75 秒,没收到ACK,则重试发送第二个探针;
  6. 错误:服务器回复异常报文,并且断开连接;
  7. 如果客户端连续发送9个探针(每两个探针之间间隔75秒)后都超时无任何响应,客户端就判断连接已断开;

连接已经断开的情况下,从开始探测到最终检测到已经断开的结果,最长需要: 7200 + 75 * 9 ≈ 7875 秒,约 2 小时 11 分钟;

而4G网络环境下,最长空闲十几分钟,大概率就被网络服务商给异常中断了,所以说,这三个核心参数如果使用默认值,TCP keepalive心跳没有任何作用,根本起不到保持长连接的作用;

所以我们在LuatOS的socket核心库中,提供了一个接口socket.config,可以配置这三个参数的值,让TCP keepalive真正能够起到保持长连接的作用; 例如keep_idle可以设置为300秒,keep_interval可以设置为10秒,keep_cnt可以设置为3次;

如果使用了TCP keepalive心跳机制,并且三个核心参数配置合适,就可以不再使用应用心跳机制来保持长连接;

如果项目中定义了应用心跳机制,并且应用心跳间隔合适,就可以不启用TCP keepalive机制,keep_idle参数为空或者传入nil即可;

参数

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口配置参数;
参数示例:暂无;

local_port

参数含义:本地端口号;
数据类型:number或者nil
取值范围:number类型时,传入的端口号需要小于60000
是否必选:可选传入此参数,如果没有传入此参数,则内核固件会自动分配一个端口号;
注意事项:暂无;
参数示例:80

is_udp

参数含义:是否是UDPtrue表示UDPfalse或者nil表示TCP
数据类型:boolean或者nil
取值范围:true或false
是否必选:可选传入此参数,默认为TCP
注意事项:暂无;
参数示例:true

is_tls

参数含义:是否是加密传输,true表示tls加密false或者nil表示非加密
数据类型:boolean或者nil
取值范围:true或false
是否必选:可选传入此参数;
注意事项:支持TLS 1.0/1.1/1.2, DTLS 1.0/1.2, 当前不支持TLS 1.3
         不支持 SSL 3.0, 该协议已经被废弃, 也不安全;
         支持的加密算法有 RSA, ECC, AES, 3DES, SHA1, SHA256, MD5等等
         完整的加密套件列表, 可通过 crypto.cipher_suites() 获取,获取代码如下:
         local suites = crypto.cipher_suites()
         if suites then
             log.info("crypto", "ciphers suites", json.encode(suites))
         end
参数示例:true

keep_idle

参数含义:连接空闲多长时间后,开始发送第一个 keepalive 探针报文;
         即允许的持续空闲时长,单位为秒;
数据类型:number或者nil
取值范围:number类型时,大于0的正整数;
是否必选:可选传入此参数;如果不传入此参数,表示不启用keepalive机制
         如果传入number类型的数值,建议大于等于120秒,最好不要超过300秒;
         这样既能起到保活的作用,另外发送心跳频率不会过快,功耗也不会过高;
注意事项:暂无;
参数示例:300

keep_interval

参数含义:发送第一个探针后,如果没收到ACK回复,间隔多久再发送下一个探针,单位为秒;
数据类型:number或者nil
取值范围:number类型时,大于0的正整数;
是否必选:可选传入此参数;
注意事项:如果keep_idle传入了number类型的有效参数,则keep_interval也必须是number类型
参数示例:10

keep_cnt

参数含义:总共发送多少次探针后,如果依然没有回复,则判定连接已断开;
数据类型:number或者nil
取值范围:number类型时,大于0的正整数;
是否必选:可选传入此参数;
注意事项:如果keep_idle传入了number类型的有效参数,则keep_cnt也必须是number类型
参数示例:3

server_cert

参数含义:TCP模式下的服务器ca证书数据UDP模式下的PSK
数据类型:string或者nil
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:如果客户端不需要验证服务器证书,此参数可以为空,也可以为nil
         当客户端需要验证服务器证书时,需要此参数,
         如果证书数据在一个文件中,要把文件内容读出来,赋值给server_ca_cert
参数示例:例如通过Luatools烧录了server_ca.crt文件
         就可以通过io.readFile("/luadb/server_ca.crt")读出文件内容赋值给赋值给server_ca_cert

client_cert

参数含义:TCP模式下的客户端证书数据UDP模式下的PSK-ID
         TCP模式下如果不需要验证客户端证书时,忽略,一般不需要验证客户端证书;
数据类型:string或者nil
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:当服务器需要验证客户端证书时,需要此参数,
         如果证书数据在一个文件中,要把文件内容读出来,赋值给client_cert
参数示例:例如通过Luatools烧录了clinet.crt文件
         就可以通过io.readFile("/luadb/clinet.crt")读出文件内容赋值给赋值给client_cert

client_key

参数含义:TCP模式下的客户端私钥加密数据
数据类型:string或者nil
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:当服务器需要验证客户端证书时,需要此参数,
         如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_key
参数示例:例如通过Luatools烧录了clinet.key文件
         就可以通过io.readFile("/luadb/clinet.key")读出文件内容赋值给client.key

client_password

参数含义:TCP模式下的客户端私钥口令数据
数据类型:string或者nil
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:当服务器需要验证客户端证书时,需要此参数,
         如果加密后的口令数据在一个文件中,要把文件内容读出来,赋值给client_password
参数示例:例如通过Luatools烧录了clinet.password文件
         就可以通过io.readFile("/luadb/clinet.password")读出文件内容赋值给client_password

返回值

local result = socket.config(socket_ctrl, local_port, is_udp, is_tls, keep_idle, keep_interval, keep_cnt, server_cert, client_cert, client_key, client_password)

result

含义说明:成功返回true,失败返回false
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:true

示例

配置为普通的tcp socket client

-- 创建socket client对象
socket_client = socket.create(nil,"tcp_client_task")
-- 如果创建socket client对象失败
if not socket_client then
    log.error("tcp_client_task", "socket.create error")
    return
end

-- 配置socket client对象为tcp client
result = socket.config(socket_client)
-- 如果配置失败
if not result then
    log.error("tcp_client_task", "socket.config error")
    return
end

配置为不做证书校验的tcp ssl socket client

-- 创建socket client对象
socket_client = socket.create(nil,"tcp_ssl_client_task")
-- 如果创建socket client对象失败
if not socket_client then
    log.error("tcp_ssl_client_task", "socket.create error")
    return
end

-- 配置socket client对象为tcp_ssl client
-- 不做证书校验
result = socket.config(socket_client, nil, nil, true)
-- 如果配置失败
if not result then
    log.error("tcp_ssl_client_task", "socket.config error")
    return
end

配置为单向证书校验的tcp ssl socket client

-- 创建socket client对象
socket_client = socket.create(nil,"tcp_ssl_ca_client_task")
-- 如果创建socket client对象失败
if not socket_client then
    log.error("tcp_ssl_ca_client_task", "socket.create error")
    return
end

-- 配置socket client对象为tcp_ssl client
-- 单向证书校验
result = socket.config(socket_client, nil, nil, true, nil, nil, nil, server_ca_cert)
-- 如果配置失败
if not result then
    log.error("tcp_ssl_ca_client_task", "socket.config error")
    return
end

配置为普通的udp socket client

-- 创建socket client对象
socket_client = socket.create(nil,"udp_client_task")
-- 如果创建socket client对象失败
if not socket_client then
    log.error("udp_client_task", "socket.create error")
    return
end

-- 配置socket client对象为udp client
result = socket.config(socket_client, nil, true)
-- 如果配置失败
if not result then
    log.error("udp_client_task", "socket.config error")
    return
end

socket.rx(socket_ctrl, buff, flag, limit)

功能

接收对端发送过来的数据,注意数据已经缓存在内核固件底层,使用本函数只是读取出来;

TCP模式下,对端发送过来的一包应用数据长度,没有特别限制;

UDP模式下,对端发送过来的一包应用数据长度,不要超过1460字节数据;

对于tcp来说,数据已经缓存到内核固件底层的数据接收缓冲区,这个数据接收缓冲区就是滑动窗口,滑动窗口的理论知识,我们在5.1.3章节有介绍,在这里就不重复描述了;

LuatOS tcp socket本地的窗口初始大小为8040字节,在接收数据过程中,会将窗口的动态大小通知给发送方,发送方必须遵守这个窗口的限制;

例如 tcp client和server建立连接后,server应用层输入了34560字节的数据要发送过来,点击发送按钮,在tcp层传输数据时,server会根据client这边的动态窗口大小,动态调整发送数据的速度;先看下Luatools抓取到的Air8000产品的应用日志:

一共收到34560字节的数据,没有丢失;

我们再看看这个数据接收过程中网络数据交互,滑动窗口机制是如何工作的;

下面这张截图是从Air8000的运行日志中导出的网络数据包:

所以说,tcp socket,不用关心内核固件底层的接收缓冲区有多大,也不用关心对端发送过来的数据有多少,tcp的滑动窗口机制、序列号和确认号机制、超时重传机制,保证了数据接收的完整性;

在编程时,唯一需要注意的就是,当内核固件接收到数据时,在应用脚本中及时读出来数据即可,这样内核固件的窗口大小就会动态变化;

对于udp来说,虽然udp协议规范应用数据一包的最大长度为65535字节,但是由于udp数据传输不可靠,另外收到大数据时,LuatOS内核分配连续的内存空间也存在失败的概率,双重失败概率叠加,会导致udp大数据接收存在失败的概率增高;

从传输可靠性说,整个udp应用数据包含在单个mtu内最佳,最常见的mtu是1500字节,减去网络层和传输层的头部,剩余1460字节的长度给应用数据,所以所以LuatOS上的udp socket,限制可以接收的一包udp应用数据长度为1460字节;这是一个推荐值,目前实际使用时,可能可以接收到更长的数据,但是为了可靠性,尽量将一包应用数据的长度控制在1460字节;

参数

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口配置参数;
参数示例:暂无;

buff

参数含义:zbuff 存放接收的数据,如果zbuff初始化的缓冲区不够大会自动扩容
数据类型:zbuff
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:一个zbuff对象

flag

参数含义:接收参数,目前预留,不起作用;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;

limit

参数含义:接收数据长度限制,如果指定了,则只取前limit个字节
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数,如果没有传入才参数,表示读取全部数据;
注意事项:暂无;
参数示例:暂无;

返回值

local succ, data_len, remote_ip, remote_port = socket.rx(socket_ctrl, buff, flag, limit)

succ

含义说明:读取结果;true表示读取成功false表示读取失败
         读取失败的情况下,后面的三个返回值没有任何意义;
         读取失败的情况下,要主动的调用libnet.close接口关闭socket,再调用socket.release释放资源
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:true

data_len

含义说明:本次读取到数据长度;第一个返回值succ为true的情况下data_len这个返回值才有意义
数据类型:number
取值范围:大于等于0的整数;
注意事项:暂无;
返回示例:1460

remote_ip

含义说明:对端的IP地址;第一个返回值succ为true的情况下remote_ip这个返回值才有意义
         TCP模式下,此返回值没有意义,一直为nil
         UDP模式下,此返回值才有意义,如果是IPV4,数据格式为:1byte 0x00 + 4byte地址
         如果是IPV6,数据格式为:1byte 0x01 + 16byte地址
数据类型:string或者nil
取值范围:暂无;
注意事项:暂无;
返回示例:如果是IPV4,以返回的hex值"00707D5908"为例,
         此返回值共有5个字节,其中第一个字节固定为0x00
         第二个字节0x70,第三个字节0x7D,第四个字节0X59,第五个字节0x08
         转化为由4个点分十进制数组成的"ip地址"就为112.125.89.8

remote_port

含义说明:对端的端口号;第一个返回值succ为true的情况下remote_port这个返回值才有意义
         TCP模式下,此返回值没有意义,一直为0
         UDP模式下,此返回值才有意义;
数据类型:number
取值范围:暂无;
注意事项:暂无;
返回示例:12354

示例

function udp_client_receiver.proc(socket_client)
    -- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
    if recv_buff==nil then
        recv_buff = zbuff.create(1024)
    end

    while true do
        -- 从内核的缓冲区中读取数据到recv_buff中
        -- 如果recv_buff的存储空间不足,会自动扩容
        local result = socket.rx(socket_client, recv_buff)

        if not result then
            log.error("udp_client_receiver.proc", "socket.rx error")
            return false
        end

        -- 如果读取到了数据, used()就必然大于0, 进行处理
        if recv_buff:used() > 0 then
            log.info("udp_client_receiver.proc", "recv data len", recv_buff:used())

            -- 读取socket数据接收缓冲区中的数据,赋值给data
            local data = recv_buff:query()

            -- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
            sys.publish("RECV_DATA_FROM_SERVER", "recv from udp server: ", data)

            -- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
            sys.publish("FEED_NETWORK_WATCHDOG")

            -- 清空socket数据接收缓冲区中的数据
            recv_buff:del()
        -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
        else
            break
        end
    end

    return true
end

socket.state(socket_ctrl)

功能

获取 socket 当前状态;

参数

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口配置参数;
参数示例:暂无;

返回值

local state, str = socket.state(socket_ctrl)

state

含义说明:状态数值;具体的数值表示读取成功,nil表示读取失败
数据类型:number或者nil
取值范围:08的整数;每个数值表示的意义如下:
         0"硬件离线"
         1"离线"
         2"等待DNS"
         3"正在连接"
         4"正在TLS握手"
         5"在线"
         6"在监听"
         7"正在离线" 
         8"未知"        
注意事项:暂无;
返回示例:5

str

含义说明:状态字符串,UTF-8编码;具体的字符串表示读取成功,nil表示读取失败
数据类型:string或者nil
取值范围:"硬件离线"
         "离线"
         "等待DNS"
         "正在连接"
         "正在TLS握手"
         "在线"
         "在监听"
         "正在离线" 
         "未知"        
注意事项:暂无;
返回示例:"在线"

示例

-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
    log.error("tcp_ssl_ca_main_task_func", "socket.create error")
    return
end

-- 读取socket_client状态
local state, str = socket.state(socket_ctrl)
log.info("socket.state(socket_ctrl)", state, str)

socket.release(socket_ctrl)

功能

主动释放掉 socket对象,此接口和socket.create接口是一对逆操作;

使用socket.release之后,如果要发起重连,需要重新从socket.create开始;

参数

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口配置参数;
参数示例:暂无;

返回值

nil

示例

-- 释放后就不能再使用了
-- 如果存在socket client对象
if socket_client then
    -- 关闭socket client连接
    libnet.close(TASK_NAME, 5000, socket_client)

    -- 释放socket client对象,释放后就不能再使用了
    socket.release(socket_client)
    socket_client = nil
end

第七部分:LuatOS上的libnet扩展库

刚才我们已经在第六部分学习了socket核心库中的同步api接口,这些api主要涉及到网卡操作,socket的创建、配置、读取、销毁等功能;

在本章节,我们先详细学习一下libnet扩展库的api接口;

socket核心库和libnet扩展库的主要区别是:

1、socket核心库提供了完整的api,可以实现socket的完整业务逻辑,但是部分api是异步接口,使用起来复杂;

2、所以我们开发一套libnet扩展库api接口,将socket核心库中的异步接口,封装成同步接口,放到libnet扩展库中,使用起来会更加方便;

最终,我们推荐使用 socket核心库中的同步api接口+libnet扩展库中的同步api接口 的方式,来开发完整的socket业务逻辑;这两部分如何组合使用,在本文的第八部分有一个详细的socket client应用开发框架来演示;

本章节所描述的libnet扩展库,可以访问:libnet扩展库API文档

libnet.connect(task_name, timeout, socket_ctrl, remote_addr, remote_port, need_ipv6_dns)

功能

连接对端,阻塞等待连接结果的返回;

只能在sys.taskInitEx创建的task的任务处理函数中使用,因为要阻塞等待并且接收定向消息;

以下三种情况会返回:

  1. 连接成功;
  2. 连接失败;
  3. 连接超时(超时时长受timeout参数控制);

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
         libnet.connect接口需要在此task的任务处理函数中被调用
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

参数含义:连接超时时长,单位为毫秒,如果为0或者空,则表示没有超时限制,一直等待连接结果的返回;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:如果要连接的对端是域名,此处需要考虑dns域名解析的耗时
         结合socket.setDNS接口综合考虑此处的超时时长
         如果设置了4个域名解析服务器,则域名解析最长耗时24秒;
         普通的udp连接:连接耗时 = 域名解析耗时;
         普通的tcp连接:连接耗时 = 域名解析耗时 + TCP握手耗时         
         tcp tls连接:连接耗时 = 域名解析耗时 + TCP握手耗时 + tls握手耗时
         所以不建议此处的timeout设置的时长过短,针对不同的连接要预留足够的时间;

         一般来说:
         对端地址如果是域名;
         如果有4个域名解析服务器,推荐30秒;
         如果只有默认的2个域名解析服务器,推荐20秒;
         再短的话,不太推荐;再长的话,只要项目业务允许,也可以使用;

         对端地址如果是IP;推荐10秒,15秒;
         再短的话,不太推荐;再长的话,只要项目业务允许,也可以使用;         
参数示例:30000

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口连接对端;
参数示例:暂无;

remote_address

参数含义:服务器地址,支持域名或者ip地址
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"docs.openluat.com"或者"112.125.89.8"

remote_port

参数含义:服务器端口号;
数据类型:number
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:34574

need_ipv6_dns

参数含义:域名解析时,是否优先解析并且返回域名remote_addr对应的IPV6地址
         true表示需要false表示不需要,默认false
数据类型:boolean
取值范围:true或false或nil
是否必选:可选传入此参数,默认为false
注意事项:暂无;
参数示例:nil或者为空

返回值

local result = libnet.connect(task_name, timeout, socket_ctrl, remote_addr, remote_port, need_ipv6_dns)

result

含义说明:连接结果;失败或者超时返回false,成功返回true
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:true

示例

-- 创建socket client对象
socket_client = socket.create(nil, "tcp_client_task")
-- 如果创建socket client对象失败
if not socket_client then
    log.error("tcp_client_main_task_func", "socket.create error")
    goto EXCEPTION_PROC
end

-- 配置socket client对象为tcp client
result = socket.config(socket_client)
-- 如果配置失败
if not result then
    log.error("tcp_client_main_task_func", "socket.config error")
    goto EXCEPTION_PROC
end

-- 连接server
result = libnet.connect("tcp_client_task", 15000, socket_client, "112.125.89.8", 34036)
-- 如果连接server失败
if not result then
    log.error("tcp_client_main_task_func", "libnet.connect error")
    goto EXCEPTION_PROC
end

libnet.tx(task_name, timeout, socket_ctrl, data, ip, port, flag)

功能

发送数据到对端,阻塞等待发送结果的返回;

只能在sys.taskInitEx创建的task的任务处理函数中使用,因为要阻塞等待并且接收定向消息;

以下三种情况会返回:

  1. 发送成功;
  2. 发送失败;
  3. 发送超时(超时时长受timeout参数控制);

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
         libnet.tx接口需要在此task的任务处理函数中被调用
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_send_task"

timeout

参数含义:发送超时时长,单位为毫秒,如果为0或者空,则表示没有超时限制,一直等待发送结果的返回;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:tcp模式下:发送耗时 = 数据发送到对端耗时 + 接收到对端ack耗时
         udp模式下:发送耗时 = 数据传递给本端协议栈,协议栈执行对外发送动作的耗时;
         tcp模式下,考虑到网络传输过程中的耗时,不建议此处的timeout设置的时长过短
         一般来说,10秒,15秒,20秒都比较合适;再短的话,不推荐;
         再长的话,只要项目业务允许,也可以使用;
参数示例:15000

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口发送数据到对端;
参数示例:暂无;

data

参数含义:待发送的数据;
数据类型:string或者zbuff
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:TCP模式下,只要ram够用,发送的一包数据长度没有特别限制;
         UDP模式下,发送的一包数据长度不要超过1460字节;
参数示例:"123456"

ip

参数含义:UDP模式下,对端IP地址
         UDP模式下,如果为空,发送数据时会使用libnet.connect接口中的remote_address参数
         UDP模式下,如果不为空,数据会直接发给这个ip参数所表示的对端IP地址
         TCP模式下,此参数没有任何意义;
数据类型:string或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:此参数和libnet.connect接口中的remote_addr都表示对端地址
         为什么这两个接口都有这个参数,是为了区分发送广播数据和发送定向数据;
         因为广播功能通常是搭配UDP来使用
         使用libnet.connect时的remote_addr指定某一个对端的地址,使用libnet.tx的ip指定广播地址
         或者
         使用libnet.connect时的remote_addr指定广播地址,使用libnet.tx的ip指定某一个对端的地址
         这样就可以在UDP上实现广播功能和定向发送数据功能
参数示例:"112.125.89.8"

port

参数含义:UDP模式下,对端端口号;
         UDP模式下,如果为空,发送数据时会使用libnet.connect接口中的remote_port参数
         UDP模式下,如果不为空,数据会直接发给这个port参数所表示的对端端口号
         TCP模式下,此参数没有任何意义;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:此参数和libnet.connect接口中的remote_port都表示对端端口号
         为什么这两个接口都有这个参数,是为了区分发送广播数据和发送定向数据;
         因为广播功能通常是搭配UDP来使用
         使用libnet.connect时的remote_port指定某一个对端的端口,使用libnet.tx的ip指定广播端口
         或者
         使用libnet.connect时的remote_addr指定广播端口,使用libnet.tx的ip指定某一个对端的端口
         这样就可以在UDP上实现广播功能和定向发送数据功能
参数示例:8888

flag

参数含义:发送参数,目前预留,不起作用;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;

返回值

local result, buff_full = libnet.tx(task_name, timeout, socket_ctrl, data, ip, port, flag)

result

含义说明:发送结果;失败或者超时返回false,成功返回true
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:true

buff_full

含义说明:缓存区是否满了;
数据类型:boolean
取值范围:true或false
注意事项:缓冲区满只有一种情况,就是此次发送的数据动态申请内存失败了;
         分为两种情况:
         1、当前socket发送缓冲区有数据正在发送,此时,动态申请内存失败了,说明整个ram的可用空间几乎没有了
            此时可以等待缓冲区内正在发送的数据发送完成后,接收到socket.TX_OK消息后,再次尝试发送;
            或者等待一段时间,再主动尝试发送一次;
         2、当前socket发送缓冲区没有数据正在发送,此时,动态申请内存失败了,说明整个ram的可用空间几乎没有了
            此时整个项目几乎不可能稳定运行了,很快就会出现ram耗尽而异常重启的情况
         所以说,无论是哪一种情况,如果返回缓冲区满,这次请求发送的新数据继续缓存到原来的位置,等待下次发送即可;
返回示例:false

示例

-- 发送这条数据,超时时间15秒钟
local result, buff_full = libnet.tx("tcp_client_send_task", 15000, socket_client, "123456")

-- 发送失败
if not result then
    log.error("tcp_client_sender.proc", "libnet.tx error")
    return false
end

-- 如果内核固件中缓冲区满了,则将send_item再次插入到send_queue的队首位置,等待下次尝试发送
if buff_full then
    log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
    -- todo:此处存储这次待发送的数据,等待下次在socket主业务逻辑中检测到socket.EVENT消息再发送
    return true
end

libnet.wait(task_name,timeout, socket_ctrl)

功能

socket连接已经成功后,阻塞等待socket对象上新的网络事件消息socket.EVENT;

只能在sys.taskInitEx创建的task的任务处理函数中使用,因为要阻塞等待并且接收定向消息;

以下三种情况会退出阻塞等待状态:

  1. socket对象和对端之间的连接出现异常(例如对端主动断开,网络环境出现异常等),此时在内核固件中会发送消息socket.EVENT;
  2. socket对象接收到对端发送过来的数据,此时在内核固件中会发送消息socket.EVENT;
  3. 在应用脚本中需要的位置,主动调用sys.sendMsg(task_name, socket.EVENT, 0)发送消息socket.EVENT给task_name对应的task;

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
         libnet.wait接口需要在此task的任务处理函数中被调用
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

参数含义:等待超时时长,单位为毫秒,如果为0或者空,则表示没有超时限制,一直等待socket.EVENT消息
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:这里的超时时长到达后,并不会认定出现异常;
         超时退出阻塞状态后,会主动的判断一下是否处理收发业务逻辑,只是增加了一层保险机制,无实际意义;
         因为需要处理收发业务逻辑时,内核固件或者脚本中的其他功能模块会及时发送socket.EVENT消息
         libnet.wait退出阻塞等待状态,然后处理首发业务逻辑;
         所以此处的timeout值不用设置的太短,一般来说,10几秒就行;
参数示例:15000

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口发送数据到对端;
参数示例:暂无;

返回值

local result, param = libnet.wait(task_name, timeout, socket_ctrl)

result

含义说明:网络异常返回false,超时或者其他情况返回true
数据类型:boolean
取值范围:true或false
注意事项:返回false时,要主动的调用libnet.close接口关闭socket,再调用socket.release释放资源
返回示例:true

param

含义说明:当result为true时,此参数才有意义;
         超时返回false,有新的网络事件消息socket.EVENT返回true
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:true

示例

local result, param = libnet.wait("tcp_client_task", 15000, socket_ctrl)
log.info("libnet", "wait", result, param)

-- 网络异常
if not result then
    log.info("socket", "connection exception")

    -- 如果存在socket client对象
    if socket_ctrl then
        -- 关闭socket client连接
        libnet.close("tcp_client_task", 5000, socket_ctrl)

        -- 释放socket client对象
        socket.release(socket_client)
        socket_client = nil
    end
end

libnet.close(task_name, timeout, socket_ctrl)

功能

主动断开socket连接;

TCP模式下,先四次挥手断开连接(超时时长为timeout),再直接强制断开;

UDP模式下,直接强制断开;

只能在sys.taskInitEx创建的task的任务处理函数中使用,因为要阻塞等待并且接收定向消息;

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
         libnet.close接口需要在此task的任务处理函数中被调用
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

参数含义:TCP模式下四次挥手断开连接的等待超时时长,单位为毫秒;
         如果为0或者空,则表示没有超时限制,一直等待四次挥手断开;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:这里的超时时长到达后,如果四次挥手还没有断开连接,则会强制断开连接;
         一般来说,此处的timeout值设置为5秒就行
参数示例:5000

socket_ctrl

参数含义:使用socket.create接口创建的socket对象
数据类型:userdata
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:必须先创建socket对象,才能使用此接口发送数据到对端;
参数示例:暂无;

返回值

nil

示例

libnet.close("tcp_client_task", 5000, socket_ctrl)

第八部分:LuatOS上的socke client 应用开发框架

现在,LuatOS socket和libnet的两个重要的库文件,基本上讲完了,接下来,我们来实际看一个完整的socket client长连接的demo项目代码,重点分析下如何在项目中使用LuatOS socket核心库和libnet扩展库;

socket client长连接的demo项目代码路径:Air8000 socket/client/long_connection demo

8.1 总体设计框图

demo项目的总体设计框图如下:

img

8.2 模拟器上运行这个项目(使用模拟器单网卡演示项目完整的业务逻辑)

首先我们在LuatOS模拟器上运行一下这个demo项目,让大家对实现的功能有一个直观的认识,关于模拟器的使用参考:LuatOS模拟器使用说明

如果要在LuatOS模拟器上运行这个项目,代码需要做以下几点修改:

1、netdrv_device.lua中打开pc模拟器的网卡驱动文件require "netdrv_pc",注释掉其他其他网卡文件;

2、 因为这个demo项目需要用到uart功能,要在pc上模拟uart收发功能,需要在pc上安装一个虚拟串口工具,生成一组串口,例如串口1和串口2,如果这两个串口id在你的pc上已经被占用,可以自定义生成任意两个id的串口就行;

  • 虚拟串口工具的使用说明参考如何使用虚拟串口工具生成一对串口;生成的这一对串口可以互相给对方发数据,也能互相接收对方发送过来的数据;

  • 如果生成的是串口1和串口2,这个demo项目中的uart_app.lua中使用的uart1,不用修改uart_app.lua的代码,此时在pc上使用sscom或者llcom串口工具打开串口2即可,这样的话,模拟器中的uart1就可以和sscom或者llcom打开的uart2进行收发数据;

  • 如果生成的一对串口没有串口1,假设是串口11和串口12,则需要修改uart_app.lua中的代码,串口id修改为11,pc上使用sscom或者llcom串口工具打开串口12即可,这样的话,模拟器中的uart11就可以和sscom或者llcom打开的uart12进行收发数据;

3、打开合宙 TCP/UDP web工具,创建一个TCP server、一个UDP server,一个TCP SSL server,根据创建的服务器地址和端口,修改代码中对应的地址和端口;

软件环境准备好之后,接下来我们在模拟器上实际运行一下这个项目看看效果;

双击 cmd 命令行窗口,然后输入下面一行命令,运行 luatos 批处理文件,同时输入要运行的 luatos 项目配置文件

luatos --llt=H:\Luatools\project\Air8000_socket_client_long_connection.ini

然后按回车键,就可以运行 socket_client_long_connection 项目软件;

8.3 分析项目代码

这个socket demo中的readme文件,以及代码中的注释都比较详细,接下来我用vscode直接打开这份demo项目代码,和大家一起分析下项目代码;

8.4 Air8000开发板上运行演示这个项目(重点演示单网卡和多网卡的使用)

准备硬件环境:

1、Air8000开发板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线一根:

  • sim卡插入开发板的sim卡槽

  • 天线装到开发板上

  • 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口或者交换机网口;

2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air8000开发板和数据线的硬件接线方式为:

  • Air8000开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)

  • TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;

准备软件环境:

  1. 手机打开WiFi热点zhutianhua 123qweasd,修改netdrv_wifi.lua和netdrv_multiple.lua中的WiFi信息;
  2. 打开合宙 TCP/UDP web工具,创建一个TCP server、一个UDP server,一个TCP SSL server,根据创建的服务器地址和端口,修改代码中对应的地址和端口;
  3. netdrv_device.lua分别打开单网卡和多网卡驱动单独演示;
  4. Luatools烧录内核固件以及修改后的demo脚本;

多网卡演示:

开机后,在Luatools运行日志中搜索 new adapter,如下图所示,按照红色文字操作演示

单网卡演示:

直接运行日志即可;

完整的指南文章参考:Air8000 TCP&UDP应用教程

第九部分:如何分析LuatOS socket日志

在socket开发使用过程中,或多或少,我们都会遇到一些问题,遇到问题时,如果我们自己可以根据日志进行初步分析,可能会大大提高解决问题的效率;在本章节我们提供三种日志分析方法;

9.1 三种日志分析方法

9.1.1 Luatools抓取的应用日志分析

这部分是Luatools抓取的应用日志,在Luatools的主窗口可以实时显示,如下图所示:

应用日志分为两种类型:

  1. 一部分是Lua脚本中输出的日志,有D/、I/、W/、E/几种前缀,这部分日志,大家根据自己编写的Lua脚本逻辑自行分析即可;
  2. 另一部分是内核固件中输出的一些关键日志,没有D/、I/、W/、E/几种前缀,这部分日志,大家不知道所表示的意义,我们会在本章节的以下内容中针对一些常见的日志进行逐一分析;

9.1.2 抓取LuatOS模拟器上的网络交互包进行分析

socket应用完全可以在LuatOS模拟器上运行,所以我们可以使用LuatOS模拟器来运行自己的程序;

在运行过程中使用wireshark抓取网络交互包,进行详细分析,例如下图是在LuatOS模拟器运行过程中,tls+单向认证连接服务器失败的网络交互包,从交互包中可以准确的得知,失败原因是Unknown CA,未识别的CA证书

这种分析方法对于解决socket,http,mqtt,ftp等网络应用的问题,帮助很大!

9.1.3 Luatools抓取的底层日志网络交互包分析

这里所说的抓取底层日志网络交互包分析,和 9.1.2 抓取LuatOS模拟器上的网络交互包进行分析 相比,一个是在真实的硬件板上(例如Air8000)运行抓日志,一个是在LuatOS模拟器上运行抓网络交互包;

在真实的硬件板上抓到日志后,然后再使用底层日志分析工具提取出来网络交互包,然后再使用wireshark对提取出来的网络交互包进行分析;

和模拟器运行直接抓网络交互包相比,这种方式有以下几种明显的缺点:

1、抓取日志以及分析操作繁琐;

2、硬件板内存资源有限,抓到的网络日志会丢包,特别是网络数据交互频繁的时候,丢包问题严重;

虽然有以上两项缺点,但是因为是真实的运行环境,对于LuatOS模拟器运行无法分析解决的问题,最终还是要通过这种方式解决;

如果需要用到这种方式来分析解决问题,当前阶段,客户只需要提交Luatools抓到的日志给合宙技术人员即可,由合宙技术人员进行分析;

后续我们也会在docs.openluat.com写一篇文章,单独讲解如何使用这种方法分析问题;大家如果有兴趣,可自行查看文档学习。

所以,在本章节接下来的内容中,我会针对一些常见的问题,分别使用Luatools应用日志分析LuatOS模拟器上的网络交互包分析 这两种方法来讲述;

9.2 socket创建

9.2.1 创建失败

Luatools应用日志分析

使用socket.create(adapter_id, task_name)接口创建socket时,常见的错误日志,如下图所示:

这种异常日志的意思是:无法为socket对象分配资源;

出现这种异常,通常是因为同时存在的socket数量超过了内核固件限制的最大数量:

  1. Air780系列/Air8000系列的模组,允许同时存在的socket对象数量为64个;
  2. Air6101系列/Air8101系列的模组,允许同时存在的socket对象数量为32个;

通常是以下两种原因造成的:

  1. 项目中开发业务代码时,socket.create和socket.release是一对逆操作,如果不断的create,但是没有release,就会出现这种问题,这种错误的使用方式比较常见;

例如在一个实际项目中,创建socket,socket连接,socket收发业务逻辑处理,如果业务逻辑处理过程中出现异常,就会断开socket,然后再销毁socket;这是一套完整的操作,出现异常后,会从socket创建开始重试;在实际代码开发过程中,有可能会忘记销毁socket;这样的话,每次重试都会创建一个新的socket;随着重试的次数增多,最终同时存在的socket对象就会超过上限而出现错误;例如下图中,如果漏写了红框内的代码,随着时间的推移,最终就会出现问题

  1. 项目中正常业务逻辑,同时使用的socket数量超过了限制,这种情况下,只能简化业务逻辑,减少对socket的使用;不过这种情况几乎不会出现,因为一个项目的正常业务逻辑,几乎不会出现同时使用几十个socket的情况;

LuatOS模拟器上的网络交互包分析

socket创建失败,不涉及网络交互包,所以不适用于此方法进行分析;

9.2 socket连接

9.2.1 dns解析失败

Luatools应用日志分析

如下图所示,解析域名:www.baideswrewru.coaswdm;

一共四个DNS服务器,每个服务器尝试解析3次,最终没有解析成功,提示:dns_run 649:no ipv6, no ipv4

出现此种错误,可以通过以下几步尝试分析解决:

1、确认下域名输入是否正确;

2、参考socket.setDNS(adapter_id, dns_index, ip)的说明配置自定义的DNS服务器;

LuatOS模拟器上的网络交互包分析

9.2.2 tcp握手连接失败

Luatools应用日志分析

当对端ip地址存在,端口不存在时,例如:连接合宙提供的netlab tcp服务器,ip地址为:112.125.89.8,端口42145(不存在)

会有以下异常日志: net_lwip_tcp_err_cb 662:adapter 1 socket 24 not closing, but error -14

这里有一个错误值:-14,表示:

ERR_RST = -14

  • 含义:连接被重置;
  • 场景:TCP 连接收到对端发送的 RST(重置)报文,导致连接强制关闭;

意味着,tcp连接已经发到了对端ip地址,但是被对端直接给rst了;

还有一种常见的错误是对端IP不存在,此时在异常日志中的错误值很可能是:-13;

如下图所示,连接一个不存在IP地址:113.126.89.86

net_lwip_tcp_err_cb 662:adapter 1 socket 5 not closing, but error -13

这里有一个错误值:-13,表示:

ERR_ABRT = -13

  • 含义:连接被中止。
  • 场景:TCP 连接被本地或对端异常中止(如收到 RST 包,或本地主动调用tcp_abort)。

具体到本日志,因为对端ip不存在,所以应该是tcp三次握手过程中,超时,内核固件主动断开了连接;

在LuatOS内核固件中,使用的是LwIP协议栈,此处的错误值的完整的取值范围如下所述(大家在平时开发过程中,如果遇到异常,根据日志中的错误值,可以参考这部分说明自行简单分析下是什么原因):

1、ERR_OK = 0:无错误,操作成功;

2、ERR_MEM = -1:内存分配失败;

3、ERR_BUF = -2:

  • 含义:缓冲区错误(不足或大小不匹配)。

  • 场景:发送数据时缓冲区空间不足(如pbuf大小不够存放待发送数据),或接收时缓冲区溢出。

4、ERR_TIMEOUT = -3

  • 含义:操作超时。

  • 场景:TCP 连接超时(未收到 SYN-ACK)、ARP 请求超时(未收到目标 MAC 地址应答)、netconn_recv等待数据超时等。

5、ERR_RTE = -4

  • 含义:路由错误(无可用路由)。

  • 场景:发送 IP 数据包时,lwip 路由表中找不到目标 IP 地址的有效路由(如未配置默认网关且无直连路由)。

6、ERR_INPROGRESS = -5

  • 含义:操作正在进行中。

  • 场景:非阻塞模式下的操作(如tcp_connect)尚未完成,需等待后续回调通知结果。

7、ERR_VAL = -6

  • 含义:无效值(参数值非法)。

  • 场景:传入 API 的参数值超出合法范围(如端口号为 0 或大于 65535,IP 地址格式错误等)。

8、ERR_WOULDBLOCK = -7

  • 含义:操作会阻塞(非阻塞模式下)。

  • 场景:非阻塞模式下调用netconn_recv时暂无数据可接收,或tcp_send时发送窗口未满导致无法立即发送。

9、ERR_USE = -8

  • 含义:地址已被使用。

  • 场景:绑定端口时(udp_bindtcp_bind),指定的端口已被其他连接占用。

10、ERR_ALREADY = -9

  • 含义:已在连接中。

  • 场景:对已处于连接过程中的 TCP 控制块再次调用tcp_connect

11、ERR_ISCONN = -10

  • 含义:连接已建立。

  • 场景:对已连接的 TCP 连接再次调用tcp_connect,或对已连接的netconn执行不需要连接的操作(如bind)。

12、ERR_CONN = -11

  • 含义:未连接状态。

  • 场景:对未建立连接的netconn调用send,或关闭未连接的连接。

13、ERR_IF = -12

  • 含义:底层网络接口(netif)错误。

  • 场景:网络接口未初始化、链路断开(如以太网物理层未连接),导致数据包无法发送。

14、ERR_ABRT = -13

  • 含义:连接被中止。

  • 场景:TCP 连接被本地或对端异常中止(如收到 RST 包,或本地主动调用tcp_abort)。

15、ERR_RST = -14

  • 含义:连接被重置。

  • 场景:TCP 连接收到对端发送的 RST(重置)报文,导致连接强制关闭。

16、ERR_CLSD = -15

  • 含义:连接已关闭。

  • 场景:对已正常关闭的连接执行读写操作(如tcp_send在连接关闭后调用)。

17、ERR_ARG = -16

  • 含义:非法参数(参数类型或指针无效)。

  • 场景:传入 API 的参数为NULL(如netconn_new传入无效的协议类型,tcp_send传入NULL缓冲区)。

18、ERR_IF_HIGH_WATER = -17

  • 含义:底层网络接口达到高水位线(缓冲区满)。

  • 场景:网络接口发送缓冲区已满,暂时无法接收新的发送请求(通常用于流量控制)。

19、ERR_IF_SUSPEND = -18

  • 含义:底层网络接口被挂起。

  • 场景:网络接口因某种原因(如手动暂停、错误恢复中)暂时不可用。

20、ERR_IF_OOS = -19

  • 含义:底层网络接口处于 “OutOfService”(服务中断)状态。

  • 场景:网络接口硬件故障、驱动错误等导致完全无法提供服务。

LuatOS模拟器上的网络交互包分析

9.2.3 tls握手连接失败

Luatools应用日志分析

在tls连接+仅支持单向认证的场景中,如果我们在客户端配置了错误的CA证书,本来应该是baidu_parent_ca.crt文件中的内容,现在配置为了错误的内容"123",如下图所示:

在连接过程中,会有以下异常日志:network_state_shakehand 807:0x2700, 3

这里有一个错误值:0x2700,MBEDTLS_ERR_X509_CERT_VERIFY_FAILED,表示证书验证失败;

在LuatOS内核固件中,使用的是MbedTLS开源库,TLS握手连接过程中,MbedTLS中有以下常见的错误值(大家在平时开发过程中,如果遇到异常,根据日志中的错误值,可以参考这部分说明自行简单分析下是什么原因,如果这里没有覆盖出现的错误值,可以使用AI工具提问,例如错误值为0x2700,可以提问:tls握手连接过程中,0x2700表示什么错误):

1、基础加密 / 解密错误

  • MBEDTLS_ERR_SSL_DECRYPTION_FAILED(0x7080):解密失败(如对称加密密钥错误、密文损坏)。

  • MBEDTLS_ERR_SSL_BAD_HMAC(0x7082):HMAC 验证失败(消息完整性校验不通过,可能被篡改)。

  • MBEDTLS_ERR_SSL_BAD_RECORD_MAC(0x7084):记录层 MAC 校验失败(与 HMAC 类似,针对 TLS 记录的完整性)。

2、协议版本与协商错误

  • MBEDTLS_ERR_SSL_UNSUPPORTED_VERSION(0x7000):不支持对方的 TLS 版本(如客户端要求 TLS 1.0,服务器仅支持 TLS 1.2+)。

  • MBEDTLS_ERR_SSL_VERSION_MISMATCH(0x7002):版本协商不匹配(如客户端和服务器协商的版本不一致)。

3、密码套件与算法错误

  • MBEDTLS_ERR_SSL_NO_SHARED_CIPHER(0x7004):无共同支持的密码套件(客户端与服务器的密码套件列表无交集)。

  • MBEDTLS_ERR_SSL_UNSUPPORTED_CIPHERSUITE(0x7006):对方选择的密码套件本地不支持。

  • MBEDTLS_ERR_SSL_UNSUPPORTED_EXTENSION(0x7008):不支持对方发送的 TLS 扩展(如 ALPN、SNI 扩展不被认可)。

4、证书验证错误

  • MBEDTLS_ERR_X509_CERT_VERIFY_FAILED(0x2700):证书验证失败(如签名无效、过期、吊销)。

  • MBEDTLS_ERR_X509_UNKNOWN_CA(0x2702):证书链中存在未知 CA(根证书不被信任)。

  • MBEDTLS_ERR_SSL_CERTIFICATE_REQUIRED(0x7040):服务器要求客户端证书,但客户端未提供。

  • MBEDTLS_ERR_SSL_BAD_CERTIFICATE(0x7042):证书格式错误或内容无效(如解析失败、字段不合法)。

5、密钥交换与认证错误

  • MBEDTLS_ERR_SSL_KEY_EXCHANGE_FAILED(0x7020):密钥交换过程失败(如 RSA 密钥解密失败、ECDH 密钥协商错误)。

  • MBEDTLS_ERR_SSL_BAD_CLIENT_KEY_EXCHANGE(0x7022):客户端密钥交换消息格式错误或内容无效。

  • MBEDTLS_ERR_SSL_BAD_CERTIFICATE_VERIFY(0x7044):客户端证书验证消息(CertificateVerify)无效(如签名不匹配)。

6、握手流程与消息错误

  • MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE(0x7060):收到意外的握手消息(如流程顺序错误,如先收到 Finished 再收到 Certificate)。

  • MBEDTLS_ERR_SSL_INVALID_HANDSHAKE_MESSAGE(0x7062):握手消息格式无效(如长度错误、字段缺失)。

  • MBEDTLS_ERR_SSL_HANDSHAKE_FAILURE(0x7064):握手失败(通用错误,可能因上述多种原因导致,如服务器拒绝客户端配置)。

  • MBEDTLS_ERR_SSL_HANDSHAKE_TIMEOUT(0x7066):握手超时(未在规定时间内收到对方响应)。

7、连接与状态错误

  • MBEDTLS_ERR_SSL_CONN_EOF(0x70a0):握手过程中连接被关闭(对方发送了关闭通知)。

  • MBEDTLS_ERR_SSL_CONNECTION_RESET(0x70a2):连接被重置(如底层 TCP 连接断开)。

  • MBEDTLS_ERR_SSL_WANT_READ / MBEDTLS_ERR_SSL_WANT_WRITE(0x70c0 / 0x70c2):非阻塞模式下需要继续读写数据(非错误,需重试)。

LuatOS模拟器上的网络交互包分析

9.2 socket收发数据和断开

这些业务逻辑的异常情况,就不再一一分析了,大家遇到问题后,可以参考上面几种异常的分析方法,自行分析;

第十部分:课后作业

实现一个TCP短连接项目(LuatOS模拟器上或者AirXXXX硬件板上开发调试,二选一);

项目需求:

1、在 https://netlab.luatos.com/ 上启动一个tcp server;

2、开发LuatOS项目,实现一个tcp client,每隔5分钟循环执行一次以下业务逻辑:

  • 连接tcp server;如果连接失败,退出本次循环;如果连接成功,继续向下执行;

  • 发送任意一包数据到server,如果发送失败,立即断开连接,退出本次循环;

  • server收到数据后,回复任意数据到client;

  • client等待接收数据,超时时间20秒;

  • 在20秒内,如果收到数据,立即断开连接,退出本次循环;

  • 20秒超时没有收到数据,立即断开连接,退出本次循环;

  • 在以上业务逻辑过程中,如果出现异常,立即断开连接,退出本次循环;

作业提交内容:

  1. 2个Lua文件:main.lua,tcp_client_main.lua;
  2. 1个运行日志文件