zoukankan      html  css  js  c++  java
  • [Cocoa]深入浅出Cocoa之Bonjour网络编程

    深入浅出Cocoa之Bonjour网络编程

    罗朝辉 (http://www.cnblogs.com/kesalin/)

    本文遵循“署名-非商业用途-保持一致”创作公用协议

    本文高度参考自 Tutorial: Networking and Bonjour on iPhone,在那个帖子里 iphone 版本的代码采用的是 MIT 开源协议,所以本例子中的 Mac 版本亦采用 MIT 开源协议。E文较好的童鞋建议阅读原文。

    本文通过使用 Bonjour 实现了一个简单的服务器/客户端聊天程序,演示了 CFSocket,NSNetService/NSNetServiceBrowser, NSInStream/NSOutStream 的用法。

    代码下载:点击这里
    效果图如下:

    Cocoa 网络框架:

    Cocoa 网络框架有三层,最底层的是基于 BSD socket库,然后是 Cocoa 中基于 C 的 CFNetwork,最上面一层是 Cocoa 中 Bonjour。通常我们无需与 socket 打交道,我们会使用经 Cocoa 封装的 CFNetwork 和 Bonjour 来完成大多数工作。注:cocoa 很多组件都有两种实现,一种是基于 C 的以 CF 开头的类(CF=Core Foundation),这是比较底层的;另一种是基于 Obj-C 的以 NS 开头的类(NS=Next Step),这种类抽象层次更高,易于使用。对于网络框架也一样。Bonjour 中 NSNetService 也有对应的 CFNetService,NSInputStream 有对应的 CFInputStream。

    Sockets vs Streams:

    Socket 相当于一条通信信道,应用程序通过创建 socket,然后使用这个 socket 连接到其他应用程序进行数据交换。我们可以通过同一个 socket 来发送数据或者接收数据。每个 socket 有一个 ip 地址和 port(通信端口,介于 1 ~ 65535之间)。

    Stream 是传送数据的单向通道,正因为是单向的,所以我们有输入/输出两种 streams:instream/outstream。stream 只是临时缓存数据,我们需要将它与文件或内存绑定,从而可以从/向文件或内存中读/写数据。在这个教程中,我们使用 stream 结合 socket 在网络上传送和接收数据。

    Bonjour 简介:

    Bonjour(法语中的你好)是一种能够自动查询接入网络中的设备或应用程序的协议。Bonjour 抽象掉 ip 和 port 的概念,让我们聚焦于更容易为人类思维理解的 service。通过 Bonjour,一个应用程序 publish 一个网络服务 service,然后网络中的其他程序就能自动发现这个 service,从而可以向这个 service 查询其 ip 和 port,然后通过获得的 ip 和 port 建立 socket 链接进行通信。通常我们是通过 NSNetService 和 NSNetServiceBrowser 来使用 Bonjour 的,前者用于建立与发布 service,后者用于监听查询网络上的 service。

    同步与异步操作:
    大多数网络操作是阻塞模式的,比如链接的建立,等待接收数据,或发送数据给网络另一端。因此如果我们不进行异步处理的话,当在进行网络通信时,我们的 UI 机会被阻塞。有两种办法来处理阻塞问题:启用多个线程或更有效地利用当前线程。在这个例子中,我们使用后一种办法,我们通过 cocoa 提供的 run loop 来做这个事情,其工作原理是:将网络消息当作普通的事件丢到当前的 run loop 中,从而我们可以异步处理它们。

    Run loops 简介:

    run loop 是 thread 中的消息处理循环,有事件来则处理,无事件则啥也不做。cocoa 中的 run loop 可以处理用户 UI 消息,网络连接消息,timer 消息等。我们也可以添加其他的消息来源,如 socket 和 stream,从而让 run loop 也可以处理它们。

    程序框架:

    理论介绍得差不多了,更多细节,请翻阅官方文档。下面我们来看看整个程序的框架设计图:

    从上图可以清晰地看出,程序分为三个主要模块:UI模块,逻辑模块,网络模块。下面我们打开工程,看看代码实现:

    从工程图可以看出,代码结构相当清晰,所有的类被分为四个 group:

    Networking: 网络相关的代码,包括 socket 的创建,连接的建立,service 的 publish 和 browser;
    Business Logic:业务逻辑相关代码。在这个例子中,我们通过 room service 来提供聊天服务。我们通过建立一个 localroom 来创建服务器,并发布一个 room service,客户端(remoteroom)能够连接到一个已有的 room service,从而加入该 room 进行对话活动。
    UI :在这个例子中,UI 很简单,只有两个 view,一个显示当前网络中的 service 列表,另一个显示 room,以及在该 room service 上进行的对话。
    Misc:一个辅助类,用于存储用户设定名称。

    网络类

    Server class:创建 server,并发布 service;
    Connection class:决议 service;与服务器建立连接;通过 socket stream 交换数据;
    ServerBrowser class:查询可用的 service;

    Room类:

    Room class: Room 基类
    LocalRoom class: 创建服务器,发布 service,相应客户端的连接请求
    RemoteRoom:  连接到服务器已有的 service,

    网络数据传输过程:


    从上图可以看出,数据从 A 的逻辑层,经 outgoing buffer 写入 write stream,然后经 socket 通过网络传输到 B 的网络层,然后 B 端的  read stream 从 socket 中读取数据,写入 incoming buffer,然后在 B 的逻辑层以及 UI 上显示出来。

    用户交互操作都在 UI layer 上进行,当用户通过 broadcastChatMessage:fromUser: 发送一条聊天信息,由逻辑层来决定是发送给服务器(由 Remote room 处理),还是发送给连接到服务器自身的所有客户端(由 Local room 处理)。当从网络连接接收到一条聊天信息时,逻辑层会得到通知,客户端只会简单地将消息显示在 UI 上,而服务器首先将收到的聊天信息转发给所有连接到它的客户端,然后将该信息在 UI 上显示出来。

    Socket+Streams+Buffers = Connection

    Connection 类对一些的交互进行了封装:
    两个 socket stream,一个用来写入,一个用来读取;两块 data buffer,每个 socket stream 对应一个 data buffer;以及各种控制 flag 和值

    因为 stream 是单向的,所以我们需要为每一个 socket 建立两个 stream,一个用来从 socket 读取数据,一个用来向 socket 写入数据。我们在 connect 和 setupSocketStreams 中初始化它们。

    在本例中,我们通过两种方式来创建 socket:
    1,(客户端)通过创建 socket 连接到指定 ip 和 port 的服务器;
    2,(服务器)通过接收来自客户端的连接请求,在这种情况下,OS 会自动创建一个用于响应的 socket,并通过 native socket handle 传递给我们使用;

    无论 socket 是由哪种方式建立的,我们都是通过相同的代码 setupSocketStreams 来初始化 stream。

    创建 server
    聊天至少需要同时运行两个 MacChatty 终端,其中至少有一个作为服务器,其他终端才能作为客户端连接到服务器进行对话。作为服务器的终端,需要创建一个 socket 来监听(listen)其他终端的连接请求(请参考 Sever class 中的 listeningSocket)。这项工作是在 Server 类中的 createServer 中完成的。

    客户端如何知道怎样连接到服务器呢?每一个网络终端必须有独一无二的 ip 和 port,ip 地址是由动态获取的或由用户设定的,因此我们在这里无需操心 ip 地址问题,因此在代码中我们使用了 INADDR_ANY。那又如何设定我们想要监听的 port 呢?一些服务必须监听约定的 port 才能工作,比如 80,20, 21等端口都是有约定用途的。在这里我们把端口设定问题交给 OS 来处理,OS 会为我们设定一个没有被占用的 port。为了实现这个目的,我们传入 port 为 0。为了让其他客户端能够连接到服务器,我们需要告知其他客户端服务器实际使用的 port,因此,我们在 createServer 方法 PART 3中获取实际使用 port。

        //// PART 3: Find out what port kernel assigned to our socket
    //
    // We need it to advertise our service via Bonjour
    NSData *socketAddressActualData = [(NSData *)CFSocketCopyAddress(listeningSocket) autorelease];

    // Convert socket data into a usable structure
    struct sockaddr_in socketAddressActual;
    memcpy(&socketAddressActual, [socketAddressActualData bytes], [socketAddressActualData length]);

    self.port = ntohs(socketAddressActual.sin_port);

    然后在 PART 4 中,我们将 listening socket 注册为 application run loop 的消息源,这样当有新连接到来的时候, OS 就会调用 serverAcceptCallback 这个回调函数通知我们。

        //// PART 4: Hook up our socket to the current run loop
    //
    CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
    CFRunLoopSourceRef runLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, listeningSocket, 0);
    CFRunLoopAddSource(currentRunLoop, runLoopSource, kCFRunLoopCommonModes);
    CFRelease(runLoopSource);

    在 serverAcceptCallback 回调处理中,我们创建一个新的 Connection 对象,然后将它与 OS 自动创建的响应新连接的 socket 绑定起来。然后再将这个 Connection 对象传递给 Server delegate。

    // Handle new connections
    - (void) handleNewNativeSocket:(CFSocketNativeHandle)nativeSocketHandle
    {
    Connection* connection = [[[Connection alloc] initWithNativeSocketHandle:nativeSocketHandle] autorelease];

    // In case of errors, close native socket handle
    if ( connection == nil ) {
    close(nativeSocketHandle);
    return;
    }

    // finish connecting
    BOOL succeed = [connection connect];
    if ( !succeed ) {
    [connection close];
    return;
    }

    // Pass this on to our delegate
    [delegate handleNewConnection:connection];
    }


    // This function will be used as a callback while creating our listening socket via 'CFSocketCreate'
    static void serverAcceptCallback(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
    {
    // We can only process "connection accepted" calls here
    if ( type != kCFSocketAcceptCallBack ) {
    return;
    }

    // for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
    CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;

    Server *server = (Server *)info;
    [server handleNewNativeSocket:nativeSocketHandle];

    NSLog(@" >> server accepted connection with socket %d", nativeSocketHandle);
    }

    通过 Bonjour 发布服务

    Bonjour 并非在网络查找服务的唯一途径,但它是最容易使用的方法之一。我们在 publishService 方法中创建一个 NSNetService 对象来发布服务。我们根据服务类型在网络查找感兴趣的服务,本聊天服务使用“_chatty._tcp.”作为服务类型。在同一网络中,服务类型名必须唯一,这样才能精准定位服务,而不至于引发冲突。

    Bonjour 操作也如 socket 一样需要异步进行,以避免长时间阻塞主线程。因此在实际发布服务时,我们将发布任务交给当前 run loop 去调度,然后设定其 delegate,由 delegate 来处理相关事件:“Publishing succeeded”, “Publishing failed”等。

    - (BOOL) publishService
    {
    // come up with a name for our chat room
    NSString* chatRoomName = [NSString stringWithFormat:@"%@'s chat room", [[AppConfig sharedInstance] name]];

    // create new instance of netService
    self.netService = [[NSNetService alloc] initWithDomain:@"" type:@"_chatty._tcp." name:chatRoomName port:self.port];
    if (self.netService == nil)
    return NO;

    // Add service to current run loop
    [self.netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

    // NetService will let us know about what's happening via delegate methods
    [self.netService setDelegate:self];

    // Publish the service
    [self.netService publish];

    return YES;
    }

    通过 Bonjour 查询服务

    我们在 ServerBrowser 类中实现 Bonjour 查询网络服务的功能。我们创建一个 NSNetServiceBrowser 对象来查询类型为 “_chatty._tcp.” 的服务。当前网络中发现有服务被添加到或移除时,NSNetServiceBrowser 的 delegate 即我们的 ServerBrowser 就能得到通知,以进行相应的逻辑处理:更新服务列表,刷新 UI 等。

    // New service was found
    - (void) netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
    didFindService:(NSNetService *)netService
    moreComing:(BOOL)moreServicesComing
    {
    // Make sure that we don't have such service already (why would this happen? not sure)
    if ( ! [servers containsObject:netService] ) {
    // Add it to our list
    [servers addObject:netService];
    }

    // If more entries are coming, no need to update UI just yet
    if ( moreServicesComing ) {
    return;
    }

    // Sort alphabetically and let our delegate know
    [self sortServers];

    [delegate updateServerList];
    }


    // Service was removed
    - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
    didRemoveService:(NSNetService *)netService
    moreComing:(BOOL)moreServicesComing
    {
    // Remove from list
    [servers removeObject:netService];

    // If more entries are coming, no need to update UI just yet
    if ( moreServicesComing ) {
    return;
    }

    // Sort alphabetically and let our delegate know
    [self sortServers];

    [delegate updateServerList];
    }

    通过 Bonjour 决议服务

    当用户选择其中一个 chat room,并加入其中时,客户端将会连接到发布该 chat room 服务的服务器。这个连接过程在 ChattyViewController 类的 joinChatRoom: 方法中实现。首选我们通过选择的 NSNetService 发送 resolveWithTimeout: 消息来进行决议应该连接到哪个服务器(请参考 Connection 类的 connect 方法中最后一种情形),同时设定 NSNetService 的 delegate 来响应决议相关的事件:didNotResolve: 和 netServiceDidResolveAddress:。当决议完成之后,在 netServiceDidResolveAddress: 方法中,我们可以建立到服务的 socket 连接并创建用于数据传输的 stream 了。

    // Called when net service has been successfully resolved
    - (void)netServiceDidResolveAddress:(NSNetService *)sender
    {
    if ( sender != netService ) {
    return;
    }

    // Save connection info
    self.host = netService.hostName;
    self.port = netService.port;

    // Don't need the service anymore
    self.netService = nil;

    // Connect!
    if ( ![self connect] ) {
    [delegate connectionAttemptFailed:self];
    [self close];
    }
    }

    至此,Bonjour 网络编程介绍就结束了,代码中的注释相当详细,细节就不多罗嗦了。

    为了演示效果,我们需要运行该程序的两个实例,可以在如下路径找到可执行文件:

    /Users/username/Library/Developer/Xcode/DerivedData/MacChatty-XXXX/Build/Products/Debug

    参考资料
    Tutorial: Networking and Bonjour on iPhone:http://mobileorchard.com/tutorial-networking-and-bonjour-on-iphone/
    Introduction to Bonjour Overview:http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/NetServices/Introduction.html
    Introduction to NSNetServices and CFNetServices Programming Guide:http://developer.apple.com/library/mac/#documentation/Networking/Conceptual/NSNetServiceProgGuide/Introduction.html#//apple_ref/doc/uid/TP40002736

  • 相关阅读:
    Postgresql HStore 插件试用小结
    postgres-xl 安装与部署 【异常处理】ERROR: could not open file (null)/STDIN_***_0 for write, No such file or directory
    GPDB 5.x PSQL Quick Reference
    postgresql 数据库schema 复制
    hive 打印日志
    gp与 pg 查询进程
    jquery table 发送两次请求 解惑
    python 字符串拼接效率打脸帖
    postgresql 日期类型处理实践
    IBM Rational Rose软件下载以及全破解方法
  • 原文地址:https://www.cnblogs.com/kesalin/p/cocoa_bonjour.html
Copyright © 2011-2022 走看看