跳转至

libnet - libnet 在socket库基础上的同步阻塞api,socket库本身是异步非阻塞api

作者:王城钧

一、概述

libnet 是基于异步非阻塞 socket 库的同步阻塞 API 封装,专为 sys.taskInitEx 创建的任务设计,提供阻塞式网络操作,简化异步编程。

二、核心示例

1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;

2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;

3、更加完整和详细的 demo,请参考 LuatOS 仓库 中各个产品目录下的 demo/socket/client/long_connection

-- 关于本库的一些说明:本文中的描述内容,使用了网卡和网络适配器这两种概念,这两种概念所表达的意思完全一样;
-- 本库用于网络通信, 支持TCP, UDP, 也支持TLS加密传输;
-- 支持加密传输版本有 TLS 1.0/1.1/1.2/1.3, DTLS 1.0/1.2, 当前不支持TLS 1.3;
-- 不支持 SSL 3.0, 该协议已经被废弃, 也不安全;
-- 支持的加密算法有 RSA, ECC, AES, 3DES, SHA1, SHA256, MD5 等等;
-- 完整的加密套件列表, 可通过 crypto.cipher_suites() 获取;

-- 本库的函数, 除非特别说明, 都是立即返回的非阻塞函数;
-- 这意味着, 函数调用成功, 并不代表网络操作成功, 只代表网络操作已经开始;

PROJECT = "SOCKET_CONNECTION"
VERSION = "001.000.000"

local libnet = require "libnet"

local SERVER_ADDR = "112.125.89.8" -- 根据实际修改
local SERVER_PORT = 47826     -- 根据实际修改
local TASK_NAME = "socket_connect_task"
local send_queue = {}
local TCP_SENDER_TASK_NAME = "tcp_send_task"
local recv_buff = nil

local function send_data_req_proc_func(tag, data, cb)
    table.insert(send_queue, {data = "send from "..tag..": "..data, cb = cb})
    sys.sendMsg(TCP_SENDER_TASK_NAME, socket.EVENT, 0)
end

function tcp_clientc_sender_proc(task_name, socket_client)
    while #send_queue > 0 do
        local send_item = table.remove(send_queue, 1)

        -- 发送这条数据,超时时间15秒钟
        local result = 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
        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
    end

    return true
end

function tcp_client_sender_exception_proc()
    while #send_queue > 0 do
        local send_item = table.remove(send_queue, 1)
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(false, send_item.cb.para)
        end
    end
end

sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

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

    while true do
        local result = socket.rx(socket_client, recv_buff)

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

        -- 如果读取到了数据, used()就必然大于0, 进行处理
        if recv_buff:used() > 0 then
            log.info("tcp_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 tcp server: ", data)

            recv_buff:del()
        else
            break
        end
    end

    return true
end

local function tcp_client_main_task_func()
    local socket_client
    local result, para1, para2

    while true do
        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("tcp_client_main_task_func", "wait IP_READY", 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")

        while true do
            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_clientc_sender_proc(TASK_NAME, socket_client) then
                log.error("tcp_client_main_task_func", "tcp_client_sender_proc error")
                break
            end

            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

        -- 出现异常
        ::EXCEPTION_PROC::

        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
        tcp_client_sender_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

        sys.wait(5000)
    end
end

sys.taskInitEx(tcp_client_main_task_func, TASK_NAME)

sys.run()

三、常量详解

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

libnet 扩展库没有常量。

四、函数详解

libnet.waitLink(task_name,timeout,socket_ctrl)

功能

阻塞等待 socket_ctrl 所表示的 socket 对象绑定的网卡网络连接正常,只能用于 sys.taskInitEx 创建的任务函数中

参数

task_name

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

timeout

参数含义:超时时间,如果=0或者空,则没有超时,一直等待;
数据类型:number
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:10

socket_ctrl

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

返回值

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,remote_addr, remote_port, need_ipv6_dns)

功能

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

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

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

参数含义:连接超时时长,单位为毫秒,如果为0或者空,则表示没有超时限制,一直等待连接结果的返回;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:如果要连接的对端是域名,此处需要考虑dns域名解析的耗时
         不建议此处的timeout设置的时长过短,针对不同的连接要预留足够的时间;
参数示例:30000

socket_ctrl

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

remote_addr

参数含义:服务器地址,支持域名或者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
是否必选:可选传入此参数,默认魏nil
注意事项:暂无;
参数示例:nil或者为空

返回值

local result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)

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.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的任务处理函数中使用,因为要阻塞等待并且接收定向消息;

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

参数含义:连接超时时长,单位为毫秒,如果为0或者空,则表示没有超时限制,一直等待连接结果的返回;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:
         一般来说,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都表示对端地址
参数示例:"112.125.89.8"

port

参数含义:UDP模式下,对端端口号;
         UDP模式下,如果为空,发送数据时会使用libnet.connect接口中的remote_port参数
         UDP模式下,如果不为空,数据会直接发给这个port参数所表示的对端端口号
         TCP模式下,此参数没有任何意义;
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:此参数和libnet.connect接口中的remote_port都表示对端端口号
参数示例: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
注意事项:缓冲区满只有一种情况,就是此次发送的数据动态申请内存失败了;
返回示例: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的任务处理函数中使用,因为要阻塞等待并且接收定向消息;

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

参数含义:等待超时时长,单位为毫秒,如果为0或者空,则表示没有超时限制,一直等待socket.EVENT消息
数据类型:number或者nil
取值范围:暂无;
是否必选:可选传入此参数;
注意事项:这里的超时时长到达后,并不会认定出现异常;
参数示例: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

含义说明:超时返回false,有新的网络事件到返回true
数据类型:boolean
取值范围:true或false
注意事项:暂无;
返回示例:暂无;

示例

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连接;

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

参数

task_name

参数含义:使用sys.taskInitEx创建的task名称
数据类型:string
取值范围:暂无;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"tcp_client_task"

timeout

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

socket_ctrl

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

返回值

nil

示例

libnet.close(socket_client_task, 5000, socket_ctrl)

五、产品支持说明

支持 LuatOS 开发的所有产品都支持 libnet 扩展库。