【轉載】初學者進階教程:閃訊實例介紹Cocoa多線程, 系統網絡設置自定義

文章來源:http://www.maiyadi.com/thread-38202-1-1.html

 

一個禮拜前看到論壇網友 豎果小子 的短信, 希望能寫一個蘋果下的撥號軟件, 完成視窗下閃訊的功能. 由於他提供了必需的用戶名算法文件, 而本人從未有網絡軟件編程的經驗, 就想以這個爲契機來學習一下這方面的知識. 經過網上資料收集, 瞭解了蘋果下撥號相關的系統API(scnetworkconnection和scnetworkconfiguration兩大類), 使用cocoa提供軟件界面和線程處理, 完成了蘋果版閃訊, 經 豎果小子 等網友測試, 確實可用.
這裏把自己的經驗寫出來, 提供給感興趣的朋友借鑑, 入門不深, 謬誤之處, 敬請見諒.
蘋果版閃訊源代碼下載   蘋果simpleDail源代碼下載
本文以完成閃訊比較重要的3個方面來介紹.

1. 自定義PPPOE用戶名, 密碼, 連接名(蘋果裏稱服務名)
閃訊是一個PPPOE撥號軟件, 需要使用用戶輸入的用戶名, 密碼和連接名來進行撥號. 這裏用戶名需要經過算法處理, 得到一個實時的真實用戶名. 蘋果自帶PPPOE撥號功能, 由於實時用戶名的關係, 沒法直接使用.
利用scnetworkconnection這類API, 可以完成對一個現有的PPPOE服務進行自定義, 從而實現實時用戶名撥號.

CFStringRef serviceToDial;
CFDictionaryRef optionsForDial;
CFDictionaryRef pppOptionsForDial;
SCNetworkConnectionRef connection;

serviceToDial = NULL;
optionsForDial = NULL;
pppOptionsForDial = NULL;
connection = NULL;

//獲得系統PPPOE服務ID和設置
SCNetworkConnectionCopyUserPreferences(NULL, &serviceToDial, &optionsForDial);  
//創建一個撥號服務接口
connection = SCNetworkConnectionCreateWithServiceID(
NULL,
serviceToDial, 
MyNetworkConnectionCallBack,
NULL
);
//這裏MyNetworkConnectionCallBack是一個自定義的函數, 在函數內將對撥號連接的狀態進行檢查並進行相應處理, 這裏直接使用蘋果的sample代碼simpleDial裏面的函數, 複製過來即可, 具體請參見蘋果版閃訊源代碼

//釋放optionsForDial, 因爲下面要創建一個新的
if (optionsForDial) CFRelease(optionsForDial);

CFStringRef keys[3] = { NULL, NULL, NULL };
CFStringRef vals[3] = { NULL, NULL, NULL };
CFIndex numkeys = 0;
keys[numkeys] = kSCPropNetPPPAuthName;
vals[numkeys++] = CFStringCreateWithCString(NULL, userName, kCFStringEncodingUTF8);  //這裏userName已經是算法處理之後的真實用戶名
keys[numkeys] = kSCPropNetPPPAuthPassword;
vals[numkeys++] = CFStringCreateWithCString(NULL, password, kCFStringEncodingUTF8);
keys[numkeys] = kSCPropNetPPPCommRemoteAddress;
vals[numkeys++] = CFStringCreateWithCString(NULL, serviceName, kCFStringEncodingUTF8);

// 創建 "PPP" 設置
pppOptionsForDial = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&vals, numkeys, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

numkeys = 0;
keys[numkeys] = kSCEntNetPPP;
vals[numkeys++] = pppOptionsForDial;

// 創建 "connection" 自定義撥號設置
optionsForDial = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&vals, numkeys, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//connection和自定義的撥號設置optionsForDial都有了, 下面直接使用蘋果sample代碼simpleDial裏的撥號部分即可
int err = 0;
Boolean ok;

if (err == 0) {
ok = SCNetworkConnectionScheduleWithRunLoop(
      connection,
      CFRunLoopGetCurrent(),
      kCFRunLoopDefaultMode
);
if ( ! ok ) {
      err = SCError();
}
}

if (err == 0) {
ok = SCNetworkConnectionStart(connection,optionsForDial,TRUE);
if ( ! ok ) {
  err = SCError();
}
}
if (err == 0) {
   CFRunLoopRun();
}
//這個runloop將在前面提到的callback函數中根據連接情況(已連接或者無法連接)來結束
//下面是清理工作
if (serviceToDial) CFRelease(serviceToDial);
if (optionsForDial) CFRelease(optionsForDial);
if (pppOptionsForDial) CFRelease(pppOptionsForDial);
if (connection != NULL) {
   (void) SCNetworkConnectionUnscheduleFromRunLoop(
            connection,
            CFRunLoopGetCurrent(),
            kCFRunLoopDefaultMode
        );
}
if (connection) CFRelease(connection);


2. 添加PPPOE服務到網絡設置
前面假定系統已經存在一個PPPOE服務(比如事先手動添加一個), 這是SCNetworkConnectionCopyUserPreferences 運行成功的前提.
如果系統中還沒有用戶添加的PPPOE服務, 那就需要自己來創建並添加進去, 這要用到 SCNetworkConfiguration類的API.

SCPreferencesRef           prefs;
AuthorizationRef            auth;
OSStatus                       authErr;
SCNetworkSetRef           set;
SCNetworkServiceRef     service, SzRef;
SCNetworkInterfaceRef  enIfRef, IfRef;
CFArrayRef                    SzsRef;

prefs = NULL;
auth = NULL;
authErr = noErr;
set = NULL;
service = NULL;
enIfRef = NULL;
IfRef = NULL;
SzRef = NULL;
SzsRef = NULL;

//下面的代碼先檢查系統中是否有可用的基於ethernet的PPP服務, 基於bluetooth的PPP服務要排除
//使用SCPreferencesCreateWithAuthorization來創建prefs才能使修改對系統生效
AuthorizationFlags rootFlags = kAuthorizationFlagDefaults
   |  kAuthorizationFlagExtendRights
   |  kAuthorizationFlagInteractionAllowed
   |  kAuthorizationFlagPreAuthorize;
authErr = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, rootFlags, &auth);
if (authErr == noErr)
prefs = SCPreferencesCreateWithAuthorization(NULL, CFSTR("com.sweec.shanXun"), NULL, auth);
else
prefs = SCPreferencesCreate(NULL, CFSTR("com.sweec.shanXun"), NULL);

//下面得到一份系統所有網絡服務, 檢查有沒有需要的PPP服務
if (prefs) SzsRef = SCNetworkServiceCopyAll(prefs);
if (SzsRef == nil) return;
CFIndex cSzs = CFArrayGetCount(SzsRef);
CFIndex i;
for (i = 0;i < cSzs;i++) {
SzRef = (SCNetworkServiceRef) CFArrayGetValueAtIndex(SzsRef, i);
IfRef = SCNetworkServiceGetInterface(SzRef);
if (SCNetworkInterfaceGetInterfaceType(IfRef) == kSCNetworkInterfaceTypePPP) {  //是否PPP類型, 是的話檢查它基於什麼接口
enIfRef = SCNetworkInterfaceGetInterface(IfRef);
if (enIfRef && (SCNetworkInterfaceGetInterfaceType(enIfRef) == kSCNetworkInterfaceTypeEthernet)) break;  //確實是基於ethernet接口, 可用, 不用自己創建
}
if (SCNetworkInterfaceGetInterfaceType(IfRef) == kSCNetworkInterfaceTypeEthernet) enIfRef = IfRef;  //如果沒有, 我們需要在ethernet接口上建一個, 這裏留下一個ethernet接口的指針, 後面用到
}
if (i < cSzs) { //找到可用的, 按照1裏面的代碼用它來創建一個撥號接口
SCNetworkConnectionCopyUserPreferences(NULL, &serviceToDial, &optionsForDial);
connection = SCNetworkConnectionCreateWithServiceID(
NULL,
serviceToDial, 
MyNetworkConnectionCallBack,
NULL
);
} else { //沒有找到, 自己創建一個並添加到網絡預置裏
  //建立一個基於ethernet接口的PPP接口
if (enIfRef && SCPreferencesLock(prefs, TRUE)) IfRef = SCNetworkInterfaceCreateWithInterface(enIfRef, kSCNetworkInterfaceTypePPP);
//在PPP接口的基礎上建立一個PPP服務
if (prefs && IfRef) {
service = SCNetworkServiceCreate(prefs,IfRef);
SCNetworkServiceSetName(service, CFSTR("PPP"));  //命名爲PPP, 或者其他你喜歡的
IfRef = SCNetworkServiceGetInterface(service); //重新得到這個服務的接口, 經測試必須做這一步, 用原來的話下面的步驟會失敗. 似乎建立服務後, 接口指向新的東西了

// 隨便添加一個用戶名和連接名到這個新建的PPP服務的PPP接口中, 否則系統會抱怨沒有可用的PPP設置, SCNetworkConnectionCopyUserPreferences也會失敗
//得到原來的設置
CFDictionaryRef oldOptions = SCNetworkInterfaceGetConfiguration(IfRef);
i = CFDictionaryGetCount(oldOptions);
//創建新的, 並添加用戶名和連接名(服務名)
CFMutableDictionaryRef pppOptions = CFDictionaryCreateMutableCopy(NULL, i + 2, oldOptions);
CFDictionaryAddValue(pppOptions, kSCPropNetPPPAuthName, CFSTR("shanXun"));
CFDictionaryAddValue(pppOptions, kSCPropNetPPPCommRemoteAddress, CFSTR("shanXunPPP"));
  //將設置寫回去
SCNetworkInterfaceSetConfiguration(IfRef, pppOptions);
CFRelease(pppOptions);
//下面將這個PPP服務添加到當前網絡位置(location)(在網絡預置裏可能顯示爲automatic(自動), 如果你沒有做過任何改動設定的話)
if (SCNetworkServiceEstablishDefaultConfiguration(service)) {
set = SCNetworkSetCopyCurrent(prefs);
if (set && SCNetworkSetAddService(set, service)) {
//使所做的修改生效, 網絡預置裏將出現一個新的PPP服務
SCPreferencesCommitChanges(prefs);
SCPreferencesApplyChanges(prefs);
//得到服務ID, 並利用它創建一個撥號接口
serviceToDial = SCNetworkServiceGetServiceID(service);
connection = SCNetworkConnectionCreateWithServiceID(
NULL,
serviceToDial, 
MyNetworkConnectionCallBack,
NULL
);
}
}
SCPreferencesUnlock(prefs);
}
}


3. 使用cocoa的NSOperation實現界面,撥號分離
window版閃訊具有取消撥號的功能, 用單線程實現比較困難, 因爲撥號時, 界面將失去反應。程序中使用cocoa在10.5之後具有的NSOperation來進行雙線程處理。
用多線程要解決數據傳遞的問題,這裏借鑑了這個網址的NSOperation示例:
http://www.cimgf.com/2008/02/16/ ... d-nsoperationqueue/
這是一個叫做 cocoa is my girlfriend 的網站, 有很多cocoa示例,值得一看。
程序中創建了兩個物件:shanXunGUI, shanXunOperation. 前者繼承NSObject,用來控制界面,以及調用後者。後者繼承NSOperation,進行真正的撥號功能。
先看shanXunGUI的部分代碼:

@implementation shanXunGUI
static shanXunGUI* shared;
//這個變量供其它線程通過shanXunGUI的類方法+ (id)shared訪問,從而達到從其他進程調用本物件方法的作用。

- (id)init {
if (shared) {
[self autorelease];
return shared;
}
if (![super init]) return nil;
//生成一個queue,用來啓動NSOperation
queue = [[NSOperationQueue alloc] init];
tStatus = kConnectTitle; //連接按鈕顯示連接
pppStatus = kPPPDisconnect; //PPP撥號初始化爲斷開狀態
theTimer = nil;
shared = self;
return self;
}

+ (id)shared { //當從其他進程訪問時調用本方法即可
if (!shared) {
[[shanXunGUI alloc] init];
}
return shared;
}

- (void)addOperation:(PPPCMD)cmd {
DialParas data; //自定義的一個structure, 用來傳遞數據到撥號進程
char* uName = (char*) [[uNameTF stringValue] UTF8String];
char* pinName = calloc(10 + strlen(uName) + 1, sizeof(char));
if (cmd == kPPPConnect) {
  //得到真實用戶名
getPIN((unsigned char*) uName, (unsigned char*) pinName);
data.uName = pinName;
[rNameTF setStringValue:[NSString stringWithUTF8String:(pinName + 2)]];
} else data.uName = uName;
data.pwd = (char*) [[pwdTF stringValue] UTF8String];
data.sName = (char*) [[sNameTF stringValue] UTF8String];
data.cmd = cmd;
  //創建撥號物件
shanXunOperation* dialOp = [[shanXunOperation alloc] initWithData:&data];
  //添加到隊列中
if (queue&&dialOp) [queue addOperation:dialOp];
free(pinName);
}

//提供一個方法供撥號進程得到shared之後調用,以便控制界面進程的一個變量pppStatus
- (void)setPPPStatus:(NSNumber*)num {
pppStatus = (PPPStatus) [num intValue];
}


再看shanXunOperation對應的代碼:

@implementation shanXunOperation
- (id)initWithData:(DialParas*)data { //用主進程傳遞過來的數據進行初始化
    if (![super init]) return nil;

if (!data->uName) return nil;
if (!data->pwd) return nil;
if (!data->sName) return nil;
dialData.uName = xstrdup(data->uName);
dialData.pwd = xstrdup(data->pwd);
dialData.sName = xstrdup(data->sName);
dialData.cmd = data->cmd;

return self;
}

- (void)setPPPStatus:(PPPStatus)status { //調用shanXunGUI的方法設置他的一個變量
NSNumber* statusNum = [NSNumber numberWithInt:status];
[[shanXunGUI shared] performSelectorOnMainThread:@selector(setPPPStatus:)
withObject:statusNum
waitUntilDone:YES];
}

- (void) main {
//這裏是1,2中的代碼獲得一個撥號接口connection
SCNetworkConnectionStatus status = SCNetworkConnectionGetStatus(connection);
//根據界面物件的指令作相應的操作
if (dialData.cmd == kPPPDisconnect) {
  if (status != kSCNetworkConnectionDisconnected) pppDisconnect(connection);
    [self setPPPStatus:kPPPDisconnected];
} else if (dialData.cmd == kPPPConnect) {
  if (![self isCancelled] && (status != kSCNetworkConnectionConnected) && (status != kSCNetworkConnectionConnecting)) {
    pppConnect(connection, &dialData); //這個函數使用1中的代碼進行撥號
    status = SCNetworkConnectionGetStatus(connection);
  //下面根據撥號結果設置主進程的pppStatus變量
    if (status == kSCNetworkConnectionConnected) [self setPPPStatus:kPPPConnected];
    if (status == kSCNetworkConnectionDisconnected) [self setPPPStatus:kPPPDisconnected];
  }
}
//這裏是清理工作
}


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章