文章來源: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];
}
}
//這裏是清理工作
}