iOS_Bonjour编程总结一

Bonjour 简介

Bonjour是这样的一种技术,设备可以通过它轻松探测并连接到相同网络中的其他设备,整个过程只需要很少的用户参与或是根本就不需要用户参与。典型的Bonjour应用有Remote应用,AirPrint等。建立一个Bonjour连接一般需要三个步骤,服务端发布服务,客户端浏览服务,客户端服务端交互。

发布服务

1. 创建socket

demo代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
-(BOOL)setupListeningSocket
{
CFSocketContext socketCtxt = {0,(__bridge void*)self, NULL, NULL, NULL};
ipv4socket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
kCFSocketAcceptCallBack,
(CFSocketCallBack)&BonjourServerAcceptCallBack,
&socketCtxt);
if (ipv4socket == NULL) {
if (ipv4socket) {
CFRelease(ipv4socket);
}
ipv4socket = NULL;
return NO;
}
int yes = 1;
setsockopt(CFSocketGetNative(ipv4socket),
SOL_SOCKET,
SO_REUSEADDR,
(void *)&yes,
sizeof(yes));
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(port);
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
if (kCFSocketSuccess != CFSocketSetAddress(ipv4socket, (__bridge CFDataRef)address4)) {
NSLog(@"Error setting ipv4 socket address");
if (ipv4socket) {
CFRelease(ipv4socket);
}
ipv4socket = NULL;
return NO;
}
if (port == 0) {
NSData *addr = (__bridge NSData*)CFSocketCopyAddress(ipv4socket);
memcpy(&addr4, [addr bytes], [addr length]);
port = ntohs(addr4.sin_port);
}
CFRunLoopRef cfr1 = CFRunLoopGetCurrent();
CFRunLoopSourceRef src4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, ipv4socket, 0);
CFRunLoopAddSource(cfr1, src4, kCFRunLoopCommonModes);
CFRelease(src4);
return YES;
}

代码解析

CFSocketContext

是一个结构体,包含了自定义数据和回调函数,可以在其中操作CFSocket对象的具体行为。

1
2
3
4
5
6
7
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFSocketContext;

version: 必须是0, 结构体版本号。

info: 指向自定义数据的指针,它会在CFSocket创建的时候与之关联,这个指针会被传递给所有定义在context内的回调方法。

retain: 一个定义在info指针上的retain 回调。可以是NULL。

release: 一个定义在info指针上的relsease回调。可以是NULL。

copyDescription: 一个定义在info指针上的拷贝描述回调。可以是NULL。

CFSocketCreate

1
2
3
4
5
6
7
CFSocketCreate(CFAllocatorRef allocator,
SInt32 protocolFamily,
SInt32 socketType,
SInt32 protocol,
CFOptionFlags callBackTypes,
CFSocketCallBack callout,
const CFSocketContext *context);

创建一个指定协议和类型的CFSocket对象。

allocater: 分配器是用来为新对象分配内存的,传递NULL或者KCFAllocatorDefault 使用当前默认的分配器。

protocolFamily: socket的协议族,如果为负数或者0,则socket默认为PE_INET。

socketType: 所创建的Socket的类型,如果protocolFamily是PE_INET并且socketType是负数或者0,socketType的默认值是SOCK_STREAM。

protocol: socket的协议。如果protocolFamily是PE_INET并且protocol是负数或者0,那么socket的protocol的默认值是IPPROTO_TCP。如果socketType是SOCK_STREAM或者SOCK_DGRAM那么默认为IPPROTO_UDP。

callBackTypes: 一个按位或结合的socket类型,会调起socket的callout.

1
2
3
4
5
6
7
8
typedef enum CFSocketCallBackType : CFOptionFlags {
kCFSocketNoCallBack = 0,
kCFSocketReadCallBack = 1,
kCFSocketAcceptCallBack = 2,
kCFSocketDataCallBack = 3,
kCFSocketConnectCallBack = 4,
kCFSocketWriteCallBack = 8
} CFSocketCallBackType;

callout: 当一种callBackTypes被激活时这个方法被调用。

context:一个保存着CFSocket对象上下文信息的结构体。函数将信息拷贝出结构体之外,所以上下文指向的内存不需要超出函数的调用,可以是NULL。

setsockopt^参考1^

1
2
3
4
5
int setsockopt(int s,
int level,
int optname,
const void * optval,
socklen_toptlen);

用来设置参数 s 所指定的socket状态。参数 level 代表代表预设置的网络层。一般设置为SOL_SOCKET 以存取socket层。参数 optname 代表欲设置的选项:

​ SO_DEBUG 打开或者关闭排错模式。

​ SO_REUSEADDR 允许在bind ()过程中本地地址可重复使用

​ SO_TYPE 返回socket 形态.

​ SO_ERROR 返回socket 已发生的错误原因

​ SO_DONTROUTE 送出的数据包不要利用路由设备来传输.

​ SO_BROADCAST 使用广播方式传送

​ SO_SNDBUF 设置送出的暂存区大小

​ SO_RCVBUF 设置接收的暂存区大小

​ SO_KEEPALIVE 期确定连线是否已终止.

​ SO_OOBINLINE 当接收到OOB 数据时会马上送至标准输入设备

​ SO_LINGER:确保数据安全且可靠的传送出去.

参数 optval 代表欲设置的值, 参数optlen 则为optval 的长度.

返回值:成功则返回0, 若有错误则返回-1, 错误原因存于errno.

CFSocketGetNative

返回系统原生socket, 如果返回值为-1,表示无效的socket

sockaddr_in6

1
2
3
4
5
6
7
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

sin_family: 指协议族,在socket编程中只能是AF_INET。

sin_port: 存储端口号,使用网络字节顺序。

size_zero: 是为了让sockaddr与sockadrr_in 两个数据结构保持大小相同而保留的空字节。

sin_addr: 网络地址。

sin_len: 根据《UNIX Network Programming Volume 1》3.1节中的说法,我们可以不关注这个细节(即可以认为这个sin_len字段存在与否对我们的应用程序是透明的)。这个字段不是每种Linux版本都提供,且POSIX标准中对struct sockaddr_in的定义是否需包含该字段不做要求。

2. 发布Bonjour服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-(void)publicBonjour {
service = [[NSNetService alloc]
initWithDomain:@""
type:@"_riverli._tcp."
name:@"riverliBonjour"
port:port];
if (service == nil) {
NSLog(@"NSNetService create failed!");
return ;
}
service.delegate = self;
[service publish];
}
#pragma mark NSNetServiceDelegate
- (void)netServiceWillPublish:(NSNetService *)sender {
NSLog(@"netServiceWillPublish");
}
- (void)netServiceDidPublish:(NSNetService *)sender {
NSLog(@"netServiceDidPublish");
}
- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary<NSString *, NSNumber *> *)errorDict {
NSLog(@"didNotPublish");
}
- (void)netServiceDidStop:(NSNetService *)sender {
port = 0;
CFRelease(ipv4socket);
NSLog(@"netServiceDidStop");
}

3. 接受socket 回调

这部分可能为三个步骤:

  1. 在第一步创建的CFSocketCallBack对象中有接收到socket消息的回调函数BonjourServerAcceptCallBack,我们在这个回调函数中拿到当前的Bonjour服务。
  2. 如果调用类型是kCFSocketAcceptCallBack,表示接受到了一个新的连接,在这里我们创建NSStream的读写对象。
  3. 在NSStream的读写对象里,我们接受客户的信息,并将信息发送给客户端。(关于NSStream的介绍可以参考这里)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
static void BonjourServerAcceptCallBack (CFSocketRef socket,
CFSocketCallBackType type,
CFDataRef address,
const void *data,
void *info) {
Bonjour *server = (__bridge Bonjour*)info;
if (type == kCFSocketAcceptCallBack) {
// AcceptCallBack: data is pointer to a CFSocketNativeHandle
CFSocketNativeHandle socketHandle
= *(CFSocketNativeHandle *)data;
CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
CFStreamCreatePairWithSocket(kCFAllocatorDefault,
socketHandle,
&readStream,
&writeStream);
if (readStream && writeStream) {
CFReadStreamSetProperty
(readStream,
kCFStreamPropertyShouldCloseNativeSocket,
kCFBooleanTrue);
CFWriteStreamSetProperty
(writeStream,
kCFStreamPropertyShouldCloseNativeSocket,
kCFBooleanTrue);
NSInputStream *is = (__bridge NSInputStream*)readStream;
NSOutputStream *os = (__bridge NSOutputStream*)writeStream;
[server handleNewConnectionWithInputStream:is
outputStream:os];
} else {
// encountered failure
// no need for socket anymore
close(socketHandle);
}
// clean up
if (readStream) {
CFRelease(readStream);
}
if (writeStream) {
CFRelease(writeStream);
}
}
}
- (void)handleNewConnectionWithInputStream:(NSInputStream*)istr
outputStream:(NSOutputStream*)ostr {
inputStream = istr;
outputStream = ostr;
inputStream.delegate = self;
outputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
// output stream is scheduled in the runloop when it is needed
if (inputStream.streamStatus == NSStreamStatusNotOpen) {
[inputStream open];
}
if (outputStream.streamStatus == NSStreamStatusNotOpen) {
[outputStream open];
}
}
#pragma mark - NSStreamDelegate
- (void)stream:(NSStream *)aStream
handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventHasBytesAvailable:
if (aStream == inputStream) {
//接收数据
}
break;
case NSStreamEventHasSpaceAvailable: {
if (aStream == outputStream) {
//发送数据
}
break;
}
case NSStreamEventOpenCompleted:
if (aStream == inputStream) {
NSLog(@"Input Stream Opened");
} else {
NSLog(@"Output Stream Opened");
}
break;
case NSStreamEventEndEncountered: {
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
break;
}
case NSStreamEventErrorOccurred:
if (aStream == inputStream) {
NSLog(@"Input error: %@", [aStream streamError]);
} else {
NSLog(@"Output error: %@", [aStream streamError]);
}
break;
default:
if (aStream == inputStream) {
NSLog(@"Input default error: %@", [aStream streamError]);
} else {
NSLog(@"Output default error: %@", [aStream streamError]);
}
break;
}
}

参考资料

参考一:c语言中文网

参考二:slvher的博客

参考三:Apple : Stream Programming Guide

参考四:iOS网络高级编程