月度存档: 九月 2011

iOS App 自定义 URL Scheme 设计

在 iOS 里,程序之间都是相互隔离,目前并没有一个有效的方式来做程序间通信,幸好 iOS 程序可以很方便的注册自己的 URL Scheme,这样就可以通过打开特定 URL 的方式来传递参数给另外一个程序。

例如在 iPad 上浏览网页,并且 iPad 已经安装了 淘宝 HD,那么就打开下面这个链接就会在淘宝 HD 中查看这个商品的详细信息,也可以方便的使用淘宝 HD 进行购买、收藏等操作。

在淘宝 HD 中查看商品“2012西藏卓明谷方舟登舰卡(船票)”

当然,如果你在 Mac OS X 中打开这个链接,或者在没有安装 淘宝 HD 的 iPad 中打开这个链接,会提示没有程序来打开这个链接。

配置

要为 iOS 程序添加自定义协议的支持是一件很方便的事,只需要在程序的 Info.plist 添加一个 URL types 节点就可以了。在这个节点里,可以设置这个程序所支持的自定义协议名称,像 http、ftp 这种,一般我们可以设置为程序英文名称,像淘宝客户端中就设置了 taobao,这样 taobao:// 这个形式的 URL 就会关联到淘宝客户端的 App。

Info.plist config

实现

在 Info.plist 里面设置完 URL types 之后,就可以在程序中处理这类 URL 的打开请求了。

在外部程序中,如果打开了指定自定义协议的 URL,程序中 application delegate 的 application:handleOpenURL: 方法就会被调用,在这个方法里,可以获取到触发这个方法的 URL,可以通过对这个 URL 进行判断,例如根据不同的 Host,不同的 Query String 来执行不同的动作。

- (void)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    NSLog(@"%@", [url absoluteString]);

    // 在 host 等于 item.taobao.com 时,说明一个宝贝详情的 url,
    // 那么就使用本地的 TBItemDetailViewController 来显示
    if ([[url host] isEqualToString:@"item.taobao.com"]) {

        // 这里只是简单地假设 url 形式为 taobao://item.taobao.com/item.htm?id=12345678
        // 先获取要查看的宝贝详情的 itemId
        NSString *itemId = [[url query] substringFromIndex:[[url query] rangeOfString:@"id="].location+3];

        // 使用本地 ViewController 来显示淘宝商品详情
        TBItemDetailViewController *controller = [[TBItemDetailViewController alloc] initWithItemId:itemId];
        [self.navigationController pushViewController:controller animated:YES];
        [controller release];
    }
}

淘宝 for iOS

现在,淘宝 和 淘宝 HD 两个客户端都支持 taobao:// 协议,来打开特定的链接。目前已经支持的有:

例如,想要在自己的程序中,使用淘宝客户端来显示一个淘宝商品的详情,以支持用户可以直接在 iPhone 上购买,收藏等,就可以使用下面的代码:

- (void)showItemInTaobao4iOS:(NSString *)itemId {
    // 构建淘宝客户端协议的 URL
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"taobao://item.taobao.com/item.htm?id=%@", itemId]];

    // 判断当前系统是否有安装淘宝客户端
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        // 如果已经安装淘宝客户端,就使用客户端打开链接
        [[UIApplication sharedApplication] openURL:url];
    } else {
        // 否则使用 Mobile Safari 或者内嵌 WebView 来显示
        url = [NSURL URLWithString:[NSString stringWithFormat:@"http://item.taobao.com/item.htm?id=%@", itemId]];
        [[UIApplication sharedApplication] openURL:url];
    }
}

使用淘宝客户端来打开淘宝链接的好处就是可以让用户更加方便的去购买商品,而不需要再重新登录,或者把用户名密码给了第三方的网站而导致安全隐患。

扩展性

在淘宝客户端中,支持的 URL 往往是淘宝网站已经有的链接,这些链接的 QueryString 中所带的参数往往已经满足了使用本地代码显示内容的需要,但是为了扩展性考虑,就需要添加一些额外的参数,并且与原有 QueryString 中不冲突的参数名称。通过这些额外的参数,再实现客户端打开链接时更多的自定义行为。

例如,如果在打开特定 URL,进行一些操作后需要再返回原来的程序,就会需要在 URL 中添加类似于 callback 这样的参数,这样在客户端处理完用户的操作后,可以将用户操作的结果返回给原来的程序,从而实现程序间的通信。

示例:

- (void)buyItemInTaobao4iOS:(NSString *)itemId {
    // 构建淘宝客户端协议的 URL
    NSString *format = @"taobao://item.taobao.com/item.htm?id=%@&_action=buy&_callback=myapp://taobaobuysuccess";
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:format, itemId]];

    // 使用淘宝客户端打开链接
    [[UIApplication sharedApplication] openURL:url];
}

注意:当前淘宝客户端并不支持这样的调用方式,这里仅是一个示例。

当然,在使用这种方式实现程序间通信的时候,需要考虑检查一下来源 URL 的合法性,防止一些非法的调用造成用户的损失。

结语

通过自定义协议地支持,可以将 iOS 程序的一些功能和服务提供给外部程序,也可以实现 Web 和本地应用之间的互相调用。

如果你的 iOS 程序有这些需求的话,那么就可以考虑在 iOS 程序中添加自定义协议的支持了。

参考资料

  1. iOS Application Programming Guide: Implementing Custom URL Schemes

— EOF —

一个路由器,两种网络,VPN和下载两不误

最近在用 Titanium 写一个 Twitter 客户端,因为要用到 OAuth 认证,所以就在手机连接 VPN,但是速度比较慢。刚好想在家里的路由器上加上 OpenVPN,昨天就折腾了一下。

在家里路由器上加上 OpenVPN 的一个主要问题是,家里的网络中还有下载机,而下载的流量是不想通过 VPN 去传输的。虽然 chnroutes 项目的路由表可以让国内的 IP 走直连,国外的 IP 走 VPN,但是 eMule 或者 BT 下载时,难免会连接到国外的用户或者服务器,这个时候也不想去浪费 VPN 的流量。

因为这些,我的想法是在路由器上做判断,如果是从下载机过来流量,就通过直接连接,如果其他机器,例如笔记本,就根据目标 IP 来判断是通过直接连接还是 VPN 来传输。

下载机是通过 LAN 连接到路由器的,本来想按进入流量的设备来判断是否为下载机,后来发现实现比较麻烦,就决定按流量来源 IP 来判断是否为下载机的数据。

准备工作

  • 运行 dd-wrt 的路由器一个,要带有 OpenVPN
  • OpenVPN 服务器一个,认证方式选择证书认证
  • Linux 知识若干

当然 dd-wrt 并不是必须的,也可以是 openwrt 或者 tomato 之类,只要带有 OpenVPN 就行,如果不带 OpenVPN,就需要在启动过程中去外部下载相关的软件,那就是另外的内容了,暂且不提。

网络分段

因为要按 IP 来区分流量是否要走 VPN,因此要先划分一下局域网里要用到的 IP 段。

路由器的 IP 是 192.168.2.1,DHCP 分配范围为 192.168.2.100~149,按照需求将一个 /24 的网段分为三个部分:

  • 192.168.2.16~31,此 IP 段的设备流量均走直连
  • 192.168.2.32~63,此 IP 段的设备流量根据目标 IP 判断走直连还是走 VPN
  • 192.168.2.100~149,此 IP 段为 DHCP 分配的 IP 段,流量也根据目标 IP 来判断是否走 VPN

因为 DHCP 分配的 IP 并不可控,所以将流量走直接的设备,例如下载机,通过静态 IP 的方式,直接分配一个在 192.168.2.16~32 中固定 IP,可以保证不会连接到有 VPN 的网络。

路由策略表

先创建一个用来直连的路由策略表,用来将所有指定 IP 段的流量走直连。

# 添加一个路由策略表,此表针对 192.168.2.16/28 IP 段有效
ip rule show | grep "lookup 10" || ip rule add from 192.168.2.16/28 ta 10

# 设置策略表的默认路由
WAN_IP=`ifconfig ppp0 | grep "inet addr" | cut -d ":" -f 2 | cut -d " " -f1`
ip route replace 192.168.2.0/24 dev br0 proto kernel scope link src 192.168.2.1 ta 10
ip route replace 127.0.0.0/8 dev lo  scope link
ip route replace 169.254.0.0/16 dev br0  proto kernel  scope link  src 169.254.255.1
ip route replace default via $WAN_IP dev ppp0 ta 10

将以上代码保存在 dd-wrt 的 Filewall Script 中,这样在每次 WAN IP 改变的时候,都可以更新这个路由策略表了。

OpenVPN 配置

OpenVPN 按默认配置即可,需要注意的是,路由器上的 tun mtu、tun mtu extra 以及 mssfix 需要与服务器一致,或者服务器与路由器上的配置一致。

因为要用到 chnroutes,但是 dd-wrt 中的 OpenVPN 配置并不支持自定义配置,没办法添加 route 选项,因此要把这些选项放到 OpenVPN 服务端的配置文件中,使用 push 指令在连接时推送到客户端来。

例如:

push "route 1.0.0.0 255.255.0.0 net_gateway 5"

另外一有点需要注意,如果在 dd-wrt 中同时启用了 OpenVPN Daemon,建议将 OpenVPN Daemon 的启动方式设置为“System Startup”而不是“WAN Up”,在我的路由器上,使用“WAN Up”时 OpenVPN Daemon 和 OpenVPN Client 会冲突,导致 OpenVPN Client 启动失败。

解决 max-routes

OpenVPN 客户端,默认最多只能添加 100 条路由记录,但是 chnroutes 正常生成的路由表,可能会在 1000 条以上,因此 100 条是远远不够的。

这个可以通过 max-routes 配置项来解决,本来打算这个配置同样从服务端推送过来,但是 OpenVPN 现在并不支持 push “max-routes 1500” 这样的指令。

在 dd-wrt 的 OpenVPN 配置中,也没有相应的选项,为了解决这个问题,只能采取一个比较取巧的办法来解决。dd-wrt 中的 OpenVPN 配置都是存在 nvram 中的,在 dd-wrt 启动后,会自动从 nvram 中取 OpenVPN 的相关配置,组合成一个 openvpn.conf,而这个配置除了可以在 dd-wrt 的 Web 界面中修改,还可以直接 SSH 到 dd-wrt 上,直接使用 nvram 命令修改。

在这里要 hack 的配置是 mssfix,当然其他的属性也可以,选择 mssfix 是因这个属性比较简单,改起来方便。

我这里设置了 mssfix 为 1400,另外服务器要推送的路由表为 1300 条左右,直接将 max-routes 设置为 1500,在路由器上运行下面这个指令:

nvram set openvpncl_mssfix="1400
max-routes"

需要注意的是,一定要分两行来输入,否则生成的 openvpn.conf 中,mssfix 1400 和 max-routes 1500 会在同一行而导致配置失效。

这样在生成的 OpenVPN 配置文件中,就有了 max-routes 选项,服务端也可以正常推送路由表了。

不过这样也有一点坏处,那就是如果再修改了 OpenVPN 配置并保存,会把 mssfix 中的那个回车给去掉,再次导致连接失败。不过 OpenVPN 一旦配置完成,也不会经常改动,倒也不是很大的问题。

配置 DNSMasq

一般手机上连接上 WiFi 的时候,设置 DNS 等内容会比较麻烦,而如果不设置 DNS,会导致在手机上解析域名时,使用了国内的 DNS 服务器,而这也会导致一些问题,可以按照 autoddvpn 中的说明,将 DNS 设置为 Google Public DNS 和 OpenDNS:

8.8.8.8
8.8.4.4
208.67.222.222

打完收工

配置完成之后,就可以方便的分配家庭局域网里设备的流量走向了,想要设备的流量走直连,只要分配到 192.168.2.16~31 这个 IP 段就可以了,至于其他的设备,可以使用静态 IP,也可以直接使用 DHCP 分配。

嗯,这样再在真机上调试 Twitter 客户端之类的程序就方便了。

PS. 非常感谢 @tjmao 在折腾过程中帮助。

参考资料

  1. autoddvpn: DNSMasq
  2. OpenVPN 2.1 Manual
  3. linux 高级路由及流量控制总结
  4. ip(8)
  5. route(8)
  6. ddwrt: Hardware

— EOF —