2011年7月11日 星期一

iPhone 開發教學 - A Simple Socket Server Example


來源:Introduction to CFNetwork Programming Guide


前陣子用了一些 iPhone app,其中有些 app 有提供小型的 web service,因此讓我想練習 iOS socket programming,反正暑假也到了,我就抽點時間東看西看。


花了不少時間,才擠出一點小範例。在 iOS 上寫 socket server 的方式,跟一般工作站寫的不一樣,以前在工作站寫 server 時,因為一開始執行並不會有 client 馬上連線,所以要將 server 程式用 loop 等著,以免程式終止。在 iOS 上寫 socket server 則有點像 Web 上寫 AJAX 的模式,改成 EVENT-DRIVEN 架構,變成要註冊 client 連線事件、client 傳輸資料、client 終止連線等相關事件。這些大概是跟 UI 操作架構有關,因此在 iOS 上寫 socket server 時,不需再用 loop 架構等在那邊(若用 loop 等在那邊也容易出錯)。


建立 iOS socket server 流程如下:



  1. 初始化 socket server 資訊,如使用的 IP 和 Port number

  2. 設定 socket accept 事件處理

  3. 設定 socket connect 事件處理

  4. 設定 socket close 事件處理


雖然 iOS 支援 BSD Socket,只是還是用 BSD Socket 練習,那就會沒學到新東西,所以此處採用稍微上層的 Core Foundation/CFNetwork 架構,物件上採用 CFSocket,此例是在 iPhone Simulator 上運行一支 socket server ,然後透過 cmd line 的 telnet 進行連線測試。僅須把程式碼都寫在 YourProjectAppDelegate.m 之裡頭即可(如:SocketServer)。


AddCFNetworkLib
記得要新增 CFNetwork framework


程式碼:


#import <CFNetwork/CFNetwork.h>
#import <arpa/inet.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#import <netdb.h>

// Global variables 
CFSocketRef server;
int serverPort = 5566;
CFMutableDictionaryRef incomingRequests;
NSFileHandle *listeningHandle;


- (void)clientHandleClose:(NSFileHandle *)incomingFileHandle close:(BOOL)closeFileHandle
{
NSLog(@"socket close");
if (closeFileHandle)
{
[incomingFileHandle closeFile];
}

[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleDataAvailableNotification object:incomingFileHandle];
CFDictionaryRemoveValue(incomingRequests, incomingFileHandle);

}

- (void)clientDataReceiveNotification:(NSNotification *)notification
{
NSLog(@"receive data");
NSFileHandle *incomingFileHandle = [notification object];
NSData *data = [incomingFileHandle availableData];

if ([data length] == 0)
{
[self clientHandleClose:incomingFileHandle close:NO];
return;
}
NSString *formatedData = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSLog(@": %@", formatedData);
[formatedData release];
    
[self clientHandleClose:incomingFileHandle close:YES];
}

- (void)clientAcceptNotification:(NSNotification *)notification
{
NSLog(@"server accept");
    
NSDictionary *userInfo = [notification userInfo];
NSFileHandle *incomingFileHandle = [userInfo objectForKey:NSFileHandleNotificationFileHandleItem];
    
if(incomingFileHandle)
{
CFDictionaryAddValue( incomingRequests, incomingFileHandle, [(id)CFHTTPMessageCreateEmpty(kCFAllocatorDefault, TRUE) autorelease]);

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clientDataReceiveNotification:) name:NSFileHandleDataAvailableNotification object:incomingFileHandle];
[incomingFileHandle waitForDataInBackgroundAndNotify];
}

    
[listeningHandle acceptConnectionInBackgroundAndNotify];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
int socketSetupContinue = 1;
struct sockaddr_in addr;
    
if( !(server = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM,IPPROTO_TCP, 0, NULL, NULL) ) )
{
NSLog(@"CFSocketCreate failed");
socketSetupContinue = 0;
}
    
if( socketSetupContinue )
{
int yes = 1;
if( setsockopt(CFSocketGetNative(server), SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(int)) )
{
NSLog(@"setsockopt failed");
CFRelease(server);
socketSetupContinue = 0;
}
}
    
if( socketSetupContinue )
{
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(struct sockaddr_in);
addr.sin_family = AF_INET;
addr.sin_port = htons(serverPort);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
        
//CFDataRef *address = CFDataCreate(NULL, (const UInt8 *)&addr, sizeof(addr));
//[(id)address autorelease];
//if (CFSocketSetAddress(server, address) != kCFSocketSuccess) 
NSData *address = [NSData dataWithBytes:&addr length:sizeof(addr)];
if (CFSocketSetAddress(server, (CFDataRef)address) != kCFSocketSuccess) 
{
NSLog(@"CFSocketSetAddress failed");
CFRelease(server);
socketSetupContinue =0;
}
}
    
if( socketSetupContinue )
{
incomingRequests = CFDictionaryCreateMutable( kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
listeningHandle = [[NSFileHandle alloc] initWithFileDescriptor:CFSocketGetNative(server) closeOnDealloc:YES];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clientAcceptNotification:) name:NSFileHandleConnectionAcceptedNotification object:nil];
[listeningHandle acceptConnectionInBackgroundAndNotify];

        
NSLog(@"Socket listening on %s:%d", addr2ascii(AF_INET, &(addr.sin_addr.s_addr),sizeof(addr.sin_addr.s_addr),NULL), serverPort);
}
    
// Override point for customization after application launch.
// Add the navigation controller's view to the window and display.
self.window.rootViewController = self.navigationController;
[self.window makeKeyAndVisible];
return YES;
}


模擬器一開啟後,就會聽在 5566 port 上頭,接著可以用 telnet localhost 進行測試:


$ telnet localhost 5566
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


一打完就可以看到 iPhone Simulator 印出 server accept 字樣


 Socket listening on 0.0.0.0:5566
 server accept


接著隨意打一些字串,就可以看到 Simulator 印出接收到的字串以及把連線中斷的訊息,當然 client 也就中斷了。


上述粗體字是全域變數,可以加在 @interface 裡頭或是之接擺在 @implementation 後面也行。藍色部分是 Event 的設定,由於 socket handle 在系統底層也算是個 file handle,所以事件的偵測就跟偵測 file 變化類似,例如檔案開檔、新增資料和關檔等等。


參考資料:



3 則留言:

  1. 請問~如果要改成聊天的方式的話 要怎麼樣改比較好呢?
    指點迷津><!!謝謝!!

    版主回覆:(06/12/2011 01:50:47 PM)


    我寫的範例看起來不太適合聊天室的架構 :P
    聊天室必須建立在雙方或多人都可以互相連到的架構
    例如遠處一台 Server ,而其他手機端都是 Client 端的架構。
    所以此例不太適合。

    你可以找看看純粹的 client 端範例,我這個是 server 端範例 orz

    回覆刪除
  2. 板主你好
    想請問你,我如果希望能夠持續輸入 字串
    不要輸入一次 socket就中斷 ~"~ 該怎麼改呢?

    回覆刪除
  3. 板主您好:
    我希望能夠持續輸入字串
    不要輸入一次 socket就中斷 , 讓server一直開著,該怎麼做呢?

    回覆刪除