Bonjour 简介

2019年8月29日 0 作者 Evgeny

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<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的介绍可以参考这里)
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;
    }
}