Luat系列教程:5、socket代码详解

阅读本文需要具有的技能:
看过该系列前几篇文章或明白前几篇文章内容的
熟悉lua语法,尤其是数组部分
可以明白字符串、字节码之间的区别
可以自己实践操作
对tcp/udp通讯有基本的了解
用过这东西

各位想看mqtt解释的,请等待下一篇文章,不过也可以顺便看看这一篇嘛,二者都差不多的

Socket(TCP/UDP)

TCP和UDP除了在lua代码声明时有一些不同,其他地方完全一样,所以下面的代码将以TCP长连接的数据收发作为示例,如果需要UDP连接,只需要改声明对象时的三个字母即可

先定义一个假装能用来测试的TCP协议(需求)

示例时序如下:

代码详解

官方demo提供的示例代码

官方代码可以在githubLuat_2G_RDA_8955/script_LuaTask/demo/socket/longConnection目录或luatools的LuaTools 1.x.x\script\script_LuaTask\demo\socket\longConnection目录找到

如果你能看懂官方例程,那么可以直接去使用,不需要再看本文了

socket连接代码的拆解分析

这一部分会将官方demo的代码拆开来,只保留基础部分,放到一个文件中来解释

建立文件

首先先新建两个文件,用于测试这个工程

main.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PROJECT = "SOCKET-TEST"
VERSION = "1.0.0"

--根据固件判断模块类型
moduleType = string.find(rtos.get_version(),"8955") and 2 or 4

require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE

require "sys"

--每1分钟查询一次GSM信号强度,每1分钟查询一次基站信息
require "net"
net.startQueryAll(60000, 60000)

--加载硬件看门狗功能模块
--如果用的是720 4g模块,请注释掉这两行
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)

--加载网络指示灯功能模块
require "netLed"
netLed.setup(true,moduleType == 2 and pio.P1_1 or pio.P2_0,moduleType == 2 and nil or pio.P2_1)

require"longlink"

--启动系统框架
sys.init(0, 0)
sys.run()

longlink.lua

1
2
3
4
module(...,package.seeall)

require"socket"
--下面代码一会儿写

找一个测试用的服务器

2G模块socket测试和wifi有着本质的区别:没法使用内网来调试,必须要使用一个公网服务器来调试

为了解决这个问题,luat官方提供了一个tcp测试实验室网站服务:http://tcplab.openluat.com/
这个工具有一个坏处,就是三分钟没有客户端连接的话就会被强行关闭服务。我们可以在本地用一个tcp调试工具提前连上,就不会被强制关闭服务了。

打开后可以直接获取从服务器分配的ip、端口,还能接收客户端数据、主动发送数据
记住自己获取到的ip和端口,在下面的代码中会被使用到

建立socket线程

一般来说,socket连接都是异步运行的,何时应该发送数据,何时应该接收数据,这些逻辑应该让socket收发的进程自己进行控制

所以我们在longlink.lua中添加一个新的线程(看不懂的回去看前几篇文章),文件改成如下(注意要自己改东西!):

longlink.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module(...,package.seeall)

require"socket"

--测试用的服务器信息,上一部分获取到的那个
local testip,testport = "",""

--启动socket客户端任务
sys.taskInit(
function()
while true do
--该区域的代码会永久循环运行(除非出现语法错误)
end
end)

进行socket连接

一般来说,我们会在模块成功获取基站分配的ip后,才会进行网络的连接操作,所以我们需要使用socket.isReady()函数来判断是否连接网络,然后再进行网络操作

在成功获取ip后,我们才能新建一个tcp对象,对其进行联网操作,socket客户端线程代码改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--启动socket客户端任务
sys.taskInit(
function()
while true do
--是否获取到了分配的ip(是否连上网)
if socket.isReady() then
--新建一个socket对象,如果是udp只需要把tcp改成udp即可
local socketClient = socket.tcp()
--尝试连接指定服务器
if socketClient:connect(testip,testport) then
--连接成功
log.info("longlink.socketClient","connect success")
else
log.info("longlink.socketClient","connect fail")
--连接失败
end
else
--没连上网,原地等待一秒,一秒后会循环回去重试
sys.wait(1000)
end
end
end)

对连接失败的处理

上述代码只是一个简单的连接服务器的代码,并且连上之后没有进行任何的其他操作

为了增加代码的稳健性,我们可以利用sys.waitUntil()函数,设置五分钟内没有获取到ip就开启飞行模式几秒,再关闭,让模块重新去获取GPRS连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--启动socket客户端任务
sys.taskInit(
function()
while true do
--是否获取到了分配的ip(是否连上网)
if socket.isReady() then
--新建一个socket对象,如果是udp只需要把tcp改成udp即可
local socketClient = socket.tcp()
--尝试连接指定服务器
if socketClient:connect(testip,testport) then
--连接成功
log.info("longlink.socketClient","connect success")
else
log.info("longlink.socketClient","connect fail")
--连接失败
end
else
--没连上网
--等待网络环境准备就绪,超时时间是5分钟
sys.waitUntil("IP_READY_IND",300000)
--等完了还没连上?
if not socket.isReady() then
--进入飞行模式,20秒之后,退出飞行模式
net.switchFly(true)
sys.wait(20000)
net.switchFly(false)
end
end
end
end)

同样,我们也可以给socketClient:connect(testip,testport)的连接加上错误次数的判断,连接错误超过五次,强制断开socket连接,等待五秒后重试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
--启动socket客户端任务
sys.taskInit(
function()
local retryConnectCnt = 0 --失败次数统计
while true do
--是否获取到了分配的ip(是否连上网)
if socket.isReady() then
--新建一个socket对象,如果是udp只需要把tcp改成udp即可
local socketClient = socket.tcp()
--尝试连接指定服务器
if socketClient:connect(testip,testport) then
--连接成功
log.info("longlink.socketClient","connect success")
retryConnectCnt = 0 --失败次数清零
else
log.info("longlink.socketClient","connect fail")
--连接失败
retryConnectCnt = retryConnectCnt+1 --失败次数加一
end
socketClient:close() --断开socket连接
if retryConnectCnt>=5 then --失败次数大于五次了
link.shut() --强制断开TCP/UDP连接
retryConnectCnt=0 --失败次数清零
end
sys.wait(5000)
else
retryConnectCnt = 0 --没连上网,失败次数清零
--没连上网
--等待网络环境准备就绪,超时时间是5分钟
sys.waitUntil("IP_READY_IND",300000)
--等完了还没连上?
if not socket.isReady() then
--进入飞行模式,20秒之后,退出飞行模式
net.switchFly(true)
sys.wait(20000)
net.switchFly(false)
end
end
end
end)

添加发送/接收处理函数

到了这一步,整个的socket线程只剩下循环处理接收和发送的数据这一部分与demo不同了,我们直接把这两句话加到socket线程的代码中吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
--启动socket客户端任务
sys.taskInit(
function()
local retryConnectCnt = 0 --失败次数统计
while true do
--是否获取到了分配的ip(是否连上网)
if socket.isReady() then
--新建一个socket对象,如果是udp只需要把tcp改成udp即可
local socketClient = socket.tcp()
--尝试连接指定服务器
if socketClient:connect(testip,testport) then
--连接成功
log.info("longlink.socketClient","connect success")
retryConnectCnt = 0 --失败次数清零

--循环处理接收和发送的数据
while true do
if not inMsgProcess(socketClient) then --接收消息处理函数
log.error("longlink.inMsgProcess error")
break
end
if not outMsgprocess(socketClient) then --发送消息处理函数
log.error("longlink.outMsgprocess error")
break
end
end

else
log.info("longlink.socketClient","connect fail")
--连接失败
retryConnectCnt = retryConnectCnt+1 --失败次数加一
end
socketClient:close() --断开socket连接
if retryConnectCnt>=5 then --失败次数大于五次了
link.shut() --强制断开TCP/UDP连接
retryConnectCnt=0 --失败次数清零
end
sys.wait(5000)
else
retryConnectCnt = 0 --没连上网,失败次数清零
--没连上网
--等待网络环境准备就绪,超时时间是5分钟
sys.waitUntil("IP_READY_IND",300000)
--等完了还没连上?
if not socket.isReady() then
--进入飞行模式,20秒之后,退出飞行模式
net.switchFly(true)
sys.wait(20000)
net.switchFly(false)
end
end
end
end)

可以看到,在接收和发送函数不返回false的情况下,接收和发送循环会一直进行下去;只有当两个函数之一返回false时,才会触发break导致退出该接收和发送循环

inMsgProcess(socketClient)函数

这段的代码相对来说比较简单,我们可以直接使用socketClient:recv(毫秒数)来接收我们的tcp消息。
我们在合适的地方,新建一个inMsgProcess(socketClient)函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function inMsgProcess(socketClient)
local result,data
while true do
result,data = socketClient:recv(2000)
--接收数据
if result then --接收成功
log.info("longlink.inMsgProcess",data)
--处理data数据,现在还没代码,空着
else --接收失败
break
end
end
--返回结果,处理成功返回true,处理出错返回false
return result or data=="timeout"
end

这段代码就是循环获取socket消息,如果没获取到,socketClient:recv(2000)就会返回false,"timeout";如果获取到了,就会返回true,获取到的数据字符串;如果返回了false,不为"timeout",则表示数据处理出错,说明socket连接有了什么问题

细心的读者可能看出来了,如果接收函数一直在2秒内有接收到数据,那么这段函数会永远无限循环下去,没办法到达outMsgprocess(socketClient)函数进行发送数据的操作,所以我们先去讲outMsgprocess(socketClient)函数的实现过程,再回来改进inMsgProcess(socketClient)函数

outMsgprocess(socketClient)函数

由于发送函数在socket线程中是一个循环的小部分,所以我们要建立一个消息发送的队列:有要发送的发数据时,将数据放到这个队列中;等运行到outMsgprocess(socketClient)函数时,将队列中的数据一个一个发出去

首先我们要建一个放这种队列的数组,在合适位置声明一下这个数组:

1
2
--数据发送的消息队列
local msgQuene = {}

接着我们构造一个可以往数组里插入数据的函数,table.insert()可以向数组添加数据,所以我们新建一个insertMsg函数:

1
2
3
local function insertMsg(data)
table.insert(msgQuene,data)
end

还记得上面说过的消息接收函数函数会永远无限循环下去的问题吗?我们在合适的地方新建一个判断发送消息队列是否为空的函数:

1
2
3
function waitForSend()
return #msgQuene > 0
end

在数组有数据时,这个函数会返回true,我们可以将inMsgProcess(socketClient)接收到数据后的代码添加一行判断发送队列是否有数据的代码,当检测到发送队列有数据时,就立即退出接收函数,转而去进行发送动作,接收函数最终改为了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function inMsgProcess(socketClient)
local result,data
while true do
result,data = socketClient:recv(2000)
--接收到数据
if result then --接收成功
log.info("longlink.inMsgProcess",data)
--处理data数据,现在还没代码,空着
--如果msgQuene中有等待发送的数据,则立即退出本循环
if waitForSend() then return true end
else --接收失败
break
end
end
--返回结果,处理成功返回true,处理出错返回false
return result or data=="timeout"
end

最后我们终于可以开始写消息发送函数了,整体的函数就是检查队列是否为空,不为空的话就发一条消息并将其从队列中删除,然后重复这一操作,函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
function outMsgprocess(socketClient)
--队列中有消息
while #msgQuene>0 do
--获取消息,并从队列中删除
local outMsg = table.remove(msgQuene,1)
--发送这条消息,并获取发送结果
local result = socketClient:send(outMsg)
--发送失败的话立刻返回nil(等同于false)
if not result then return end
end
return true
end

完成基本的socket线程

经过上述的更改,最终,longlink.lua已经实现了连接服务器并自动处理错误的功能,并且预留了消息接收以及向发送队列添加数据的接口,文件的所有代码如下:

longlink.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
module(...,package.seeall)

require"socket"

--测试用的服务器信息
local testip,testport = "180.97.81.180","50798"

--数据发送的消息队列
local msgQuene = {}

local function insertMsg(data)
table.insert(msgQuene,data)
end

function waitForSend()
return #msgQuene > 0
end

function outMsgprocess(socketClient)
--队列中有消息
while #msgQuene>0 do
--获取消息,并从队列中删除
local outMsg = table.remove(msgQuene,1)
--发送这条消息,并获取发送结果
local result = socketClient:send(outMsg)
--发送失败的话立刻返回nil(等同于false)
if not result then return end
end
return true
end

function inMsgProcess(socketClient)
local result,data
while true do
result,data = socketClient:recv(2000)
--接收到数据
if result then --接收成功
log.info("longlink.inMsgProcess",data)
--处理data数据,现在还没代码,空着
--如果msgQuene中有等待发送的数据,则立即退出本循环
if waitForSend() then return true end
else --接收失败
break
end
end
--返回结果,处理成功返回true,处理出错返回false
return result or data=="timeout"
end



--启动socket客户端任务
sys.taskInit(
function()
local retryConnectCnt = 0 --失败次数统计
while true do
--是否获取到了分配的ip(是否连上网)
if socket.isReady() then
--新建一个socket对象,如果是udp只需要把tcp改成udp即可
local socketClient = socket.tcp()
--尝试连接指定服务器
if socketClient:connect(testip,testport) then
--连接成功
log.info("longlink.socketClient","connect success")
retryConnectCnt = 0 --失败次数清零

--循环处理接收和发送的数据
while true do
if not inMsgProcess(socketClient) then --接收消息处理函数
log.error("longlink.inMsgProcess error")
break
end
if not outMsgprocess(socketClient) then --发送消息处理函数
log.error("longlink.outMsgprocess error")
break
end
end

else
log.info("longlink.socketClient","connect fail")
--连接失败
retryConnectCnt = retryConnectCnt+1 --失败次数加一
end
socketClient:close() --断开socket连接
if retryConnectCnt>=5 then --失败次数大于五次了
link.shut() --强制断开TCP/UDP连接
retryConnectCnt=0 --失败次数清零
end
sys.wait(5000)
else
retryConnectCnt = 0 --没连上网,失败次数清零
--没连上网
--等待网络环境准备就绪,超时时间是5分钟
sys.waitUntil("IP_READY_IND",300000)
--等完了还没连上?
if not socket.isReady() then
--进入飞行模式,20秒之后,退出飞行模式
net.switchFly(true)
sys.wait(20000)
net.switchFly(false)
end
end
end
end)

烧录到模块中,可以得到正常的连接结果:

实现协议需求

心跳包需求

这个需求极其简单,只需要建立一个新的线程,在联网后往消息队列中添加数据即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
--启动心跳包任务
sys.taskInit(
function()
while true do
if socket.isReady() then --连上网再开始运行
insertMsg("heart beat") --队列里塞个消息
sys.wait(10000) --等待10秒
else --没连上网别忘了延时!不然会陷入while true死循环,导致模块无法运行其他代码
sys.wait(1000) --等待1秒
end
end
end)

收到back开头的数据要回复相同的数据

这一条功能十分简单,只需要在inMsgProcess()函数中,写了--处理data数据,现在还没代码,空着这段注释的地方添加相应代码即可,代码如下:

1
2
3
4
--处理data数据
if data:sub(1,4) == "back" then --收到back开头的数据要回复相同的数据
insertMsg(data)
end

客户端收到bin要回复二进制数组0x11 0x22 0x33

这个功能和上面差不多,返回的东西不一样而已,拼接字节码我们可以用pack,也可以直接用fromHex,下面两种方法都示范一下:

pack方式:

1
2
3
if data == "bin" then --收到bin要回复二进制数组0x11 0x22 0x33
insertMsg(pack.pack(">bbb",0x11,0x22,0x33))
end

fromHex方式:

1
2
3
if data == "bin" then --收到bin要回复二进制数组0x11 0x22 0x33
insertMsg(string.fromHex("112233"))
end

收到time要回复当前时间的时间戳字符串

进行这个操作,要在开机的时候先同步一下时间,我们可以使用demo中的同步ntp来实现。在main.lua中的启动系统框架上方添加两行同步代码即可:

1
2
require"ntp"
ntp.timeSync()

接着和上面一样,在消息接收处返回需要的数据即可:

1
2
3
if data == "time" then --收到time要回复当前时间的时间戳字符串
insertMsg(tostring(os.time()))
end

完整代码

经过上面的删删改改,功能以及基本实现了,整个文件的代码如下:

longlink.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
module(...,package.seeall)

require"socket"

--测试用的服务器信息
local testip,testport = "180.97.81.180","50798"

--数据发送的消息队列
local msgQuene = {}

local function insertMsg(data)
table.insert(msgQuene,data)
end

function waitForSend()
return #msgQuene > 0
end

function outMsgprocess(socketClient)
--队列中有消息
while #msgQuene>0 do
--获取消息,并从队列中删除
local outMsg = table.remove(msgQuene,1)
--发送这条消息,并获取发送结果
local result = socketClient:send(outMsg)
--发送失败的话立刻返回nil(等同于false)
if not result then return end
end
return true
end

function inMsgProcess(socketClient)
local result,data
while true do
result,data = socketClient:recv(2000)
--接收到数据
if result then --接收成功
log.info("longlink.inMsgProcess",data)
--处理data数据
if data:sub(1,4) == "back" then --收到back开头的数据要回复相同的数据
insertMsg(data)
elseif data == "bin" then --收到bin要回复二进制数组0x11 0x22 0x33
insertMsg(string.fromHex("112233"))
elseif data == "time" then --收到time要回复当前时间的时间戳字符串
insertMsg(tostring(os.time()))
end
--如果msgQuene中有等待发送的数据,则立即退出本循环
if waitForSend() then return true end
else --接收失败
break
end
end
--返回结果,处理成功返回true,处理出错返回false
return result or data=="timeout"
end



--启动socket客户端任务
sys.taskInit(
function()
local retryConnectCnt = 0 --失败次数统计
while true do
--是否获取到了分配的ip(是否连上网)
if socket.isReady() then
--新建一个socket对象,如果是udp只需要把tcp改成udp即可
local socketClient = socket.tcp()
--尝试连接指定服务器
if socketClient:connect(testip,testport) then
--连接成功
log.info("longlink.socketClient","connect success")
retryConnectCnt = 0 --失败次数清零

--循环处理接收和发送的数据
while true do
if not inMsgProcess(socketClient) then --接收消息处理函数
log.error("longlink.inMsgProcess error")
break
end
if not outMsgprocess(socketClient) then --发送消息处理函数
log.error("longlink.outMsgprocess error")
break
end
end

else
log.info("longlink.socketClient","connect fail")
--连接失败
retryConnectCnt = retryConnectCnt+1 --失败次数加一
end
socketClient:close() --断开socket连接
if retryConnectCnt>=5 then --失败次数大于五次了
link.shut() --强制断开TCP/UDP连接
retryConnectCnt=0 --失败次数清零
end
sys.wait(5000)
else
retryConnectCnt = 0 --没连上网,失败次数清零
--没连上网
--等待网络环境准备就绪,超时时间是5分钟
sys.waitUntil("IP_READY_IND",300000)
--等完了还没连上?
if not socket.isReady() then
--进入飞行模式,20秒之后,退出飞行模式
net.switchFly(true)
sys.wait(20000)
net.switchFly(false)
end
end
end
end)

--启动心跳包任务
sys.taskInit(
function()
while true do
if socket.isReady() then --连上网再开始运行
insertMsg("heart beat") --队列里塞个消息
sys.wait(10000) --等待10秒
else --没连上网别忘了延时!不然会陷入while true死循环,导致模块无法运行其他代码
sys.wait(1000) --等待1秒
end
end
end)

main.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
PROJECT = "SOCKET-TEST"
VERSION = "1.0.0"

require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE

require "sys"

--每1分钟查询一次GSM信号强度,每1分钟查询一次基站信息
require "net"
net.startQueryAll(60000, 60000)

--加载硬件看门狗功能模块
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)

--加载网络指示灯功能模块
require "netLed"
netLed.setup(true,pio.P1_1)

require"longlink"
require"ntp"
ntp.timeSync()

--启动系统框架
sys.init(0, 0)
sys.run()

验证功能

把最终代码烧录进去,按需求测试即可:

很明显,功能符合预期。

如有错误或疑问请在下方留言,谢谢!

上次更新 2021-01-28