基于 Bonjour 实现的自动跟服务器建立连接

627 阅读7分钟

我之前的写的日志系统,我发现每次都要手动输入 IP 地址才能开始收集日志,觉得很麻烦,所以我想着写一个自动连接日志服务器的功能。其功能流程如下:

image.png

1. 什么是 Bonjour ?

具体参考地址:

  1. Bonjour Concepts
  2. 局域网中的服务(设备)发现
  3. DNS-Based Service Discovery
  4. Multicast DNS

Bonjour 是苹果公司提出的一套零配置网络协议,旨在通过 IP 实现设备的自动发现和服务注册。它源于互联网工程任务组(IETF)下的 ZEROCONF 工作组,主要解决以下三个方面的问题:

  1. 地址分配:为主机分配 IP 地址,支持自分配的链接本地地址。
  2. 命名:使用多播 DNS(mDNS)在本地网络中通过名称而非 IP 地址进行设备识别。
  3. 服务发现:自动查找网络上的服务,使应用程序能够轻松识别和连接所需的服务。

Bonjour 使用户无需手动配置网络设置,简化了设备的连接和服务的使用,提升了用户体验。

2. 基于 Bonjour 的约定

日志系统提供的是日志和网络记录,日志上传的方式目前是基于 http 实现的,所以现在广播的内容如下:

{
  "name": "Log Record Server",
  "type": "http",
  "port": 27751,
  "protocol": "tcp",
  "txt": {
    "path": "join",
    "token": "***"
  }
}

之所以有 token 是为了标识用户,保证是日志系统发出的。

所有请求接口公共请求头:

{
  "Content-Type": "application/json;charset=utf-8"
}

接口说明: 加入日志系统接口

  • 接口地址: /join
  • 请求方式: POST

请求参数:

参数名类型必填描述
modelstring手机型号
tokenstring发现返回的 token
idstring设备信息md5签名

请求示例:

{
  "model": "john@example.com",
  "token": "***",
  "id": ""
}

成功响应示例:

{
  "code": 0 | 1 | 2,
  "message": "ok | token错误 | Access denied"
}

3. 环境说明以及使用

我们 App 采用的是 react-native ,其对应的库为:react-native-zeroconf ;日志系统使用 electron 开发的,其环境是 nodejs ,其对应的库为:bonjour-service 。

3.1. react-native-zeroconf 安装与使用

官网对应的教程地址:react-native-zeroconf

使用 yarn 安装:

yarn add react-native-zeroconf

对于 android 来说,打开项目的 AndroidManifest.xml 文件,添加以下权限:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

这样 android 的配置就完成了。对于 ios 来说在 plist 文件中添加以下内容:

<key>NSBonjourServices</key>
	<array>
		<string>my_service._tcp.</string>
		<string>my_other_service._tcp.</string>
	</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Describe why you want to use local network discovery here</string>

其中 my_service._tcp.my_other_service._tcp. 代表本地服务名称,也就是:服务类型.协议.,日志系统的类型是 type ,协议是 tcp ,所以就写成 http.tcp.NSLocalNetworkUsageDescription 则代表对应的提示,打开 App 后会出现这样的提示。

使用 react-native 查看附近的服务很简单:

import Zeroconf from "react-native-zeroconf";

const zeroconf: Zeroconf = new Zeroconf();
zeroconf.on("resolved", async (service) => {
  console.log(service)
});
zeroconf.scan("http", "tcp");

当发现后就会打印,打印的信息就是跟发现服务相关的信息。打印信息大致如下(日志系统打印的):

{
  "addresses": [
    "192.168.118.103",
    "fe80::14a4:ff35:6d14:3c25"
  ],
  "fullName": "admindeiMac.local._http._tcp.",
  "host": "admindeiMac.local.",
  "name": "Log Record Server$$1932364ea87-0-8822403450b6",
  "port": 27751,
  "txt": {
    "path": "/join",
    "token": "1932364ea87-0.8822403450b6"
  }
}

其中每个参数的大致含义为:

  • addresses 保存的是发布服务的 IP 地址,这里是 IPv4 和 IPv6
  • fullName 发布服务的完整名称
  • host 跟网址差不多,提供服务的主机名
  • name 发布时指定的服务名称
  • port 服务需要使用的端口号
  • txt 额外的数据,如果服务提供方需要额外的数据,那就会通过这个

当 zeroconf 去掉订阅后会收到 remove 的事件:

zeroconf.on("remove", (name) => {
  console.log("remove-----====", name);
});

当我关闭日志系统后,这里会打印,打印内容大致为:Log Record Server$$1932364ea87-0-8822403450b6 ,也就是发布的 name 名称。

扫描附近的服务使用 scan 方法,其 API 参数说明:

scan(
    /** @default "http" */
    type?: string,
    /** @default "tcp" */
    protocol?: string,
    /** @default "local." */
    domain?: string,
    /** @default "NSD" */
    implType?: ImplType,
): void;

其中 type 为服务类型,比如日志系统就是 http 。不管事发现服务还是发布服务都可能出现错误,所以一般需要监听是否有错误发生,这样也方便排查问题。

zeroconf.on("error", (err) => {
  logger.warn(LOG_KEY, "zeroconf出现错误", err);
})

至于发布服务,也能使用 react-native-zeroconf 来发布服务,但是由于我这里不需要所以就不再说明,如果仍然想知道,可以参考 bonjour-service 这个库。

3.2. bonjour-service

我用于在 electron 端发布服务的;在日志系统中有一个 http 的服务,专门用来收集 app 上报上来的数据。如果 app 知道日志系统的 IP 地址和端口号可以立马将本地的日志上报到日志系统中;我觉得手动配置 IP 地址啥的比较麻烦,于是我就编写了这套自动发现的模式,只要你的手机和日志系统在同一个局域网的网一网段下就能实现自我发现。

官网对应的教程地址:bonjour-service

使用 yarn 安装:

yarn add bonjour-service

接下来就是开心的使用了,不像 rn 那样还需要那么多设置,非常麻烦。

import { Bonjour } from 'bonjour-service';

const bonjour = new Bonjour()

bonjour.publish({
  name: `Log Record Server$$${this.token}`,
  type: 'http',
  port: httpPort,
  protocol: 'tcp',
  txt: { path: JOIN_PATH, token: this.token },
});

这样就发布了一个服务,等着被其他需要的发现。接下来我们说说各个参数的含义:

其中 new Bonjour() 也可以传入一些参数:

{
  multicast: true // use udp multicasting
  interface: '192.168.0.2' // explicitly specify a network interface. defaults to all
  port: 5353, // set the udp port
  ip: '224.0.0.251', // set the udp ip
  ttl: 255, // set the multicast ttl
  loopback: true, // receive your own packets
  reuseAddr: true // set the reuseAddr option when creating the socket (requires node >=0.11.13)
}
参数功能默认值参数类型技术标准注意事项
multicast是否启用多播模式true布尔值RFC 1112• 启用后使用 UDP 多播通信 • 需要网络和操作系统支持 • 某些云环境可能默认禁用
interface指定多播网络接口all字符串RFC 3678• 多网卡环境特别有用 • 指定后仅在特定接口收发包 • 需确保接口支持多播
port指定 mDNS 服务端口5353数字RFC 6762• IANA 官方 mDNS 端口 • 不建议修改 • <1024 端口可能需要 root 权限
ip指定 mDNS 多播地址'224.0.0.251'字符串RFC 6762 Section 5• 标准 mDNS 多播地址 • 不建议修改 • IPv6 使用 FF02::FB
ttl多播包生存时间255数字(0-255)RFC 1112 Section 6.1• 影响数据包传播距离 • 较大值传播更远 • 局域网可设小值
loopback是否接收自己的多播包true布尔值RFC 3678 Section 5.2• 开发调试建议启用 • 生产环境可禁用 • 影响 IP_MULTICAST_LOOP
reuseAddr允许端口复用true布尔值POSIX.1-2017 RFC 3493• Node.js ≥ 0.11.13 • 多进程部署必须启用 • 影响 SO_REUSEADDR

只不过在使用过程中,会提示没有这样的属性,这是因为这个库的类型文件有问题导致的。可以不管这样的报错,实际上是有的。只不过我暂时用不到设置这些值。对于 publish 来说,下面属性就跟发现很多相似了。

参数功能默认值参数类型技术标准注意事项
name服务实例名称必填字符串RFC 6763 Section 4.1.1• 在网络中必须唯一 • 支持 UTF-8 编码 • 用于服务发现显示
type服务类型必填字符串RFC 6763 Section 7• 格式:_服务名._协议 • 例:_http._tcp • IANA 注册列表
port服务端口号必填数字RFC 6763 Section 6.1• 1-65535 范围 • <1024 可能需要权限 • 建议使用注册端口(1024-49151)
host主机名本机名字符串RFC 6763 Section 4.1.2• 可以是 FQDN • 默认使用系统主机名 • 需要能被 DNS 解析
txt服务元数据{}对象RFC 6763 Section 6.3• 键值对格式 • 值限制为字符串或 Buffer • 单条记录不超过 255 字节
subtypes服务子类型[]字符串数组RFC 6763 Section 7.1• 用于服务细分 • 可选功能 • 便于服务过滤
protocol协议类型'tcp'字符串RFC 6763• 通常为 'tcp' 或 'udp' • 影响服务发现 • 建议保持默认

使用 bonjour-service 来进行服务发现,大致参考 react-native-zeroconf

4. 功能演示

如果你的 react-native App 集成了 @wutiange/log-listener-plugin 插件,版本需要大于等于 2.0.1-alpha.3 版本;然后在 App 中安装了 react-native-zeroconf 插件就可以实现自动发现日志服务的功能。然后就可以在日志系统中看到:

当你点击连接以后就会变成下面的页面:

这样就代表已经跟日志系统建立了连接,接下来你在 App 中的任何操作所产生的日志都能上报到日志系统中。