SOCKET(套接字)
# 简介
套接字(socket)是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序),各种进程使用这个相同的域互相之间用Internet协议簇来进行通信
---
# API说明
socket的API在LuatOS-Air lib有做封装,建议直接用lib的API接口。
| API接口 | 描述 |
| ------------------------- | -------------------------------------- |
| socket.isReady() | 检测模块是否注册上网络,socket是否可用 |
| socket.tcp() | 创建基于TCP的socket对象 |
| socket.udp() | 创建基于UDP的socket对象 |
| mt:connect() | 连接服务器 |
| mt:asyncSelect() | 异步收发选择器 |
| mt:asyncRecv() | 异步发送数据 |
| mt:send() | 发送数据 |
| mt:recv() | 接收数据 |
| mt:close() | 销毁一个socket |
| socket.setTcpResendPara() | 设置TCP层自动重传的参数 |
| socket.setDnsParsePara() | 设置域名解析参数 |
| socket.printStatus() | 打印所有socket的状态 |
> 详细的API介绍见[socket API章节](https://doc.openluat.com/wiki/21?wiki_page_id=2294)
# 实现流程
- 创建任务
通过sys.taskInit() 创建一个协程。
- 等待网络就绪
采用socket.isReady()这个接口阻塞操作,程序运行到这里会进入等待直到底层网络注册完成,网络状态就绪,否则等待至超时。
- 创建一个对象
LuatOS-Air的socket操作是一个面向对象的操作所以首先使用[socket.tcp(ssl, cert)](https://doc.openluat.com/wiki/21?wiki_page_id=2294#sockettcpssl_cert_23)创建一个对象。
- 连接服务器
然后使用[mt:connect(address, port, timeout)](https://doc.openluat.com/wiki/21?wiki_page_id=2294#mtconnectaddress_port_timeout_70)连接服务器
- 发送
- 同步方式
使用mt:send(data)接口即可发送数据,因为同步方式大多数时间都是阻塞在接收部分的,所以根据前文同步接收数据的说明可以通过配置msg退出阻塞,然后发送数据。可以参考demo做法。在rev配置msg为pub_msg然后通过其他协程使用sys.publish向pub_msg发送数据,退出阻塞以后直接发送。
- 异步方式
异步方式也相对简单直接使用mt:asyncSend(data)发送即可。需要说明的一件事是异步方式没有timeout所以心跳需要自己维护或者使用mt:asyncSelect(keepAlive, pingreq)配置心跳时间及内容。
- 接收
- 同步方式
同步方式采用[mt:recv(timeout, msg, msgNoResume)](https://doc.openluat.com/wiki/21?wiki_page_id=2294#mtrecvtimeout_msg_msgNoResume_184)这个接口阻塞操作,程序运行到这里会进入等待直到满足条件才会退出。
- 异步方式
异步采用[mt:asyncRecv()](https://doc.openluat.com/wiki/21?wiki_page_id=2294#mtasyncRecv_139)接口接收数据,相对于同步方式,异步的参数及返回值相对简单,使用时无需传递参数,返回值直接就是收到的数据。
-----
# 同步与异步
在脚本目录的demo/socket文件夹里有两种示例代码,async是异步socket,sync是同步socket
- **同步:**
同步的思想是:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界面,如果关闭了,即迁移程序就中断了。
- **异步:**
将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。
同步和异步本身是相对的
同步就相当于是 当客户端发送请求给服务端,在等待服务端响应的请求时,客户端不做其他的事情。当服务端做完了才返回到客户端。这样的话客户端需要一直等待。用户使用起来会有不友好。
异步就是,当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。
存在就有其道理 异步虽然好 但是有些问题是要用同步用来解决,比如有些东西我们需要的是拿到返回的数据在进行操作的。这些是异步所无法解决的。所以请根据实际需求选择。
# 示例
相关实例程序在脚本库的demo\socket文件夹下,包含同步异步以及tcp到串口透传实例。可以根据实际需要选择demo进行研究。
## 开机与连接网络
以\demo\socket\async\asyncSocketCallback目录的demo作为基础进行修改。demo中在开机以后进入正式应用的一开始使用了一个while进行循环阻塞判断。socket.isReady()表示网络连接是否可用,可用即为true,不可以为false。
```lua
-- 启动socket客户端任务
local ip, port, c = "112.125.89.8", "35624"
sys.taskInit(function()
while true do
while not socket.isReady() do sys.wait(1000) end
asyncClient = socket.tcp()
while not asyncClient:connect(ip, port) do sys.wait(2000) end
clientConnected = true
--异步发送数据,如果推出了while循环,表示发送失败
while asyncClient:asyncSelect(60, "ping") do end
--此行代码不要删除
clientConnected = false
asyncClient:close()
end
end)
```
有些情况下可能由于欠费等原因设备socket可能一直不可用,所以可以加一个异常机制,当开机以后socket长时间不可用就重启设备。可以进行如下修改。
```lua
--网络业务逻辑看门狗task
socketAppDogCo = sys.taskInit(function()
while true do
--连续5分钟没有喂狗,根据项目需求自行修改时长
if sys.wait(300000) == nil then
sys.restart("socketApp exception software dog timeout")
end
end
end)
--喂狗代码(根据产品业务逻辑,在适当的位置去调用):
--如何去确认这个“适当的位置”呢?下面列举几种常见的场景:
--1、如果模块和服务器之间有应用心跳的应答机制,则可以在模块每次收到服务器的心跳应答时去喂狗
--2、如果没有心跳应答机制,可以在连接服务器成功后,起个定时器,每隔一段时间去喂一次狗;连接断开时,关闭这个喂狗定时器
--3、如果模块定时会向服务器发送数据,可以在每次发送数据成功后,去喂狗
--4、......
--网络业务逻辑看门狗的设计初衷是:
--sim卡识别异常、网络注册异常、PDP激活异常、socket连接异常、socket发送数据异常、socket接收数据异常时,都可以通过网络业务逻辑看门狗控制软重启
coroutine.resume(softwareDogCo,"feed")
)
```
## 连接服务器
我这里使用的windows系统,直接使用网络调试助手作为server,没有的也可以用[https://netlab.luatos.com/ ](https://netlab.luatos.com/ "https://netlab.luatos.com/ ")测试。
注意:无论2G还是4G模块连接的服务器必须是公网的,局域网ip无法使用
首先通过**socket.tcp()**创建一个新的tcp对象,后面的操作都基于这个对象进行。
然后使用**c:connect(ip, port)**开始连接,这里链接等待时间设置为2秒
```lua
asyncClient = socket.tcp()
while not asyncClient:connect(ip, port) do sys.wait(2000) end
clientConnected = true
--异步发送数据,如果推出了while循环,表示发送失败
while asyncClient:asyncSelect(20, "ping") do end
--此行代码不要删除
clientConnected = false
asyncClient:close()
```
连接成功以后进入死循环,根据asyncClient:asyncSelect的返回条件判断模块所处状态进行业务处理。
## socket发送与接收消息
这个用的是非线程发送方式10秒发送一次数据
```lua
-- 这里演示如何用非线程发送数据
sys.timerLoopStart(function()
if clientConnected then
asyncClient:asyncSend("0123456789")
end
end, 10000)
```
数据接收,当模块收到数据的时候会有SOCKET_RECV消息上报,我们这里通过subscribe注册这个消息的回调函数来处理模块接收到的数据
```lua
sys.subscribe("SOCKET_RECV", function(id)
if asyncClient.id == id then
local data = asyncClient:asyncRecv()
log.info("这是服务器下发数据:", #data, data:sub(1, 30))
end
end)
```
## 测试效果
默认DEMO修改IP地址和端口号测试效果



默认DEMO修改IP地址,端口号,心跳包时间,数据发送时间测试效果


## 其它参考资料
深入了解和学习可以参考下面链接中的如下章节内容
[LuatOS-Air 应用开发视频教程](https://hmi.wiki.luatos.com/doc/65042949/e6zPC3k9/7P16mRIy)

# 常见问题
## 网络测试工具
[LuatOS网络测试工具](https://netlab.luatos.com/)
## 连接服务器失败
1. 服务器必须是公网地址
2. 使用PC上的TCP UDP测试工具客户端、或者mqtt.fx,连接服务器确认一下是否可以连接成功,排除服务器故障
3. 如果连接ssl服务器,确认下core文件是否支持ssl功能(例如2G模块的某些core文件不支持ssl功能)
4. 2G模块不要使用中国联通卡
5. 检查下模块信号、网络注册、网络附着、PDP激活状态
6. 检查下SIM卡是否欠费【4G模块有一种欠费表现:无法注册4G网络,可以注册2G网络】
7. 日志分析网络连接失败:先检查SIM卡是否被识别(+CPIN: READY)。识卡后检查观察“^MODE”的值,^MODE 17,17为4G,其他如下:3,1(2G) , 3,2(2.5G) , 3,3(3G) , 5,7(5G)。最后观察PDP是否激活(AT+CGDCONT?)(+CGDCONT: 5,"IP","cmiot","10.39.132.191",0,0)。正常如果都满足,网络是可以正常使用的。如果出现网络随机断开连接,注意看网络信号值(AT+CESQ)(AT+CSQ)。详细信号值讲解:
[网络状态与信号强度查询](https://doc.openluat.com/wiki/21?wiki_page_id=1997)
## 最多同时支持多少个连接
10个。
## socket异常的情况排查
1. 搜索socket,如果出现socket:connect: core sock conn error或者socket:connect: connect fail,则表示socket连接失败
2. 搜索socket,如果出现send fail则表示发送失败
3. 搜索socket,如果出现socket.rtos.MSG_SOCK_CLOSE_IND则表示socket被动关闭
## tcp连接,心跳包建议多长时间一次
因为基站资源有限,如果不发心跳包保活,基站会主动断掉链路,回收资源,模块和服务器无感,并不知道链路已经断开。建议心跳包的频率不要超过4分钟,一般都是建议使用2分钟
## SOCKET使用注意事项
- 同步
1. 创建、连接、发送数据、接收数据、关闭,必须在同一个task中
2. 发送数据等待发送结果时,task被挂起,此时如果接收到数据,只能暂存到接收缓冲区,应用无法及时处理
3. 接收数据存在粘包问题
- 异步
1. 创建、连接、异步发送、关闭,必须在同一个task中;异步缓存待发送数据在其他task中;接收数据可以在其他task中,也可以在非task中
2. 发送数据等待发送结果时,task被挂起,此时如果接收到数据,应用可以及时处理
3. 接收数据存在粘包问题
如下在别的TASK调用关闭接口,会报如下错误

## 数据收发延迟大、速度慢、经常失败、掉线断开
1. 检查下是否存在代码逻辑错误,导致异常
2. 检查下是否不断重启,导致异常
3. 检查下服务器网络是否稳定,不要用内网穿透方式搭建服务器
4. 检查下使用环境是否网络覆盖不好,例如车库、地下、电梯、山区等
5. 检查下模块信号、网络注册、网络附着、PDP激活状态
6. 排查是否为设备天线问题:发出来设备的天线调试指标参数给合宙技术支持人员
7. 如果经常出现连接被动断开:
1) 如果直接使用的是tcp udp socket连接,检查下心跳包的频率,基站策略会关闭长时间没有数据传输的连接,建议心跳包的频率不要超过4分钟,一般都是建议使用2分钟
2) 如果使用的是mqtt连接,检查下mqtt keep alive的时间,基站策略会关闭长时间没有数据传输的连接,建议心跳包的频率不要超过4分钟,一般都是建议使用2分钟
3) 如果使用的是mqtt连接,检查下是否在1.5倍的mqtt keep alive的时间,没有成功发送数据到服务器,就会被被服务器主动断开,这种情况一般都是发送数据超时引起的
8. 排查是否为网络问题:如果同一地点的所有设备都有问题,可能和网络环境有关系;使用手机设置为同样的网络模式,对比测试确认下;或者把设备移动到其他距离此地点比较远的地方对比测试。
9. 排查是否设备单体问题:如果同一地点,某些设备正常,某些设备异常,按照如下几种情况分析
1) 分析正常设备和异常设备的使用环境是否相同:
A. 如果不同,例如异常设备固定在钢制墙壁上,正常设备放置在桌子上,钢制墙壁可能对天线射频有干扰,将异常设备和正常设备放置在同样的使用环境中,再对比测试
B. 如果相同,参考第2)步
2) 分析正常和异常的设备,驻留的小区是否相同:
A. 如果相同,重点排查异常设备的天线射频部分,分析不出结果的话,异常设备寄给合宙分析
B. 如同不同,多测试几次,确认下,是不是在异常小区内很容易出问题,如果异常小区很容易出问题,可能就是小区拥堵造成的
10. 提供日志给合宙技术支持人员
## 有没有哪个函数可以知道当前系统总共建立了几个SOCKET连接?
socket.printStatus()可以打印当前连接的socket信息
## 如何统计流量
运营商按照ip包来统计流量,ip包包含:ip包头+tcp包头+用户数据,以IPv4为例:
ip包头包含固定的20字节+可选的4字节,至少20字节
tcp包头包含固定的20字节+可选的4字节,至少20字节
用户数据就是用户能够感知到的数据内容了,如果直接使用socket,就是用户感知的数据;如果使用http、mqtt、ssl,这部分数据就不是用户能够感知的原始数据了,http会加上http包头,mqtt会加入mqtt的包裹部分,ssl会加密数据
tcp数据收发时,有ack确认机制,例如设备发数据给服务器后,还会收到服务器返回的tcp ack包,这个tcp ack包(至少40字节)也是计算在流量之内的。接收到服务器下发的数据时,设备也会回复一个tcp ack包,同样这个ack包仍然计算在流量之内
另外,socket连接以及断开连接,都有多次数据收发,这部分也会消耗流量
例如有一个tcp socket连接,连接成功后,每分钟设备发送2字节的心跳数据到服务器,服务器收到数据后,再回复2字节的心跳应答数据。这个过程中,每分钟消耗的流量至少有42(设备发送)+40(服务器回复ack)+42(服务器发送)+40(设备回复ack)=164字节,实际处理中,服务器回复ack和服务器发送可能合并成一个IP包42字节,这样的话,至少也要42+42+40 = 124字节。这是最简单的算法,实际应用中,还要考虑到重传、包头中的可选字节,应该会比这里计算的流量多一些。如果要准确计算,建议在服务器端用wireshark抓包分析
另外一个常见的例子是,为什么通过http下载一个文件,实际消耗的流量比文件本身要多呢?跟上个例子类似,文件本身大小仅仅是用户数据,除了用户数据外,还有如下几部分的流量消耗:
1. 连接服务器消耗流量
2. http请求时有http头,服务器下发数据时也有http头
3. 每包数据都有tcp头和ip头
4. 重传也会消耗流量
5. 与服务器断开连接也需要消耗流量
## 心跳包发送的数据是什么?
- 同步
mt:send("ping")手动发送,ping心跳包内容可以根据自己需要写
- 异步
mt:asyncSelect(60, "ping")连接后设置,60心跳包时间,单位秒,ping心跳包内容可以根据自己需要写
## 4g模块可以同时作为服务器和客户端使用吗?
不能作为服务器使用, 模块能获取的是运营商分配的内网IP
## 看到SOCKET的一个DEMO里,有掉用 link.shut() ,这个link.shut()是起什么作用?
可以先看一下\lib\link.lua里shut()说明
```lua
--[[
如果是默认承载,是去激活不了的,
如果是手动激活的pdp,去激活cid_manual后也还是有默认承载存在,
所以如果上层在去激活后要发起socket是能连上的,所以这里直接上报IP_ERROR_IND,由上层自己管理shut之后的逻辑
]]
function shut()
--ril.request("AT+CGACT?",nil,procshut)
ready = false
sys.publish('IP_ERROR_IND')
if net.getState() ~= 'REGISTERED' then return end
sys.timerStart(Pdp_Act, 2000)
end
```
## 服务端意外关闭了,socket断开了,设备这边怎么能快速检测到
可以通过注册LIB_SOCKET_CLOSE_IND消息来判断
[LuatOS-Air script lib内部消息 socket](https://doc.openluat.com/wiki/21?wiki_page_id=2302#socket_104 "LuatOS-Air script lib内部消息 socket")
## netlab.luatos.com这个测试服务器,如果模块刚上线就被踢,或者上线后发送给模块的数据模块未收到,或者拿HTTP去请求它,出现刚上线就被踢
可以尝试重置你的网络(重启路由器)
## 用socket/demo,连接https://netlab.luatos.com/测试服务器测试,掉线严重
可以换一张卡测试,确认是不是卡的问题。
## 为什么我一包数据只有不到50B的数据,为什么一天消耗的流量要远远大于实际传输值
例如:如果使用的是TCP协议,需要三次握手四次挥手才算完成了一次数据交互,原始数据不多但是由于TCP协议决定的一包数据必须要加包头包尾帧校验等,所以实际消耗的流量不止50B,部分运营商有限制每一包数据必须1KB起发,不足1KB也会加各种校验凑足1kb。
其它参考:[如何统计流量](https://hmi.wiki.luatos.com/doc/65042949/e6zPC3k9/t0ofut80#nav_16 "如何统计流量")