mDNS Module

mDNS Module

组播DNS被用作Bonjour / Zeroconf的一部分。这允许系统识别它们自己以及它们在局域网中提供的服务。然后,客户机就能够发现这些系统并连接到它们。

这是一个mDNS服务器模块。如果你正在为NodeMCU寻找一个mDNS客户端(即查询mDNS),那么udaygin/ NodeMCU - mns -client可能是一个选项。

mdns.register()注册主机名并启动mDNS服务。
mdns.close()关闭 mDNS 服务。

mdns.register()

注册主机名并启动 mDNS 服务。如果服务已经在运行,那么它将使用新参数重新启动。

语法

mdns.register(hostname [, attributes])

参数

  • hostname此设备的主机名。字母数字字符最好。
  • attributes可选的选项表。键必须都是字符串。
    attributes包含两种属性——具有特定名称的属性和特定于服务的属性。RFC 6763 定义了额外的、特定于服务的属性如何编码到 DNS 中。一个示例是,如果设备支持打印,则可以将队列名称指定为附加属性。该模块最多支持 10 个此类属性。

具体名称为:
port服务的端口号。默认值为 80。
service服务的名称。默认值为“http”。
description描述服务的简短短语(63 个字符以下)。默认是主机名。

返回

nil

错误

在参数验证期间可能会产生各种错误。NodeMCU在调用时必须有IP地址,否则会报错。

例子

mdns.register("fishtank", {hardware='NodeMCU'})

在 macOS 上使用dns-sd,您可以看到fishtank.local提供_http._tcp服务。您也可以直接浏览到fishtank.local. 在 Safari 中,您可以将所有 mDNS 网页作为书签菜单的一部分。

mdns.register("fishtank", { description="Top Fishtank", service="http", port=80, location='Living Room' })

mdns.close()

关闭 mDNS 服务。这通常不需要。

语法

mdns.close()

返回

nil

nodemcu-mdns-clent

用于 nodemcu 平台的纯 Lua 中的多播 DNS (mDNS) 服务客户端/浏览器。mDNS 提供了为本地设备提供和查找 DNS 名称的能力。有关 mDNS 和 DNS 服务发现的更多信息,请参阅http://www.dns-sd.org。

背景

我经常遇到硬编码Mqtt 代理的问题, iot-device 固件中的 Web 服务器 IP 和我最喜欢的 nodemcu,我的 lua 代码有很多硬编码的 IP。当服务器 IP 发生变化时,这种模式就会失效。因此希望设备能够按类型自动发现服务并开始寻找解决方案。最后,zeroconf/mdns 看起来是这个用例的最佳选择。在搜索 nodemcu 模块时,我在 mrpace2/lua-mdns 找到了 @mrpace2 的工作,并将其移植到 nodemcu 平台网络堆栈和回调样式中。

这仅用于从 nodemcu 查找其他设备。相反,如果您正在寻找一种在不知道 IP 的情况下让您的 esp8266 在本地网络中被发现的方法,它已经在 nodemcu 固件中作为一个模块提供。

下载和安装

由于这是单个模块文件,我建议将mdnsclient.lua 模块的原始文件下载到您的项目中,而不是 repo 签出。这意味着要在您的项目中使用。

例子

下面的代码查询本地网络上所有可用的 mqtt 代理,并打印第一个的 IP 地址和端口号

    mc = require('mdnsclient')
    local service_to_query = '_mqtt._tcp' --service pattern to search. this is for mqtt brokers
    local query_timeout = 2 -- 2 seconds

    -- handler to do some thing useful with mdns query results
    local query_result_handler  = function(err,query_result)
        if (query_result ~= nil) then
            print("Got Query results")
            local broker_ip,broker_port = mc.extractIpAndPortFromResults(res,1)
            print('Broker '..broker_ip ..":"..broker_port)
        else
            print('no mqtt brokers found in local network. please ensure that they are running and advertising on mdns')
        end
    end
    
    print('Connecting to wifi')
    wifi.setmode(wifi.STATION)
    wifi.sta.config('<SSID>', '<PASSWORD>')
    wifi.sta.getip()
    wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(T)
        print("\n\tSTA - GOT IP".."\n\tStation IP: "..T.IP)
        mc.mdns_query( service_to_query, query_timeout, T.IP, query_result_handler)
    end)

如果不带参数调用,query则在默认超时 2 秒后返回所有可用服务。其他示例可以在examples子目录中找到。

参考

唯一导出的函数是query。

询问

用法

    mdnsclient = require('mdnsclient')
    result = mdnsclient.query([<service>, <timeout_in_sec>,<esp8266_ip>,<callback>])

参数

查询最多需要两个参数:

  • service : mDNS 服务名称(例如 _printers._tcp.local)。.local后缀可以省略。如果此参数缺失或计算结果为nil,mdns_resolve通过枚举_services._dns-sd._udp.local服务来查询所有可用的 mDNS服务。
  • timeout:等待 mDNS 响应的超时时间(以秒为单位)。如果此参数缺失或计算结果为nil,则 mdns_resolve使用 2 秒的默认超时。
  • own_ip:等待 mDNS 响应的超时时间(以秒为单位)。如果此参数缺失或计算结果为nil,则 mdns_resolve使用 2 秒的默认超时。
  • callback:如果查询成功,服务描述符的关联数组作为 Lua 表返回给回调方法,回调方法应该有两个这样的参数callback(err,result)。请注意,如果本地网络上没有可用的 mDNS 服务,则该数组可能为空。如果出现错误,则会填充err广告结果为零。

mdns_query返回的服务描述符可能包含以下字段的组合:

  • name : mDNS 服务名称(例如HP Laserjet 4L @
  • service : mDNS 服务类型(例如_ipps._tcp.local)
  • hostname:主机名
  • port:端口号
  • ipv4 : IPv4 地址
  • ipv6 : IPv6 地址

mdns_resolve返回mDNS守护进程提供的任何信息。某些字段的存在并不意味着运行lua-mdns的系统支持所有特性。例如,即使系统上安装的LuaSocket库不支持IPv6,也可能返回IPv6地址。解决这种潜在的不匹配超出了lua-mdns的范围。
Return value nil.

执照

nodemcu-mdns-client在 MIT 许可下发布。

Original work Copyright (c) 2015 Frank Edelhaeuser
Modified work Copyright (c) 2017 Uday G

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

mdns.lua

--[[
    Original work Copyright (c) 2015 Frank Edelhaeuser
    Modified work Copyright (c) 2017 Uday G
    
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    
]]--

local mdnsclient = {}
do

local function mdns_make_query(service)
    -- header: transaction id, flags, qdcount, ancount, nscount, nrcount
    local data = '\000\000'..'\000\000'..'\000\001'..'\000\000'..'\000\000'..'\000\000'
    -- question section: qname, qtype, qclass
    for n in service:gfind('([^\.]+)') do
        data = data..string.char(#n)..n
    end
    return data..string.char(0)..'\000\012'..'\000\001'
end

local function mdns_parse(service, data, answers)

    --- Helper function: parse DNS name field, supports pointers
    -- @param data     received datagram
    -- @param offset    offset within datagram (1-based)
    -- @return  parsed name
    -- @return  offset of first byte behind name (1-based)
    local function parse_name(data, offset)
        local n,d,l = '', '', data:byte(offset)
        while (l > 0) do
            if (l >= 192) then -- pointer
                local p = (l % 192) * 256 + data:byte(offset + 1)
                return n..d..parse_name(data, p + 1), offset + 2
            end
            n = n..d..data:sub(offset + 1, offset + l)
            offset = offset + l + 1
            l = data:byte(offset)
            d = '.'
        end
        return n, offset + 1
    end

    --- Helper function: check if a single bit is set in a number
    -- @param val       number
    -- @param mask      mask (single bit only)
    -- @return  true if bit is set, false if not
    local function bit_set(val, mask)
        return val % (mask + mask) >= mask
    end

    -- decode and check header
    if (not data) then
        return nil, 'no data'
    end
    local len = #data
    if (len < 12) then
        return nil, 'truncated'
    end

    local header = {
        id = data:byte(1) * 256 + data:byte(2),
        flags = data:byte(3) * 256 + data:byte(4),
        qdcount = data:byte(5) * 256 + data:byte(6),
        ancount = data:byte(7) * 256 + data:byte(8),
        nscount = data:byte(9) * 256 + data:byte(10),
        arcount = data:byte(11) * 256 + data:byte(12),
    }
    if (not bit_set(header.flags, 0x8000)) then
        return nil, 'not a reply'
    end
    if (bit_set(header.flags, 0x0200)) then
        return nil, 'TC bit is set'
    end
    if (header.ancount == 0) then
        return nil, 'no answer records'
    end

    -- skip question section
    local name
    local offset = 13
    if (header.qdcount > 0) then
        for i=1, header.qdcount do
            if (offset > len) then
                return nil, 'truncated'
            end
            name, offset = parse_name(data, offset)
            offset = offset + 4
        end
    end

    -- evaluate answer section
    for i=1, header.ancount do
        if (offset > len) then
            return nil, 'truncated'
        end

        name, offset = parse_name(data, offset)
        local type = data:byte(offset + 0) * 256 + data:byte(offset + 1)
        local rdlength = data:byte(offset + 8) * 256 + data:byte(offset + 9)
        local rdoffset = offset + 10

        -- A record (IPv4 address)
        if (type == 1) then
            if (rdlength ~= 4) then
                return nil, 'bad RDLENGTH with A record'
            end
            answers.a[name] = string.format('%d.%d.%d.%d', data:byte(rdoffset + 0), data:byte(rdoffset + 1), data:byte(rdoffset + 2), data:byte(rdoffset + 3))
        end

        -- PTR record (pointer)
        if (type == 12) then
            local target = parse_name(data, rdoffset)
            table.insert(answers.ptr, target)
        end

        -- AAAA record (IPv6 address)
        if (type == 28) then
            if (rdlength ~= 16) then
                return nil, 'bad RDLENGTH with AAAA record'
            end
            local offs = rdoffset
            local aaaa = string.format('%x', data:byte(offs) * 256 + data:byte(offs + 1))
            while (offs < rdoffset + 14) do
                offs = offs + 2
                aaaa = aaaa..':'..string.format('%x', data:byte(offs) * 256 + data:byte(offs + 1))
            end

            -- compress IPv6 address
            for _, s in ipairs({ ':0:0:0:0:0:0:0:', ':0:0:0:0:0:0:', ':0:0:0:0:0:', ':0:0:0:0:', ':0:0:0:', ':0:0:' }) do
                local r = aaaa:gsub(s, '::')
                if (r ~= aaaa) then
                    aaaa = r
                    break
                end
            end
            answers.aaaa[name] = aaaa
        end

        -- SRV record (service location)
        if (type == 33) then
            if (rdlength < 6) then
                return nil, 'bad RDLENGTH with SRV record'
            end
            answers.srv[name] = {
                target = parse_name(data, rdoffset + 6),
                port = data:byte(rdoffset + 4) * 256 + data:byte(rdoffset + 5)
            }
        end

        -- next answer record
        offset = offset + 10 + rdlength
    end

    return answers
end


--- Locate MDNS services in local network
--
-- @param service   MDNS service name to search for (e.g. _ipps._tcp). A .local postfix will
--                  be appended if needed. If this parameter is not specified, all services
--                  will be queried.
--
-- @param timeout   Number of seconds to wait for MDNS responses. The default timeout is 2
--                  seconds if this parameter is not specified.
--
-- @param own_ip    querying device IP address
--
-- @param callback  to receive Table of MDNS services. Entry keys are service identifiers. Each entry
--                  is a table containing all or a subset of the following elements:
--                      name: MDNS service name (e.g. HP Laserjet 4L @ server.example.com)
--                      service: MDNS service type (e.g. _ipps._tcp.local)
--                      hostname: hostname
--                      port: port number
--                      ipv4: IPv4 address
--                      ipv6: IPv6 address
--
-- @return          Nothing 
--
function query(service, timeout,own_ip,callback)

    -- browse all services if no service name specified
    local browse = false
    if (not service) then
        service = '_services._dns-sd._udp'
        browse = true
    end

    -- append .local if needed
    if (service:sub(-6) ~= '.local') then
        service = service..'.local'
    end

    -- default timeout: 2 seconds
    local timeout = timeout or 2.0

    local mdns_multicast_ip, mdns_port = '224.0.0.251', 5353
    net.multicastJoin(own_ip, mdns_multicast_ip)
    udpSocket = net.createUDPSocket()
    -- collect responses until timeout
    local answers = { srv = {}, a = {}, aaaa = {}, ptr = {} }
    udpSocket:on("receive", function(s, data, port, ip)
        print(string.format("received '%s' from %s:%d", data, ip, port))
        if data and (port == mdns_port) then
            mdns_parse(service, data, answers)
            if (browse) then
                for _, ptr in ipairs(answers.ptr) do
                    s:send(mdns_port, mdns_multicast_ip, mdns_make_query(ptr))
                end
                answers.ptr = {}
            end
        end
    end)
    udpSocket:listen()
    port, ip = udpSocket:getaddr()
    local mdns_query = mdns_make_query(service)
    udpSocket:send(mdns_port, mdns_multicast_ip,mdns_query)

    tmr.alarm(0,timeout*1000,tmr.ALARM_SINGLE, function()
        --once the timer is over, cleanup thesockets and collect the results
        udpSocket:close()
        net.multicastLeave(own_ip,mdns_multicast_ip)

        local services = {}
        for k,v in pairs(answers.srv) do
            local pos = k:find('%.')
            if (pos and (pos > 1) and (pos < #k)) then
                local name, svc = k:sub(1, pos - 1), k:sub(pos + 1)
                if (browse) or (svc == service) then
                    if (v.target) then
                        if (answers.a[v.target]) then
                            v.ipv4 = answers.a[v.target]
                        end
                        if (answers.aaaa[v.target]) then
                            v.ipv6 = answers.aaaa[v.target]
                        end
                        if (v.target:sub(-6) == '.local') then
                            v.hostname = v.target:sub(1, #v.target - 6)
                        end
                        v.target = nil
                    end
                    v.service = svc
                    v.name = name
                    services[k] = v
                end
            end
        end

        local createCallbackWithArgs = function (err,data)
            return function()
                callback(err,data)
            end
        end
        node.task.post(createCallbackWithArgs(nil,services))
    end)
end

-- query results and 1 based index to identify the result to return when there are more than one matches
-- query results and 1 based index to identify the result to return when there are more than one matches
local extractIpAndPortFromResults = function(results,index)
    local ip,port

    local result_index = 1
    for k,v in pairs(results) do
        print(k)
        for k1,v1 in pairs(v) do
            print('  '..k1..': '..v1)
            if(k1=="ipv4") then ip = v1 end
            if(k1=="port") then port = v1 end
        end

        if result_index == index then
            return ip , port
        else
            result_index  = result_index + 1
        end

    end
    return nil , nil
end

mdnsclient = {
    extractIpAndPortFromResults=extractIpAndPortFromResults,
    query=query
}

end
return mdnsclient

discover_all_services.lua

--[[
    Original work Copyright (c) 2015 Frank Edelhaeuser
    Modified work Copyright (c) 2017 Uday G
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
]]--

mc = require('../mdnsclient')

-- constants
local service_to_query = '' --service pattern to search
local query_timeout = 2 -- seconds

-- handler to do some thing useful with mdns query results
local result_handler  = function(err,res)
    if (res) then
        print('Results:')
        for k,v in pairs(res) do
            print(k)
            for k1,v1 in pairs(v) do
                print('  '..k1..': '..v1)
            end
        end
    else
        print('no result')
    end
end

print('Connecting to wifi')
wifi.setmode(wifi.STATION)
wifi.sta.config('SSID', 'PASSWORD')
wifi.sta.getip()

wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(T)
    local own_ip = T.IP
    print('Starting mdns discovery')
    mc.query( service_to_query, query_timeout, own_ip,  result_handler)
end)

discover_connect_mqtt.lua

--[[
    Copyright (c) 2017 Uday G
        
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
]]--

--discovers a mqtt broker in the network and connects to it
mc = require('mdnsclient')

--constants
local service_to_query = '_mqtt._tcp' --service pattern to search
local query_timeout = 2 -- seconds
local query_repeat_interval = 10 -- seconds
local query_count_max = 3

local foundBroker = false
local query_count = 0 -- seconds

-- handler to do some thing useful with mdns query results
local result_handler  = function(err,res)
    if (res) then
        print("Got Query results")
        local b_ip, b_port = mc.extractIpAndPortFromResults(res,1)
        if b_ip and b_port then
            foundBroker = true
            m = mqtt.Client("clientid", 120, "user", "password")
            local broker_ip,broker_port,secure = b_ip,b_port,0
            local topic,message,QOS,retain= "/topic", "hello", 0, 0
            print('Connecting to Broker '..broker_ip ..":"..broker_port)
            m:connect(broker_ip, broker_port, secure,
                function(client)
                    client:publish(topic,message,QOS,retain,function(client)
                        m:close();
                    end)
                end)
        else
            print('Browse attempt returned no matching results')
        end
    else
        print('no mqtt brokers found in local network. please ensure that they are running and advertising on mdns')
    end
end

print('Connecting to wifi')
wifi.setmode(wifi.STATION)
wifi.sta.config('SSID', 'PASSWORD')
wifi.sta.getip()

wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(T)
    print("\n\tSTA - GOT IP".."\n\tStation IP: "..T.IP.."\n\tSubnet mask: "..
            T.netmask.."\n\tGateway IP: "..T.gateway)
    local own_ip = T.IP
    print('Starting mdns discovery')
    query_count = query_count + 1
    mc.query( service_to_query, query_timeout, own_ip,  result_handler)

    tmr.alarm(1,query_repeat_interval * 1000 ,tmr.ALARM_AUTO,function()
        if foundBroker == true then
            tmr.stop(1)
        elseif query_count > query_count_max then
            print("Reached max number of retries. Aborting")
            tmr.stop(1)
        else
            print('Retry mdns discovery - '..query_count)
            query_count = query_count + 1
            mc.query( service_to_query, query_timeout, own_ip,  result_handler)
        end
    end)

end)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Wireshark是一个开放源代码的网络封包分析软件,可用于捕获和分析网络数据包。你可以在本机上使用Wireshark来捕获通过本机的数据流量。引用 MDNS(Multicast DNS)是一种用于在局域网中发现和解析网络设备的服务。它使用特定的网络协议来实现设备之间的通信。如果你想使用Wireshark来捕获和分析MDNS数据包,你可以使用过滤器"mdns"来只捕获与MDNS相关的数据包。这样,你就可以观察局域网中设备之间的MDNS通信。引用 要使用Wireshark来抓取MDNS数据包,你需要按照以下步骤进行操作: 1. 首先,确保你已经安装了最新版本的Wireshark。你可以从官方网站下载安装包并按照指示进行安装。引用 2. 打开Wireshark软件,并选择你想要抓取数据包的网络接口。这可以是你的无线网卡或以太网接口。 3. 在过滤器框中输入"mdns",以只捕获与MDNS相关的数据包。这将过滤掉其他类型的数据包,使你能够更好地分析MDNS通信。引用 4. 点击"Start"按钮开始捕获数据包。Wireshark将开始监听指定的网络接口,并显示捕获到的数据包列表。 5. 当你希望停止捕获数据包时,点击"Stop"按钮。然后,你可以使用Wireshark的分析功能来查看和解析捕获到的MDNS数据包。引用 如果你对Wireshark的使用和MDNS的分析更加详细的了解,可以参考一些提供了相关信息的文章。其中包括提到的《Wireshark的下载安装及简单使用教程》和提到的《WireShark抓包分析》。这些文章将为你提供更多关于Wireshark和MDNS的信息和指导。引用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值