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 變化類似,例如檔案開檔、新增資料和關檔等等。


參考資料:



2011年7月10日 星期日

第九位成員的誕生

工作之後,幾乎一年換一台電腦?真是噴錢噴真兇,只是說起來大概是 n 年前的主機已經不敷使用吧?仔細回想一下,家中第一台電腦是 586 等級,16MB 吧?硬碟 1.6 GB吧?有點忘了,但價錢都比第三台主機之後的貴;第二台是 P-200 ,我用他玩了太空戰士七等遊戲,還有 BBS 啦;第三台就隔得比較久,就是我升大二的 AMD XP 1800+ (大一下我還在用 P-200那台寫 C 語言交作業),也是第一次組裝電腦,感謝我的好友 Kudo 啦,而第一台跟第二台也就退役了;隔年第四台是 AMD XP 2200+ 組給求學的家人;第五台是買二手零件組了台 AMD 800 ,給家人上網;第六台算是努力工作的收穫, AMD SP 2600;第七台就是碩一時期,宿舍常跳電,讓我的主機掛了,這時起就沒換機殼了,那時還用第五台撐了會,最後則是兩台退役;工作後沒多久 AMD 2200+ 也掛了,第八台產生,這次就組得很好 XD 現在想起來還真的有些浪費,使用上並沒有那麼大的需求;第九台則是這次的新成員,也代表第六台的退役。往後大概會開始用筆電了。


這次的清單(不包含機殼):


處理器 CPU		AMD AM3 AthlonII X3-450三核/3.2G/95W/45nm/L2 Cache 512KB*3,AMD AM3 腳位
主機板 MB 華碩 M4A78LT-M LE/760G/DVI+RGB/最佳DDR3入門合板,AMD AM3/合板內建顯示
記憶體 RAM 兩隻,金士頓 DDR3 4G-1333單支,桌上型 DDR3-1333/1600單支
內接式硬碟 HDD WD 500G容量 SATA3 5000AAKX/16M/藍標/SATA3/三年保,最新一代 SATA3規格
燒錄器 CD/DVD/BD LITE-ON IHAS524/24XDVD燒錄/LabelTag功能可在光碟資料面寫上圖片及文字,終極DVD燒錄器 SATA介面
電源供應器 全漢 藍寶石二代 350W(PEAK 400)日系電容三年保/90天換新,全漢三年免費/POWER界霸主

設備再加上宅急便的運費、電話聯絡跟跨行轉帳費,不到 8900 元。若是想拿來當一台新主機,那大概要再加個 700~1000,多買個機殼吧。


第一次連燒錄器都買 SATA 介面,這是有風險的,例如作業系統中沒有支援 SATA 驅動程式(Windows XP?),可能導致無法讀取光碟機吧(不曉得透過 BIOS 將 SATA 弄成 IDE 模可否解決)?另外則是要擔心 Power 的 SATA 線夠不夠多,這次買的 Power 只有提供一條 SATA 電源線,上頭有兩個接頭,所以在安裝硬體上,會碰到光碟機跟硬碟的距離問題,必須確認兩台設備都可以接到電源線,這些問題又跟機殼有關。慶幸的,一切在舊機殼上都還順利完工了。


往後還是買這種中低階的零件就好了,CPU不用兩千五,而記憶體一條 4G-1333 才 8xx 元,也感謝主機板只有兩個記憶體插槽,不然我應該會噴更多錢吧 XD 唯一比較差的應該是硬碟只買 500 GB ,計算 CP 值並不划算,但用來組一台主機的基本零件,倒是還算夠用。這次組裝就比較留意,例如 CPU 跟主機板搭配時,是否有支援到記憶體的最大使用效率(DDR3-1333),還有 CPU 所需的瓦數主機板有沒支援(有的 AMD X3 或 X4 的 CPU 要 125W,這款主機板只有支援到 95W)。


就這樣,有了一台 3.2G 三核的 CPU 並可使用 8GB 記憶體,應該非常非常夠用了 :) 


F_AUDIO 接法 (前面板音效/前置音效)

F_AUDIO


回老家前,在某 3C 店家線上購物,剛剛好在我回到家之前送到,周末就著手組裝電腦及其相關的調教,於是碰到前面板音效的問題。新買的主機板是 ASUS M4A78LT-M LE ,此主機板支援 HD Audio 也支援 AC' 97 ,處理這個任務第一步就是要翻說明書啦,各家主機板可能都不一樣。


F_AUDIO manual
ASUS M4A78LT-M LE 說明書


F_AUDIO_NOTE
N 年前的 BBS 筆記


只是我家的機殼真的太舊了,其前置音效、USB的線都是拆開的一個 pin 腳一條,實在累人,所以翻了以前自己寫的 F_AUDIO 筆記,終於搞定了,結果測試的時候又發現前面麥克風沒聲音?最後才發現 BIOS 預設是 HD Audio 的,必須去 BIOS 裡設定


F_AUDIO_BIOS


我想,這類舊型機殼大概還是要淘汰掉才對,新的機殼都幫你把這些線包成一個大接頭,加上防呆針腳,基本上都不會再接錯,除此之外,想到舊主機板的前置 USB 還滿危險的,如果接錯的話,隨身碟一插就可能燒壞了,因此面對未知且老舊的主機,安全一點還是先接主機板上的 USB 孔吧。另外,用 "F_AUDIO 接法" 問 Google 可以看到別人寫的超詳細筆記,有需要可以參考看看:《教學》AC97和HD規範簡介和前置音效接法


2011年7月9日 星期六

Windows 7 之 KMS 啟用失敗 0xC004C003 (設定為大量授權版)

幫學弟安裝校園版軟體,校園版採用 KMS 大量授權,只是學弟一開始安裝錯作業系統,安裝成非大量授權版本(安裝過程可以看到詢問序號的畫面),雖然可以不輸入序號繼續安裝,但產品內的設定已不適用 KMS 認證了。


再重新準備大量授權版本途中,查了一些網路文章,發現 Windows 7 KMS Client Setup Keys 清單:


Windows 7 Professional		FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4
Windows 7 Professional N MRPKT-YTG23-K7D7T-X2JMM-QY7MG
Windows 7 Professional E W82YF-2Q76Y-63HXB-FGJG9-GF7QX
Windows 7 Enterprise 33PXH-7Y6KF-2VJC9-XBBR8-HVTHH
Windows 7 Enterprise N YDRBP-3D83W-TY26F-D46B2-XCKRJ
Windows 7 Enterprise E C29WB-22CC8-VJ326-GHFJW-H9DH4

並且發現 slmgr 指令可以設定 Product key ,所以就可以更新產品金鑰轉成大量授權版,進而完成大量授權之 KMS 認證機制囉!


此例恰好學弟想從 Windows 7 32-bit 升成 64-bit ,所以有前者的資料可以查閱,在 Windows 7 32-bit 之大量授權版本,其 sources/product.ini 中可以瞧見最後一行的設定:


[BuildInfo]
staged=PROFESSIONAL


而非大量授權的:


[BuildInfo]
staged=HOMEBASIC,HOMEPREMIUM,PROFESSIONAL,ULTIMATE


透過這一丁點資訊,就採用 Client Windows 7 Professional Key 了:


slmgr -ipk FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4


隨即進行 slmgr -ato 驗證,果真就成功啦!


2011年7月7日 星期四

Android 開發筆記 - 使用 Android NDK (Native Development Kit)

之把某個 android app 產品進行反組譯的動作,只是單純地想要驗證該 app 的實作流程,但在 Java 層只看到一些 UI 介面的操作,算是驗證了半個想法,但總覺得缺少什麼,所幸在原本的 apk 檔案內發現了 xxx.so 檔案,透過很基本的 strings 指令可以查看一些敏感字眼,就這樣我找到了鐵證。心中有股莫名踏實感,雖然 xxx.so 檔只能瞧見一些關鍵字,但因為撰寫程式的把 function name 取得很容易猜測功能,所以加加減減就成了一種驗證。


前陣子上過幾個小時的 android 課程,恰好稍微提到 NDK 的部份,此部份主要是設計給遊戲類的使用,例如之前寫 OpenGL ES 時,必須透過 Android Java API 來呼叫,現在則可以直接用 NDK 寫 C 語言達成,除了解決繁瑣的 C to Java 語法,更可以避開 JVM 來提昇效能。除此之外,我覺得 NDK 也是可以用拿來保護關鍵程式碼囉。此筆記就當作簡單的測試 NDK 的用法,在此不提及安裝 Android 開發環境,有興趣可以查看Android & Eclipse 開發環境- 第一次安裝筆記。在此使用 Ubuntu 10.04 64-bit 環境。


首先,從 Android NDK | Android Developers 下載 Android NDK (我在 Ubuntu 所以下載 android-ndk-r5c-linux-x86.tar.bz2) 並挑選一個地方解壓縮(此例為 ~/android-ndk-r5c),除此之外要把 Android SDK 更新至最新版,如果已經安裝好 Android 開發環境,更新方式透過 Eclipse -> [Window]-> [Android SDK and AVD Manager] -> [Installed packages] -> [Update All...]


接著要進行 NDK 環境設置,可查看 android-ndk-r5c/docs/INSTALL.html,可以得知 Android NDK 至少需使用 Android 1.5 系統,而編譯環境至少是 GNU Make 3.8.1,除此之外還需要 Nawk 或 GNU Awk 工具,而常見的 awk 則不適用。


在 Ubuntu 10.04 64-bit desktop 環境上:


$ awk -W version
GNU Awk 3.1.6
Copyright (C) 1989, 1991-2007 Free Software Foundation.
~$ make -version
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc


透過 Eclipse 建立一個 android 2.3.3 project,例如 MyNDK:


Project name: MyNDK
Build Target: Android 2.3.3/Android Open Source Project/2.3.3/10
Application name: MyNDK
Package name: com.example.ndk
Create Activity: MyNDK
Min SDK Version: 3


接著,在 Eclipse 左邊 Project Explorer 建立 jni 和 libs 兩個目錄,位置擺在剛剛的 Project 裡(點一下 MyNDK,接著按右鍵選 New -> Folder 輸入 jni,之後在建立 libs 目錄):


MyNDK/jni
MyNDK/libs


在 MyNDK/jni 裡頭新增兩個檔案,分別是 JNI 程式碼和 Android.mk,結構如下:


MyNDK/jni
MyNDK/jni/Android.mk
MyNDK/jni/my-jni.c


其中 Android.mk 和 *.c 可以從 android-ndk-r5c/samples/hello-jni/jni 裡頭尋得範例,簡單筆記:


MyNDK/jni/Android.mk (參考 android-ndk-r5c/samples/hello-jni/jni/Android.mk)


LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := my-jni
LOCAL_SRC_FILES := my-jni.c
include $(BUILD_SHARED_LIBRARY)


MyNDK/jni/my-jni.c (參考 android-ndk-r5c/samples/hello-jni/jni/hello-jni.c)


#include <string.h>
#include <jni.h>
jstring Java_com_example_ndk_MyNDK_stringFromJNI( JNIEnv* env, jobject thiz )
{
    return (*env)->NewStringUTF(env, "Hello from My JNI !");
}


其中要留意 function name,裡頭包括 packages name 和 Activity name 等資訊,此例是 com_example_ndk 和 MyNDK。


接著要透過 android ndk 去編譯 my-jni.c,除了在 Command line 可以切換到 MyNDK 目錄,用 android-ndk-r5c/ndk-build 進行編譯外,還可以透過 Eclipse 設定好,讓你每次自動編譯好:


Properties for MyNDK


在 Eclipse 左邊 Project Explorer 點選 MyNDK,選右鍵 -> Properties -> Builders -> New -> Program


在 Main 分頁:


Name: NDK_Builder
Location(ndb-build位置): /home/user/android-ndk-r5c/ndk-build
Working Directory:(project位置): ${workspace_loc:${project_path}}


Builders


在 Refresh 分頁:


將 Refresh resources upon completion 打勾,並且在 Specific resources 選擇 MyNDK/libs 目錄


在 Build Options 分頁:


勾選 Allocate Console(necessary for input)、Launch in backgroud、After a "Clean"、During manual builds、During auto builds 和 Specify working set of relevant resources


在 Specific resources 選擇 MyNDK/jni


如此一來,只要編譯或清除時,自動會編譯 jni 的部份,可以使用 Build all(快速鍵Ctrl+B) 試試,可以看到 Console 的輸出:


Compile thumb  : my-jni <= my-jni.c
SharedLibrary  : libmy-jni.so
Install        : libmy-jni.so => libs/armeabi/libmy-jni.so


接著,才正式要寫 Java 程式部份,切換到 MyNDK.java:


package com.example.ndk;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MyNDK extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.main);
        TextView  tv = new TextView(this);
        tv.setText( stringFromJNI() );
        setContentView(tv);

    }
    public native String  stringFromJNI();
    static {
        System.loadLibrary("my-jni");
    }

}


其中特別的是要宣告 jni 使用到的函數,這跟 my-jni.c 裡頭的宣告有關,以及 System.loadLibrary 的部份,跟 jni/Android.mk 設定的編譯結果也有關係,如果都不相符,執行時會出現 exception。


最後跑起來的成果:


MyJNI


整個 Project 結構:


Project Explorer


相關文章: