LuatOS socket
Hello,大家好,我是朱天华。
欢迎大家来到合宙LuatOS直播课堂,一起学习LuatOS课程。
第一部分:LuatOS课程背景
因为今天是我们LuatOS系列课程的第二讲,所以在这里我就不重复讲解整个LuatOS课程的背景了;
如果您还不清楚LuatOS课程背景,可以访问:LuatOS课程背景 这个链接,进行了解;
第二部分:LuatOS socket课程讲哪些内容
今天是LuatOS课程的第002讲,LuatOS socket;
LuatOS socket是LuatOS开发中最常用到的网络应用之一;
LuatOS socket课程主要包含以下几个部分:
- TCP/IP总体介绍;
- LuatOS上的
4G/WiFi/以太网
三种网络环境下的TCP/IP协议栈总体介绍; - TCP/UDP/SSL/证书以及socket概念;
- LuatOS socket核心库和libnet扩展库功能介绍;
- LuatOS socket client应用开发框架(TCP,UDP,SSL,网络环境检测看门狗);
- 如何分析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五层模型 |
---|---|---|
应用层 | 应用层 | 应用层 |
表示层 | ||
会话层 | ||
传输层 | 传输层 | 传输层 |
网络层 | 网络层 | 网络层 |
数据链路层 | 网络接口层 | 数据链路层 |
物理层 | 物理层 |
看了这张表之后,我们应该有以下几点认识:
- 都采用了分层的思想,将复杂的通信过程分解为更小、更易于管理的部分;
- 每一层都为其上层提供服务,并使用其下层提供的服务;
- 这三种网络模型,只是分层的颗粒度不一样,实际上,这三种网络模型的本质内容都是一样的;
既然这三种网络模型的本质内容是一样的,为什么还要存在三种网络模型呢?每种网络模型又有什么作用呢?
我们简单地看一下这三种网络模型的历史:
最终TCP/IP模型在实践中得到广泛应用;
至于TCP/IP四层模型和TCP/IP五层模型,二者的差别不大,主要体现在对最底层的划分不同:
- 四层模型隐藏了底层细节,将网络接入视为一个黑盒,更关注对软件层面的设计,不关心具体的硬件;
- 五层模型明确包含了物理硬件层;
四层模型和五层模型本质上是同一个东西的两种不同表述方式,所以我们接下来不纠结四层还是五层模型,而是统称为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等),它管的是“全局”的事情;
例如下图中红框所标注的网络单元中的网络路由选择,就是靠网络层实现的:
4. 数据链路层:数据链路层的主要工作是在本地局域网内将数据发送给接收方;
可以是带交换机功能的WiFi路由器和这个WiFi下的设备相互发送数据,或者这个WiFi路由器下的两个设备相互发送数据;
也可以是以太网交换机和这个交换机下的设备相互发送数据,或者这个交换机下的两个设备相互发送数据;
也可以是4G网络中,某个运营商基站和这个基站下的设备(例如手机)相互发送数据;
本地局域网的数据链路层也需要寻址,以太网和WiFi网络环境中,需要用到设备的MAC地址进行寻址;4G网络中,需要用到RNTI(无线网络临时标识符)进行寻址;
数据链路层的通信视野是局部的,只负责局域网内将数据发送给接收方,它管的是“局部”的事情;
例如下图中红框所标注的网络单元中的局域网内的寻址,就是靠数据链路层实现的:
5. 物理层:物理层的主要工作是传输0和1的电信号,这些电信号通过物理传输介质进行传输;
常见的物理层传输介质有:
(1) 光纤(我们日常生活中可以接触到的就是,运营商将光纤直接从运营商机房铺设到用户家中,提供百兆、千兆的高速互联网接入。你家办理的中国电信、联通或移动的“千兆光网”。你家的“光猫”就是接收光信号并将其转换为电信号的设备。)
(2) 双绞线(例如在以太网网络环境中常用的网线就是双绞线的一种)
(3) 无线电波(例如Wi-Fi、4G、5G网络都必须依赖无线电波作为载体才能进行通信。它们本质上都是通过调制解调技术,将数字信号(0和1)加载到无线电波上发射出去,接收端再解调回来)
不同的物理层传输介质,决定了电信号(0和1)的传输方式,物理介质的不同决定了电信号的传输带宽、速率、传输距离以及抗干扰性等等。
例如下图中红框所标注的网络单元间的数据传输,就是靠物理层实现的:
TCP/IP协议的上面四层(应用层,传输层,网络层,数据链路层),可以理解为是软件层面的实现,最终封装成数据包;
而TCP/IP协议的最下面一层(物理层)将数据包转化为 0 和 1 的电信号,通过物理介质进行传输才能到达下一个网络单元,因此物理介质可以认为是网络通信的基石。
大家现在对每一层的功能先有一个总体认识即可,不用关注细节;
后续会有各种网络环境下的实例来进一步说明如何工作;
3.3.2 数据封装/解封装
理解了功能分层的概念之后,我们再来看下面这张图片,了解一下数据的封装和解封装;
- 在数据发送端,
应用层
的网络协议生成应用数据
; 应用数据
向下传,交给传输层
,增加了传输层的头部信息,在头部信息中,有很多字段,其中有两个字段分别是源端口号
和目标端口号
,这两个端口号的作用是:定义了发送端和接收端的应用程序,例如http应用的默认端口号是80;可以提供两个主机之上端到端的通信;传输层头部+应用数据
向下传,交给网络层
,增加了网络层的头部信息,在头部信息中,有很多字段,其中有两个字段分别是源IP地址
和目标IP地址
,这两个IP地址的作用是:定义了发送端和接收端的网络设备地址;可以提供主机到主机的通信;网络层头部+传输层头部+应用数据
向下传,交给数据链路层
,增加了数据链路层的头部信息;- 最终,
数据链路层头部+网络层头部+传输层头部+应用数据
的数据包,通过物理层转换为0和1的电信号,然后通过物理传输介质发给了接收端; - 接收端收到数据后,从下到上,依次解析并且剥离出数据链路层头部、网络层头部、传输层头部,最终将应用数据交给应用程序去处理;
- 接收端处理完应用数据后,如果需要返回应用数据给发送端;此时发送端和接收端的角色互换,再走一遍数据分层的封装和解封装过程;
了解了功能分层和数据封装/解封装的核心工作原理之后,大家可能对TCP/IP网络数据传输的理解还是比较模糊,因为刚才说的这些都是偏理论的东西,理解起来不是那么直观;
接下来我使用电脑的网络浏览器访问http://airtest.openluat.com/,抓取一个完整的数据包封装实例,再来加深一下对功能分层和数据封装/解封装的理解:
接下来我们打开下面这张图片,实际分析一下http请求数据包
通过上面这个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/
我们打开下面这张图总体了解一下这个过程
第二种实例:使用中国移动4G手机卡网络的手机访问托管在中国移动的服务器https://docs.openluat.com/
和 中国移动4G手机卡的手机访问托管在中国电信的服务器 相比,中国移动4G手机卡的手机访问托管在中国移动的服务器,不需要经过中国移动和边界路由器和中国电信边界路由器,在中国移动的地市、省干、国干核心路由器之间进行路由转发,如下图所示(差异部分已通过浅绿色背景进行区分):
3.4.2 WiFi网络环境下TCP/IP协议的工作实例
第一种实例:使用中国移动WIFI网络的手机访问托管在中国电信的服务器https://docs.openluat.com/
第二种实例:使用中国移动WiFi网络的手机访问托管在中国移动的服务器https://docs.openluat.com/
和 中国移动WiFi网络的手机访问托管在中国电信的服务器 相比,中国移动4G手机卡的手机访问托管在中国移动的服务器,不需要经过中国移动和边界路由器和中国电信边界路由器,在中国移动的地市、省干、国干核心路由器之间进行路由转发,如下图所示(差异部分已通过浅绿色背景进行区分):
3.4.3 以太网网络环境下TCP/IP协议的工作实例
第一种实例:使用中国移动以太网网络的电脑访问托管在中国电信的服务器https://docs.openluat.com/
第二种实例:使用中国移动以太网网络的电脑访问托管在中国移动的服务器https://docs.openluat.com/
和 中国移动WiFi网络的电脑访问托管在中国电信的服务器 相比,中国移动以太网的电脑访问托管在中国移动的服务器,不需要经过中国移动和边界路由器和中国电信边界路由器,在中国移动的地市、省干、国干核心路由器之间进行路由转发,如下图所示(差异部分已通过浅绿色背景进行区分):
第四部分: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连接成功的网络包进行分析:
下面我们对一个TCP连接失败的网络包进行分析,客户端去连接一个不存在的服务器:
作用:
-
交换初始序列号 (ISN): 这是最核心的作用。序列号是TCP实现可靠性(确认、重传、排序)的根基。双方通过握手告知对方自己将从哪个号码开始编号字节数据。
-
确认双方通信能力: 证明客户端和服务器之间的网络路径在两个方向上(Client->Server 和 Server->Client)都是通的。如果握手失败,说明根本没法通信,应用层会立刻得到错误信息,而不用等待数据传输超时。
-
分配资源: 在握手成功后,双方的操作系统都会为这个连接分配资源,如创建socket、分配发送和接收缓冲区等。如果连接无法建立,就避免了资源的浪费。
2. 终止连接(四次挥手)
过程:
-
一方发送FIN包,表示“我数据发完了,要关闭我到你方向的连接”。
-
另一方ACK这个FIN包,表示“我知道你要关了”。
-
可能另一方向也还有数据要发送,等发完后,它再发送一个FIN包。
-
最初的一方再ACK这个FIN包。
抓包分析:
作用:
-
可靠地释放资源: 确保双方都知道通信即将结束,可以安全地释放为这个连接分配的所有内核资源(TCB、缓冲区等)。
-
保证数据完整性: 确保在连接关闭前,所有在途的数据包都已经被正确处理完毕。防止一方以为连接已断而另一方还在发送数据的情况。
而UDP呢?提供的是一个无连接的传输层服务,意思就是在正式发送应用数据之前,客户端和服务端没有三次握手连接,客户端和服务器端之间没有网络数据交互,如下图所示:
同理,因为就没有连接动作,所以UDP断开时,也没有四次挥手的断开连接动作;
5.1.2 是否可靠
TCP 提供的是一种可靠的传输层服务;TCP 通过多种机制确保数据准确无误地送达:
- 确认应答机制 (ACK): 接收方收到数据后,会发送一个确认报文(ACK)给发送方。
- 超时重传机制: 发送方在发送一个数据段后启动一个定时器。如果在规定时间内没有收到对应的ACK,它会重新发送该数据段。
- 序列号和确认号机制: 每个字节的数据都被分配一个序列号(Seq),ACK号则告诉发送方“我期望收到的下一个字节的序列号是多少”。这解决了数据包乱序和重复的问题。
- 校验和机制: 每个数据包都包含一个校验和。接收方会验证它,如果校验失败则丢弃该包,从而触发发送方的重传机制。
而UDP没有这些可靠的机制,我们看一下UDP发送数据的网络包:
和TCP相比,UDP数据发送出去之后,没有机制确认数据是否发送成功;在网络不稳定的情况下,会出现丢失数据的问题;
5.1.3 是否支持流量控制
TCP 提供的是一种支持流量控制的传输层服务;TCP 通过滑动窗口协议确保数据准确无误地送达:
- 使用滑动窗口协议来防止发送方发送数据过快,导致接收方缓冲区溢出。
- 接收方在ACK中会告知发送方自己当前剩余的缓冲区大小(窗口大小),发送方根据这个窗口大小调整发送速率。
下面的例子是一个出现0窗口的网络包,因为我这边没有环境复现这个问题,所以从网上找了一张图片,给大家看一下:
而UDP是没有ACK机制的,所以也没有窗口控制协议;
5.2 数字证书
5.2.1 数字证书是什么?
数字证书,是一种数字文件,其核心作用类似于现实世界中的身份证或营业执照。
它的根本目的是:将一个数字证书中的公钥与一个实体(个人、设备、组织、域名)的身份信息进行强绑定,并由一个受信任的第三方进行担保。
简单来说,数字证书就是证明“这个公钥确实属于某某某” 的电子凭证。
5.2.2 为什么需要数字证书?
要理解证书的价值,必须先理解它要解决的问题:中间人攻击。
- 当客户端(如浏览器)访问一个服务器时,如果一个攻击者拦截了通信,他可以将自己的数字证书(包含公钥)发送给客户端,并冒充服务器。
- 客户端用攻击者的公钥加密数据。
- 攻击者截获数据,用自己的私钥解密,窃取信息。
- 攻击者再用服务器的真实公钥加密数据,转发给服务器。整个过程神不知鬼不觉。
核心问题:如何确保你拿到的服务器的证书公钥,确实来自你真正想通信的一方,而不是攻击者?
数字证书就是答案。它由一个受信任的第三方机构(Certificate Authority, CA)对公钥和身份信息进行签名,任何信任该CA的人都可以验证这张证书的真伪,从而信任其中的公钥。
5.2.3 数字证书里包含什么?(X.509标准)
数字证书遵循X.509标准格式。它包含以下核心信息:
-
基本身份信息(证书主体)
-
版本号:使用的X.509版本(如v3)。
-
序列号:由CA分配的唯一标识符,用于吊销查询。
-
签名算法:CA签发此证书时使用的算法(如SHA256withRSA)。
-
颁发者:签发此证书的证书颁发机构(CA) 的名称。
-
有效期:证书有效的起止日期和时间。证书在此时间窗口外无效。
-
主题:证书持有者的身份信息。最重要的是通用名称(Common Name, CN),通常是域名(如 docs.openluat.com)。
- 核心密码学材料
-
公钥本身(一串很长的数字)。
-
该公钥所使用的算法(如RSA 2048位、ECC)。
- 信任的证明(CA的签名)
- 数字签名:这是最关键的部分。首先对上述所有信息(除了签名本身)计算一个哈希摘要,然后对这个摘要使用CA自己的私钥进行加密,得到的结果就是数字签名。
我们现在访问docs.openluat.com,看下docs网站的数字证书:
点击docs.openluat.com的证书文件的每个字段,我们来对照说明一下;
5.2.4 数字证书的工作原理:信任链(Chain of Trust)
数字证书的信任并非凭空产生,它依赖于一个层级化的信任模型,称为公钥基础设施(Public Key Infrastructure, PKI)。
5.2.4.1 信任链的组成:
-
根证书(Root CA Certificate)
-
位于信任链顶端的、自签名的证书。
-
它们的公钥被预先内置在操作系统(如Windows、macOS)和浏览器(如Chrome、Firefox)的信任根证书存储区中。这是所有信任的起点。
-
根CA的私钥被极其严格地离线保管,几乎不直接用于签发服务器证书。
- 中间证书
-
由根CA签发,用于代表根CA去实际签发最终的用户证书。
-
这种层级结构增加了安全性:即使中间CA的私钥被泄露,可以迅速吊销该中间证书,而根CA依然安全,无需重建整个信任体系。
-
一个服务器证书通常伴随着一个或多个中间证书。
- 终端实体证书
- 也就是我们通常为网站、服务申请和使用的证书。
如下图所示,docs.openluat.com服务器使用就是一个三层证书结构:
5.2.4.2 证书验证过程(以浏览器访问https://docs.openluat.com网站为例):
当浏览器收到网站的证书时,它会执行以下验证步骤:
1. 验证基本信息:检查证书的有效期是否当前时间在内,检查证书的“主题”或“SAN”字段是否包含正
2. 验证签名(构建信任链):
-
浏览器使用中间证书A里的公钥,去解密网站证书的数字签名,得到一个哈希值H1。
-
浏览器对网站证书的内容(除签名外)用相同的哈希算法计算,得到另一个哈希值H2。
-
如果H1 == H2,则说明:
- 网站证书的内容完整,未被篡改。
- 该证书确实是由中间证书A对应的CA签发的(因为只有用中间CA的私钥才能生成能用其公钥解开的签名)。
3. 递归验证:
-
浏览器接着发现,它可能不直接信任中间证书A。于是它继续向上验证:使用根证书的公钥,去验证中间证书A的签名。
-
因为根证书的公钥已经内置在浏览器的信任存储中,且是自签名的,验证到此结束。
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。
主要目标:
- 加密(Encryption): 混淆传输的数据,防止第三方窃听。
- 认证(Authentication): 验证通信方的身份(通常是验证服务器的身份,也可选择验证客户端身份),防止中间人攻击。
- 完整性(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,如下图所示:
如果在TCP之上支持了TLS,在传输数据之前,Client和Server会动态协商一个密钥,密钥协商成功后,后续发送方传输的应用数据都会通过密钥加密,然后再传输到接收方,接收方使用密钥进行解密;只有发送端和接收端的应用程序才能加密数据以及解密为原始明文数据,在网络传输过程中,网络中间节点因为不知道密钥,即使截获了数据,因为没有密钥,也无法解密数据,如下图所示:
5.3.3 SSL/TLS 如何工作?—— 握手过程详解
SSL/TLS 的核心是其握手过程。这个过程在双方开始传输实际应用数据之前进行,目的是协商连接参数、验证身份并生成会话密钥。以下是TLS 1.2 握手流程:
整个过程的目标是在客户端和服务器之间安全地协商出两样东西:
- 会话密钥(Session Keys):用于后续通信使用的对称加密密钥。
- 加密参数:使用哪种对称加密算法(如 AES)、哪种哈希算法(如 SHA256)等。
- 整个握手过程在 TCP 连接建立之后进行,可以概括为以下四个主要阶段和 10 个步骤:
第一阶段:Hello 消息交换(协商安全参数)
步骤 1: Client Hello
客户端向服务器发起握手,发送一个 Client Hello
消息,包含以下关键信息:
- 客户端随机数(Client Random):一个由客户端生成的 32 字节随机数,用于后续生成主密钥。
- 支持的协议版本:例如
TLS 1.2
。 - 支持的密码套件列表(Cipher Suites):客户端支持的所有加密算法组合的列表,按优先级排序。例如
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
。
步骤 2: Server Hello
服务器响应客户端的 Hello
消息,发送一个 Server Hello
消息,包含以下关键信息:
- 服务器随机数(Server Random):一个由服务器生成的 32 字节随机数,同样用于生成主密钥。
- 选择的协议版本:通常是双方都支持的最高版本,例如
TLS 1.2
。 - 选择的密码套件:从客户端提供的列表中选出一个服务器也支持的套件,例如
TLS_RSA_WITH_AES_128_CBC_SHA
。 - 会话ID(Session ID):服务器为本次会话生成的一个唯一ID,用于后续会话恢复。
- 选择的扩展。
至此,双方就使用哪种加密算法
进行通信达成了一致。
第二阶段:服务器认证与密钥交换(Server Key Exchange)
步骤 3: Server Certificate
服务器将自己的数字证书发送给客户端。这个证书包含了服务器的公钥、域名、颁发机构(CA)等信息,并由CA的私钥签名。客户端会验证该证书的有效性(是否过期、域名是否匹配、签发者是否可信等),以确认正在通信的确实是目标服务器,而不是中间人。
步骤 4: Server Key Exchange Message(可选)
注意:此步骤仅在选择的密码套件不是 RSA
密钥交换时才需要(例如使用 DHE
或 ECDHE
时)。如果使用 RSA
套件(如本例),服务器会跳过这一步。 对于 DHE
/ECDHE
套件,服务器会在此消息中发送其临时密钥交换参数(如迪菲-赫尔曼参数),并用证书对应的私钥签名,以供客户端验证。
步骤 5: Server Hello Done
服务器发送一个 Server Hello Done
消息,表示它已经将所有初始协商信息发送完毕。
第三阶段:客户端认证与密钥交换(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
。
步骤 8: Certificate Verify(可选)
如果客户端发送了证书(步骤6),它还需要用其私钥对之前所有握手消息的哈希值进行签名,并发送给服务器。服务器用客户端证书中的公钥验证这个签名,以证明客户端确实拥有该证书对应的私钥,完成了客户端认证。
第四阶段:完成握手(Switch to Encryption)
此时,客户端和服务器已经拥有了生成会话密钥的所有材料:
Client Random
Server Random
Pre-Master Secret
双方使用相同的伪随机函数(PRF),根据这三个参数独立计算出相同的:
- Master Secret(主密钥)
-
最终用于加密和完整性验证的会话密钥块(Key Block),其中包含:
-
客户端到服务器的对称加密密钥
-
服务器到客户端的对称加密密钥
-
客户端到服务器的MAC验证密钥
-
服务器到客户端的MAC验证密钥
步骤 9: Change Cipher Spec
客户端发送一个 Change Cipher Spec
消息。这是一个简单的信号,通知服务器:“从现在开始,我发送的所有消息都将使用我们刚刚协商好的加密算法和密钥进行加密。”
紧接着,客户端发送 Finished
消息。这条消息本身已经是加密状态,它包含之前所有握手消息的哈希值的MAC(消息认证码)。服务器收到后,会解密并验证这个MAC。如果验证成功,说明:
- 客户端计算出的密钥与服务器的一致。
- 握手过程没有被篡改。
步骤 10: Change Cipher Spec & Finished
服务器同样发送 Change Cipher Spec
消息,通知客户端:“我也准备好了,后续消息将加密。”
紧接着,服务器发送加密后的 Finished
消息。客户端同样进行解密和验证。
握手完成
至此,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 | 在局域网内传输数据帧 |
物理层 | 物理层 | 传输原始比特流 |
从这张表可以清晰地看到:
- 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 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;
关于网络适配器类型的常量:
注意:
1. 网络适配器,它的一个更广为人知的名字是——网卡,后续我们会使用网卡和网络适配器这两种名称,大家只要知道这两种名称表示同一个概念就行了;
2. LuatOS上的网络适配器和电脑上的网络适配器的作用是完全一样的,我们先来看一张电脑上的网络适配器图片
在上面这张图片中,有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地址的分配,完全由核心库或者扩展库实现,使用起来非常简单;
socket.LWIP_GP
常量含义:4G网卡;
类似于我们平常手机使用运营商的手机卡上网时使用的网卡;
LWIP是指:传输层和网络层使用的是LuatOS内核固件中的LwIP协议栈;
GP是GPRS的缩写,GPRS是2G网络时代的分组数据网络,此处用来代指移动蜂窝数据网络,例如4G网络;
数据类型:number;
取值范围:1;
示例代码:socket.localIP(socket.LWIP_GP),基于4G网卡上网时,获取本地的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地址;
socket.LWIP_ETH
常量含义:使用LwIP协议栈的以太网卡;
例如通过SPI外挂CH390实现以太网上网时的网卡;
例如通过RMII接口外挂PHY芯片实现以太网上网时的网卡;
LWIP是指:传输层和网络层使用的是LuatOS内核固件中的LwIP协议栈;
ETH是Ethernet的缩写,意思是以太网;
数据类型:number;
取值范围:4;
示例代码:socket.localIP(socket.LWIP_ETH),基于LwIP协议栈的以太网卡上网时,获取本地的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地址;
socket.USB
常量含义:USB接口的以太网卡;
USB接口的以太网卡是一种通过USB线虚拟出有线网卡功能的技术;
以电脑为例,它允许电脑通过一根USB数据线,将手机或者合宙AirXXX硬件板连接到电脑,
并让电脑像使用普通有线网卡一样通过该USB线,使用手机或者合宙AirXXX硬件板来上网,
手机或者合宙AirXXX硬件板就可以看做电脑的USB接口的以太网卡;
常见的USB以太网卡又可以分为 USB RNDIS以太网卡 和 USB ECM以太网卡两种:
在USB之上
通过USB接口模拟的以太网卡(如 RNDIS 协议),
使用LWIP协议栈的USB网卡,值为6;
数据类型:number;
取值范围:暂无;
示例代码:socket.localIP(socket.USB);
socket.LWIP_USER0
常量含义:使用LWIP协议栈的自定义网卡0,值为0;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER1
常量含义:使用LWIP协议栈的自定义网卡1,值为1;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER2
常量含义:使用LWIP协议栈的自定义网卡2,值为2;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER3
常量含义:使用LWIP协议栈的自定义网卡3,值为3;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER4
常量含义:使用LWIP协议栈的自定义网卡4,值为4;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER5
常量含义:使用LWIP协议栈的自定义网卡5,值为5;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER6
常量含义:使用LWIP协议栈的自定义网卡6,值为6;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_USER7
常量含义:使用LWIP协议栈的自定义网卡7,值为7;
数据类型:number;
取值范围:暂无;
示例代码:暂无;
socket.LWIP_GP_GW
常量含义:4G代理网关;
数据类型:number;
取值范围:暂无;
示例代码:netdrv.setup(socket.LWIP_GP_GW, netdrv.WHALE);
关于网络事件的常量:
socket.LINK
常量含义:表示socket的物理链路连接状态事件,
当设备与网络的物理连接(如4G模块的基站连接)建立或断开时触发;
数据类型:number;
取值范围:暂无;
示例代码:if event == socket.LINK then
log.info("连接成功")
socket.ON_LINE
常量含义:表示socket的网络就绪状态事件,
设备成功接入网络时触发;
数据类型:number;
取值范围:暂无;
示例代码:if event == socket.ON_LINE then
socket.tx(netc, "hello,luatos!")
socket.EVENT
常量含义:表示通用事件标识,具体含义需结合上下文或参数判断,
用于标识自定义事件或未明确分类的socket事件;
数据类型:number;
取值范围:暂无;
示例代码:if event == socket.EVENT then
socket.rx(netc, rxbuf)
socket.wait(netc)
if rxbuf:used() > 0 then
log.info("收到", rxbuf:toStr(0,rxbuf:used()):toHex())
log.info("发送", rxbuf:used(), "bytes")
socket.tx(netc, rxbuf)
end
socket.TX_OK
常量含义:表示数据发送成功事件,
当socket成功发送数据到对端时触发;
数据类型:number;
取值范围:暂无;
示例代码:if event == socket.TX_OK then
socket.wait(netc)
log.info("发送完成")
socket.CLOSED
常量含义:表示socket连接已关闭事件,
当连接因主动关闭、故障中断或服务器断开时触发;
数据类型:number;
取值范围:暂无;
示例代码:if event == socket.CLOSED then
sys.publish("socket_disconnect")
end
6.2 函数详解
socket.localIP(adapter)
功能
获取本地 ip
参数
adapter
参数含义:表示适配器序号;
数据类型:number;
取值范围:本文常量详解章节中的所有网络适配器常量,例如:socket.LWIP_GP;
是否必选:可选传入此参数;
注意事项:若不填入此参数,默认是平台自带的能上外网的适配器;
参数示例:socket.LWIP_GP;
返回值
local ip, netmask, gateway = socket.localIP()
有三个返回值 ip, netmask, gateway
ip
含义说明:返回IP地址;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"10.39.154.32";
netmask
含义说明:返回网络掩码;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"255.255.255.255";
gateway
含义说明:返回网关IP;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"0.0.0.0";
示例
local function get_ip()
while 1 do
sys.wait(3000)
local ip, netmask, gateway = socket.localIP()
log.info("socket", "ip", ip, "netmask", netmask, "gateway", gateway)
end
end
sys.taskInit(get_ip)
-- 日志打印示例
-- socket ip 10.39.154.32 netmask 255.255.255.255 gateway 0.0.0.0
socket.create(adapter, task_name)
功能
在指定网卡上申请一个 socket_ctrl
参数
adapter
参数含义:上网使用的网卡ID;
数据类型:number或者nil;
取值范围:number类型时,取值范围参考socket api中的常量详解;
是否必选:可选传入此参数;
注意事项:如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡;
除非你socket请求时,一定要使用某一种网卡,才设置此参数;
如果没什么特别要求,不要设置此参数,使用系统中设置的默认网卡即可;
一般来说,LuatOS的网络应用demo中都会有netdrv_device功能模块设置默认网卡;
所以建议不要设置此参数,直接使用netdrv_device设置的默认网卡就行;
参数示例:socket.LWIP_GP表示使用4G网卡;
task_name
参数含义:为消息通知的taskName;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
返回值
local netc = socket.create(nil, "Mysocket")
netc
含义说明:成功返回socket_ctrl,失败返回nil;
数据类型:userdata;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
--[[
当通过回调函数回调消息时,输入给function一共3个参数:
param1为申请的socket_ctrl
param2为具体的消息,只能是socket.LINK, socket.ON_LINE, socket.TX_OK, socket.EVENT, socket.CLOSED等等
param3为消息对应的参数,目前只有0和-1,0表示成功或者可能有新数据(具体消息为socket.EVENT),-1表示失败或者有异常,需要断开重连
]]
-- 创建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.debug(ctrl, onoff)
功能
配置是否打开 debug 信息
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
onoff
参数含义:表示是否打开debug信息;
数据类型:boolean;
取值范围:true打开debug开关,false关闭debug开关;
是否必选:可选传入此参数,若不填此参数,默认为false;
注意事项:暂无;
参数示例:true;false;
返回值
nil
示例
local function socketTask()
local netc = socket.create(nil, netCB) --创建一个链接
socket.debug(netc, true)--开启socket层的debug日志,方便寻找问题
socket.config(netc, nil, is_udp, nil, 300, 5, 6) --配置TCP链接的参数,开启保活,防止长时间无数据交互服务器踢掉模块
while true do
--真正去链接服务器
local succ, result = socket.connect(netc, server_ip, server_port)
--链接成功后循环发送数据
while succ do
local Heartbeat_interval = Heartbeat_interval * 60 * 1000
sys.wait(Heartbeat_interval)
socket.tx(netc, heart_data)
end
--链接不成功5S重连一次
if not succ then
log.info("未知错误,5秒后重连")
uart.write(1, "未知错误,5秒后重连")
else
local result, msg = sys.waitUntil("socket_disconnect")
end
log.info("服务器断开了,5秒后重连")
uart.write(1, "服务器断开了,5秒后重连")
socket.close(netc)
sys.wait(5000)
end
end
sys.taskInit(socketTask)
socket.config(ctrl, local_port, is_udp, is_tls, keep_idle, keep_interval, keep_cnt, server_cert, client_cert, client_key, client_password)
功能
配置 socket ctrl 一些信息
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
local_port
参数含义:本地端口号;
数据类型:number;
取值范围:端口号需要小于60000;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:80;
is_udp
参数含义:是否是UDP;
数据类型:boolean;
取值范围:true或false;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:true;
is_tls
参数含义:是否是加密传输;
数据类型:boolean;
取值范围:true或false;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:true;
keep_idle
参数含义:在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,
即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h);
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数,如果留空则表示不启;
注意事项:如果是不支持标准posix接口的网卡(比如W5500),则为心跳间隔;
参数示例:300;
keep_interval
参数含义:在tcp保活设定时间之后,没有接收到对方确认,
继续发送保活探测包的发送间隔,默认值为75s。;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:75;
keep_cnt
参数含义:在TCP保活打开的情况下,最大允许发送保活探测包的次数,
到达此次数后直接放弃尝试,并关闭连接,默认值为9(次);
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:9;
server_cert
参数含义:TCP服务器ca证书数据;
数据类型:string或者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 netc = socket.create(nil, netCB)
netc
含义说明:成功返回true,失败返回false;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
local function socketTask()
local netc = socket.create(nil, netCB) --创建一个链接
socket.debug(netc, true)--开启socket层的debug日志,方便寻找问题
socket.config(netc, nil, is_udp, nil, 300, 5, 6) --配置TCP链接的参数,开启保活,防止长时间无数据交互服务器踢掉模块
while true do
--真正去链接服务器
local succ, result = socket.connect(netc, server_ip, server_port)
--链接成功后循环发送数据
while succ do
local Heartbeat_interval = Heartbeat_interval * 60 * 1000
sys.wait(Heartbeat_interval)
socket.tx(netc, heart_data)
end
--链接不成功5S重连一次
if not succ then
log.info("未知错误,5秒后重连")
uart.write(1, "未知错误,5秒后重连")
else
local result, msg = sys.waitUntil("socket_disconnect")
end
log.info("服务器断开了,5秒后重连")
uart.write(1, "服务器断开了,5秒后重连")
socket.close(netc)
sys.wait(5000)
end
end
sys.taskInit(socketTask)
socket.linkup(ctrl)
功能
等待网卡 linkup(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, result = socket.linkup(ctrl)
succ
含义说明:表示函数调用的同步结果,true没有异常发生,false失败了,如果false则不需要看下一个返回值了;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
result
含义说明:true已经linkup,false没有linkup,之后需要接收socket.LINK消息;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
socket.connect(ctrl, ip, remote_port, need_ipv6_dns)
功能
作为客户端连接服务器(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
ip
参数含义:ip地址 或者 int ip或者域名,如果是IPV4,可以是大端格式的number值;
数据类型:string或者number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
remote_port
参数含义:服务器端口号,小端格式;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
need_ipv6_dns
参数含义:域名解析是否要IPV6,true要,false不要,默认false不要,只有支持IPV6的协议栈才有效果;
数据类型:boolean;
取值范围:true或false;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, result = socket.connect(ctrl, ip, remote_port)
succ
含义说明:true没有异常发生,false失败了,如果false则不需要看下一个返回值了,如果有异常,后续要close;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
result
含义说明:true已经connect,false没有connect,之后需要接收socket.ON_LINE消息;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
--[[
常见的连接失败的code值, 会在日志中显示
-1 底层内存不足
-3 超时
-8 端口已经被占用
-11 链接未建立
-13 模块主动断开连接
-14 服务器主动断开连接
]]
socket.discon(ctrl)
功能
作为客户端断开连接(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, result = socket.discon(ctrl)
succ
含义说明:true没有异常发生,false失败了,如果false则不需要看下一个返回值了;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
result
含义说明:true已经断开,false没有断开,之后需要接收socket.CLOSED消息;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
socket.close(ctrl)
功能
强制关闭 socket(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
nil
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
socket.tx(ctrl, data, ip, port, flag)
功能
发送数据给对端,UDP 单次发送不要超过 1460 字节,否则很容易失败(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
data
参数含义:待发送的数据 或者 userdata zbuff 要发送的数据;
数据类型:string/zbuff;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
ip
参数含义:对端IP,如果是TCP应用则忽略,
如果是UDP,如果留空则用connect时候的参数,
如果是IPV4,可以是大端格式的number值;
数据类型:string或者number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
port
参数含义:对端端口号,小端格式,如果是TCP应用则忽略,如果是UDP,如果留空则用connect时候的参数;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
flag
参数含义:发送参数,目前预留,不起作用;
数据类型:int;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, full, result = socket.tx(ctrl, "123456", "xxx.xxx.xxx.xxx", xxxx)
succ
含义说明:true没有异常发生,false失败了,如果false则不需要看下一个返回值了,
如果false,后续要close;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
full
含义说明:true缓冲区满了,false没有满,
如果true,则需要等待一段时间或者等到socket.TX_OK消息后再尝试发送,同时忽略下一个返回值;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
result
含义说明:true已经收到应答,false没有收到应答,
之后需要接收socket.TX_OK消息, 也可以忽略继续发送,直到full==true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
socket.rx(ctrl, buff, flag, limit)
功能
接收对端发出的数据,注意数据已经缓存在底层,使用本函数只是提取出来,UDP 模式下一次只会取出一个数据包
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
buff
参数含义:zbuff 存放接收的数据,如果缓冲区不够大会自动扩容;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
flag
参数含义:接收参数,目前预留,不起作用;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
limit
参数含义:接收数据长度限制,如果指定了,则只取前N个字节;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, data_len, remote_ip, remote_port = socket.rx(ctrl)
succ
含义说明:true没有异常发生,false失败了,如果false则不需要看下一个返回值了,
如果false,后续要close;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
data_len
含义说明:本次接收到数据长度;
数据类型:int;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
remote_ip
含义说明:对端IP,只有UDP模式下才有意义,TCP模式返回nil,
注意返回的格式,如果是IPV4,1byte 0x00 + 4byte地址;如果是IPV6,1byte 0x01 + 16byte地址;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:如果是IPV4,以返回的hex值"00707D5908"为例,
此返回值共有5个字节,其中第一个字节固定为0x00,第二个字节0x70,第三个字节0x7D,第四个字节0X59,第五个字节0x08
转化为由4个点分十进制数组成的"ip地址"就为112.125.89.8;
remote_port
含义说明:对端port,只有UDP模式下才有意义,TCP模式返回0;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
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.read(netc, len)
功能
读取数据(非 zbuff 版本)
netc
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
len
参数含义:限制读取数据长度,可选,不传就是读出全部;
数据类型:int;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local success, data, ip, port = socket.read(netc, 1024)
success
含义说明:true表示读取成功,false表示读取失败;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
data
含义说明:读取的数据,仅当读取成功时有效;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
ip
含义说明:对方IP地址,仅当读取成功且UDP通信时有效;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
port
含义说明:对方端口,仅当读取成功且UDP通信时有效;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
-- 本函数于2024.4.8添加, 用于简易读取不大的数据
-- 请优先使用socket.rx函数, 本函数主要用于固件不含zbuff库时的变通调用
local ok, data = socket.read(netc, 1500)
if ok and data > 0 then
log.info("读取到的数据", data)
end
socket.wait(ctrl)
功能
等待新的 socket 消息,在连接成功和发送数据成功后,使用一次将 network 状态转换到接收新数据
(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, result = socket.wait(ctrl)
succ
含义说明:true没有异常发生,false失败了,
如果false则不需要看下一个返回值了,如果false,后续要close;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
result
含义说明:true有新的数据需要接收,false没有数据,之后需要接收socket.EVENT消息;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
socket.listen(ctrl)
功能
作为服务端开始监听(此函数给 libnet 扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用)
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, result = socket.listen(ctrl)
succ
含义说明:true没有异常发生,false失败了,
如果false则不需要看下一个返回值了,如果false,后续要close;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
result
含义说明:true已经connect,false没有connect,之后需要接收socket.ON_LINE消息;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
--此函数给libnet扩展库使用,用户不用深入了解,也不建议在应用脚本中去使用,不再举例。
socket.accept(ctrl)
功能
作为服务端接收到一个新的客户端,注意,如果是类似 W5500 的硬件协议栈不支持 1 对多,则不需要第二个参数
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local succ, new_netc = socket.accept(ctrl, cb)
succ
含义说明:true没有异常发生,false失败了,
如果false则不需要看下一个返回值了,如果false,后续要close;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
new_netc
含义说明:或者 nil 如果支持1对多,则会返回新的ctrl,自动create,如果不支持则返回nil;
数据类型:userdata;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
function ipv6task(d1Name, txqueue, rxtopic)
-- 本地监听的端口
local port = 14000
local rx_buff = zbuff.create(1024)
local tx_buff = zbuff.create(4 * 1024)
local netc = socket.create(nil, d1Name)
socket.config(netc, 14000)
log.info("任务id", d1Name)
while true do
log.info("socket", "开始监控")
local result = libnet.listen(d1Name, 0, netc)
if result then
log.info("socket", "监听成功")
result = socket.accept(netc, nil) --只支持1对1
if result then
log.info("客户端连上了")
end
else
log.info("socket", "监听失败!!")
end
while result do
-- log.info("socket", "调用rx接收数据")
local succ, param = socket.rx(netc, rx_buff)
if not succ then
log.info("客户端断开了", succ, param, ip, port)
break
end
if rx_buff:used() > 0 then
log.info("socket", "收到客户端数据,长度", rx_buff:used())
local data = rx_buff:query() -- 获取数据
sys.publish(rxtopic, "downlink", data)
rx_buff:del()
end
-- log.info("libnet", "调用wait开始等待消息")
result, param, param2 = libnet.wait(d1Name, 15000, netc)
log.info("libnet", "wait", result, param, param2)
if not result then
-- 网络异常了
log.info("socket", "客户端断开了", result, param)
break
elseif #txqueue > 0 then
local force_close = false
while #txqueue > 0 do
local data = table.remove(txqueue, 1)
if not data then
break
end
log.info("socket", "上行数据长度", #data)
if data == "close" then
--sys.wait(1000)
force_close = true
break
end
tx_buff:del()
tx_buff:copy(nil, data)
result,param = libnet.tx(d1Name, 15000, netc, tx_buff)
log.info("libnet", "发送数据的结果", result, param)
if not result then
log.info("socket", "数据发送异常", result, param)
break
end
end
if force_close then
break
end
end
end
log.info("socket", "连接已断开,继续下一个循环")
libnet.close(d1Name, 5000, netc)
-- log.info(rtos.meminfo("sys"))
sys.wait(50)
end
end
sys.taskInit(ipv6test)
socket.state(ctrl)
功能
获取 socket 当前状态
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local state, str = socket.state(ctrl)
state
含义说明:如果支持1对多,则会返回新的ctrl,自动create,如果不支持则返回nil;
数据类型:number或者nil;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
_str _
含义说明:输入参数正确的情况下,返回状态的中文描述,否则返回nil;
数据类型:string/nil;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
local state, str = socket.state(ctrl)
log.info("state", state, str)
state 0 "硬件离线",
1 "离线",
2 "等待DNS",
3 "正在连接",
4 "正在TLS握手",
5 "在线",
6 "在监听",
7 "正在离线",
8 "未知"
socket.release(ctrl)
功能
主动释放掉 socket_ctrl
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
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
socket.setDNS(adapter_index, dns_index, ip)
设置 DNS 服务器
参数
adapter_index
参数含义:适配器序号,前面章节网络适配器常量里面的适配器,
如果不填,会选择最后一个注册的适配器;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
dns_index
参数含义:dns服务器序号,从1开始;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
ip
参数含义:ip地址 或者 int ip或者域名,如果是IPV4,可以是大端格式的number值;
数据类型:string或者number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local result = socket.setDNS(adapter_index, dns_index, ip)
result
含义说明:成功返回true,失败返回false;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
function ipv6test()
-- 启用IPv6, 默认关闭状态,必须在驻网前开启
-- 注意, 启用IPv6, 联网速度会慢2~3秒
mobile.ipv6(true)
log.info("ipv6", "等待联网")
sys.waitUntil("IP_READY")
log.info("ipv6", "联网完成")
sys.wait(100)
socket.setDNS(nil, 1, "119.29.29.29")
socket.setDNS(nil, 2, "114.114.114.114")
-- 开始正在的逻辑, 发起socket链接,等待数据/上报心跳
local taskName = "ipv6client"
local topic = taskName .. "_txrx"
local txqueue = {}
sys.taskInitEx(ipv6task, taskName, netCB, taskName, txqueue, topic)
while 1 do
local result, tp, data = sys.waitUntil(topic, 30000)
if not result then
-- 等很久了,没数据上传/下发, 发个日期心跳包吧
table.insert(txqueue, string.char(0))
sys_send(taskName, socket.EVENT, 0)
elseif tp == "uplink" then
-- 上行数据, 主动上报的数据,那就发送呀
table.insert(txqueue, data)
sys_send(taskName, socket.EVENT, 0)
elseif tp == "downlink" then
-- 下行数据,接收的数据, 从ipv6task来的
-- 其他代码可以通过 sys.publish()
log.info("socket", "收到下发的数据了", #data)
end
end
end
sys.taskInit(ipv6test)
socket.sslLog(log_level)
功能
设置 SSL 的 log 等级
参数
log_level
参数含义:mbedtls log等级;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
nil
示例
--[[
SSL/TLS log级别说明
0不打印
1只打印错误和警
2大部分info
3及3以上详细的debug
过多的信息可能会造成内存碎片化
]]
-- 打印大部分info日志
socket.sslLog(2)
socket.adapter(index)
功能
查看网卡适配器的联网状态
参数
index
参数含义:需要查看的适配器序号,可以留空会查看全部网卡,直到遇到IP READY的,
如果指定网卡,支持上文中网卡适配器中的所有常量;
数据类型:int;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local isReady,index = socket.adapter()
isReady
含义说明:被查看的适配器是否IP READY,true表示已经准备好可以联网了,false暂时不可以联网;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
index
含义说明:最后一个被查看的适配器序号;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
while not socket.adapter(socket.dft()) do
log.warn("tcp_ssl_ca_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.remoteIP(ctrl)
功能
获取对端 ip
参数
ctrl
参数含义:socket.create得到的ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local ip1,ip2,ip3,ip4 = socket.remoteIP(ctrl)
ip1
含义说明:IP1,如果为nil,则表示没有获取到IP地址;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"10.39.154.32";
ip2
含义说明:IP2,如果为nil,则表示没有IP2;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"10.39.154.32";
ip3
含义说明:IP3,如果为nil,则表示没有IP3;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"10.39.154.32";
ip4
含义说明:IP4,如果为nil,则表示没有IP4;
数据类型:string;
取值范围:暂无;
注意事项:暂无;
返回示例:"10.39.154.32";
示例
-- 注意: ,必须在接收到socket.ON_LINE消息之后才可能获取到,最多返回4个IP。
-- socket.connect里如果remote_port设置成0,则当DNS完成时就返回socket.ON_LINE消息
local ip1,ip2,ip3,ip4 = socket.remoteIP(ctrl)
socket.dft(id)
功能
设置默认网络适配器编号
参数
id
参数含义:默认适配器编号,若不传,则打包获取;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local id, last_id = socket.dft()
id
含义说明:默认适配器编号;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:socket.LWIP_ETH;
last_id
含义说明:最后一个注册的适配器编号(2025.7.25新增);
数据类型:int;
取值范围:暂无;
注意事项:暂无;
返回示例:socket.LWIP_ETH;
示例
-- 本函数于 2025.1.6 新增
-- 获取当前默认适配器编号
local id = socket.dft()
-- 设置默认适配器编号
socket.dft(socket.LWIP_ETH)
-- 获取当前默认适配器编号, 及最后一个注册的适配器编号
local id, last_id = socket.dft()
log.info("当前默认适配器编号", id, "最后一个注册的适配器编号", last_id)
-- 1. 当前的默认网卡, 可以获取, 可以设置, 就是socket.dft(id)的第一个参数, 也是第一个返回值
-- 2. 最后注册的网卡, 可以获取, 不支持设置, 就是socket.dft()的第二个返回值
socket.sntp(sntp_server,index)
功能
sntp 时间同步
参数
sntp_server
参数含义:sntp服务器地址 选填;
数据类型:string或者table;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
index
参数含义:上网使用的网卡ID;
数据类型:number或者nil;
取值范围:number类型时,取值范围参考socket api中的常量详解;
是否必选:可选传入此参数;
注意事项:如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡;
除非你socket请求时,一定要使用某一种网卡,才设置此参数;
如果没什么特别要求,不要设置此参数,使用系统中设置的默认网卡即可 ;
一般来说,LuatOS的网络应用demo中都会有netdrv_device功能模块设置默认网卡;
所以建议不要设置此参数,直接使用netdrv_device设置的默认网卡就行;
参数示例:socket.LWIP_GP表示使用4G网卡;
返回值
nil
示例
socket.sntp()
--socket.sntp("ntp.aliyun.com") --自定义sntp服务器地址
--socket.sntp({"ntp.aliyun.com","ntp1.aliyun.com","ntp2.aliyun.com"}) --sntp自定义服务器地址
--socket.sntp(nil, socket.ETH0) --sntp自定义适配器序号
sys.subscribe("NTP_UPDATE", function()
log.info("sntp", "time", os.date())
end)
sys.subscribe("NTP_ERROR", function()
log.info("socket", "sntp error")
socket.sntp()
end)
socket.ntptm()
功能
网络对时后的时间戳(ms 级别)
参数
nil
返回值
local tm = socket.ntptm()
table类型,表示包含时间信息的数据;
此参数的内容格式说明如下
{
"sms":0, -- number类型
-- 系统启动时刻与1900.1.1 0:0:0的毫秒偏移量
"tms":365, -- number类型
-- 当前毫秒数
"tsec":0, -- number类型
-- 当前秒数,从1900.1.1 0:0:0 开始算, UTC时间
"lms":365, -- number类型
-- 本地毫秒数计数器,基于mcu.tick64()
"ndeley":0, -- number类型
-- 网络延时平均值,单位毫秒
"lsec":0, -- number类型
-- 本地秒数计数器,基于mcu.tick64()
"ssec":0, -- number类型
-- 系统启动时刻与1900.1.1 0:0:0的秒数偏移量
}
示例
-- 本API于 2023.11.15 新增
-- 注意, 本函数在执行socket.sntp()且获取到NTP时间后才有效
-- 而且是2次sntp之后才是比较准确的值
-- 网络波动越小, 该时间戳越稳定
local tm = socket.ntptm()
-- 对应的table包含多个数据, 均为整数值
-- 标准数据
-- tsec 当前秒数,从1900.1.1 0:0:0 开始算, UTC时间
-- tms 当前毫秒数
-- vaild 是否有效, true 或者 nil
-- 调试数据, 调试用,一般用户不用管
-- ndelay 网络延时平均值,单位毫秒
-- ssec 系统启动时刻与1900.1.1 0:0:0的秒数偏移量
-- sms 系统启动时刻与1900.1.1 0:0:0的毫秒偏移量
-- lsec 本地秒数计数器,基于mcu.tick64()
-- lms 本地毫秒数计数器,基于mcu.tick64()
log.info("tm数据", json.encode(tm))
log.info("时间戳", string.format("%u.%03d", tm.tsec, tm.tms))
socket.sntp_port(port)
功能
设置 SNTP 服务器的端口号
参数
Port
参数含义:port 端口号, 默认123;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:123;
返回值
含义说明:返回当前的端口号;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:暂无;
示例
-- 本函数于2024.5.17新增
-- 大部分情况下不需要设置NTP服务器的端口号,默认123即可
local current_port = socket.sntp_port(123)
log.info("Current SNTP port:", current_port)
第七部分:LuatOS上的libnet扩展库(待细化)
libnet.waitLink(task_name,timeout,socket_ctrl)
功能
阻塞等待 socket_ctrl 所表示的 socket 对象绑定的网卡网络连接正常,只能用于 sys.taskInitEx 创建的任务函数中
参数
task_name
参数含义:为消息通知的task_name;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
timeout
参数含义:超时时间,如果=0或者空,则没有超时,一直等待;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:10;
socket_ctrl
参数含义:socket.create得到的socket_ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local result = libnet.waitLink(socket_client_task, 0, socket_ctrl)
result
含义说明:失败或者超时返回false,成功返回true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
local result = libnet.waitLink(socket_client_task, 0, socket_ctrl)
libnet.connect(task_name,timeout,socket_ctrl, ip, remote_port, need_ipv6_dns)
功能
阻塞等待 IP 或者域名连接上,如果加密连接还要等握手完成,只能用于 sys.taskInitEx 创建的任务函数中
参数
task_name
参数含义:为消息通知的task_name;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
timeout
参数含义:超时时间,如果=0或者空,则没有超时一致等待;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:10;
socket_ctrl
参数含义:socket.create得到的socket_ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
ip
参数含义:ip地址或者ip或者域名,如果是IPV4,可以是大端格式的int值;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
remote_port
参数含义:服务器端口号,小端格式;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
need_ipv6_dns
参数含义:域名解析是否要IPV6,true要,false不要,默认false不要,只有支持IPV6的协议栈才有效果;
数据类型:boolean;
取值范围:true或false;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
result
含义说明:失败或者超时返回false 成功返回true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
local result = libnet.connect(socket_client_task, 5000, socket_ctrl, ip, port)
libnet.listen(task_name,timeout,socket_ctrl)
功能
阻塞等待客户端连接上,只能用于 sys.taskInitEx 创建的任务函数中。
参数
task_name
参数含义:为消息通知的task_name;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
timeout
参数含义:超时时间,如果=0或者空,则没有超时一致等待;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:10;
socket_ctrl
参数含义:socket.create得到的socket_ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local result = libnet.listen(task_name,timeout,socket_ctrl)
result
含义说明:失败或者超时返回false 成功返回true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
local result = libnet.listen(socket_client_task, 0, socket_ctrl)
libnet.tx(task_name,timeout,socket_ctrl,data, ip, port, flag)
功能
阻塞等待数据发送完成,只能用于 sys.taskInitEx 创建的任务函数中
参数
task_name
参数含义:为消息通知的task_name;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
timeout
参数含义:超时时间,如果=0或者空,则没有超时一致等待;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:10;
socket_ctrl
参数含义:socket.create得到的socket_ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
data
参数含义:待发送的数据 或者 userdata zbuff 要发送的数据;
数据类型:string/zbuff;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
ip
参数含义:对端IP,如果是TCP应用则忽略,
如果是UDP,如果留空则用connect时候的参数,
如果是IPV4,可以是大端格式的number值;
数据类型:string或者number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
port
参数含义:对端端口号,小端格式,如果是TCP应用则忽略,如果是UDP,如果留空则用connect时候的参数;
数据类型:number;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
flag
参数含义:发送参数,目前预留,不起作用;
数据类型:int;
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
local result, buff_full = libnet.tx(task_name,timeout,socket_ctrl, data, ip, port, flag)
result
含义说明:失败或者超时返回false 成功返回true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
buff_full
含义说明:缓存区是否满了;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
function tcp_client_sender.proc(task_name, socket_client)
local send_item
local result, buff_full
-- 遍历数据发送队列send_queue
while #send_queue>0 do
-- 取出来第一条数据赋值给send_item
-- 同时从队列send_queue中删除这一条数据
send_item = table.remove(send_queue,1)
-- 发送这条数据,超时时间15秒钟
result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("tcp_client_sender.proc", "libnet.tx error")
-- 如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(false, send_item.cb.para)
end
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")
table.insert(send_queue, 1, send_item)
return true
end
log.info("tcp_client_sender.proc", "send success")
-- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(true, send_item.cb.para)
end
-- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
return true
end
libnet.wait(task_name,timeout, socket_ctrl)
功能
阻塞等待新的网络事件,只能用于 sys.taskInitEx 创建的任务函数中,可以通过 sys.sendMsg(task_name,socket.EVENT,0)强制退出
参数
task_name
参数含义:为消息通知的task_name;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
timeout
参数含义:超时时间,如果=0或者空,则没有超时一致等待;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:10;
返回值
local result, param = libnet.wait(task_name,timeout, socket_ctrl)
result
含义说明:网络异常返回false,其他返回true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
param
含义说明:超时返回false,有新的网络事件到返回true;
数据类型:boolean;
取值范围:true或false;
注意事项:暂无;
返回示例:暂无;
示例
local result, param, param2 = libnet.wait(socket_client_task, 15000, socket_ctrl)
log.info("libnet", "wait", result, param, param2)
if not result then
-- 网络异常了, 那就断开了, 执行清理工作
log.info("socket", "服务器断开了", result, param)
break
libnet.close(task_name,timeout, socket_ctrl)
功能
阻塞等待网络断开连接,只能用于 sys.taskInitEx 创建的任务函数中
task_name
参数含义:为消息通知的task_name;
数据类型:string;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"socket_client_task";
timeout
参数含义:超时时间,如果=0或者空,则没有超时一致等待;
数据类型:number;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:socket_client_task;
socket_ctrl
参数含义:socket.create得到的socket_ctrl;
数据类型:userdata;
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:暂无;
返回值
nil
示例
libnet.close(socket_client_task, 5000, socket_ctrl)
第八部分:LuatOS上的socke client 应用开发框架
现在,LuatOS socket和libnet的两个重要的库文件,基本上讲完了,接下来,我们来实际看一个完整的socket client长连接的demo项目代码,重点分析下如何在项目中使用LuatOS socket;
socket client长连接的demo项目代码路径:Air8000 socket/client/long_connection demo ;
demo项目的总体设计框图如下:
首先我们在LuatOS模拟器上运行一下这个demo项目,让大家对实现的功能有一个直观的认识,关于模拟器的使用参考:LuatOS模拟器使用说明
如果要在LuatOS模拟器上运行这个项目,代码需要做以下几点修改:
-
netdrv_device.lua中打开pc模拟器的网卡驱动文件require "netdrv_pc",注释掉其他其他网卡文件;
-
因为这个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进行收发数据;
-
根据创建的socket服务器地址和端口,修改代码中对应的地址和端口
软件环境准备好之后,接下来我们在模拟器上实际运行一下这个项目看看效果;
双击 cmd 命令行窗口,然后输入下面一行命令,运行 luatos 批处理文件,同时输入要运行的 luatos 项目配置文件
luatos --llt=H:\Luatools\project\Air8000_socket_client_long_connection.ini
然后按回车键,就可以运行 socket_client_long_connection 项目软件;
这个socket demo中的readme文件,以及代码中的注释都比较详细,接下来我用vscode直接打开这份demo项目代码,和大家一起分析下项目代码;
第九部分:LuatOS上的socke server 应用开发框架(待补充)
第十部分:如何分析LuatOS socket日志(待补充)
基于socket demo的各种应用场景,正常运行的日志,以及异常运行的日志,使用模拟器以及Air8000核心板来分析,分析Luatools日志以及网络抓包日志;
第十一部分:课后作业(待补充)
实现一个TCP短连接项目