聊天至少需要同时运行两个 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。
通过 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 等。
通过 Bonjour 决议服务
当用户选择其中一个 chat room,并加入其中时,客户端将会连接到发布该 chat room 服务的服务器。这个连接过程在 ChattyViewController 类的 joinChatRoom: 方法中实现。首选我们通过选择的 NSNetService 发送 resolveWithTimeout: 消息来进行决议应该连接到哪个服务器(请参考 Connection 类的 connect 方法中最后一种情形),同时设定 NSNetService 的 delegate 来响应决议相关的事件:didNotResolve: 和 netServiceDidResolveAddress:。当决议完成之后,在 netServiceDidResolveAddress: 方法中,我们可以创建用于数据传输的 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 网络编程介绍就结束了,代码中的注释相当详细,细节就不多罗嗦了。
参考资料: