ios局域网联机—苹果官方源码之WiTap剖析(一)(二)
http://www.it165.net/pro/html/201204/2094.html
http://www.it165.net/pro/html/201204/2165.html
在过去的时间里,我一直在考虑的事情是,我该写一篇什么样的文章呢?之前的两篇文章都是先有问题,然后我才有目的的解决问题,现在我的困扰是,我不知道该写什么了呵呵。因为其实,大多数的问题,只要在网上搜索一下(google远比baidu要强得多),基本上都能找到解决的办法,已经有了许多相关方面的教程或参考资料了,我并不是一个喜欢重复做别人已经做得很好的工作的人,所以我现在需要你的帮助,如果你有好的关于写什么方面的文章的建议,请留言告诉我(声明:应用我并不在行!),如果我能实现的话,一定会写出来分享给大家,如果写不出来,大家一起讨论下解决也是很好的!!谢谢!!我甚至翻译了raywenderlich的“怎么在ios5上做一个简单的iphone应用程序系列”的第一部分,但是当我想要把他发布的时候我放弃了,这个不是我擅长的,子龙山人博客翻译团队做这个更专业,我不应该把这种翻译的文章放在我自己的博客上,所以我想我还是把raywenderlich的这个“怎么在ios5上做一个简单的iphone应用程序系列”的3部分都翻译完后在送给山人比较好(当然得在人家同意的前提下哈哈)。
最终,我觉得把一些经典的源码分析一下也许会是一个好主意,所以,今天我要写的是苹果官方的源码witap例子的分析。所以,首先你需要下载这个官方的源码。
前提
我们文章的标题已经揭示了这个witap例子的内容是局域网联机的,这个对联机游戏来说真的很有用,联机的话你需要真机设备才能体验到,正常情况下你需要两个真机,不过其实一个真机加一个模拟器也是可以的,我在学习这个例子的时候就是一个真机加模拟器的组合呵呵.(人穷没办法呀☹)
首先我们先在模拟器上运行一下这个例子好让我们对这个程序先有个直观的感受。
没有太多的东西,一个状态栏,下面是一个UIView,这个UIView里有一些元素(每个元素,在后面的具体代码里我们会一一指出),总之,现在界面上显示的是3条信息,一条是提示我们等待另一个玩家加入,下面一条是设备的名字(这里我是在模拟器上运行的,所以显示的是我的计算机的名字),最后一条是提示我们或者要加入另一个游戏。我们随便在屏幕上点点看,没有任何反应。我们还是乖乖的听话,让另一个玩家加入我们吧,不然真的我们什么都做不了,首先确保你的两台设备(我是我的电脑和touch)都在同一个网络内,然后打开真机上的这个程序。
look,我们的模拟器发现了我的touch,哈哈,同样的,在真机的tableView里,你也会看到你的模拟器的名字。现在两台机器已经发现彼此了,然我们点一下tableView里的名字试试看吧,有反应了,我们进入了苹果给我们带来的小游戏:
点击弹出通知的continue来继续游戏,在游戏里随便点点,我们点击屏幕上的任意一个色块,我们的另一台设备上的同一色块出现被同步的点击的效果,其实还挺好玩的呵呵。
开始
好了,我们现在对这个例子有了直观的认识了,让我们来开始一步步的分析它吧,对于代码里涉及到的知识点我们会进行不限于代码范围的讲述。
首先,让我们来从main.m文件开始吧。在other Source文件夹里,我们选中main.m来看一下它的代码。
01.
#
import
<UIKit/UIKit.h>
02.
03.
int
main(
int
argc,
char
*argv[])
04.
{
05.
NSAutoreleasePool *pool = [NSAutoreleasePool
new
];
06.
UIApplicationMain(argc, argv, nil, @
"AppController"
);
07.
[pool release];
08.
return
0
;
09.
}
第一行是导入UIKit框架,这个不用说了。
下面是main函数,和c程序一样,这个main函数也是我们的程序的入口。其实我们的程序的起点是start函数,在start函数里调用了main函数,然后在main函数里边,构建了一个自动释放池(这个witap的例子最新的版本是1.8,这个版本并没有针对ios5更新,所以这里的代码还是用的自动释放池,在ios5之后由于引入了ARC,所以ios5的main函数和之前的版本的main函数是有变化的,不再使用自动释放池了)。
在这个main函数里边最重要的一句代码就是:UIApplicationMain(argc, argv, nil, @"AppController"),这句是开始我们程序的关键,前两个参数就是main自己的参数,一个代表命令行参数的个数,一个是指向所有命令行参数的指针。第三个参数是代表我们的程序的主类,如果为nil则代表主类是UIApplication类,如果程序中使用自定义的UIApplication类的子类作为主类,你需要自己在这里指定,不过不推荐这样做!!。第四个参数是我们的代理类,如果为nil的话,则程序假设程序的代理来自Main nib文件(ios5之前,ios5改成用委托类的类名生成的字符串来指定了)。
那么UIApplicationMain这个函数又做了什么呢?在这个函数里边,我们根据我们的参数“主类名”,这里是UIApplication类,来实例化一个主类。然后对这个实例会设置他的委托为我们在第四个参数里指定的类,并调用_run方法,_run方法又会调用CFRunLoopRunInMode(⋯⋯),CFRunLoopRunInMode方法又会根据它的参数来以相应的模式运行RunLoop(RunLoop的概念很重要,我们后边会说明),这样注册到这个模式下的事件在发生时,我们相应的事件处理方法就会收到消息并处理。(这个需要结合下一段来理解)
RunLoop是一个运行回路,每个线程都有一个自己的RunLoop,我们平时并不用管理它是因为我们的主线程中的RunLoop默认情况下就启动了,UIApplication类帮我们做的。RunLoop做的具体的工作是监测输入源,如果有输入源事件的话,RunLoop分发这个事件给事件的处理方法来处理事件,如果没有输入源事件的话,RunLoop就让我们的线程休眠,什么都不做来节省资源消耗。那么什么是输入源呢?输入源包括:用户设备输入、网络连接、周期或延迟事件、异步回调。RunLoop能监测3中类型的对象:sources (CFRunLoopSource Reference), timers (CFRunLoopTimer Reference), and observers (CFRunLoopObserver Reference),要让RunLoop监测这些对象,需要先把他们加入到RunLoop中,针对这三种对象,有3个不同的方法用来加入到RunLoop中:CFRunLoopAddSource
, CFRunLoopAddTimer
, CFRunLoopAddObserver,这样加入到RunLoop后,RunLoop才会监测这些输入源,如果不想继续监测这个输入源的话,可以用CFRunLoopRemoveSource方法从RunLoop中移出输入源。还有一点要强调的是,在把这些输入源加入到RunLoop时,我们必须要把这些输入源关联到一个或多个RunLoop模式,模式决定了在RunLoop的一次迭代中,什么事件需要处理,因为RunLoop在运行时就指定了以什么模式运行,所以,RunLoop只处理那些关联到它当前运行模式的输入源。通常情况下我们如果添加输入源到RunLoop的话,我们会把它和默认模式
kCFRunLoopDefaultMode相关联,在应用程序或线程闲置的时候就会处理这些事件。事实上你可以定义自己的模式,通过自己的模式你可以对需要处理的事件进行限制(具体请参考官方文档)。
最后对于一个程序来说,简单地来总结一下,RunLoop做的工作是什么。
我们的程序有一个UIApplication的实例变量app,这个app实例变量为我们在我们的主线程里(以某种模式,我个人猜测会是默认模式吧)运行了一个RunLoop,并且它把一些输入源加入到了这个RunLoop中(比如触摸事件、网络连接⋯⋯等等),并与当前运行的这个模式关联,这样有相应事件发生的时候,RunLoop就会监测到这些事件了,当RunLoop监测到这些事件后,就通过委托(前面设定了为AppController)层层分发,直到分发给我们的相应事件的处理方法进行处理,(比如触摸事件的处理方法是touchBegan、touchMove、touchEnd等),这样我们就能正确的处理这些事件了。这也正是我们能处理触摸事件的原因,UIApplication类,已经把相应的输入源加入到我们主线程的RunLoop中了。
下面我们来看看applicationDidFinishLaunching:方法,它是告诉我们UIApplication已经准备好了,可以运行了,是我们的程序可见代码部分,在main函数之后运行的第一个方法,打开AppController.m文件:
01.
- (
void
) applicationDidFinishLaunching:(UIApplication *)application
02.
{
03.
CGRect rect;
//1
04.
UIView* view;
05.
NSUInteger x, y;
06.
07.
//Create a full-screen window //2
08.
_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
09.
[_window setBackgroundColor:[UIColor darkGrayColor]];
10.
11.
//Create the tap views and add them to the view controller's view
12.
rect = [[UIScreen mainScreen] applicationFrame];
//3
13.
for
(y =
0
; y < kNumPads; ++y) {
//4
14.
for
(x =
0
; x < kNumPads; ++x) {
15.
view = [[TapView alloc] initWithFrame:CGRectMake(rect.origin.x + x * rect.size.width / (
float
)kNumPads, rect.origin.y + y * rect.size.height / (
float
)kNumPads, rect.size.width / (
float
)kNumPads, rect.size.height / (
float
)kNumPads)];
//5
16.
[view setMultipleTouchEnabled:NO];
//6
17.
[view setBackgroundColor:[UIColor colorWithHue:((y * kNumPads + x) / (
float
)(kNumPads * kNumPads)) saturation:
0.75
brightness:
0.75
alpha:
1.0
]];
18.
[view setTag:(y * kNumPads + x +
1
)];
//7
19.
[_window addSubview:view];
//8
20.
[view release];
//9
21.
}
22.
}
23.
24.
//Show the window
25.
[_window makeKeyAndVisible];
//10
26.
27.
//Create and advertise a new game and discover other availble games
28.
[self setup];
//11
29.
}
注释1,就是声明一些变量,一个rect,一个view,两个整数。
注释2,实例化一个UIWindow变量,这是我们的主窗口,并把它的大小区域设为全屏大小,[[UIScreen mainScreen] bounds]]得到的就是全屏的区域大小,然后设置它的
颜色为灰色。
注释3,把我们前面申请的rect变量,设为整个屏幕除了状态栏的大小区域,[[UIScreen mainScreen] applicationFrame]得到的就是除了状态栏的屏幕大小区域。
注释4,这个for循环是添加游戏中的色块儿的(就是上面游戏运行图中的9个色块),在AppController.m文件的最上边,我们看到我们用宏定义了KNumPads为3,所以这里
是外循环和内循环都是3次,共9次。
注释5,这是实例化我们的色块儿,并分配给我们前面申请的view变量。我们的色块是单独的类TapView的实例,它是继承自UIView的,我们先不管它的实现,就把它当一个
UIView来对待就好了,后面我们会详细介绍它的内容。在实例化这些色块的时候我们通过简单的计算来给这9个色块划分不同的区域和位置,使这9个色块均匀的分布在
我们的屏幕上,当然,是去除了状态栏之后的区域。
注释6,设置我们的色块的多点触摸为否,这样我们的色块是不会相应多点触摸了。
注释7,给我们的色块设置不同的tag,这里是编号1到9。
注释8,把我们的色块加入主窗口的子集,这样当我们的主窗口显示的时候,我们的色块作为子视图,也就会显示了。
注释9,以为我们把色块加入window的时候,他帮我们retain了,所以这里我们要release。
注释10,显示我们的window。
注释11,这是调用我们这个类的setup方法,这个方法后面会详述,现在先不管了。
我们写着来看看,这个setup方法是什么:
01.
[_server release];
//1
02.
_server = nil;
03.
04.
[_inStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//2
05.
[_inStream release];
06.
_inStream = nil;
07.
_inReady = NO;
08.
09.
[_outStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//3
10.
[_outStream release];
11.
_outStream = nil;
12.
_outReady = NO;
13.
14.
_server = [TCPServer
new
];
//4
15.
[_server setDelegate:self];
16.
NSError *error = nil;
17.
if
(_server == nil || ![_server start:&error]) {
//5
18.
if
(error == nil) {
19.
NSLog(@
"Failed creating server: Server instance is nil"
);
20.
}
else
{
21.
NSLog(@
"Failed creating server: %@"
, error);
22.
}
23.
[self _showAlert:@
"Failed creating server"
];
//6
24.
return
;
25.
}
26.
27.
//Start advertising to clients, passing nil for the name to tell Bonjour to pick use default name
28.
if
(![_server enableBonjourWithDomain:@
"local"
applicationProtocol:[TCPServer bonjourTypeFromIdentifier:kGameIdentifier] name:nil]) {
//7
29.
[self _showAlert:@
"Failed advertising server"
];
30.
return
;
31.
}
32.
NSLog(@
"the server in appController is: %@"
,_server.description);
33.
34.
[self presentPicker:nil];
//8
35.
}
注释1、2和3,都是一些清理方法,用来确保当我们操作的这些变量之前用内容的话,先把它们清空,再重新进行操作。(当程序里第二次调用这个方法时,就显出了这几句的效果)
,这里先是对一个TCPServer类(后面会讲)实例进行置空操作;然后把一对输入输出流对象,从当前RunLoop中移出,这样我们的RunLoop就不再监测这两个输入
输出流事件了,从RunLoop中移除后,也对它们进行置空操作;最后又把两个用来标示输入输出流是否准备好的Bool变量设为假,标示我们没有准备好。
标示4,重新初始化这个TCPServer类变量,并把它的委托设为这个AppController。
注释5,判断这个TCPServer类实例变量_server是否为空,并对它调用start:方法,并判断start:方法的返回值。如果有问题根据判断条件输出相应的错误信息。
注释6,当有错的时候弹出警告窗口来说明失败情况。
注释7,对这个_server变量调用一个方法,这个方法是用来发布我们的服务的。(这个例子用Bonjour实现联机,而boujour实现是通过NSNetService来发布服务,用
NSNetServiceBrowser来搜索服务来实现的,这也是一个重要的知识点,后面会讲)
注释8,这个是显示我们的这篇文章中第一个图中的界面的一个方法。
这个例子真是千头万绪呀,我希望一部分一部分的拆开来分析,可是它的每一部分总是和其他内容相关联,摘不出来呀,郁闷!好了,我们继续吧,如果要弄明白这个setup方法到底做了什么,我们必需得先分析这个TCPServer类才行,然后还要细分这个注释8中的方法才能真正了解这个setup方法做了哪些工作。
难啃的骨头
我们来见识一下这个TCPServer的真面目吧,打开TCPServer.h文件:
01.
@class
TCPServer;
02.
03.
NSString *
const
TCPServerErrorDomain;
04.
05.
typedef
enum
{
06.
kTCPServerCouldNotBindToIPv4Address =
1
,
07.
kTCPServerCouldNotBindToIPv6Address =
2
,
08.
kTCPServerNoSocketsAvailable =
3
,
09.
} TCPServerErrorCode;
在文件的最上面部分,我们看到,我们先用@class来修饰我们的TCPServer,这是告诉编译器,这个TCPServer是一个类,这样我们就可以在还没有声明这个类的时候在方法里先用,在以后在实现它的定义。(这样也就是为什么我们后面的协议方法中用到了TCPServer类,但这个类的声明却在协议之后,而我们在编译是不报错的原因)
我们又声明了一个字符串常量,它的定义是在TCPServer.m文件里的,在这里只是声明。
定义了TCPServer的错误代码的枚举值,用来表示不同的错误情况。
接着,我们定义了一个协议(协议是objective-c中,不同的类之间沟通的好方法,个人觉得和symbian里的M类基本上一样):
1.
@protocol
TCPServerDelegate <NSObject>
2.
@optional
3.
- (
void
) serverDidEnableBonjour:(TCPServer*)server withName:(NSString*)name;
4.
- (
void
) server:(TCPServer*)server didNotEnableBonjour:(NSDictionary *)errorDict;
5.
- (
void
) didAcceptConnectionForServer:(TCPServer*)server inputStream:(NSInputStream *)istr outputStream:(NSOutputStream *)ostr;
6.
@end
协议的名字叫:TCPServerDelegate,这个协议有3个可选的方法,这三个方法第一个是TCPServer用boujour发布服务成功之后我们用来处理一些东西的方法,第二个是失败的时候我们用来处理一些东西的方法,第三个是当TCPServer接受了其他设备的连接请求之后,我们用来处理东西的方法。
下面让我们看看这个TCPServer类的声明:
01.
@interface
TCPServer : NSObject <NSNetServiceDelegate> {
02.
@private
03.
id _delegate;
04.
uint16_t _port;
05.
uint32_t protocolFamily;
06.
CFSocketRef witap_socket;
07.
NSNetService* _netService;
08.
}
09.
10.
- (BOOL)start:(NSError **)error;
11.
- (BOOL)stop;
12.
- (BOOL) enableBonjourWithDomain:(NSString*)domain applicationProtocol:(NSString*)protocol name:(NSString*)name;
13.
- (
void
) disableBonjour;
14.
15.
@property
(assign) id<TCPServerDelegate> delegate;
16.
17.
+ (NSString*) bonjourTypeFromIdentifier:(NSString*)identifier;
这个TCPServer类,被声明继承自NSObject类,并且它遵守NSNetServiceDelegate协议,这个协议是我们的NSNetService类的一些回调方法,就是说如果我们的NSNetService服务发布成功或者失败的话,会调用这个协议里的相应方法来进行处理。事实上这个协议的所有方法都是可选的,如果你不实现他们也不会出错,不过那样的话,我们就不能在服务发布状态改变是做相应的处理了。
在这个interface里,声明了5个私有变量,一个id类的_delegate,它用来跟踪我们这个TCPServer类的委托,一个uint16_t类型的_port变量,存储我们发布服务时绑定的Socket的端口号,一个uint32_t类型的protocolFamily,来存储我们的socket的协议族,一个CFSocketRef类的 witap_socket,就是我们的等待其他设备连接的socket,一个NSNetService类的_netService,就是我们用来发布服务的NSNetService。
start:方法,我们在这个方法里创建并配置我们用来监听网络连接的socket,并创建RunLoop输入源,加入到当前RunLoop中,这样只要有我们的这个socket有连接事件,我们就能得到通知并触发相应的回调。
stop方法,明显不过了,它是停止我们的网络连接服务的,让我们取消对网络连接事件的监听,并释放这个监听的socket。
disableBonjour方法,停止我们的当前的已经发布的服务。
enableBonjourWithDomain:applicationProtocol:name:方法是事实上进行NSNetService服务发布的方法。
接着是一个声明,声明了一个id<TCPServerDelegate>的属性delegate,这是一个满足TCPServerDelegate协议的属性。
最后是一个bonjourTypeFromIdentifier:方法,这是个辅助方法,它用来返回我们要发布的服务的协议的(这个协议不是委托类的协议,它只是一个代表唯一标识的字符串),并且这个字符串是用要求的,它不能超过14个字符,并且只能包含小写字母、数字和连接符,开关和结尾不能是连接符。
下面,我们该看看这个类的具体实现了,打开TCPServer.m文件:
1.
#
import
"TCPServer.h"
2.
3.
NSString *
const
TCPServerErrorDomain = @
"TCPServerErrorDomain"
;
4.
5.
@interface
TCPServer ()
6.
@property
(nonatomic,retain) NSNetService* netService;
7.
@property
(assign) uint16_t port;
8.
@end
首先是包含TCPServer.h文件,然后是我们在.h文件中声明的那个常量字符串的定义。
然后下面……(其实我不太明白苹果这个地方的用法,正常情况下我会觉得这是一个分类,但是如果是分类的话,是不可以添加实例变量的,这个地方是添加了两个属性,这个什么情况??就算可以在分类里添加属性,那为什么要这么做呢?为什么不在正常的原始类里添加呢?是不是因为这样的话这个属性是在TCPServer.m文件里的,那么在这个分类里声明的属性就对外不可见不可用了呢?那又为什么分类没有自己的implementation呢?它怎么和原始类共用一个呢?如果你知道这个原因,请告诉我,不胜感激!!!)
再接着是这个TCPServer类的实现部分:
01.
@implementation
TCPServer
02.
03.
@synthesize
delegate=_delegate, netService=_netService, port=_port;
04.
05.
- (id)init {
06.
return
self;
07.
}
08.
09.
- (
void
)dealloc {
10.
[self stop];
11.
[
super
dealloc];
12.
}
这里合成了三个属性的set和get方法;然后是初始化方法,只是简单地返回自己;然后是dealloc方法,这会在我们这个类销毁时调用,这个方法里是先调用stop方法停掉网络连接服务,然后调用父类的dealloc方法。
再接着看这个handleNewConnectionFromAddress方法:
1.
- (
void
)handleNewConnectionFromAddress:(NSData *)addr inputStream:(NSInputStream *)istr outputStream:(NSOutputStream *)ostr {
2.
3.
// if the delegate implements the delegate method, call it
4.
if
(self.delegate && [self.delegate respondsToSelector:
@selector
(didAcceptConnectionForServer:inputStream:outputStream:)]) {
5.
[self.delegate didAcceptConnectionForServer:self inputStream:istr outputStream:ostr];
6.
}
7.
}
在这个方法里,我们先判断self的委托是否为空(我们在AppController.m的setup方法里的注释4中,把委托设为了AppController),并判断这个委托响不响应didAcceptConnectionForServer:inputStream:outputStream:方法,如果响应,就对self的委托调用这个方法来处理一些事情。
现在轮到一个重量级的方法了,TCPServerAcceptCallBack:
01.
static
void
TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address,
const
void
*data,
void
*info) {
02.
03.
TCPServer *server = (TCPServer *)info;
04.
NSLog(@
"the server in call back is: %@"
,server.description);
05.
06.
if
(kCFSocketAcceptCallBack == type) {
07.
// for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
08.
CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
09.
uint8_t name[SOCK_MAXADDRLEN];
10.
socklen_t namelen = sizeof(name);
11.
NSData *peer = nil;
12.
if
(
0
== getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen)) {
13.
peer = [NSData dataWithBytes:name length:namelen];
14.
}
15.
CFReadStreamRef readStream = NULL;
16.
CFWriteStreamRef writeStream = NULL;
17.
CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStream, &writeStream);
18.
if
(readStream && writeStream) {
19.
CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
20.
CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
21.
[server handleNewConnectionFromAddress:peer inputStream:(NSInputStream *)readStream outputStream:(NSOutputStream *)writeStream];
22.
}
else
{
23.
// on any failure, need to destroy the CFSocketNativeHandle
24.
// since we are not going to use it any more
25.
close(nativeSocketHandle);
26.
}
27.
if
(readStream) CFRelease(readStream);
28.
if
(writeStream) CFRelease(writeStream);
29.
}
30.
}
这是一个回调方法,在我们的监听网络连接的socket,接收到连接事件后,这个回调方法就被调用。需要说明的是,这个回调方法的格式是固定的。原因是这样的,这个回调方法是要在创建socket的时候传递给socketCreat方法的callout参数的,这个callout是一个函数指针,这个函数指针是CFSocketCallBack 类型的,所以我们的这个回调方法也应该是这个类型的。我们来看一下这个类型:
1.
typedef
void
(*CFSocketCallBack) (
2.
CFSocketRef s,
3.
CFSocketCallBackType callbackType,
4.
CFDataRef address,
5.
const
void
*data,
6.
void
*info
7.
);
这个类型就是我们的函数指针的定义,它的返回值是void的,也就是没有返回值;再来看看它的参数:
很明显,第一个参数是触发了这个回调的socket本身,第二个是触发这个回调的事件类型,第三个代表请求连接的远端设备的地址,第四个参数有点神奇,它根据回调事件的不同,它代表的东西也不同,如果这个是连接失败回调事件,那它就代表一个错误代码的指针,如果是连接成功的回调事件,它就是一个Socket指针,如果是数据回调事件,这就是包含这些数据的指针,其它情况下它是NULL的,最后一个参数是我们创建socket的时候用的那个CFSocketContext结构的info成员。
明白这个函数指针的类型再对照着我们的回调函数看是不是结构完全一样,呵呵。
这个例子要写的东西远比我想的要多,太长了,所以,这篇就先到这儿吧,后面的内容会再发后续章节。
这篇文章是"ios局域网联机——苹果官方源码之WiTap剖析"系列的第二部分,它和第一部分紧紧相连,因此阅读此文章的前提是你已经阅读了ios局域网联机—苹果官方源码之WiTap剖析(一)。
打起精神继续战斗
好吧,让我们接着第一部分继续剖析这个TCPServer.m文件吧,上一部分中我们是讲到了TCPServerAcceptCallBack:这个回调方法,讲到了它的格式,它是一个符合CFSocketCallBack这个函数指针类型的函数。
现在让我们继续研究它的实现吧,为了方便阅读,我们再一次把这个函数的实现展示出来:
01.
static
void
TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address,
const
void
*data,
void
*info) {
02.
03.
04.
05.
TCPServer *server = (TCPServer *)info;
//1
06.
07.
08.
if
(kCFSocketAcceptCallBack == type) {
//2
09.
10.
// for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
11.
12.
CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
//3
13.
14.
uint8_t name[SOCK_MAXADDRLEN];
//4
15.
16.
socklen_t namelen = sizeof(name);
17.
18.
NSData *peer = nil;
//5
19.
20.
if
(
0
== getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen)) {
21.
22.
peer = [NSData dataWithBytes:name length:namelen];
23.
24.
}
25.
26.
CFReadStreamRef readStream = NULL;
//6
27.
28.
CFWriteStreamRef writeStream = NULL;
29.
30.
CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStream, &writeStream);
31.
32.
if
(readStream && writeStream) {
33.
34.
CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
//7
35.
36.
CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
37.
38.
[server handleNewConnectionFromAddress:peer inputStream:(NSInputStream *)readStream outputStream:(NSOutputStream *)writeStream];
39.
40.
}
else
{
41.
42.
// on any failure, need to destroy the CFSocketNativeHandle
43.
44.
// since we are not going to use it any more
45.
46.
close(nativeSocketHandle);
47.
48.
}
49.
50.
if
(readStream) CFRelease(readStream);
//8
51.
52.
if
(writeStream) CFRelease(writeStream);
53.
54.
}
55.
56.
}
注释1,我们看到这里它把这个函数的参数info转成了一个TCPServer类型,并申请了一个TCPServer类的变量来跟踪它。(我们在第一部分里已经说了,这里的info参数就是触发这个回调的socket再被创建时,传入到创建函数里的CFSocketContext结构的info成员,在后面的start方法里我们会看到,这个结构的info成员是我们的这个TCPServer类本身)
注释2,这里是判断一下我们这次回调的事件类型,如果事件是成功连接我们就进行一系列操作,否则我们什么也不做。
注释3,这里我们是把这个函数的参数data转成了一个CFSocketNativeHandle类型。(同样的也是我们在第一部分里说过的,当这个回调事件的类型是连接成功的时候,这个data就是一个CFSocketNativeHandle类型指针,这个CFSocketNativeHandle类型其实就是我们的特定平台的socket,你就当成正常的socket理解就行了,值得注意的是这里的socket是什么,是呢儿来的?我们知道,在正常的socket流程中,作为服务器的一方会有一个socket一直处于监听连接的状态,一旦有新的连接请求到来,系统会自己创建一个新的socket与这个请求的客户端进行连接,此后客户端和服务器端就通过这个新的连接进行通讯,而服务器负责监听网络连接的socket则继续监听连接。现在这个函数里的这个data应该就是在响应连接请求的时候系统自己创建的新的socket吧。<声明:这些概念我是了解自互联网,如果有什么不对的地方请及时指出,我会及时纠正,以免误导他人>)
注释4,申请了一个255大小的数组用来接收这个新的data转成的socket的地址,还申请了一个socklen_t变量来接收这个地址结构的大小。
注释5,这里又申请了一个NSData类的变量peer,在这个注释的if语句的{}里,我们看到它是用来存储我们的新的socket的地址的。我们先来看这个if语句的判断表达式吧,其实这里是一个getpeername()函数的调用,这个函数有3个参数,第一个参数是一个已经连接的socket,这里就是nativeSocketHandle;第二个参数是用来接收地址结构的,就是说这个函数从第一个参数的socket中获取与它捆绑的端口号和地址等信息,并把它存放在这第二个参数中;第三个参数差不多了,它是取出这个socket的地址结构的数据长度放到这个参数里面。如果没有错误的话这个函数会返回0,如果有错误的话会返回一个错误代码。这里判断了getpeername的返回值,没有错误的情况下,把得到的地址结构存储到我们申请的peer里。
注释6,申请了一对输入输出流,用CFStreamCreatePairWithSocket()方法把我们申请的这一对输入输出流和我们的已建立连接的socket(即现在的nativeSocketHandle)进行绑定,这样我们的这个连接就可以通过这一对流进行输入输出的操作了,这个函数操作完成之后,这两个输入输出流会被重新指向,使其指向有效的地址区域。此函数的的一个参数是一个内存分配器(苹果管理优化内存的一种措施,更多信息可网上查询),第二个参数就是想用我们第三和第四个参数代表的输入输出流的socket,第三和第四个参数就是要绑定到第二个参数表示的socket的输入输出流的地址。
注释7,如果我们的CFStreamCreatePairWithSocket()方法操作成功的话,那么我们现在的readStream和writeStream应该指向有效的地址,而不是我们在刚申请时赋给的NULL了。此时,判断这两个流是不是NULL就等于说我们判断函数CFStreamCreatePairWithSocket()有没有操作成功。
如果成功的话,我们就设置这两个流的属性,这里是把这两个流的属性kCFStreamPropertyShouldCloseNativeSocket设置为真,默认情况下这个属性是假的,这个设为真就是说,如果我们的流释放的话,我们这个流绑定的socket也要释放。(这里是对两个流都进行了相同的属性设置,事实上苹果的文档里在对CFStreamCreaterPairWithSocket()这个方法说明的时候提到,大多数流的属性是共享的,你只要对这一对中的一个设置了属性,那么也会自动为另一个流设置这个属性)。然后我们对注释1中得到的TCPServer类的server变量调用handleNewConnectionFromAddress:方法,这个方法的三个参数,一个是已连接socket的地址,另两个就是输入和输出流了。这个方法的内容很简单,在本系列的第一部分我们已经介绍了,这里就不重复了。
如果失败的话,我们就销毁着了已经连接的socket。
注释8,这里是先对流的内容进行清空操作,防止在使用它们的时候,里面有我们不需要的垃圾数据。
真是出了一头汗呀,这个函数太费劲了讲起来⋯⋯好在是说完了,不过,呵呵,下一个函数依然是艰巨呀。
艰苦继续,你敢坚持吗?
哈哈,又是一个巨大的函数呀,恐怖的start方法,不要害怕,我们会把它分析的支离破碎的哈哈哈,好吧,先看看它的实现:
001.
(BOOL)start:(NSError **)error {
002.
003.
004.
005.
CFSocketContext socketCtxt = {
0
, self, NULL, NULL, NULL};
//1
006.
007.
008.
009.
// Start by trying to do everything with IPv6. This will work for both IPv4 and IPv6 clients
010.
011.
// via the miracle of mapped IPv4 addresses.
012.
013.
014.
015.
witap_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET6, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, (CFSocketCallBack)&TCPServerAcceptCallBack, &socketCtxt);
//2
016.
017.
018.
019.
if
(witap_socket != NULL)
// the socket was created successfully //3
020.
021.
{
022.
023.
protocolFamily = PF_INET6;
//4
024.
025.
}
else
// there was an error creating the IPv6 socket - could be running under iOS 3.x //5
026.
027.
{
028.
029.
witap_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, (CFSocketCallBack)&TCPServerAcceptCallBack, &socketCtxt);
030.
031.
if
(witap_socket != NULL)
032.
033.
{
034.
035.
protocolFamily = PF_INET;
036.
037.
}
038.
039.
}
040.
041.
042.
043.
if
(NULL == witap_socket) {
//6
044.
045.
if
(error) *error = [[NSError alloc] initWithDomain:TCPServerErrorDomain code:kTCPServerNoSocketsAvailable userInfo:nil];
046.
047.
if
(witap_socket) CFRelease(witap_socket);
048.
049.
witap_socket = NULL;
050.
051.
return
NO;
052.
053.
}
054.
055.
056.
057.
058.
059.
int
yes =
1
;
//7
060.
061.
setsockopt(CFSocketGetNative(witap_socket), SOL_SOCKET, SO_REUSEADDR, (
void
*)&yes, sizeof(yes));
062.
063.
064.
065.
// set up the IP endpoint; use port 0, so the kernel will choose an arbitrary port for us, which will be advertised using Bonjour
066.
067.
if
(protocolFamily == PF_INET6)
//8
068.
069.
{
070.
071.
struct sockaddr_in6 addr6;
072.
073.
memset(&addr6,
0
, sizeof(addr6));
074.
075.
addr6.sin6_len = sizeof(addr6);
076.
077.
addr6.sin6_family = AF_INET6;
078.
079.
addr6.sin6_port =
0
;
080.
081.
addr6.sin6_flowinfo =
0
;
082.
083.
addr6.sin6_addr = in6addr_any;
084.
085.
NSData *address6 = [NSData dataWithBytes:&addr6 length:sizeof(addr6)];
086.
087.
088.
089.
if
(kCFSocketSuccess != CFSocketSetAddress(witap_socket, (CFDataRef)address6)) {
090.
091.
if
(error) *error = [[NSError alloc] initWithDomain:TCPServerErrorDomain code:kTCPServerCouldNotBindToIPv6Address userInfo:nil];
092.
093.
if
(witap_socket) CFRelease(witap_socket);
094.
095.
witap_socket = NULL;
096.
097.
return
NO;
098.
099.
}
100.
101.
102.
103.
// now that the binding was successful, we get the port number
104.
105.
// -- we will need it for the NSNetService
106.
107.
NSData *addr = [(NSData *)CFSocketCopyAddress(witap_socket) autorelease];
108.
109.
memcpy(&addr6, [addr bytes], [addr length]);
110.
111.
self.port = ntohs(addr6.sin6_port);
112.
113.
114.
115.
}
else
{
//9
116.
117.
struct sockaddr_in addr4;
118.
119.
memset(&addr4,
0
, sizeof(addr4));
120.
121.
addr4.sin_len = sizeof(addr4);
122.
123.
addr4.sin_family = AF_INET;
124.
125.
addr4.sin_port =
0
;
126.
127.
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
128.
129.
NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
130.
131.
132.
133.
if
(kCFSocketSuccess != CFSocketSetAddress(witap_socket, (CFDataRef)address4)) {
134.
135.
if
(error) *error = [[NSError alloc] initWithDomain:TCPServerErrorDomain code:kTCPServerCouldNotBindToIPv4Address userInfo:nil];
136.
137.
if
(witap_socket) CFRelease(witap_socket);
138.
139.
witap_socket = NULL;
140.
141.
return
NO;
142.
143.
}
144.
145.
146.
147.
// now that the binding was successful, we get the port number
148.
149.
// -- we will need it for the NSNetService
150.
151.
NSData *addr = [(NSData *)CFSocketCopyAddress(witap_socket) autorelease];
152.
153.
memcpy(&addr4, [addr bytes], [addr length]);
154.
155.
self.port = ntohs(addr4.sin_port);
156.
157.
}
158.
159.
160.
161.
// set up the run loop sources for the sockets
162.
163.
CFRunLoopRef cfrl = CFRunLoopGetCurrent();
//10
164.
165.
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, witap_socket,
0
);
166.
167.
CFRunLoopAddSource(cfrl, source, kCFRunLoopCommonModes);
168.
169.
CFRelease(source);
170.
171.
172.
173.
return
YES;
174.
175.
}
先看一下这个函数的定义,他返回BOOL类型的值以表示期望的操作是否成功;他还有一个NSError**类型的参数,我们看定义会发现,其实NSError是一个继承者NSObject的类,NSError**是一个指针的指针,就是说error这个参数是一个指针的指针,那么*error是一个指针,*error指向一个NSError对象。
注释1,我们定义了一个CFSocketContext结构类型变量socketCtxt,并对这个结构进行初始化。我们来解释一下这个结构的定义,这个结构有5个成员。第一个成员是这个结构的版本号,这个必需是0;第二个成员可以是一个你程序内定义的任何数据的指针,这里我们传入的是self,就是这们这个类本身了,所以我们的TCPServerAcceptCallBack这个回调方法可以把它的info参数转成TCPServer,并且这个参数会被传入在这个结构内定义的所有回调函数;第三、四、五这三个成员其实就是3个回调函数的指针,一般我们都设为NULL,就是不用它们。(如果你要具体了解这个结构,请参考官方文档)
注释2,这是创建一个socket并把它赋给我们在TCPServer.h文件里定义的witap_socket变量,这个CFSocketCreate()方法有7个参数之多,我们来一个一个解释吧,你如果想了解的更清楚我建议你还是查阅官方文档。第一个是一个内存分配器(我们前边已经提到过了);第二个是我们要创建的socket的协议族(更多这方面的知识你可能需要参考TCP/IP协议),这里我们是传入PF_INET6(表明我们希望用IPv6协议);第三个参数是socket的类型,我们这里是传入SOCK_STREAM(表明我们是要数据流服务的,还有一种选择是数据报服务,这两种是基于不同的协议的,数据流是基于TCP/IP协议的,数据报是基于UDP协议的);第四个参数是我们要创建的socket所用的具体的协议,这里我们传入IPPROTO_TCP 表明我们是遵守TCP/IP协议的;第五个是回调事件的类型,就是说当这个类型的事件发生时我们的回调函数会被调用,我们这里传入kCFSocketAcceptCallBack表明当连接成功里我们的回调会被触发。(这里可以设置不只一个回调事件类型,多个不同的事件类型用"|"(位或运算符)连起来就可以了) ;第六个就是我们的回调函数的地址,当我们指定的回调事件出现时就调用这个回调函数,我们传入我们的TCPServerAcceptCallBack()回调函数的地址;第七个是一个结构指针,这个结构就是CFSocketContext类型,它保存socket的上下文信息,我们这里传入我们在注释1中定义的socketCtxt的地址。(这个CFSocketCreate()函数会拷贝一份这个结构的数据,所以在出了这个create函数之后,这个结构可以被置为NULL。)
注释3,我们通过判断这个witap_socket是否为空来判断我们刚才执行的socket创建工作有没有成功。
注释4,如果我们刚才的创建工作成功了,我们就把我们在TCPServer.h文件中定义的protocolFamily设为PF_INET6。
注释5,如果刚才的创建失败了,我们再进行一次socket的创建工作,这次和刚才不同的是,这次我们把CFSocketCreate函数的协议族参数设为PF_INET,这表示这次是使用IPv4协议。同样的,在创建操作完成之后判断witap_socket的状态,如果创建成功了就把protocolFamily设为PF_INET,如果又失败了就什么也不做。
注释6,如果两次创建操作都失败了,如果我们的参数error这个指针的指针是用效的,我们就初始化一个NSError,这个NSError的内容就是我们在TCPServer.h里定义好的错误代码,和在TCPServer.m里定义的常量字符串TCPServerErrorDomain,然后我们把这个NSError对象赋给*error,*error才是一个指向NSError类型的指针;下面一句是如果这个witap_socket不为空,就把这个witap_socket释放了。(我个人认为这句是多余的,这句代码本身就包含在witap_socket是空的if语句里了,还有必要再判断一次吗?),然后按苹果的逻辑,把这个witap_socket释放,之后把它设为NULL,防止野指针。然后返回一个NO,这是告诉我们的这个函数的调用都,我们创建socket失败了。
注释7,首先是定义了一个int变量yes,并初始化它的值是1。然后调用setsockopt()方法来设置socket的选项,这个方法的第一个参数要求是一个socket的描述符,这里是通过CFSocketGetNative()方法来得到我们的socket对象针对于这个ios平台的描述符;第二个是需要设置的选项定义的层次,这里是SOL_SOCKET(这方面的东西我并不了解);第三个参数是我们要设置的选项的名字,这里是SO_REUSEADDR。(表示允许重用本地地址和端口,就是说充许绑定已被使用的地址(或端口号),缺省条件下,一个套接口不能与一个已在使用中的本地地址捆绑。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。);第四个参数是一个指针,指向要设置的选项的选项值的缓冲区,这里是传入上面申请的int变量yes的地址,就是说我们把这个选项设为1;第五个参数是这个选项值数据缓冲区的大小,这里用sizeof得友yes的数据长度并传了进去。
注释8,如果我们的protocolFamily是PF_INET6的话,我们对这个socket进行相应的配置。先是申请一个sockaddr_in6的结构变量addr6,这个结构是一个IPv6协议的地址结构,我们来看一下它的成员定义:
1.
struct sockaddr_in6 {
2.
u_char sin6_len;
3.
u_char sin6_family;
4.
u_int16m_t sin6_port;
5.
u_int32m_t sin6_flowinfo;
6.
struct in6_addr sin6_addr;
7.
}
我们来看,很明显这个sin6_len是这个结构的大小;sin6_family成员是指的协议;sin6_port指的是端口;sin6_flowinfo这个在微软MSDN上的说明只有一句话,IPv6的流信息;sin6_addr这是一个IN6_ADDR的结构,这个结构是真正存储我们的地址的。
(对于这个sin6_flowinfo,我并不懂这些协议上的东西,所以我查阅资料上的解释是,sin6_flowinfo是与IPv6新增流标和流量类字段类相对应的一个选项,我们在编程时通常设为0。)
现在让我们来看看这里就做了什么吧,我们先用memset方法把这个刚申请的结构清零;然后把结构的大小赋给了结构成员sin6_len;把结构的协议族设为AF_INET6;这里把这个结构里的端口号设为0,那么在socket进行绑定操作的时候,系统会为我们分配一个任意可用的端口;这个sin6_flowinfo置为0就不说了;把这个地址结构的sin6_addr成员设置为in6addr_any,这里更清晰的解释还是请你查阅资料,这里可以简单理解为填上这个值系统会自动为我们填上一个可用的本地地址(这个一个可用的本地地址是说,有的机器可能会用多个网卡,会有多个地址);然后申请一个NSData变量address6把我们的地址结构addr6的信息进行拷贝存储。
然后下面,调用CFSocketSetAddress方法把我们的witap_socket和上面刚设置好的地址address6进行绑定,这其实就是BSDSocket里的bind一样的。然后这里判断了绑定操作的执行结果,如果绑定失败的话,就进行相应的清理工作并返回失败(这些具体代码我们之前已经说过了),如果成功的话,就从这个绑定好的socket里拷贝出实际的地址并存储在addr6里,并把在TCPServer.h里定义的属性port设为系统为这个socket分配的实际的端口号,在后面发布NSNetService的时候需要用这个端口号。
注释9,这个基本上和注释8是一样的,略微不同的是这个是基于IPv4的操作,小小的不同相信只要一看就明白了,我就不再重复说明了吧呵呵。
注释10,这里申请了一个RunLoop的变量cfrl用来跟踪当前的RunLoop,通过CFRunLoopGetCurrent()方法得到当前线程正在运行的RunLoop,然后把它赋给cfrl;然后创建了一个RunLoop的输入源变量source,这里通过CFSocketCreateRunLoopSource()方法,这个方法的第一个参数是内存分配器,第二个就是我们想要做为输入源来监听的socket对象,第三个参数是代表在RunLoop中处理这些输入源事件时的优先级,数小的话优先级高。然后把这个创建的RunLoop输入源赋给变量source,接着把这个输入源source加入到当前RunLoop来进行监测,把输入源加入到RunLoop是通过CFRunLoopAddSource()这个方法实现的,这个方法的第一个参数就是我们希望加入的RunLoop,第二个参数是要加入到第一个参数里的输入源,这里是source,最后一个参数就是这个我们要加入的输入源要关联的模式,这里是传入的kCFRunLoopCommonModes。(这里的kCFRunLoopCommonModes需要说明,这个kCFRunLoopCommonModes它并不是一个模式,苹果称它为伪模式,它其实是几个模式的合集,kCFRunLoopDefaultMode必定是这个KCFRunLoopCommonModes的一个子集。你可以自己加入一些其它的模式到这个KCFRunLoopCommonModes里,这个通俗点解释怎么说呢,比如说这个KCFRunLoopCommonModes里有两个子集,即有两个模式,我们假设是模式1和模式2,那么当我们把输入源关联到模式的时候传入KCFRunLoopCommonModes的话,这个输入源就会和这两个模式,模式1和模式2,都进行关联,这样不管我们的RunLoop是以模式1运行的还是以模式2运行的,它都会监测我们的这个输入源);加入了输入源之后RunLoop就自动保持了这个输入源,我们现在就可以释放这个输入源了。最后返回操作成功。
最黑暗的已经过去,准备迎接光明
终于这个巨无霸方法start告一段落了,剩下的都没有这么长的了,可以稍微放松一下了,来看看简单的stop方法:
01.
- (BOOL)stop {
02.
03.
[self disableBonjour];
//1
04.
05.
if
(witap_socket) {
//2
06.
07.
CFSocketInvalidate(witap_socket);
08.
09.
CFRelease(witap_socket);
10.
11.
witap_socket = NULL;
12.
13.
}
14.
15.
return
YES;
16.
17.
}
看过了start的庞大,再看看这个stop是不是有点小清新的感觉哈哈哈哈。首先看看它是一个返回BOOL型变量的方法,不过它在任何情况下都返回YES,其实这个返回值就没什么意义了。
注释1,这是调用它的disableBonjour方法来停止它的NSNetService服务的。(后面我们会讲这个方法)
注释2,判断这个用来监听网络连接的socket是否有效,如果为真,就先把这个socket设为无效,再释放这个sockt资源,并把它置为NULL。
让我们看一个stop方法里调用的disableBonjour方法又做了什么:
01.
(
void
) disableBonjour
02.
03.
{
04.
05.
if
(self.netService) {
06.
07.
[self.netService stop];
08.
09.
[self.netService removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
10.
11.
self.netService = nil;
12.
13.
}
14.
15.
}
它也是很简单的,先判断这个发布的netService服务是不是有效,如果不是就先停止这个服务,然后把它从RunLoop里移除使其不再被监听(后面会看到NSNetService也是需要加入RunLoop来进行监测的),然后把这个netService置为NULL。
下面我们再来看一个比较关键的方法:enableBonjourWithDomain方法:
01.
(BOOL) enableBonjourWithDomain:(NSString*)domain applicationProtocol:(NSString*)protocol name:(NSString*)name
02.
03.
{
04.
05.
06.
07.
if
(![domain length])
//1
08.
09.
domain = @
""
;
//Will use default Bonjour registration doamins, typically just ".local"
10.
11.
if
(![name length])
12.
13.
name = @
""
;
//Will use default Bonjour name, e.g. the name assigned to the device in iTunes
14.
15.
16.
17.
if
(!protocol || ![protocol length] || witap_socket == NULL)
//2
18.
19.
return
NO;
20.
21.
22.
23.
24.
25.
self.netService = [[NSNetService alloc] initWithDomain:domain type:protocol name:name port:self.port];
//3
26.
27.
if
(self.netService == nil)
28.
29.
return
NO;
30.
31.
32.
33.
[self.netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
//4
34.
35.
[self.netService publish];
36.
37.
[self.netService setDelegate:self];
38.
39.
return
YES;
40.
41.
}
这个方法也用一个BOOL的返回值表示操作的成功或失败。并且这个方法有三个参数,它们都是我们用来发布NSNetService服务时要用的参数。第一个参数是发布服务用的域,第二个参数是我们要发布的网络服务的类型信息,第三个参数是用来表示我们这个服务的名字。
注释1,如果我们的参数domain和name如果字符长度是0的话,就把它们设为@“”。(等下会解释这个@“”在这里的意义)
注释2,如果参数protocol,即服务的协议,如果它不存在,或者它的字符长度为0,又或者witap_socket为NULL(也就是说我们这个用来监听的socket无效的话),直接返回失败。
注释3,这里是初始化一个NSNetService服务,并把它赋给在TCPServer.h里定义的netService属性。这个NSNetService的初始化方法用到了4个参数,前3个就是我们现在正在解释的这个方法的3个参数,最后一个是我们之前得到的端口号。第一个参数domain,它代表我们发布服务用的域,本地域是用@"local."来表示的,但是当我们想用本地域的时候,是不用直接传这个@"local."字符串进去的,我们只要传@""进去就行了,系统会自己把它按成本地域来使用的;第二个参数这个网络服务的类型,这个类型必需包含服务类型和传输层信息(传输层概念请参考TCP/IP协议),这个服务类型的名字和传输层的名字都要有“_”字符作为前缀。比如这个例子的服务类型的完整的名字其实是@"_witap._tcp.",看到了吧,它们都有前缀"_",这里还有一点是要强调的,在这字符串结尾的"."符号是必需的,它表示这个域名字是绝对的;第三个参数是这个服务的名字,这个名字必需是唯一的,如果这个名字是@""的话,系统会自动把设备的名字作为这个服务的名字;第四个参数就是端口号了,这是我们发布这个服务用的。这个端口号必须是在应用程序里为这个服务获得的,这里就是witap_socket在绑定时我们获得的那个端口号,获得之后赋给了port属性,所以这里传入的是self.port。初始化之后,把这个初始化过的NSNetService赋给.h文件里定义的netService属性,接着通过判断这个netService属性是否有效来判断这个NSNetService的初始化是否成功。如果初始化失败的话,直接返回操作失败。
注释4,对这个netService属性调用scheduleInRunLoop:forMode:方法,从名字也能看得出来,这是把这个netService加入到当前RunLoop中,并关联到相应的模式。这部分内容在前面的start方法的注释10中已经有了详细的讲述,这里就不啰嗦了。然后对这个netService调用publish方法,这个方法是真正的发布我们的服务的。接着又设置这个netService的委托为这个TCPService类本身,netService是一个NSNetService类,这个类的委托是一个符合NSNetServiceDelegate的通用类型,在TCPService类的interface声明部分,我们看到这个类是声明自己符合NSNetServiceDelegate这个协议的,所以这里可以把netService的委托设为这个TCPService类本身。最后,返回操作成功。
柳暗花明
到了这一步,这个类剩下的内容就很明了了,让我们看看NSNetServiceDelegate协议的两个方法吧:
01.
(
void
)netServiceDidPublish:(NSNetService *)sender
02.
03.
{
04.
05.
06.
07.
if
(self.delegate && [self.delegate respondsToSelector:
@selector
(serverDidEnableBonjour:withName:)])
08.
09.
[self.delegate serverDidEnableBonjour:self withName:sender.name];
10.
11.
}
12.
13.
14.
15.
- (
void
)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict
16.
17.
{
18.
19.
if
(self.delegate && [self.delegate respondsToSelector:
@selector
(server:didNotEnableBonjour:)])
20.
21.
[self.delegate server:self didNotEnableBonjour:errorDict];
22.
23.
}
这是NSNetServiceDelegate协议的其中两个方法,一个会在NSNetService发布成功时被调用,一个会在发布失败时会调用。它们都有一个参数,这个参数是触发这个回调的NSNetService本身。两个方法的内容都很简单。
发布成功方法中先判断self的委托是否有效,如果有效的话它接着判断这个委托是不是响应serverDidEnableBonjour:withName方法,如果响应的话,就对它的委托调用这个方法。在ios局域网联机—苹果官方源码之WiTap剖析(一)的setup方法的注释4中,我们已经解释过了这里的self的委托被设置为AppController了,所以这里是判断AppController是不是响应这个serverDidEnableBonjour:withName方法,如果响应就对它调用这个方法。(关于这个被调用的方法我们在以后讲回到AppController时再讲)
发布失败方法和这个成功方法基本上一样,不同的只是对应的方法,也不再重复说明了。
最后是两个辅助性的方法:
01.
(NSString*) description
02.
03.
{
04.
05.
return
[NSString stringWithFormat:@
"<%@ = 0x%08X | port %d | netService = %@>"
, [self
class
], (
long
)self, self.port, self.netService];
06.
07.
}
08.
09.
10.
11.
+ (NSString*) bonjourTypeFromIdentifier:(NSString*)identifier
12.
{
13.
14.
15.
16.
if
(![identifier length])
17.
18.
return
nil;
19.
20.
return
[NSString stringWithFormat:@
"_%@._tcp."
, identifier];
21.
22.
}
description方法是帮助我们调试程序时方便用的,它把这个类的信息返回给我们,方便我们输出到控制台进行查看。它把这个TCPService类对象的类名,地址,port属性,netService属性都返回了,非常方便。
bonjourTypeFromIdentifier:这个方法真是一个纯粹的辅助方法,它在这个例子中的用途就是通过传入在AppController.m中定义的宏kGameIdentifier,然后返回一个完整的事实上的NSNetService的初始化方法中用的网络服务的类型。
黎明即将到来
至此,这个TCPService类我们介绍完了,这篇文章就到这里,后面的内容会在后续文章中继续讲述。
(能力有限,文中可能会有不对的地方,希望大家指教。谢谢!!)