文章来源: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];
}
}
//这里是清理工作
}