OS X/IOS 网络编程

来源:互联网 发布:win7网络发现自动关闭 编辑:程序博客网 时间:2024/04/29 11:02

之前在os x和ios上都写过一些简单的网络通信程序,都是用的基于c的bsd socket,因为之前在linux,windows上写过很多网络通信程序,都是用的c语言版本的socket,所以os x/ios上也用这套东西了。用基本的bsd socket,比较灵活,但是相对也比较难控制一点,需要关注很多细节,也比较繁琐。其实每个平台上面,都有一些封装好的类库,比如windows上有mfc,甚至boost,当然boost是跨平台的,linux上也可以用。os x/ios上则有cfnetwork,还有cocoa的stream等等。昨天下午突然有兴趣,就看了一下cfnetwork和stream,写了个简单的测试程序。


服务端

服务端采用cfnetwork写的,结合了run-loop。先看看监听socket的创建,整体步骤跟c语言的差不多,了不起就是用不同的函数。

- (BOOL)createServer{    //// PART 1: Create a socket that can accept connections        // Socket context    //  struct CFSocketContext {    //   CFIndex version;    //   void *info;    //   CFAllocatorRetainCallBack retain;    //   CFAllocatorReleaseCallBack release;    //   CFAllocatorCopyDescriptionCallBack copyDescription;    //  };    CFSocketContext socketContext = {0, self, NULL, NULL, NULL};        listeningSocket = CFSocketCreate(     kCFAllocatorDefault,     PF_INET,        // The protocol family for the socket     SOCK_STREAM,    // The socket type to create     IPPROTO_TCP,    // The protocol for the socket. TCP vs UDP.     kCFSocketAcceptCallBack,  // New connections will be automatically accepted and the callback is called with the data argument being a pointer to a CFSocketNativeHandle of the child socket.     (CFSocketCallBack)&serverAcceptCallback,     &socketContext );        // Previous call might have failed    if ( listeningSocket == NULL ) {    status = @"listeningSocket Not Created";    return FALSE;    }    else {    status = @"listeningSocket Created";    int existingValue = 1;                // Make sure that same listening socket address gets reused after every connection        setsockopt( CFSocketGetNative(listeningSocket),                   SOL_SOCKET, SO_REUSEADDR, (void *)&existingValue,                   sizeof(existingValue));                        //// PART 2: Bind our socket to an endpoint.        // We will be listening on all available interfaces/addresses.        // Port will be assigned automatically by kernel.        struct sockaddr_in socketAddress;        memset(&socketAddress, 0, sizeof(socketAddress));        socketAddress.sin_len = sizeof(socketAddress);        socketAddress.sin_family = AF_INET;   // Address family (IPv4 vs IPv6)        socketAddress.sin_port = 0;           // Actual port will get assigned automatically by kernel        socketAddress.sin_addr.s_addr = htonl(INADDR_ANY);    // We must use "network byte order" format (big-endian) for the value here                // Convert the endpoint data structure into something that CFSocket can use        NSData *socketAddressData =        [NSData dataWithBytes:&socketAddress length:sizeof(socketAddress)];                // Bind our socket to the endpoint. Check if successful.        if ( CFSocketSetAddress(listeningSocket, (CFDataRef)socketAddressData) != kCFSocketSuccess ) {            // Cleanup            if ( listeningSocket != NULL ) {                status = @"Socket Not Binded";                CFRelease(listeningSocket);                listeningSocket = NULL;            }                        return FALSE;        }        status = @"Socket Binded";                //// 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]);                port = ntohs(socketAddressActual.sin_port);           //     char* ip = inet_ntoa(socketAddressActual.sin_addr);                        //// 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);                KAppDelegate* d = (KAppDelegate*)[self delegate];        [d ShowLog:[NSString stringWithFormat:@"Create server socket successfully, port: %d", port]];                return TRUE;    }}
基本步骤就是:

1. 创建一个监听socket

2. 绑定本地地址和端口

3. 绑定run-loop

注意:在CFSocketCreate中的参数kCFSocketAcceptCallBack,这里就是设置了一个回调,当有客户端连上来的时候,当前线程的run-loop将会被调用这个回调。

看看这个回调函数:

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;        KServer *server = (KServer*)info;    [server handleNewNativeSocket:nativeSocketHandle];}
这个回调函数里面就是简单的调用另外一个函数:

// Handle new connections- (void)handleNewNativeSocket:(CFSocketNativeHandle)nativeSocketHandle {    ClientSocket* c = [[[ClientSocket alloc] init] autorelease];    c.sock = nativeSocketHandle;    [m_AllClients addObject:c];        uint8_t name[SOCK_MAXADDRLEN];    socklen_t nameLen = sizeof(name);    if (0 != getpeername(nativeSocketHandle, (struct sockaddr *)name, &nameLen)) {        NSLog(@"error");        exit(1);    }        KAppDelegate* d = (KAppDelegate*)[self delegate];    struct sockaddr_in* addr = (struct sockaddr_in *)name;    [d ShowLog:[NSString stringWithFormat:@"connected, client: %s, %d", inet_ntoa(addr->sin_addr), ntohs(addr->sin_port)]];        //写一些数据给客户端    CFReadStreamRef iStream;    CFWriteStreamRef oStream;    // 创建一个可读写的socket连接    CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &iStream, &oStream);    if (iStream && oStream) {        CFStreamClientContext streamContext = {0, self, NULL, NULL};        if (!CFReadStreamSetClient(iStream, kCFStreamEventHasBytesAvailable,                                   readStream, // 回调函数,当有可读的数据时调用                                   &streamContext)){            exit(1);        }                if (!CFWriteStreamSetClient(oStream, kCFStreamEventCanAcceptBytes, writeStream, &streamContext)){            exit(1);        }                CFReadStreamScheduleWithRunLoop(iStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);   //     CFWriteStreamScheduleWithRunLoop(oStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);        CFReadStreamOpen(iStream);   //     CFWriteStreamOpen(oStream);                NSString *stringTosend = @"Welcome CFSocker server";        [self SendData:stringTosend];    } else {        close(nativeSocketHandle);    }}
在这里,我们简单的读取了一下client的ip和端口,并且显示。

然后给新生成的用于处理远程client的socket创建一个readstream,并且集成到run-loop中,这样client有数据发过来的时候,run-loop就会触发一个响应。这个例子里面一旦有client连上,就先给client发一些信息。另外,我还将每个处理client的socket放到了一个array中。
看一下读取信息的回调函数

// 读取数据void readStream(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo) {    UInt8 buff[255];    CFReadStreamRead(stream, buff, 255);  //  printf("received: %s", buff);    KServer* context = (KServer*)clientCallBackInfo;    [context ShowLog:[NSString stringWithFormat:@"recv: %s", buff]];}
收取数据后,打印一下。

哈哈,这个简单的server,基本就是这个样子。

其实,这个server例子只是用最最简单的cfnetwork函数创建了一个最最简单的server,这些代码还有很多问题,比如:

1. 各种网络错误处理,如果断线等

2. 并发性的问题

3. 甚至一些可能的内存泄漏

等等,还有很多其他的问题。anyway,这段代码只是用来演示基本的cfnetwork函数调用。


客户端

有了服务端,在创建一个客户端。客户端也可以用cfnetwork。

如:

-(void) CreateSocketClient: (NSString*) serverIP PORT: (in_port_t) port{    CFSocketContext sockContext = {0, // 结构体的版本,必须为0        self,  // 一个任意指针的数据,可以用在创建时CFSocket对象相关联。这个指针被传递给所有的上下文中定义的回调。        NULL, // 一个定义在上面指针中的retain的回调, 可以为NULL        NULL, NULL};        _client = CFSocketCreate(                   kCFAllocatorDefault,                   PF_INET,        // The protocol family for the socket                   SOCK_STREAM,    // The socket type to create                   IPPROTO_TCP,    // The protocol for the socket. TCP vs UDP.                   kCFSocketConnectCallBack,  // New connections will be automatically accepted and the callback is called with the data argument being a pointer to a CFSocketNativeHandle of the child socket.                   (CFSocketCallBack)&TCPServerConnectCallBack,                   &sockContext );            if (_client != nil) {        int existingValue = 1;                // Make sure that same listening socket address gets reused after every connection        setsockopt( CFSocketGetNative(_client),                   SOL_SOCKET, SO_REUSEADDR, (void *)&existingValue,                   sizeof(existingValue));                struct sockaddr_in addr4;   // IPV4        memset(&addr4, 0, sizeof(addr4));        addr4.sin_len = sizeof(addr4);        addr4.sin_family = AF_INET;        addr4.sin_port = htons(port);        addr4.sin_addr.s_addr = inet_addr([serverIP UTF8String]);  // 把字符串的地址转换为机器可识别的网络地址                // 把sockaddr_in结构体中的地址转换为Data        CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&addr4, sizeof(addr4));        CFSocketConnectToAddress(_client, // 连接的socket                                 address, // CFDataRef类型的包含上面socket的远程地址的对象                                 -1  // 连接超时时间,如果为负,则不尝试连接,而是把连接放在后台进行,如果_socket消息类型为kCFSocketConnectCallBack,将会在连接成功或失败的时候在后台触发回调函数                                 );                CFRunLoopRef cRunRef = CFRunLoopGetCurrent();    // 获取当前线程的循环        // 创建一个循环,但并没有真正加如到循环中,需要调用CFRunLoopAddSource        CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _client, 0);        CFRunLoopAddSource(cRunRef, // 运行循环                           sourceRef,  // 增加的运行循环源, 它会被retain一次                           kCFRunLoopCommonModes  // 增加的运行循环源的模式                           );        CFRelease(sourceRef);                            }       }
基本过程就是:

1. 创建一个socket

2. connect服务器

注意这次在CFSocketCreate里面,我们使用了kCFSocketConnectCallback的回调,这样当client的connect动作完成的时候,都会调用这个回调(无论成功与否)。

// socket回调函数的格式:static void TCPServerConnectCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info){    if (data != NULL) {        // 当socket为kCFSocketConnectCallBack时,失败时回调失败会返回一个错误代码指针,其他情况返回NULL        NSLog(@"连接失败");        return;    }        KClient *client = (KClient *)info;        CFReadStreamRef iStream;        CFSocketNativeHandle h = CFSocketGetNative(socket);    CFStreamCreatePairWithSocket(kCFAllocatorDefault, h, &iStream, nil);    //   CFStreamCreatePairWithSocket(kCFAllocatorDefault, socket, &iStream, nil);    if (iStream) {        CFStreamClientContext streamContext = {0, NULL, NULL, NULL};        if (!CFReadStreamSetClient(iStream, kCFStreamEventHasBytesAvailable |                                   kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered,                                   readStream, // 回调函数,当有可读的数据时调用                                   &streamContext)){            exit(1);        }                CFReadStreamScheduleWithRunLoop(iStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);  //      CFReadStreamOpen(iStream);                //       size_t sent = send(CFSocketGetNative(socket), "client sent", 11, 0);                char buf[100] = {0};        //    recv(CFSocketGetNative(socket), buf, 100, 0);                printf("recv: %s", buf);    }    }
在这个回调里面如果使用recv可以接收到服务端发过来的信息,但是使用ReadStream好象收不到,我也不知道为什么,我没有花太多时间去研究它。如果哪位大哥能够指出错误的话,将不胜感激。

其实apple公司在Cocoa里面还提供了一些其他的用于网络通信的接口,比如NSStream,这个是基于objective-c语言的。使用也是相当的方便。

写了几行测试代码:

- (void)work_thread:(NSURL *)url{    NSInputStream * readStream;    NSOutputStream* writeStream;        NSString* strHost = [url host]; //   strHost = @"127.0.0.1";    int port = [[url port] integerValue];       [NSStream getStreamsToHostNamed:strHost port: port inputStream:&readStream outputStream:&writeStream];            [readStream setDelegate:self];    [readStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];    [readStream open];        [writeStream open];    [writeStream write:"abcd" maxLength:4];    [[NSRunLoop currentRunLoop] run];}
这次我们将通信程序放到一个线程里面。上面就是一个线程函数,在这个函数里面,指定了服务端的ip和端口,并且创建了2个stream,一个input,一个output。然后集成到这个线程的run-loop里面。

为了接收nsstream的响应,必须给nsstream指定一个delegate,并且实现stream函数。

@interface KAppDelegate : NSObject <NSApplicationDelegate, NSStreamDelegate>

实现:

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode{    NSLog(@" >> NSStreamDelegate in Thread %@", [NSThread currentThread]);        switch (eventCode) {        case NSStreamEventHasBytesAvailable: {                                    uint8_t buf[100] = {0};            int numBytesRead = [(NSInputStream *)stream read:buf maxLength:100];                        NSString* str = [NSString stringWithFormat:@"recv: %s", buf];            [self performSelectorOnMainThread:@selector(ShowLog:) withObject:str waitUntilDone:YES];                        break;        }                    case NSStreamEventErrorOccurred: {                       break;        }                    case NSStreamEventEndEncountered: {                                               break;        }                    default:            break;    }}
真的是相当的简单,在stream函数里面可以处理各种事件。

最后只要启动这个线程就可以了,如:

    NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@",  [self TextIP].stringValue, [self TextPort].stringValue]];        NSThread * backgroundThread = [[NSThread alloc] initWithTarget:self                                                          selector:@selector(work_thread:)                                                            object:url];    [backgroundThread start];

这个线程启动后,就会在线程函数里面创建inputstream和outputstream,这样就可以发送和读取数据了,使用NSStream,真的可以很轻松的实现网络通信,我们所要做的就是:

1. 创建发送和读取stream;

2. 实现一个delegate,用于处理各种网络事件。

所以,我们在写os x/ios网络程序的时候,应该尽量使用NSStream等这种高级的工具,因为apple公司已经帮我们处理了很多细节问题,可以节省很多的时间。

另外有一个需要注意的就是:NSStream并没有提供连接服务端的基于字符串的函数,没有关系,我们可以扩展一个,如下:

@implementation NSStream(StreamsToHost)+ (void)getStreamsToHostNamed:(NSString *)hostName                         port:(NSInteger)port                  inputStream:(out NSInputStream **)inputStreamPtr                 outputStream:(out NSOutputStream **)outputStreamPtr{    CFReadStreamRef     readStream;    CFWriteStreamRef    writeStream;        assert(hostName != nil);    assert( (port > 0) && (port < 65536) );    assert( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) );        readStream = NULL;    writeStream = NULL;        CFStreamCreatePairWithSocketToHost(                                       NULL,                                       (__bridge CFStringRef) hostName,                                       port,                                       ((inputStreamPtr  != NULL) ? &readStream : NULL),                                       ((outputStreamPtr != NULL) ? &writeStream : NULL)                                       );        if (inputStreamPtr != NULL) {        *inputStreamPtr  = CFBridgingRelease(readStream);    }        if (outputStreamPtr != NULL) {        *outputStreamPtr = CFBridgingRelease(writeStream);    }}@end
通过这个函数,我们可以方便地通过字符串表示的ip和端口来连接服务端。

例子使用的OS X,其实在IOS里面也是类似的做法。

 

代码:http://download.csdn.net/detail/zj510/5388471

原创粉丝点击