Bonjour 简介
Bonjour是这样的一种技术,设备可以通过它轻松探测并连接到相同网络中的其他设备,整个过程只需要很少的用户参与或是根本就不需要用户参与。典型的Bonjour应用有Remote应用,AirPrint等。建立一个Bonjour连接一般需要三个步骤,服务端发布服务,客户端浏览服务,客户端服务端交互。
发布服务
1. 创建socket
demo代码:
-(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对象的具体行为。
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
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.
typedef enum CFSocketCallBackType : CFOptionFlags {
kCFSocketNoCallBack = 0,
kCFSocketReadCallBack = 1,
kCFSocketAcceptCallBack = 2,
kCFSocketDataCallBack = 3,
kCFSocketConnectCallBack = 4,
kCFSocketWriteCallBack = 8
} CFSocketCallBackType;
callout: 当一种callBackTypes被激活时这个方法被调用。
context:一个保存着CFSocket对象上下文信息的结构体。函数将信息拷贝出结构体之外,所以上下文指向的内存不需要超出函数的调用,可以是NULL。
setsockopt^参考1^
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
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服务
-(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 *)errorDict {
NSLog(@"didNotPublish");
}
- (void)netServiceDidStop:(NSNetService *)sender {
port = 0;
CFRelease(ipv4socket);
NSLog(@"netServiceDidStop");
}
3. 接受socket 回调
这部分可能为三个步骤:
- 在第一步创建的CFSocketCallBack对象中有接收到socket消息的回调函数BonjourServerAcceptCallBack,我们在这个回调函数中拿到当前的Bonjour服务。
- 如果调用类型是kCFSocketAcceptCallBack,表示接受到了一个新的连接,在这里我们创建NSStream的读写对象。
- 在NSStream的读写对象里,我们接受客户的信息,并将信息发送给客户端。(关于NSStream的介绍可以参考这里)
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网络高级编程
交流群
移动开发交流群:264706196