深入浅出 Cocoa 之 Bonjour 网络编程
罗朝辉(http://blog.csdn.net/kesalin/)
CC许可,转载请注明出处
本文高度参考自 Tutorial: Networking and Bonjour on iPhone,在那个帖子里 iphone 版本的代码采用的是 MIT 开源协议,所以本例子中的 Mac 版本亦采用 MIT 开源协议。E文较好的童鞋建议阅读原文。
本文通过使用 Bonjour 实现了一个简单的服务器/客户端聊天程序,演示了 CFSocket,NSNetService/NSNetServiceBrowser, NSInStream/NSOutStream 的用法。
从上图可以清晰地看出,程序分为三个主要模块:UI模块,逻辑模块,网络模块。下面我们打开工程,看看代码实现:
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: Hook up our socket to the current run loop // CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent(); CFRunLoopSourceRef runLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, listeningSocket, 0); CFRunLoopAddSource(currentRunLoop, runLoopSource, kCFRunLoopCommonModes); CFRelease(runLoopSource);
// 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); }
- (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]; }
// 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]; } }
为了演示效果,我们需要运行该程序的两个实例,可以在如下路径找到可执行文件:
/Users/username/Library/Developer/Xcode/DerivedData/MacChatty-XXXX/Build/Products/Debug