版权声明:本文为博主原创文章,未经博主允许不得转载。
之前在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的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);
- }
- }
其实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];
- }
为了接收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;
- }
- }
最后只要启动这个线程就可以了,如:
- 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
例子使用的OS X,其实在IOS里面也是类似的做法。