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)