第 2 章 沟通无限——苹果的网络

第 2 章 沟通无限——苹果的网络

苹果设备上的应用离不开网络通信,苹果公司也提供了自己独有的网络解决方案。这些网络包括广域网和局域网,以及蓝牙等短距离通信协议组成的网络。还有令人赞叹的Bonjour服务发现。

本章将向大家介绍网络架构以及iOS和Mac OS X下的网络通信基础知识。

2.1 网络结构

计算机网络是一门庞大的学科,无法用一本书介绍其全部内容。这里介绍的重点是与iOS等移动平台以及Mac OS X相关的网络应用层知识。首先了解一下网络结构,网络结构是网络的构建方式,目前流行的有客户端服务器结构网络和对等结构网络。

2.1.1 客户端服务器结构网络

客户端服务器(Client Server,C/S)结构网络,是一种主从结构网络。如图2-1所示,服务器一般处于等待状态,如果有客户端请求,服务器响应请求建立连接提供服务。服务器是被动的,有点像餐厅的服务员。而客户端是主动的,像在餐厅吃饭的顾客。

事实上,我们身边的很多网络服务都采用这种结构。例如: Web服务、文件传输服务和邮件服务等。虽然它们存在的目的不一样,但基本结构是一样的。这种网络结构与设备类型无关,服务器不一定是计算机,也可能是手机等移动设备。笔者曾经用过一款iPhone应用的内置Web服务,可以在计算机上通过浏览器下载手机中图片和资料。

本章将介绍基于客户端服务器结构网络的Socket通信方式。下一章将介绍客户端服务器结构网络的Web Service通信编程。

2.1.2 对等结构网络

对等结构网络也叫点对点网络(Peer to Peer,P2P),每个节点之间是对等的。如图2-2所示,每个节点既是服务器又是客户端,这种结构有点像吃自助餐。

图 2-1 客户端服务器结构网络

图 2-2 对等结构网络

对等结构网络分布范围比较小。通常在一间办公室或一个家庭内,因此它非常适合iOS设备间的网络通信,网络链路层是由蓝牙和WiFi实现。iOS SDK提供了这方面的API供开发者使用。

2.2 基于Socket的C/S结构网络通信

上一节介绍了C/S结构网络的基本概念,这一节具体介绍在iOS平台下的C/S结构网络通信的实现。

苹果公司为iOS下C/S结构网络通信开发提供了一些API框架和类库。这些API有面向高层次的Web Service通信开发,例如: NSURLRequest、NSMutableURLRequest、NSURLConnection、NSURLDownload和NSURL类。

面向低层次开发的API,例如: Socket通信用到的NSInputStream和NSOutputStream类,还有CFStreamCreatePairWithSocketToCFHost和CFSocketCreate函数。而且Socket还有面向C语言的BSD Socket。

还有基于苹果Bonjour发现服务的API,例如NSNetService和NSNetServiceBrowser类,函数有CFStreamCreatePairWithSocketToNetService。

2.2.1 Socket概念

Socket中文翻译为“套接字”,每次看到这个译名的时候,都感觉“丈二和尚摸不到头脑”,“套接字”太晦涩难懂了!我们这些搞计算机的人总是喜欢把简单的问题复杂化。看看它的解释吧!

Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换。这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程接口,一个Socket由一个IP地址和一个端口号唯一确定。

我们把Socket编程叫做“低层次网络编程”。这是因为它比较复杂,需要了解很多网络中的概念,包括它们的细节,例如TCP/IP等。开发的时候直接使用C语言编写比较麻烦。但是“低层次网络编程”并不等于它功能不强大。恰恰相反,正因为层次低,Socket编程与基于Web Service“高层次网络编程”比较,能够提供更强大的功能和更灵活的控制,但是要更复杂一些。

Socket采用TCP/IP协议模型。TCP/IP协议的传输层又有两种传输协议: TCP(传输控制协议)和 UDP(用户数据报协议)。TCP是基于连接的,而UDP是无连接的; TCP对系统资源的要求较多,而UDP少; TCP保证数据正确性而UDP可能丢包; TCP保证数据顺序而UDP不保证。

2.2.2 Socket编程

使用Socket进行C/S结构编程,连接过程如图2-3所示。

{%}

图 2-3 Socket连接过程

服务器端监听某个端口是否有连接请求。服务器端程序处于堵塞状态,直到客户端向服务器端发出连接请求,服务器端接受请求程序才能向下运行。一旦连接建立起来,通过Socket可以获得输入输出流对象。借助于输入输出流对象就可以实现与客户端的通信,最后不要忘记关闭Socket和释放一些资源(包括关闭输入/输出流)。

客户端流程是先指定要通信的服务器IP地址、端口和采用的传输协议(TCP或UDP),向服务器发出连接请求,服务器有应答请求之后,就会建立连接。之后与服务器端是一样的。

在iOS中,客户端Socket编程可以使用的技术有3种:

1. 使用NSStream——面向Objective-C语言的实现,由苹果提供的Foundation框架的API;

2. 使用CFStream——面向C语言的实现,由苹果提供的Core Foundation框架的API;

3. BSD Socket——也叫伯克利套接字(Berkeley Socket),是UNIX平台下广泛使用的Socket编程。它是面向C语言实现的,完全使用C编写,使用起来比较麻烦。它是伯克利加州大学(University of California,Berkeley)的学生开发的。

在iOS中,服务器端Socket编程可以使用的技术有两种:

1. 使用CFStream——面向C语言的实现,由苹果提供的Core Foundation框架的API;

2. BSD Socket——也叫伯克利套接字(Berkeley Socket),是UNIX平台下广泛使用的Socket编程。它是面向C语言实现的,完全使用C编写的,使用起来比较麻烦。它是伯克利加州大学(University of California,Berkeley)的学生开发的。

提示 Socket编程是一种网络编程的标准,客户端和服务器端可以不受编程语言的限制,完全自由地通信。客户端可以是Objective-C编写的iOS程序,服务器端可以是Java编写的程序,通信双方定义好数据交互格式就可以了。

2.2.3 实例: NSStream&CFStream实现TCP Socket服务器端

下面通过一个实例来介绍基于TCP协议的Socket编程,服务器端采用CFStream实现,客户端都采用NSStream实现。

使用Xcode创建一个Mac OS X的命令应用程序,安装如图2-4所示,选择Command Line Tool工程模板创建应用TCPServer。

{%}

图 2-4 选择命令行工程模板

工程创建完成之后,只有一个main.m文件。需要在main.m中的main函数中编写服务器端代码。

首先,在函数外面引入头文件、定义函数和宏,代码如下:

#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 9000
void AcceptCallBack(CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *);
void WriteStreamClientCallBack(CFWriteStreamRef stream, CFStreamEventType eventType, void *);
void ReadStreamClientCallBack (CFReadStreamRef stream, CFStreamEventType eventType,void *);

其中,AcceptCallBack函数作用是,服务器端接收到客户端请求后回调,它是CFSocketCallBack类型,CFSocketCallBack定义如下:

typedef void (*CFSocketCallBack) (
    CFSocketRef s,                     //Socket对象
    CFSocketCallBackType callbackType, //Socket回调类型
    CFDataRef address,                 //Socket地址
    const void *data,                  //如果Socket回调类型是kCFSocketAcceptCallBack类型,则data是CFSocketNativeHandle类型的指针
    void *info                         //用户传递任何数据类型的指针
)

回调函数WriteStreamClientCallBack,当客户端在Socket中读取数据时调用。它是CFWriteStreamClientCallBack类型,CFWriteStreamClientCallBack定义如下:

typedef void (*CFWriteStreamClientCallBack) (
    CFWriteStreamRef stream,       //输出流对象
    CFStreamEventType eventType,   //流的类型,输出流使用kCFStreamEventCanAcceptBytes类型
    void *clientCallBackInfo
)

回调函数ReadStreamClientCallBack,当客户端把数据写入Socket时调用,它是CFReadStreamClientCallBack类型,CFReadStreamClientCallBack定义如下:

typedef void (*CFReadStreamClientCallBack) (
    CFReadStreamRef stream,            //输入流对象
    CFStreamEventType eventType,//流的类型,输入流使用kCFStreamEventHasBytesAvailable类型
    void *clientCallBackInfo           //CFStreamClientContext类型
)

再看看main函数:

int main(int argc, const char * argv[])
{
     /* 定义一个Server Socket引用 */
     CFSocketRef sserver;

     /* 创建socket context */
     CFSocketContext CTX = { 0, NULL, NULL, NULL, NULL };        ①

     /* 创建server socket  TCP IPv4 设置回调函数 */
     sserver = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP,
                kCFSocketAcceptCallBack, (CFSocketCallBack)AcceptCallBack, &CTX);    ②
     if (sserver == NULL)
          return -1;

     /* 设置是否重新绑定标志 */
     int yes = 1;
     /* 设置socket属性 SOL_SOCKET是设置tcp SO_REUSEADDR重新绑定, yes 是否重新绑定*/
     setsockopt(CFSocketGetNative(sserver), SOL_SOCKET, SO_REUSEADDR,
                 (void *)&yes, sizeof(yes));                                        ③

     /* 设置端口和地址 */
     struct sockaddr_in addr;                                                        ④
     memset(&addr, 0, sizeof(addr));  //memset函数对指定的地址进行内存复制
     addr.sin_len = sizeof(addr);
     addr.sin_family = AF_INET;       //AF_INET是设置 IPv4
     addr.sin_port = htons(PORT);     //htons函数 无符号短整型数转换成"网络字节序"
     addr.sin_addr.s_addr = htonl(INADDR_ANY);    //INADDR_ANY有内核分配,⑤
                                      //htonl函数 无符号长整型数转换成"网络字节序"

     /* 从指定字节缓冲区复制,一个不可变的CFData对象*/
     CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8*)&addr, sizeof(addr));

     /* 绑定Socket*/
     if (CFSocketSetAddress(sserver, (CFDataRef)address)   = kCFSocketSuccess) {      ⑥
          fprintf(stderr, "Socket绑定失败\n");
          CFRelease(sserver);
          return -1;
     }

     /* 创建一个Run Loop Socket源 */
     CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, sserver, 0); ⑦
     /* Socket源添加到Run Loop中 */
     CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes); ⑧
     CFRelease(sourceRef);

     printf("Socket listening on port %d\n", PORT);
     /* 运行Loop */
     CFRunLoopRun();                                                                     ⑨
}

第①行代码创建CFSocketContext变量,CFSocketContext是一个结构体,用来配置Socket对象的行为。它提供程序定义数据和回调函数,CFSocketContext结构体定义如下:

struct CFSocketContext {
     CFIndex version;                       //结构体的版本必须是0
     void *info;                            //任何程序定义数据的指针
     CFAllocatorRetainCallBack retain;      //retain info时回调函数,可以为NULL
     CFAllocatorReleaseCallBack release;    //release info时回调函数,可以为NULL
     CFAllocatorCopyDescriptionCallBack copyDescription;//copy info时回调函数,可以为NULL
};
typedef struct CFSocketContext CFSocketContext;

第②行代码使用函数CFSocketCreate创建Socket对象,CFSocketCreate函数定义如下:

CFSocketRef CFSocketCreate (
    CFAllocatorRef allocator,        //指定创建对象的时候,内存分配方式,NULL或
                                     //kCFAllocatorDefault
    SInt32 protocolFamily,           //指定Socket的协议族类型,PF_INET是传递0或负数
    SInt32 socketType,               //指定Socket的类型,SOCK_STREAM是TCP协议,
                                     //SOCK_DGRAM是UDP协议
    SInt32 protocol,                 //指定Socket的协议类型,
                                     //IPPROTO_TCP是TCP协议 IPPROTO_UDP是UDP协议
    CFOptionFlags callBackTypes,     //回调类型,本例中kCFSocketAcceptCallBack类型
                                     //是接受客户端请求时回调
    CFSocketCallBack callout,        //回调函数名
    const CFSocketContext *context   //Socket Context对象
);

第③行代码使用setsockopt函数设置Socket属性,setsockopt函数定义如下:

setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

函数中参数解释如下:

1. socket参数指定本地Socket对象,可以使用函数CFSocketGetNative获得;

2. level参数指定Socket属性的级别,一般情况下都是指定SOL_SOCKET常量;

3. option_name参数指定要设置的Socket属性名,本例中的SO_REUSEADDR设置可重用地址属性;

4. option_value参数指定设置属性的值;

5. option_len参数指定设置属性的值的长度。

第④~⑤行代码设置Socket的端口和地址。代码中都有详细的注释,我们不再解释了。第⑥行代码绑定Socket,这样应用会等待客户端请求,使用CFSocketSetAddress(sserver, (CFDataRef)address)函数,其中第1个参数是服务器Socket对象,第2个参数是上面设置地址端口,它需要强制转换为CFData类型。

第⑦~⑨行代码创建一个Run Loop Socket源,这样就可以把服务器端Socket放入到Run Loop中,不会堵塞主线程。

服务器接收到客户端后,回调AcceptCallBack函数,AcceptCallBack函数代码如下:

/* 接收客户端请求后,回调函数  */
void AcceptCallBack(
                 CFSocketRef socket,
                 CFSocketCallBackType type,
                 CFDataRef address,
                 const void *data,
                 void *info)
{
     CFReadStreamRef readStream = NULL;
     CFWriteStreamRef writeStream = NULL;

     /* data 参数含义是,如果回调类型是kCFSocketAcceptCallBack,data就是CFSocketNativeHandle类型的指针 */
     CFSocketNativeHandle sock = *(CFSocketNativeHandle *) data;

     /* 创建读写Socket流 */
     CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock, &readStream, &writeStream);                           ①

     if (!readStream || !writeStream) {
          close(sock);
          fprintf(stderr, "CFStreamCreatePairWithSocket() 失败\n");
          return;
     }

     CFStreamClientContext streamCtxt = {0, NULL, NULL, NULL, NULL};
     //注册两种回调函数
     CFReadStreamSetClient(readStream, kCFStreamEventHasBytesAvailable, ReadStreamClientCallBack, &streamCtxt);    ②
     CFWriteStreamSetClient(writeStream, kCFStreamEventCanAcceptBytes, WriteStreamClientCallBack, &streamCtxt);    ③

     //加入到循环中
     CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(),kCFRunLoopCommonModes);                     ④
     CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(),kCFRunLoopCommonModes);                   ⑤

     CFReadStreamOpen(readStream);                                                                                 ⑥
     CFWriteStreamOpen(writeStream);                                                                               ⑦
}

在AcceptCallBack函数中第①行代码使用了CFStreamCreatePairWithSocket函数,它的作用是连接Socket并创建输入输出流对象,函数定义如下:

void CFStreamCreatePairWithSocket (
     CFAllocatorRef alloc,            //内存分配方式
     CFSocketNativeHandle sock,       //有main函数传递过来的Socket对象
     CFReadStreamRef *readStream,     //输入流对象指针
     CFWriteStreamRef *writeStream    //输出流对象指针
);

代码第②和第③行代码注册读写客户端Socket回调函数。第④行把输入流对象放到当前的Run Loop中,第⑤行是把输出流对象放到当前的Run Loop中,这是流处理的常规做法。

代码第⑥行和第⑦行打开输入输出流对象,流在使用之前必须打开。同理流使用完成必须关闭使用。

一旦客户端开始读写Socket就回调下面的两个函数:

/* 读取流操作 客户端有数据过来时调用 */
void ReadStreamClientCallBack(CFReadStreamRef stream, CFStreamEventType eventType, void* clientCallBackInfo){

     UInt8 buff[255];
     CFReadStreamRef inputStream = stream;

     if(NULL   = inputStream)
     {
         CFReadStreamRead(stream, buff, 255);                                      ①
         printf("接收到数据: %s\n",buff);
         CFReadStreamClose(inputStream);
         CFReadStreamUnscheduleFromRunLoop(inputStream,
                   CFRunLoopGetCurrent(),kCFRunLoopCommonModes);
         inputStream = NULL;
     }
}

/* 写入流操作 客户端在读取数据时调用 */
void WriteStreamClientCallBack(CFWriteStreamRef stream, CFStreamEventType eventType, void* clientCallBackInfo)
{
     CFWriteStreamRef     outputStream = stream;
     //输出
     UInt8 buff[] = "Hello Client!";
     if(NULL   = outputStream)
     {
         CFWriteStreamWrite(outputStream, buff, strlen((const char*)buff)+1);      ②
         CFWriteStreamClose(outputStream);
         CFWriteStreamUnscheduleFromRunLoop(outputStream,
                   CFRunLoopGetCurrent(),kCFRunLoopCommonModes);
         outputStream = NULL;
     }
}

ReadStreamClientCallBack函数的第①行接收客户端数据,使用CFReadStreamRead函数,函数定义如下:

CFIndex CFReadStreamRead (
     CFReadStreamRef stream,          //输入流对象
     UInt8 *buffer,                   //接收数据准备的数据缓冲区
     CFIndex bufferLength             //读入的数据长度
);

WriteStreamClientCallBack函数的第②行代码向客户端输出数据,使用CFWriteStreamWrite函数,函数定义如下:

CFIndex CFWriteStreamWrite (
     CFWriteStreamRef stream,         //输出流对象
     const UInt8 *buffer,             //发送数据缓冲区
     CFIndex bufferLength             //发送的数据长度
);

本例中的发送的数据长度是strlen((const char*)buff)+1,其中strlen是C语言中获取字符的长度,由于C语言中字符串是遇到'\0'为止,因此需要+1,也要把'\0'发送给客户端。

上面的服务器端代码使用CFStream编写,由于是面向C语言的,因此使用起来比较麻烦。下面我们看看客户端代码。

2.2.4 实例: NSStream&CFStream实现TCP Socket客户端

客户端我们使用iPhone应用程序,画面比较简单,如图2-5所示。单击“发送”按钮,给服务器发送一些字符串过去。单击“接收”按钮就会从服务器读取一些字符串,并且显示在画面上。

图 2-5 iPhone客户端设计原型画面

有关客户端应用的UI部分不再介绍了,我们直接看代码部分,Socket客户端可以采用CFStream或NSStream实现,CFStream实现方式与服务器端基本一样。为了给读者介绍更多的知识,本例我们采用NSStream实现。NSStream实现采用Objective-C语言,一些面向对象的类。

下面我们看看客户端视图控制器ViewController.h代码:

#import <CoreFoundation/CoreFoundation.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 9000

@interface ViewController : UIViewController<NSStreamDelegate>
{
    int flag ;                         //操作标志 0为发送 1为接收
}

@property (nonatomic, retain) NSInputStream *inputStream;
@property (nonatomic, retain) NSOutputStream *outputStream;

@property (weak, nonatomic) IBOutlet UILabel *message;

- (IBAction)sendData:(id)sender;
- (IBAction)receiveData:(id)sender;

@end

定义属性inputStream和outputStream,它们输入流NSInputStream和输出流NSOutputStream类。它们与服务器CFStream实现中的输入流CFReadStreamRef和输出流CFWriteStreamRef对应的。

视图控制器ViewController.m的初始化网络方法initNetworkCommunication代码:

- (void)initNetworkCommunication
{
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;
    CFStreamCreatePairWithSocketToHost(NULL,
         (CFStringRef)@"192.168.1.103", PORT, &readStream, &writeStream);          ①

    _inputStream = (__bridge_transfer NSInputStream *)readStream;                  ②
    _outputStream = (__bridge_transfer NSOutputStream*)writeStream;                ③
    [_inputStream setDelegate:self];                                               ④
    [_outputStream setDelegate:self];                                              ⑤
    [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                        forMode:NSDefaultRunLoopMode];             ⑥
    [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                        forMode:NSDefaultRunLoopMode];             ⑦
    [_inputStream open];                                                           ⑧
    [_outputStream open];                                                          ⑨
}

代码第①行调用函数CFStreamCreatePairWithSocketToHost实现与服务器进行连接,并且返回输入输出流对象,CFStreamCreatePairWithSocketToHost函数的定义如下:

void CFStreamCreatePairWithSocketToHost (
     CFAllocatorRef alloc,              //内存分配方式
     CFStringRef host,                  //服务器IP地址
     UInt32 port,                       //服务器端口
     CFReadStreamRef *readStream,       //返回的输入流对象
     CFWriteStreamRef *writeStream      //返回的输出流对象
);

第②和第③行代码把CFStream对象转换为NSStream对象。其中__bridge_transfer经常用于把Core Foundation框架的对象转换为Foundation框架对象,这叫做“toll-free bridging”(无开销桥接)。

第④和第⑤行代码设置self为委托协议NSStreamDelegate实现对象。NSStreamDelegate协议定义的方法是: stream:handleEvent:。

第⑥和第⑦行代码通过NSStream方法scheduleInRunLoop:设置Run Loop,它与函数CFReadStreamScheduleWithRunLoop 和CFWriteStreamScheduleWithRunLoop作用是一样的。

第⑧和第⑨行代码通过NSStream的open方法打开流对象,与函数CFReadStreamOpen和CFWriteStreamOpen作用是一样的。

单击“发送”和“接收”按钮触发的方法如下:

/* 单击"发送"按钮  */
- (IBAction)sendData:(id)sender {
    flag = 0;
    [self initNetworkCommunication];
}
/* 单击"接收"按钮  */
- (IBAction)receiveData:(id)sender {
    flag = 1;
    [self initNetworkCommunication];
}

它们都调用initNetworkCommunication方法,并设置操作标识flag,如果flag为0发送数据,flag为1接收数据。

流的状态的变化触发很多事件,并回调NSStreamDelegate协议中定义的方法stream:handleEvent:,其代码如下:

-(void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
    NSString *event;
    switch (streamEvent) {
        case NSStreamEventNone:
                event = @"NSStreamEventNone";
                break;
        case NSStreamEventOpenCompleted:
                event = @"NSStreamEventOpenCompleted";
                break;
        case NSStreamEventHasBytesAvailable:
                event = @"NSStreamEventHasBytesAvailable";
                if (flag ==1 && theStream == _inputStream) {
                      NSMutableData *input = [[NSMutableData alloc] init];
                      uint8_t buffer[1024];                                                 ①
                      int len;
                      while([_inputStream hasBytesAvailable])                               ②
                      {
                          len = [_inputStream read:buffer maxLength:sizeof(buffer)];        ③
                          if (len > 0)
                          {
                                  [input appendBytes:buffer length:len];
                          }
                      }
                      NSString *resultstring = [[NSString alloc]
                              initWithData:input encoding:NSUTF8StringEncoding];
                      NSLog(@"接收:%@",resultstring);
                      _message.text = resultstring;
                 }
                 break;
         case NSStreamEventHasSpaceAvailable:
                 event = @"NSStreamEventHasSpaceAvailable";
                 if (flag ==0 && theStream == _outputStream) {
                      //输出
                      UInt8 buff[] = "Hello Server!";                                       ④
                      [_outputStream write:buff maxLength: strlen((const char*)buff)+1];    ⑤
                      //关闭输出流
                     [_outputStream close];
                 }
                 break;
         case NSStreamEventErrorOccurred:
                 event = @"NSStreamEventErrorOccurred";
                 [self close];                                                              ⑥
                 break;
         case NSStreamEventEndEncountered:
                 event = @"NSStreamEventEndEncountered";
                 NSLog(@"Error:%d:%@",[[theStream streamError] code],
                              [[theStream streamError] localizedDescription]);
                 break;
         default:
                [self close];                                                               ⑦
                event = @"Unknown";
                break;
     }
     NSLog(@"event------%@",event);
}

这些流的事件有:

1. NSStreamEventNone,没有事件发生;

2. NSStreamEventOpenCompleted,成功打开流;

3. NSStreamEventHasBytesAvailable,这个流有数据可以读,在读取数据时使用;

4. NSStreamEventHasSpaceAvailable,这个流可以接收数据的写入,在写数据时使用;

5. NSStreamEventErrorOccurred,流发生错误;

6. NSStreamEventEndEncountered,流结束。

因此NSStreamEventHasBytesAvailable分支可以进行读数据的处理,分支NSStreamEventHasSpaceAvailable可以进行写数据的处理。

在读取数据分支(NSStreamEventHasBytesAvailable)中,代码第①行为读取数据准备缓冲区,本例中设置的是1024个字节,这个大小会对流的读取有很多的影响。第②行代码使用hasBytesAvailable方法判断是否流有数据可以读,如果有可读数据就进行循环读取。第③行代码使用流的read:maxLength:方法读取数据到缓冲区,第1个参数是缓冲区对象buffer,第2个参数是读取的缓冲区的字节长度。

在写入数据分支(NSStreamEventHasSpaceAvailable)中,代码第④行是要写入的数据,第⑤行代码[_outputStream write:buff maxLength: strlen((const char*)buff)+1]是写入数据方法。

第⑥和第⑦行代码[self close]调用close方法关闭,close方法代码如下:

-(void)close
{
    [_outputStream close];
    [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop]
                  forMode:NSDefaultRunLoopMode];
    [_outputStream setDelegate:nil];
    [_inputStream close];
    [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop]
                forMode:NSDefaultRunLoopMode];
    [_inputStream setDelegate:nil];
}

2.3 Bonjour服务发现

2.2 节实现的Socket有一个问题,需要指定服务器的端口和IP地址。在有些情况下,获得服务器的这些信息是很困难的。苹果公司开发了一种零配置服务发现协议,命名为Bonjour(法语“你好”),使我们的应用不必指定服务器端口和IP地址就可以动态发现。

发现服务是通过特定命名搜索服务的,例如“tony._tonyipp._tcp.local”这样的命名,发现服务命名格式如下:

<服务名>._<服务类>. _<传输协议名>.<域名>

tony为服务名,服务名是一个描述性的名字。tonyipp是服务类,是我们自己命名的,也可使用已经注册的服务类型,例如,ipp是打印服务。目前已经注册的服务类型名有400多种,可以在http://www.dns-sd.org/ServiceTypes.html网址查看。tcp是传输协议,local是本地域名。

苹果公司提供的Bonjour编程比较简单,主要是两个类: NSNetService和NSNetServiceBrowser,以及它们的委托协议NSNetServiceDelegate和NSNetServiceBrowserDelegate。在服务器端,我们需要发布服务,而客户端需要解析服务或查找服务。一旦连接建立起来,就可以通信了,之后的事情与Bonjour无关。

2.3.1 发布服务

服务器端需要通过Bonjour发布服务,发布服务使用NSNetService和NSNetServiceDelegate。首先需要实例化NSNetService对象代码如下:

- (BOOL) publishService
{
    //创建服务器实例
    NSNetService _service = [[NSNetService alloc] initWithDomain:@"local."
                                     type:@"_tonyipp._tcp."
                                     name:@"tony" port:<端口>];
    //添加服务到当前的Run Loop
    [_service scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                   forMode:NSRunLoopCommonModes];
    [_service setDelegate:self];

    //发布服务
    [_service publish];

    return YES;
}

实例化NSNetService构造方法是-initWithDomain:type:name:port:,initWithDomain指定域名,type是服务类型,name是服务名,port是端口。此外,NSNetService还有另一个构造方法- initWithDomain:type:name:。这个构造方法没有端口,它常用于客户端创建NSNetService对象。[_service setDelegate:self]是指定NSNetService委托self。发布服务时,需要调用[_service publish]方法。发布过程中会调用NSNetService委托指定的方法:

#pragma mark - NSNetServiceDelegate

- (void)netServiceWillPublish:(NSNetService *)netService
{
    NSLog(@"netServiceWillPublish");
}

- (void)netServiceDidPublish:(NSNetService *)netService {
    NSLog(@"netServiceDidPublish");
}

- (void)netService:(NSNetService *) netService didNotPublish:(NSDictionary *)errorDict {
    NSLog(@"didNotPublish");
}

其中,netServiceWillPublish:方法是发布服务开始调用的方法,发布成功时回调netServiceDidPublish:方法,发布失败时回调netService:didNotPublish:方法。

2.3.2 解析服务

服务器端Bonjour服务发布成功之后,客户端可以通过NSNetService解析服务,解析成功后,可以获得通信的数据细节,例如IP地址、端口等信息。

首先需要实例化NSNetService对象代码如下:

-(id)init {
    _service = [[NSNetService alloc] initWithDomain:@"local."
                type:@"_tonyipp._tcp." name:@"tony"];
    [_service setDelegate:self];
    //设置解析地址超时时间
    [_service resolveWithTimeout:1.0];

    _services = [[NSMutableArray alloc] init];
    return self;
}

实例化NSNetService对象的构造方法是- initWithDomain:type:name:,不需要指定它的端口。解析服务需要调用[_service resolveWithTimeout:1.0]语句,开始解析服务,在规定的时间里进行解析,参数单位是秒。

#pragma mark - NSNetServiceDelegate Methods
- (void)netServiceWillResolve:(NSNetService *)netService {
    NSLog(@"netServiceWillResolve");
}

- (void)netServiceDidResolveAddress:(NSNetService *)netService {
    NSLog(@"netServiceDidResolveAddress");
    [_services addObject:netService];
}

- (void)netService:(NSNetService *)netService didNotResolve:(NSDictionary *)errorDict {
    NSLog(@"didNotResolve: %@",errorDict);
}

netServiceWillResolve:方法在解析开始时回调,解析成功时回调netServiceDidResolveAddress:方法,解析失败时回调netService:didNotResolve:方法。

2.3.3 查找服务

服务器端Bonjour服务发布成功之后,客户端可以通过NSNetService解析服务外,还可以通过NSNetServiceBrowser查找服务。查找服务是,当有新的服务被发现或消失的时候,回调NSNetServiceBrowser的委托方法,可以借助于发现成功后,获得通信的数据细节,例如IP地址、端口等信息。

查找服务实例代码如下:

-(id)init {
   _serviceBrowser = [[NSNetServiceBrowser alloc] init];                            ①
   _serviceBrowser.delegate = self;
   [_serviceBrowser searchForServicesOfType:@"_tonyipp._tcp." inDomain:@"local."];  ②
    return self;
}

#pragma mark - NSNetServiceBrowserDelegate Methods

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
   didFindDomain:(NSString *)domainName moreComing:(BOOL)moreDomainsComing {
    NSLog(@"didFindDomain");
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
didRemoveDomain:(NSString *)domainName moreComing:(BOOL)moreDomainsComing {
 NSLog(@"didRemoveDomain");
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
   didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing {
    NSLog(@"didFindService: %@  lenght:%d",netService.name,[netService.name length]);
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
   didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing {
    NSLog(@"didRemoveService");
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
   didNotSearch:(NSDictionary *)errorInfo {
    NSLog(@"didNotSearch");
}

- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)netServiceBrowser {
    NSLog(@"netServiceBrowserWillSearch");
}

- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)netServiceBrowser {
    NSLog(@"netServiceBrowserDidStopSearch");
}

第①行代码实例化NSNetServiceBrowser对象,第②行代码通过NSNetServiceBrowser的searchForServicesOfType: inDomain:方法实现在指定的域中按照服务类型搜索服务。NSNetServiceBrowserDelegate的委托方法如下:

1. netServiceBrowser:didFindDomain:moreComing: 发现指定域时回调;

2. netServiceBrowser:didRemoveDomain:moreComing: 指定域消失时回调;

3. netServiceBrowser:didFindService:moreComing: 发现指定域和服务类型时回调;

4. netServiceBrowser:didRemoveService:moreComing: 指定域和服务类型消失时回调;

5. netServiceBrowserWillSearch: 搜索开始时回调;

6. netServiceBrowser:didNotSearch: 没有搜索到时回调;

7. netServiceBrowserDidStopSearch: 停止搜索时回调。

2.3.4 实例: 基于服务发现的Socket通信服务器端

下面我们把2.2节介绍的Socket实例,再通过Bonjour服务发现实现一下。服务器端程序代码运行流程如图2-6所示。

图 2-6 服务器端程序流程

发现服务并不包含Socket服务器的启动,因此启动Socket服务器的代码需先编写,服务器启动后会获得动态端口,再把这个端口作为参数传递给Bonjour发现服务,发布成功建立Socket,再往后的流程与2.2节是一样的。

我们看一下服务器代码,服务器端还是一个Mac OS X的命令行应用程序(Command Line Tool),它的main.m的main函数代码如下:

#import <Foundation/Foundation.h>
#import "Server.h"

int main(int argc, const char * argv[])
{
     Server *server = [[Server alloc] init];
     CFRunLoopRun();
     return 0;
}

几乎全部处理代码都放在Server类中实现,CFRunLoopRun()可以在当前线程启动一个Run Loop,使得服务器一直在运行状态。Server类的中Server.h代码如下:

#import <netinet/in.h>
#import <sys/socket.h>

@interface Server : NSObjectNSNet <ServiceDelegate,NSStreamDelegate>

@property(nonatomic,strong) NSNetService *service;
@property(nonatomic,strong) NSSocketPort *socket;

@property(nonatomic,strong) NSInputStream *inputStream;
@property(nonatomic,strong) NSOutputStream  *outputStream;

@property(nonatomic,assign) int port;
@end

Server类的Server.m代码如下:

void AcceptCallBack(CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *);
void WriteStreamClientCallBack(CFWriteStreamRef stream, CFStreamEventType eventType, void *);
void ReadStreamClientCallBack (CFReadStreamRef stream, CFStreamEventType eventType,void *);
@implementation Server

-(id)init {
    BOOL succeed = [self startServer];                                                     ①
    if (succeed) {
         //通过Bonjour 发布服务
         succeed = [self publishService];                                                  ②
    } else {
         NSLog(@"服务器启动失败.");
    }
    return self;
}

/* 启动服务器 */
- (BOOL) startServer
{
    /* 定义一个Server Socket引用 */
    CFSocketRef sserver;
    /* 创建socket context */
    CFSocketContext CTX = {0, (__bridge void *)(self), NULL, NULL, NULL};
    /* 创建server socket  TCP IPv4 设置回调函数 */
    sserver = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP,
                        kCFSocketAcceptCallBack, (CFSocketCallBack)AcceptCallBack, &CTX);

    if ( sserver == NULL ) {
     return NO;
    }

    /* 设置是否重新绑定标志 */
    int yes = 1;
    /* 设置socket属性,SOL_SOCKET设置tcp SO_REUSEADDR是重新绑定, yes 是否重新绑定*/

    setsockopt(CFSocketGetNative(sserver), SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes)); 

    /* 设置端口和地址 */
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr)); //memset函数对指定的地址进行内存复制
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;      //AF_INET是设置 IPv4
    addr.sin_port = 0;//htons(PORT);//htons函数 无符号短整型数转换成"网络字节序"           ③
    addr.sin_addr.s_addr = htonl(INADDR_ANY);


    /* 从指定字节缓冲区复制,一个不可变的CFData对象*/
    CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8*)&addr, sizeof(addr));

    /* 设置Socket*/
    if (CFSocketSetAddress(sserver, (CFDataRef)address)   = kCFSocketSuccess) {
         fprintf(stderr, "Socket绑定失败\n");
         CFRelease(sserver);
         return NO;
    }                                                                                      ④
    //通过Bonjour广播服务器时使用
    NSData *socketAddressActualData = (__bridge NSData *)CFSocketCopyAddress(sserver) ;    ⑤

    //转换sockaddr_in-socketAddressActual
    struct sockaddr_in socketAddressActual;
    memcpy(&socketAddressActual, [socketAddressActualData bytes],
                  [socketAddressActualData length]);
    self.port = ntohs(socketAddressActual.sin_port);                                       ⑥

    printf("Socket listening on port %d\n", self.port);

    /* 创建一个Run Loop Socket源 */
    CFRunLoopSourceRef sourceRef
                   = CFSocketCreateRunLoopSource(kCFAllocatorDefault, sserver, 0);
    /* Socket源添加到Run Loop中 */
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
    CFRelease(sourceRef);

    return YES;
}

- (BOOL) publishService
{
    //创建服务器实例
  _service = [[NSNetService alloc] initWithDomain:@"local."
                 type:@"_tonyipp._tcp." name:@"tony" port:self.port];                      ⑦

    //添加服务到当前的Run Loop
  [_service scheduleInRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSRunLoopCommonModes];
    [_service setDelegate:self];

    //发布服务
  [_service publish];

    return YES;
}

#pragma mark - NSNetServiceDelegate

- (void)netServiceDidPublish:(NSNetService *)netService {
    NSLog(@"netServiceDidPublish");
    if ([@"tony" isEqualToString:netService.name]) {
        if (![netService getInputStream:&_inputStream outputStream:&_outputStream]) {      ⑧
                NSLog(@"连接到服务器失败.");
                return;
        }
    }
}
…
@end

/* 接收客户端请求后,回调函数  */
void AcceptCallBack( CFSocketRef socket, CFSocketCallBackType type,
                      CFDataRef address, const void *data, void *info){
  <参考2.2.3一节>
  …
}
/* 读取流操作 客户端有数据过来时调用 */
void ReadStreamClientCallBack(CFReadStreamRef stream,
    CFStreamEventType eventType, void* clientCallBackInfo){
    <参考2.2.3一节> 
    …
}
/* 写入流操作 客户端在读取数据时调用 */
void WriteStreamClientCallBack(CFWriteStreamRef stream,
         CFStreamEventType eventType, void* clientCallBackInfo){
  <参考2.2.3一节>
  …
}

第①行代码调用startServer方法启动服务,这是一个Socket服务器端程序代码,但是需要注意第③行代码端口设定的是0,这代表动态分配。但是实际运行的时候,它的端口通过第⑤~⑥行代码重新获得,最后把获得端口赋值给port属性。为了在发布服务中使用。

第⑦行代码发布服务方法publishService中实例化NSNetService对象方法。发布成功之后回调委托方法netServiceDidPublish:,这个方法的参数netService是一个NSNetService对象,我们可以借助于它的getInputStream:outputStream:方法获得其中的输入流和输出流对象,代码第⑧行所示。关于后面的回调函数与2.2.3已经完全一样,这里就不再介绍了。

2.3.5 实例: 基于服务发现的Socket通信客户端

客户端实现有两个方式: 解析服务实现和查找服务实现,我们先介绍一些解析服务实现。

1. 解析服务实现的客户端

主要代码在视图控制器ViewController类和服务发现客户端Client类中完成。

先看看视图控制器ViewController的ViewController.h代码如下:

#import <UIKit/UIKit.h>
#import "Client.h"

@interface ViewController : UIViewController <NSStreamDelegate>
{
    int flag ;                         //操作标志 0为发送 1为接收
}

@property (nonatomic, strong) NSInputStream *inputStream;
@property (nonatomic, strong) NSOutputStream *outputStream;

@property (nonatomic,strong) Client *client;

@property (weak, nonatomic) IBOutlet UILabel *message;

- (IBAction)sendData:(id)sender;
- (IBAction)receiveData:(id)sender;

@end

ViewController.m代码如下:

- (IBAction)sendData:(id)sender {
    flag = 0;
    [self openStreams];
}
- (IBAction)receiveData:(id)sender {
    flag = 1;
    [self openStreams];
}
-(void)closeStreams
{
    [_outputStream close];
    [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop]
                forMode:NSDefaultRunLoopMode];
    [_outputStream setDelegate:nil];
    [_inputStream close];
    [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [_inputStream setDelegate:nil];
}
-(void)openStreams {

    for (NSNetService *service in _client.services) {
        if ([@"tony" isEqualToString:service.name]) {
               if (![service getInputStream:&_inputStream outputStream:&_outputStream]) {
                    NSLog(@"连接服务器失败.");
                    return;
               }
               break;
        }
    }
    _outputStream.delegate = self;
    [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSDefaultRunLoopMode];
    [_outputStream open];
    _inputStream.delegate = self;
    [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                            forMode:NSDefaultRunLoopMode];
    [_inputStream open];
}

-(void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
    NSString *event;
    switch (streamEvent) {
        case NSStreamEventNone:
                event = @"NSStreamEventNone";
                break;
        case NSStreamEventOpenCompleted:
                event = @"NSStreamEventOpenCompleted";
                break;
        case NSStreamEventHasBytesAvailable:
                event = @"NSStreamEventHasBytesAvailable";
                if (flag == 1 && theStream == _inputStream) {
                     NSMutableData *input = [[NSMutableData alloc] init];
                     uint8_t buffer[1024];
                     int len;
                     while([_inputStream hasBytesAvailable])
                     {
                          len = [_inputStream read:buffer maxLength:sizeof(buffer)];
                          if (len > 0)
                          {
                                  [input appendBytes:buffer length:len];
                          }
                     }
                     NSString *resultstring = [[NSString alloc] initWithData:input
                                encoding:NSUTF8StringEncoding];
                     NSLog(@"接收:%@",resultstring);
                     _message.text = resultstring;
                }
                break;
        case NSStreamEventHasSpaceAvailable:
                event = @"NSStreamEventHasSpaceAvailable";
                if (flag == 0 && theStream == _outputStream) {
                     //输出
                     UInt8 buff[] = "Hello Server!";
                     [_outputStream write:buff maxLength: strlen((const char*)buff)+1];
                     //关闭输出流
                     [_outputStream close];
                }
                break;
        case NSStreamEventErrorOccurred:
                event = @"NSStreamEventErrorOccurred";
                [self closeStreams];
                break;
        case NSStreamEventEndEncountered:
                event = @"NSStreamEventEndEncountered";
                break;
        default:
                [self closeStreams];
                event = @"Unknown";
                break;
    }
    NSLog(@"event------%@",event);
}

在视图控制器ViewController中,没有任何与服务发现有关的代码。ViewController从Client类中获得输入和输出流对象,然后进行通信就可以了,关于流的读写,这里不再介绍。

下面再看看Client.h代码:

#import <netinet/in.h>
#import <sys/socket.h>

@interface Client : NSObjectNSNetServiceDelegate{
    int port;
}
@property(nonatomic,strong) NSMutableArray *services;
@property(nonatomic,strong) NSNetService *service;

@end

属性services存放发现的服务对象,因为可能发现多个满足条件的服务对象,因此使用可变数组类型。Client.m代码如下:

-(id)init {
    _service = [[NSNetService alloc] initWithDomain:@"local."
                            type:@"_tonyipp._tcp." name:@"tony"];
    [_service setDelegate:self];
    //设置解析地址超时时间
    [_service resolveWithTimeout:1.0];
    _services = [[NSMutableArray alloc] init];
    return self;
}

#pragma mark - NSNetServiceDelegate Methods

- (void)netServiceDidResolveAddress:(NSNetService *)netService {
    NSLog(@"netServiceDidResolveAddress");
    [_services addObject:netService];
}

- (void)netService:(NSNetService *)netService didNotResolve:(NSDictionary *)errorDict {
    NSLog(@"didNotResolve: %@",errorDict);
}

2. 查找服务实现的客户端

查找服务实现与解析服务实现类似,视图控制器ViewController类完全一样,不同的是Client类,Client.h代码如下:

#import <netinet/in.h>
#import <sys/socket.h>

@interface Client : NSObjectNSNetServiceBrowserDelegate{
    int port;
}

@property(nonatomic,strong) NSMutableArray *services;
@property(nonatomic,strong) NSNetServiceBrowser *serviceBrowser;

@end

属性services存放发现的服务对象,因为可能发现多个满足条件的服务对象,因此使用可变数组类型。Client.m代码如下:

-(id)init {
    _services = [[NSMutableArray alloc] init];
    _serviceBrowser = [[NSNetServiceBrowser alloc] init];
   _serviceBrowser.delegate = self;
   [_serviceBrowser searchForServicesOfType:@"_tonyipp._tcp." inDomain:@"local."];
    return self;
}

#pragma mark - NSNetServiceBrowserDelegate Methods

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
        didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing {
    NSLog(@"didFindService: %@  lenght:%d",netService.name,[netService.name length]);
    [_services addObject:netService];                                                  ①
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
                 didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing {
    NSLog(@"didRemoveService");
    [_services removeObject:netService];                                               ②
}

当服务被发现时,将服务放入到成员变量_services集合中,如代码①所示。当服务消失时,该服务要从成员变量_services集合移除。

2.4 对等结构网络

对等结构网络是苹果公司的Ad Hoc网络1的一种,在小空间里构建无限网络的解决方案。苹果公司在Game Kit框架中提供了开发这种网络的API。

1Ad Hoc网络是一个没有有线基础设施或中央控制器支持的移动网络。——引自于http://zh.wikipedia.org/wiki/Ad_hoc网络

2.4.1 使用Game Kit开发对等结构网络应用

在iOS 3之后苹果公司提供了用于自己的游戏中心(Came Center)开发的API,这就是Game Kit框架。在新发布的iOS 6中,苹果公司对Game Kit进行了比较大的调整。借助于Game Kit中的对等网络,API不仅可以开发基于Ad Hoc的网络游戏,也可以在其他类型的应用中使用这个些API。

在Game Kit中有关对等结构网络API有:

1. GKSession,描述连接的会话对象;

2. GKSessionDelegate,会话对象的委托协议,当会话状态发生变化时回调其定义的方法;

3. GKPeerPickerController,有苹果公司提供的一套标准UI,连接的确认对话框等UI;

4. GKPeerPickerControllerDelegate,与GKPeerPickerController对应的为委托协议,用来响应UI中的事件处理。

提示 Game Kit中还提供了基于对等网络的语言聊天API: GKVoiceCharService和GKVoiceChatClient,它们可以基于蓝牙通信,也基于自定义的Socket通信。

连接一旦创建,会话也就建立起来了。会话是与网络中运行应用的对等点(Peer)对应的,每一对等点都会有一个PeerId作为标识区别彼此,可以由我们指定也可以系统分配。在彼此发现方面,Game Kit采用Bonjour发现服务,这些对于开发人员是不可见的,不用关心它们的细节问题。

为了能够发现彼此,会话可以配置为“服务器”(发布服务)、“客户端”(搜索服务)和“对等点”(发布和搜索服务)三种类型,如图2-7所示。

图 2-7 会话类型

Game Kit对这些苹果公司的设备进行连接时,链路层采用蓝牙2或WiFi实现,并采用Bonjour发现服务。这些对于开发人员是不可见的,开发人员不用关心它们的细节问题。

2蓝牙(Bluetooth) 是由Sony Ericsson公司研发的,是一种无线通信协议(Wireless),主要用于短程和低耗电的设备。

基于蓝牙连接的对等网络在数据传输时,传输的距离有限制,另外Game Kit对于传输的数据量也有一定的限制,数据量最大不能超过87KB,处于性能的考虑传输数据不要超过1000字节,如果超过,分割成几个数据包传输。

2.4.2 实例: 基于蓝牙对等网络通信

基于蓝牙对等网络通信就是使用Game Kit中的GKSession、GKSessionDelegate、GKPeerPickerController和GKPeerPickerControllerDelegate来实现。开发过程分为3个步骤: 连接、发送数据和接收数据。

下面我们通过一个实例介绍一下基于蓝牙对等网络通信过程。该实例如图2-8所示,用户单击“连接”按钮,建立连接过程中会出现连接对话框,如图2-8的中图和右图所示,根据具体情况也会弹出其他的对话框。这些都是针对蓝牙对等网络标准对话框,而WiFi对等网络没有标准对话框可以使用,需要开发者自己实现。当两个设备连接好之后,两个玩家就可以连续轻击“单击”按钮,单击的次数会传递给对方,倒计时时间是30s。

{%}

图 2-8 蓝牙对等网络通信实例

1. 连接

由于对等网络连接过程有点复杂,贯穿了这些协议和类,我们绘制了连接过程的流程,如图2-9所示。

图 2-9 对等网络连接过程

下面通过代码直接介绍连接流程,其中ViewController.h代码如下:

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>

#define  GAMING 0                        //游戏进行中
#define  GAMED  1                        //游戏结束

@interface ViewController : UIViewController <GKSessionDelegate, GKPeerPickerControllerDelegate>
{
    NSTimer *timer;
}
@property (weak, nonatomic) IBOutlet UILabel *lblTimer;

@property (weak, nonatomic) IBOutlet UILabel *lblPlayer2;
@property (weak, nonatomic) IBOutlet UILabel *lblPlayer1;
@property (weak, nonatomic) IBOutlet UIButton *btnConnect;
@property (weak, nonatomic) IBOutlet UIButton *btnClick;

@property (nonatomic, strong) GKPeerPickerController *picker;

@property (nonatomic, strong) GKSession *session;

- (IBAction)onClick:(id)sender;
- (IBAction)connect:(id)sender;

//清除UI画面上的数据
-(void) clearUI;

//更新计时器
-(void) updateTimer;

@end

使用Game Kit需要引入头文件<GameKit/GameKit.h>,之前需要把GameKit.framework框架添加到工程中。而且定义类的时候需要实现协议GKSessionDelegate和GKPeerPickerControllerDelegate,并且定义GKPeerPickerController类型的属性picker,定义GKSession类型的属性session。

ViewController.m中创建GKPeerPickerController对象的代码如下:

- (IBAction)connect:(id)sender {
    _picker = [[GKPeerPickerController alloc] init];

    _picker.delegate = self;                                                     ①
    _picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;              ②
    [_picker show];
}

用户单击“连接”按钮时,触发connect:方法。在该方法中创建GKPeerPickerController对象。创建完成不要忘记设置GKPeerPickerController委托为self,第①行代码所示。在第②行代码中connectionTypesMask属性是设置对等网络连接类型,其中有两种类型选择: GKPeerPickerConnectionTypeNearby和GKPeerPickerConnectionTypeOnline,GKPeerPicker-ConnectionTypeNearby用于蓝牙通信也是默认的通信方法,对话框GKPeerPickerConnectionTypeOnline用于WiFi通信的局域网通信,这种方式会很麻烦,需要开发人员自己设计UI画面,自己使用Bonjour服务发现管理连接,以及自己编写输入输出流实现通信。如果给用户一个如图2-10所示的选择对话框,代码可以如下编写:

_picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby |
GKPeerPickerConnectionTypeOnline;

图 2-10 网络类型选择

其中“在线”就是GKPeerPickerConnectionTypeOnline类型,“附近”就是GKPeerPickerConnectionTypeNearby类型。

连接成功之后回调ViewController.m中的回调委托方法peerPickerController:didConnectPeer:toSession:代码如下:

- (void)peerPickerController:(GKPeerPickerController *)pk didConnectPeer:(NSString *)peerID
                   toSession:(GKSession *) session
{
    NSLog(@"建立连接");
    _session = session;                                                           ①
    _session.delegate = self;                                                     ②
    [_session setDataReceiveHandler:self withContext:nil];                        ③
    _picker.delegate = nil;
    [_picker dismiss];                                                            ④
    [_btnClick setEnabled:YES];
    [_btnConnect setTitle:@"断开连接" forState:UIControlStateNormal];

    //开始计时
    timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self
                                         selector:@selector(updateTimer)
                                         userInfo:nil repeats:YES];               ⑤

}

上述代码第①行_session = session将委托方法中返回的会话参数赋值给成员变量,这样我们就获得了一个会话对象。这种方式中,会话ID是应用程序的包ID,如果想自己分配会话ID,可以实现下面委托方法,在方法中使用GKSession的构造方法initWithSessionID:displayName: sessionMode:,自己创建会话对象。

- (GKSession *)peerPickerController:(GKPeerPickerController *)picker
        sessionForConnectionType:(GKPeerPickerConnectionType)type {
    GKSession *session = [[GKSession alloc] initWithSessionID: <自定义SessionID>
                        displayName:<显示的名字> sessionMode:GKSessionModePeer];
    return session;
}

第②行代码中_session.delegate = self是设置GKSession的委托对象为self,第③行代码[_session setDataReceiveHandler:self withContext:nil]设置接收数据的处理方法。第④行代码[_picker dismiss]是在关闭对话框。

第⑤行代码开启一个定时器(NSTimer),这个定时器被设置为1秒钟调用一次updateTimer方法更新UI。

有的时候会话的状态会发生变化,我们要根据状态的变化做一些UI的清理和资源的释放。监测状态变化在委托方法session:peer:didChangeState:中实现,方法代码如下:

- (void)session:(GKSession *)session peer:(NSString *)peerID
    didChangeState:(GKPeerConnectionState)state
{
     if (state == GKPeerStateConnected)
     {
         NSLog(@"connected");
         [_btnConnect setTitle:@"断开连接" forState:UIControlStateNormal];
         [_btnClick setEnabled:YES];
     } else if (state == GKPeerStateDisconnected)
     {
         NSLog(@"disconnected");
         [self clearUI];
     }

}

其中GKPeerStateConnected常量是已经连接状态,GKPeerStateDisconnected常量是断开连接状态。

2. 发送数据

发送数据的代码如下:

- (IBAction)onClick:(id)sender {

    int count = [_lblPlayer1.text intValue];
    _lblPlayer1.text = [NSString stringWithFormat:@"%i",++count];

    NSString *sendStr = [NSString
        stringWithFormat:@"{\"code\":%i,\"count\":%i}",GAMING,count];              ①
    NSData* data = [sendStr dataUsingEncoding: NSUTF8StringEncoding];
    if (_session) {
        [_session sendDataToAllPeers:data
                        withDataMode:GKSendDataReliable  error:nil];               ②
    }
}

第①行代码构建要传输的数据,无论是服务器客户端网络还是对等网络,它们都不负责数据的格式化,这里把数据定义成为JSON格式,其格式如下:

{"code":1,"count":20}

其中code数据项描述的是游戏状态代码,0代码游戏进行中,1代码游戏结束。count数据项代码单击次数。关于JSON格式我们会在第3章介绍。

第②行代码通过GKSession的sendDataToAllPeers:withDataMode:error:方法给所有已经连接的对等点发送数据。类似的方法还有sendData:toPeers:withDataMode:error:,该方法是给指定对等点发送数据。

3. 接收数据

为了接收数据首先需要在设置会话时通过[_session setDataReceiveHandler:self withContext:nil]语句设置接收数据的处理程序是self。这样当数据到达时就会触发下面的方法特定:

- (void) receiveData:(NSData *)data  fromPeer:(NSString *)peer
    inSession:(GKSession *)session  context:(void *)context
{
     id jsonObj = [NSJSONSerialization JSONObjectWithData:data
                       options:NSJSONReadingMutableContainers error:nil];
     NSNumber *codeObj = [jsonObj objectForKey:@"code"];

     if ([codeObj intValue]== GAMING) {
         NSNumber * countObj= [jsonObj objectForKey:@"count"];
         _lblPlayer2.text = [NSString stringWithFormat:@"%@",countObj];

     } else if ([codeObj intValue]== GAMED) {
         [self clearUI];
     }

}

上面的代码是接收到数据之后,进行JSON解码,取出游戏状态和单击次数。

主要的程序代码就是这些,根据具体的业务情况还可以有所变化,读者可以下载完整代码在两台之间设备或是一个设备一个模拟器之间进行测试。

本章小结

通过本章的学习,读者了解了苹果的网络结构有哪些,其中包括: 客户端服务器结构和对等结构。然后介绍了基于Socket实现的客户端服务器结构网络通信,以及基于蓝牙实现对等结构网络通信。此外,还介绍了零配置的Bonjour发现服务协议的使用。

目录

  • 推荐序(一)
  • 推荐序(二)
  • 赞誉
  • 前言
  • 第 1 章 开篇综述
  • 网络基础篇
  • 第 2 章 沟通无限——苹果的网络
  • 第 3 章 数据交换格式
  • 云服务篇
  • 第 4 章 使用Web Service——基于客户端服务器结构网络通信
  • 第 5 章 iCloud编程
  • 社交篇
  • 第 6 章 社交网络编程
  • 第 7 章 定位服务与地图应用开发
  • 电子商务篇
  • 第 8 章 发布你的促销信息——推送通知
  • 第 9 章 报刊杂志——Newsstand应用编程
  • 第 10 章 应用内购买
  • 第 11 章 iOS 6 Passbook应用开发
  • 实战篇
  • 第 12 章 重构MyNotes应用——iOS网络通信中的设计模式与架构设计
  • 第 13 章 iOS敏捷开发项目实战——价格线酒店预订iPhone客户端开发