本例需求 : iOS通过NetworkExtension实现本地连接并成功拦截IP数据包pakcet.


建议:建议阅读本文前先仔细阅读并理解下App extension原理,有助于在项目中解决很多问题。App extension总结


注意:由于简大叔对XXX关键字过敏,而本文只是苹果App extension中一个重要内容,他们自然无法理解,所以本文均用XXX代替VPN,建议可以去本人个人博客或者掘金阅读

注意:运行并实现本Demo需要实现Personal-XXX功能是苹果开发者账号才有权限开启,所以如果没有开发者账号次Demo无法运行

本Demo中已经测试可以通过,只需要下载Demo并按照Demo中所说提供两个合法的bundle id即可运行。


GitHub地址(附代码) : XDXVPNExtensionDemo

简书地址 : XDXVPNExtensionDemo

博客地址 : XDXVPNExtensionDemo

掘金地址 : XDXVPNExtensionDemo


一. App extension 定义及工作原理

1. App extension, 更多请了解App extension总结

  • 定义:App Extension 可以让开发者们拓展自定义的功能和内容到应用程序之外,并在用户与其他应用程序或系统交互时提供给用户。

  • 苹果定义了很多种app extension, 每一种不同功能的app extension 称为extension point,其中我们要学习的xxx Extension就是一种extension point。

二. NetworkExtension extension 简介

1. 背景

我们先来简单了解一下国内墙的原理

network1

network2

2. 定义

苹果提供的NetworkExtension Extension可以帮助我们配置xxx通道,自定义和拓展核心网络功能。

苹果官方文档中定义如下

5-VPN Extension

3.使用框架

NetworkExtension :包含在iOS和macOS中用于自定义和拓展核心网络功能的API集合。

其中我们要讲的是NetworkExtension框架中的Personal xxx服务。

  • Personal XXX

    NEVPNManager API提供给我们去创建和管理个人XXX的配置。它通常用来向用户提供服务确保用户安全的上网,例如使用公共场所的wifi。

4. 核心API

  • NETunnelProviderManager 和 NEPacketTunnelProvider

    在 iOS 9 中,开发者可以用 NETunnelProvider 扩展核心网络层,从而实现非标准化的私密xxx技术。最重要的两个类是 NETunnelProviderManager 和 NEPacketTunnelProvider。

二. NetworkExtension extension 创建步骤

1. 新建Target

在已经创建好的项目中新建Target

  • Target : 指定了应用程序中构建product的设置信息和文件,即包含App extension point的代码集合。

Xcode会为每一种extension point提供一个模板,每种模板里会提供特定的源文件(源文件中会包含一些示例代码)和设置信息,build这个target将会生成一个指定的二进制文件被添加到app’s bundle中。

1-NewTarget

在弹出列表中选择Packet Tunnel Provider,注意在最新的Xcode中该模板已经被取消,需要我们手动下载,安装好后重启Xcode即可,这里提供下载链接链接:

https://pan.baidu.com/s/1V3xIljdRedmqGyp5QSwbTA
密码: ne8x

2-extensionList

选择好后我们可以看到我们项目中的变化如下:

4-target

我们的项目中增加了对Target的配置,左侧项目工程文件中增加了系统为我们自动生成的模板,注意如果在新建Target时选择的是Ojective-C语言模板中有一处错误信息,因为与我们要实现的并无关联,模板中代码我们均不使用,忽略即可。

2. 删除模板中多余代码

由于我们这里只演示简单的建立连接的过程,所以模板中生成的很多代码使用不到,我们只需要留下如下代码。

- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler
{}- (void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler
{// Add code here to start the process of stopping the tunnel[self.connection cancel];completionHandler();
}

在这里我们需要建立和断开连接的回调即可。此时Build通过。

3. 配置NetworkExtension所需信息

3-1. 配置签名信息,开启权限

  • Personal-XXX功能是苹果开发者账号才有权限开启,所以如果没有开发者账号此Demo无法运行。

  • Bundle Identifier不可随意写,需要按照规范com.xxx.xxx形式来书写,否则Build会出错

  • 因为xxx extension并不是一个独立的app,所以创建target时前缀必须为主app的前缀再.extensionName.

主app  Bundle Identifier : com.xdx.TVURouterDemo.XDXVPNExtensionDemoextension Bundle Identifier : com.xdx.TVURouterDemo.XDXVPNExtensionDemo.XDXVPNTunnelDemo

3-2 开启权限

  • 因为personal XXX权限较高,不能直接在Xcode中打开,所以我们需要先登录苹果开发者中心

8-selectAccount

9-selectProfile

  • 需要手动为主工程的target和xxx的target创建父子App ID。创建APP ID的方法网上较多,不再说明。

  • 在创建APP ID时打开Service 中的
    Network Extensions 和
    Personal XXX两个功能。对于已经创建好的APP ID需要在编辑中增加这两个Service。

注意:主target和extension target 对应的app id中都需要打开这两个权限。

  • 创建好APP ID后需要继续创建Profile文件,如果是已经存在的app id中增加了这两项服务后需要重新激活配置文件Profile。

3-3 在Xcode中配置xxx

  • 在两个Target中选择正确的Profile。(前提是在3-2中正确配置了APP ID并开启了对应两项服务)由于我们只是测试,所以这里只配置了开发证书.
  • 在Capabilities中开启Personal xxx 和 Network Extension(同时如图勾选前三项),然后你的两个Target项目文件中会新增两个.entitlements结尾的配置文件。

12-XcodeConfig

4. 实现步骤

4-1 导入必需框架

在项目中导入NetworkExtension.framework框架

13-exportNE

之后在主控制器导入#import <NetworkExtension/NetworkExtension.h>

4-1 初始化并配置NETunnelProviderManager对象

  • NETunnelProviderManager :配置并控制由Tunnel Provider app extension提供的XXX 连接。

可以理解为建立XXX连接前负责配置基本参数信息保存设置到系统(即一般XXX app中都会在第一次打开时授权并保存到系统的XXX设置中)

  • 包含Packet Tunnel Provider extension的containing app使用NETunnelProviderManager去创建和管理使用自定义协议配置XXX。

注意:配置好的NETunnelProviderManager对象仅在系统设置中起展示作用,或者说这里的设置并不真正生效,真正生效的设置在app extension target中,如果为了保持一致,如果仅仅测试,配置信息可以随便填写(但target bundle id 必须为项目target真实的ID),如果为了与XXX真正配置保持一致,可填写正确地址。


知识点,了解可跳过

知识点 1. 区别

NETunnelProviderManager类继承了 NEVPNManager大多数基本的功能,主要的区别如下:

  • protocolConfiguration 属性只有 NETunnelProviderProtocol类才能设置
  • connection这个只读属性只能通过 NETunnelProviderSession这个类设置。

每个NETunnelProviderManager实例对应一个XXX配置被存储在 Network Extension偏好设置中。可以创建多个NETunnelProviderManager实例来管理多个XXX配置的。

使用NETunnelProviderManager创建的XXX配置被归属为常规企业XXX配置(而不是由NEVPNManager创建的个人XXX)。一次只能在系统启用一个XXX配置。如果同时在系统中激活个人XXX和企业XXX,企业XXX优先使用。

知识点 2. 将网络数据路由到XXX两种方式

  • IP 地址

    这是默认的路由方式。IP通道通过Packet Tunnel Provider extension被标记在XXX通道完全被建立。

  • By source application (Per-App XXX)

    Per-App XXX 应用程序规则同时用于路由规则和XXX 需求规则。这与基于路由的IP地址形成鲜明的对比,当onDemandEnabled属性被设置成true并且app匹配he Per-App XXX规则试图通过网络进行通信,XXX将自动开始。


4-2 初始化NETunnelProviderManager对象

  • 设置NETunnelProviderManager所需的各个参数的值

    在本例中利用各个参数的值均用模型实现,在主控制器直接调用以下即可

注意

  • TunnelBundleId 需要传入XXX Extension Target 的BundleID
  • serverAddress:即在手机设置的XXX中显示的XXX地址
  • 其余参数根据真实情况填写,如果只是测试即可直接用以下参数(或设置其他参数也可)
  • 以下参数只是在系统设置中XXX设置里显示的参数,真正设置网络参数在XXX Target项目中重新设置。
    [model configureInfoWithTunnelBundleId:@"com.tvunetworks.TVURouterDemo.TVURouterVPNExtensionDemo"serverAddress:@"10.10.10.1"serverPort:@"54345"mtu:@"1400"ip:@"10.8.0.2"subnet:@"255.255.255.0"dns:@"8.8.8.8,8.4.4.4"];

而在封装管理NETunnelProviderManager对象真正起作用的是如下代码

- (void)applyVpnConfiguration {[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {if (managers.count > 0) {self.vpnManager = managers[0];log4cplus_error("XDXVPNManager", "The vpn already configured. We will use it.");return;}else {log4cplus_error("XDXVPNManager", "The vpn config is NULL, we will config it later.");}[self.vpnManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {if (error != 0) {const char *errorInfo = [NSString stringWithFormat:@"%@",error].UTF8String;log4cplus_error("XDXVPNManager", "applyVpnConfiguration loadFromPreferencesWithCompletionHandler Failed - %s !",errorInfo);return;}NETunnelProviderProtocol *protocol = [[NETunnelProviderProtocol alloc] init];protocol.providerBundleIdentifier  = self.vpnConfigurationModel.tunnelBundleId;NSMutableDictionary *configInfo = [NSMutableDictionary dictionary];[configInfo safeSetObject:self.vpnConfigurationModel.serverPort       forKey:@"port"];[configInfo safeSetObject:self.vpnConfigurationModel.serverAddress    forKey:@"server"];[configInfo safeSetObject:self.vpnConfigurationModel.ip               forKey:@"ip"];[configInfo safeSetObject:self.vpnConfigurationModel.subnet           forKey:@"subnet"];[configInfo safeSetObject:self.vpnConfigurationModel.mtu              forKey:@"mtu"];[configInfo safeSetObject:self.vpnConfigurationModel.dns              forKey:@"dns"];protocol.providerConfiguration        = configInfo;protocol.serverAddress                = self.vpnConfigurationModel.serverAddress;self.vpnManager.protocolConfiguration = protocol;self.vpnManager.localizedDescription  = @"NEPacketTunnelVPNDemoConfig";[self.vpnManager setEnabled:YES];[self.vpnManager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) {if (error != 0) {const char *errorInfo = [NSString stringWithFormat:@"%@",error].UTF8String;log4cplus_error("XDXVPNManager", "applyVpnConfiguration saveToPreferencesWithCompletionHandler Failed - %s !",errorInfo);}else {[self applyVpnConfiguration];log4cplus_info("XDXVPNManager", "Save vpn configuration successfully !");}}];}];}];
}

以上代码通过NETunnelProviderManager的类方法+ (void)loadAllFromPreferencesWithCompletionHandler:(void (^)(NSArray<NETunnelProviderManager *> * __nullable managers, NSError * __nullable error))completionHandler实现

  • 如果是第一次配置XXX,XXX的设置未保存在系统中,所以上述代码managers.count读取结果应该为0,需要我们做如上设置,第一次设置成功后,XXX的配置信息会保存在设置信息里,所以以后无需重复设置,只需要读取到就好。
  • 注意,第一次配置时需要手机授权,如果拒绝则无法继续进行。

此函数异步读取调用所有app创建且先前保存在本地的NETunnelProvider配置信息,并将它们在回调中以一个存放NETunnelProvider对象的数组的形式返回。+ (void)loadAllFromPreferencesWithCompletionHandler:(void (^)(NSArray<NETunnelProviderManager *> * __nullable managers, NSError * __nullable error))completionHandler该函数从调用者的VPN设置中加载当前VPN配置
- (void)loadFromPreferencesWithCompletionHandler:(void (^)(NSError * __nullable error))completionHandler;

然后将存入模型的所有参数放入一个字典,并存入NETunnelProviderProtocol对象的providerConfiguration中,该字典在tunnel建立成功后会传递给NETunnelProviders对象。最后将配置好的NETunnelProviderProtocol对象赋值给NETunnelProviderManager对象的protocolConfiguration即可。

调用此方法用来保存上述配置好的VPN到本机设备的VPN设置中。
- (void)saveToPreferencesWithCompletionHandler:(nullable void (^)(NSError * __nullable error))completionHandler;
调用此函数会使用NEVPNConnection对象当前VPN配置来建立VPN连接。返回YES表示成功。
[self.vpnManager.connection startVPNTunnelAndReturnError:&error];
- (BOOL)startVPNTunnelAndReturnError:(NSError **)error;调用此函数会关闭当前建立的VPN连接。
[self.vpnManager.connection stopVPNTunnel];
- (void)stopVPNTunnel;

4-3. 主控制器启动VPN

  • 在viewDidLoad中完成初始化操作

    • 初始化模型即根据实际情况对各个网络参数赋值(注意TunnelBundleId为本项目XXX Extension的BundleID)
    • 对XDXVPNManager进行初始化配置模型,设置代理。
    • 注册通知(NEVPNStatusDidChangeNotification),监听XXX状态变化
  • - (void)vpnDidChange:(NSNotification *)notification方法中根据XXX的状态动态改变按钮状态

  • 点击按钮实现开启/关闭XXX连接

4-4.在PacketTunnelProvider回调中完成剩余工作

  • 当我们在主控器调用开启XXX连接后,会进入XXX Target项目中PacketTunnelProvider文件的下述方法中
当一个新的通道被建立会会调用此函数。我们必须通过重写该类来完成建立连接。@param : options - 在主控制调用开启XXX连接时传入的字典,可空。
@param : completionHandler - 在该方法彻底完成时必须调用这个Block。如果不能建立连接要将错误信息传给该block,如果成功建立连接则只需将nil传给该Block.- (void)startTunnelWithOptions:(nullable NSDictionary<NSString *,NSObject *> *)options completionHandler:(void (^)(NSError * __nullable error))completionHandler

注意:因为此时是处于另一个Target中,在手机相当于另一条进程,因此我们无法直接在控制台看到任何我们打印的log信息,这里我使用log4cplus则可以在我们的终端中来截获debug信息。如果你的本机装有log4cplus直接使用下面的命令即可获取信息,若未装则可以在github里直接搜索log4cplus mac版直接运行,也可以在log4cplus mac版的控制台里过滤关键字得到debug信息。

$ deviceconsole |grep XDXVPNManager
#define XDX_NET_MTU                        1400
#define XDX_NET_REMOTEADDRESS              "192.168.3.14"
#define XDX_NET_SUBNETMASKS                "255.255.255.255"
#define XDX_NET_DNS                        "223.5.5.5"
#define XDX_LOCAL_ADDRESS                  "127.0.0.1"
#define TVU_NET_TUNNEL_IPADDRESS           "10.10.10.10"- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler
{log4cplus_info("XDXVPNManager", "XDXPacketTunnelManager - Start Tunel !");NEPacketTunnelNetworkSettings *tunnelNetworkSettings = [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:@XDX_NET_REMOTEADDRESS];tunnelNetworkSettings.MTU = [NSNumber numberWithInteger:XDX_NET_MTU];tunnelNetworkSettings.IPv4Settings = [[NEIPv4Settings alloc] initWithAddresses:[NSArray arrayWithObjects:@TVU_NET_TUNNEL_IPADDRESS, nil]  subnetMasks:[NSArray arrayWithObjects:@XDX_NET_SUBNETMASKS, nil]];tunnelNetworkSettings.IPv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]];NEIPv4Settings *excludeRoute = [[NEIPv4Settings alloc] initWithAddresses:[NSArray arrayWithObjects:@"10.10.10.11", nil] subnetMasks:[NSArray arrayWithObjects:@XDX_NET_SUBNETMASKS, nil]];tunnelNetworkSettings.IPv4Settings.excludedRoutes = @[excludeRoute];[self setTunnelNetworkSettings:tunnelNetworkSettings completionHandler:^(NSError * _Nullable error) {if (error == nil) {log4cplus_info("XDXVPNManager", "XDXPacketTunnelManager - Start Tunel Success !");completionHandler(nil);}else {log4cplus_error("XDXVPNManager", "XDXPacketTunnelManager - Start Tunel Failed - %s !",error.debugDescription.UTF8String);completionHandler(error);return;}}];[self.packetFlow readPacketsWithCompletionHandler:^(NSArray<NSData *> * _Nonnull packets, NSArray<NSNumber *> * _Nonnull protocols) {log4cplus_debug("XDXVPNManager", "XDXPacketTunnelManager - Read Packet !");[packets enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {NSString *packetStr = [NSString stringWithFormat:@"%@",obj];log4cplus_debug("XDXVPNManager", "XDXPacketTunnelManager - Read Packet - %s !",packetStr.UTF8String);}];}];
}

当你上述的每一步都正确配置,并在主控器中调用-startVPNTunnelAndReturnError方法时,紧接着会在vpn target中调用以上回调,我们需要在此回调中建立tunnel并配置我们需要的各种网络设置,最后在调用- (void)setTunnelNetworkSettings回调中调用completionHandler(nil);来建立vpn连接。

注意,在设置网络参数成功后必须显式调用completionHandler(nil)才能正常建立连接(官方API要求),如果设置网络参数出错则将错误信息传入completionHandler(error),否则无法正常建立连接。

这里是真正设置XXX tunnel 网络参数的地方,主控制器中设置的仅为系统XXX设置中的展示名称

  • 首先新建一个NEPacketTunnelNetworkSettings对象,此对象是用来设置并建立XXX tunnel初始化对象时提供的RemoteAddress即为我们要建立连接的远程服务器的地址,注意如果是域名需要手动解析为IP地址再传入。下面提供了方法可转换。
  • MTU:最大传输单元,即每个packet最大的容量
  • includedRoutes:即XXX tunnel需要拦截包的地址,如果全部拦截则设置[NEIPv4Route defaultRoute],也可以指定部分需要拦截的地址
  • excludeRoute : 设置不拦截哪些包的地址,默认不会拦截tunnel本身地址发出去的包。
  • 最后调用- (void)setTunnelNetworkSettings,在回调若成功直接调用completionHandler(nil);来建立XXX连接。
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>// 域名转换IP
- (NSString *)queryIpWithDomain:(NSString *)domain {struct hostent *hs;struct sockaddr_in server;if ((hs = gethostbyname([domain UTF8String])) != NULL) {server.sin_addr = *((struct in_addr*)hs->h_addr_list[0]);return [NSString stringWithUTF8String:inet_ntoa(server.sin_addr)];}return nil;
}

在调用完setTunnelNetworkSettings后如果回调返回error且我们按照上述所说完成completionHandler(nil);的配置后,XXX成功建立连接,此时我们的app状态栏里也会相应出现XXX的标识,如下图。

14-VPN

  • 附加步骤:在XXX Tunndel 中持续读取packet包
    因为我们已经成功建立了XXX连接,所以网络数据包我们可以在此回调中截取到

利用NEPacketTunnelProvider对象的packetFlow 完成下面的方法即可。

从流中读入可用的IP包
- (void)readPakcets {__weak PacketTunnelProvider *weakSelf = self;[self.packetFlow readPacketsWithCompletionHandler:^(NSArray<NSData *> * _Nonnull packets, NSArray<NSNumber *> * _Nonnull protocols) {for (NSData *packet in packets) {// log4cplus_debug("TVUVPNManager", "Read Packet - %s",[NSString stringWithFormat:@"%@",packet].UTF8String);__typeof__(self) strongSelf = weakSelf;// TODO ...NSLog(@"XDX : read packet - %@",packet);}[weakSelf readPakcets];}];
}

以下为我读到的包

read packet

另外如需Debug app extension target可以通过Xcode如下方法

Debug

Debug info

  • 停止方法较为简单,不再叙述,详细可在Demo中查阅

总结:此Demo为利用苹果官方提供的XXX Extension在一个项目中通过新建Target来完成一个简单建立XXX的过程。其中涉及到App extension的知识可在本文所附的另一篇文章中查阅,主要涉及App extension在项目中的配置,以及建立XXX连接时一些参数的设置及方法的调用时机,以及成功建立连接后可拦截手机中的网络Packet。

作者:__小___东邪___
链接:https://www.jianshu.com/p/d8fbb658a00f
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

App extension实战 - NetworkExtension 讲解连接并捕获packet相关推荐

  1. 精通移动App测试实战:技术、工具和案例

    本文是根据书籍<精通移动App测试实战:技术.工具和案例>进行学习记录,方便后期查阅,感谢书籍作者提供的学习机会. 目录 第1章 Android系统基础内容介绍 1.6创建模拟器 第2章J ...

  2. 《移动App测试实战》——2.2 App UI层面的自动化

    本节书摘来自华章出版社<移动App测试实战>一 书中的第2章,第2.2节,作者:邱鹏 陈吉 潘晓明,更多章节内容可以访问云栖社区"华章计算机"公众号查看. 2.2 Ap ...

  3. 低功耗蓝牙工具APP开发实战

    <低功耗蓝牙工具APP开发实战> 什么是 LightBLE? ​ 一个功能比较全面的蓝牙调试工具.支持所有使用蓝牙4.0低功耗的设备接入调试,提供蓝牙设备搜索.读取服务.浏览特征等操作. ...

  4. App项目实战之路(二):API篇

    原创文章,转载请注明:转载自Keegan小钢 并标明原文链接:http://keeganlee.me/post/practice/20160812 微信订阅号:keeganlee_me 写于2016- ...

  5. php app接口开发,「PHP开发APP接口实战005」基础示例接口的实现一

    前一章,我们对接口参数基本定义做了一个简要说明.里面提到了几个示例接口,接下来,我们就来讲解这个几点个示例接口的具体实现. 「PHP开发APP接口实战004」基础响应参数说明 前言 由于我们的接口返回 ...

  6. 《移动App测试实战》——1.2 测试用例设计和评审

    本节书摘来自华章出版社<移动App测试实战>一 书中的第1章,第1.2节,作者:邱鹏 陈吉 潘晓明,更多章节内容可以访问云栖社区"华章计算机"公众号查看. 1.2 测试 ...

  7. Vue.js 3.0快速入门(附电影购票APP开发实战源码)

    前言 文档笔记来源:kuangshenstudy,清华大学出版社,结合视频资源食用更佳,相关资源源码在文末,有需要自取. 一.概述 Vue是什么? Vue.js是基于JavaScript的一套MVVC ...

  8. 最新仿映客直播APP开发实战项目IOS开发实战8天(最全最新)

    最新仿映客直播APP开发实战项目IOS开发实战8天 第 1 章:直播准备 1: [录播] 课程大纲介绍 09:56 2: [录播] 了解直播技术和腾讯云直播 09:54 3: [录播] 基础封装 23 ...

  9. 《HTML5移动网站与App开发实战》简介

    #好书推荐##好书奇遇季#<HTML5移动网站与App开发实战>,京东当当天猫都有发售.定价79元,网店打折销售更便宜.本书内容非常系统全面,配套示例源码与PPT课件. 本书由浅入深出.全 ...

  10. Cordova+React+OnsenUI+Redux新闻App开发实战教程-姜博-专题视频课程

    Cordova+React+OnsenUI+Redux新闻App开发实战教程-779人已学习 课程介绍         Cordova+React+OnsenUI+Redux新闻App开发实战视频培训 ...

最新文章

  1. 美团二面:Redis与MySQL双写一致性如何保证?
  2. Visual Studio Code的用户设置相关
  3. linux6.5下配置nfs,CentOS 6.5 NFS配置详细教程
  4. PWN-PRACTICE-BUUCTF-6
  5. tensorflow 学习资料汇总
  6. leetcode —— 783. 二叉搜索树结点最小距离
  7. 电脑软件上的按钮原来是这样来的:按钮组件
  8. MongoDB更新文档(非常详细,不要错过~)
  9. 二维点云数据椭圆拟合算法及C++实现
  10. Redis通信协议和集群通信算法
  11. 企业如何做好网址安全,防止入侵。
  12. 【制作fnt格式字体】 BMFont中文字体图集制作的方法~
  13. Nginx是什么?怎么用?(新手入门版)教程
  14. Android中把图片、视频保存到相册中
  15. 农场世界农场游戏开发
  16. 李航《统计学习方法》学习日记【1】
  17. 数学实验——函数绘图实验
  18. 使用cephadm部署单节点ceph集群,后期可扩容(基于官方文档,靠谱,读起来舒服)
  19. unknown target connected的解决方法
  20. 地理信息系统软件工程技术

热门文章

  1. 双网卡双线路DNS解析分析
  2. 计算机音乐名侦探柯南简谱,《名侦探柯南》主题旋律|卡林巴琴简谱专用谱...
  3. 手机NFC天线的集总参数设计
  4. 人物画像及“七步人物角色法”
  5. 华为云计算HCIE学习总结-云计算主流技术
  6. 简单说一下寄存器寻址
  7. 怎么调大计算机浏览器内字体,电脑网页字体大小怎么调整(电脑里点击哪个是变换字体的)...
  8. linux 删除开机密码,6种清除开机密码方法,总有一个适合你
  9. win8 不显示计算机,Win8电脑插上U盘不显示盘符怎么办?
  10. 密歇根州立大学教授刘小明讲解:人脸识别的新技术 | 大牛讲堂