Bonjour 是苹果的一套零配置网络协议,用于发现局域网内的其他设备并进行通信,比如发现打印机、手机、电视等。
一句话:发现局域网其他设备和让其他设备发现。
Bonjour 可以完成的工作
- IP 获取
- 名称解析
- 搜索服务

实际应用场景示例,HomeKit/Matter 智能设备入网后,需要通过 Bonjour 来发现设备,获取 ip,进行 http 局域网通信,获取只能设备信息,绑定用户账号等。
权限要求
Bonjour 功能需要在info.plist 开启本地网络,和指定 services。

Tips:
_hap._tcp
遵循了 HAP(HomeKie Accessory Protocol) 智能家居协议的硬件,会在这个服务发送数据。
类似的,Matter 配件使用服务类型为_matter._tcp
检测本地网络是否权限是否开启
苹果并没有提供本地网络权限回调和查询,所有我们并不知道用户是否拒绝了本地网络权限。
可以在StackOverflow的How to check local network permission in iOS14,iOS 14 How to trigger Local Network dialog and check user answer?中找到检测代码,还是有效的。
Bonjour 扫描服务
通过 Bonjour 发现设备的实现分为以下情况
iOS2.0 - 15.0: NSNetServiceBrowser
iOS13.0+: NWBrowser (暂不知解析 ip 方式)
虽然按照苹果的习惯,被标记过期的接口,在未来很长时间都可以使用。但是也说明苹果提供了其他方式(NWBrowser)来实现 Bonjour
流程
先搜索服务,再连接服务,最后解析ip等信息。
NSNetserviceBrowser
在 iOS2.0到 iOS15.0系统中,Bonjour 可与通过 Foundation -> Bonjour 框架中的 NetserviceBrowser 来实现扫描和服务处理。
示例代码
开始扫描
objective-c
@property (nonatomic, strong) NSNetServiceBrowser *brower;
- (void)startSearch {
//[self.services removeAllObjects];
//[self.scanDevices removeAllObjects];
self.brower = [[NSNetServiceBrowser alloc] init];
self.brower.delegate = self;
[self.brower stop];
[self.brower searchForServicesOfType:@"_hap._tcp" inDomain:@"local."];
}
NetServiceBrowserDelegate代理
objective-c
/*
* 即将查找服务
*/
- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)browser {
NSLog(@"-----------------netServiceBrowserWillSearch");
}
/*
* 停止查找服务
*/
- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser {
NSLog(@"-----------------netServiceBrowserDidStopSearch");
}
/*
* 查找服务失败
*/
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didNotSearch:(NSDictionary<NSString *, NSNumber *> *)errorDict {
NSLog(@"----------------netServiceBrowser didNotSearch");
}
/*
* 发现域名服务
*/
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindDomain:(NSString *)domainString moreComing:(BOOL)moreComing {
NSLog(@"---------------netServiceBrowser didFindDomain");
// 过滤服务 添加到数组中(避免服务被释放,不走代理),连接服务
[self.services addObject:service];//此处添加为了避免sevice被过早释放,不走代理方法
service.delegate = self;
//解析服务
[service resolveWithTimeout:5];
}
/*
* 发现客户端服务
*/
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing {
NSLog(@"didFindService---------=%@ =%@ =%@",service.name,service.addresses,service.hostName);
[aNetService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
aNetService.delegate = self;
[aNetService resolveWithTimeout:6];
CFRunLoopRun();
}
/*
* 域名服务移除
*/
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveDomain:(NSString *)domainString moreComing:(BOOL)moreComing {
NSLog(@"---------------netServiceBrowser didRemoveDomain");
}
/*
* 客户端服务移除
*/
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing {
NSLog(@"---------------netServiceBrowser didRemoveService");
}
NSNetserviceDelegate
objective-c
- (void)netServiceDidResolveAddress:(NSNetService *)sender{
// 连接到服务,可以从 sender 中获取网络信息 比如 ip4 ip6 port 等
// 进行数据通信,比如 http数据交互
}
停止扫描
在业务中应该在一段时间后或者触发某些情况下关闭扫描,避免一致扫描。
objective-c
[self.brower stop];
从 NSNetservice中解析网络数据
objective-c
- (NSDictionary *)parsingIP:(NSNetService *)sender{
int sPort = 0;
NSString *ipv4;
NSString *ipv6;
for (NSData *address in [sender addresses]) {
typedef union {
struct sockaddr sa;
struct sockaddr_in ipv4;
struct sockaddr_in6 ipv6;
} ip_socket_address;
struct sockaddr *socketAddr = (struct sockaddr*)[address bytes];
if(socketAddr->sa_family == AF_INET) {
sPort = ntohs(((struct sockaddr_in *)socketAddr)->sin_port);
struct sockaddr_in* pV4Addr = (struct sockaddr_in*)socketAddr;
int ipAddr = pV4Addr->sin_addr.s_addr;
char str[INET_ADDRSTRLEN];
ipv4 = [NSString stringWithUTF8String:inet_ntop( AF_INET, &ipAddr, str, INET_ADDRSTRLEN )];
}
else if(socketAddr->sa_family == AF_INET6) {
sPort = ntohs(((struct sockaddr_in6 *)socketAddr)->sin6_port);
struct sockaddr_in6* pV6Addr = (struct sockaddr_in6*)socketAddr;
char str[INET6_ADDRSTRLEN];
ipv6 = [NSString stringWithUTF8String:inet_ntop( AF_INET6, &pV6Addr->sin6_addr, str, INET6_ADDRSTRLEN )];
}
else {
NSLog(@"Socket Family neither IPv4 or IPv6, can't handle...");
}
}
NSDictionary *data = @{@"type": [sender type],
@"domain": [sender domain],
@"name": [sender name],
@"ipv4": ipv4,
@"ipv6": ipv6,
@"port": [NSNumber numberWithInt:sPort]};
return data;
}
NWBrowser
在iOS13+在NetWork 框架中提供了 NWBrowser 类来实现 Bonjour,且只支持 Swfit.
示例代码
swift
import Foundation
import Network
@available(iOS 13.0, *)
@objc
class BonjourManager: NSObject {
@objc
static let shared = BonjourManager()
let browser = NWBrowser(for: .bonjourWithTXTRecord(type:"_hap._tcp", domain: ""), using: NWParameters())
/// 搜索, 先搜索,再连接,才能解析到网络信息
@objc
func start() {
browser.browseResultsChangedHandler = { (results, changes) in
print("Results:")
for result in results // 建议存在数组中,注意去重
{
if case .service(let name,let type,let domain,let interface) = result.endpoint
{
// Bonjour Service
debugPrint("Bonjour设备:\(name)")
// 条件过滤
let connection = NWConnection(to: result.endpoint, using: .tcp)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
if let innerEndpoint = connection.currentPath?.remoteEndpoint, case .hostPort(let host, let port) = innerEndpoint, case .ipv4(let ip4) = host {
switch host {
case .ipv4( let ip4):
print("Bonjour \(name) ip4:\(ip4.debugDescription)")
case .ipv6(let ip6):
print("Bonjour ip6:\(ip6.debugDescription)")
default:
print("Bonjour host:\(host.debugDescription)")
}
}
default :
break
}
// 断开连接 connection.cancel()
}
connection.start(queue: .main)
} else if case .hostPort(let host, let port) = result.endpoint {
debugPrint("\(host)")
}
else
{
assert(false, "This nevers gets executed")
}
}
// print("Changes:")
//
// for change in changes
// {
// if case .added(let added) = change
// {
// if case .service(let service) = added.endpoint
// {
// debugPrint(service)
// }
// else
// {
// assert(false, "This nevers gets executed")
// }
// }
// }
}
browser.start(queue: .main)
}
/// 取消
@objc
func cancel() {
browser.cancel()
}
}
参考
官方资料
Support local network privacy in app
WWDC2020视频资料,介绍为什么需要本地网络权限,怎么设置权限,什么情况下可以不使用访问本地网络,而使用其他技术。其中以Boujour
技术举例对本地网络权限的使用。
如何在你的 App 中使用组播网络
通过组播网络查看其他设备和通信。
组播在其他平台可能称为:组播DNS、mDNS 或者 DNS 服务发现
介绍本地网络权限和 Boujour info.plist 配置。以及如何利用 NWConnectionGroup 进行组播数据发送与接收。
如何获取 NWEndpoint 的 ip 信息 -- stackoverflow
该问题提供了 iOS14 通过 NWBrowser 使用 Bonjour 的方式。虽然得到的是 NWEndpoint,但是可以通过 NWConnection来获取 ip 信息。