前言

最近有一个跟HTTPS相关的问题需要解决,因此花时间学习了一下Android平台HTTPS的使用,同时也看了一些HTTPS的原理,这里分享一下学习心得。
 

HTTPS原理

HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTP数据都是在SSL/TLS协议封装之上进行传输的。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也属于应用层协议。所以,研究HTTPS协议原理,最终就是研究SSL/TLS协议。

SSL/TLS协议作用

不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:
1. 窃听风险:第三方可以获知通信内容。
2. 篡改风险:第三方可以修改通知内容。
3. 冒充风险:第三方可以冒充他人身份参与通信。
SSL/TLS协议是为了解决这三大风险而设计的,希望达到:
1. 所有信息都是加密传输,第三方无法窃听。
2. 具有校验机制,一旦被篡改,通信双方都会立刻发现。
3. 配备身份证书,防止身份被冒充。

基本的运行过程

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。但是这里需要了解两个问题的解决方案。
1. 如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
2. 公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。
因此,SSL/TLS协议的基本过程是这样的:
1. 客户端向服务器端索要并验证公钥。
2. 双方协商生成“对话密钥”。
3. 双方采用“对话密钥”进行加密通信。
上面过程的前两布,又称为“握手阶段”。

握手阶段的详细过程

“握手阶段”涉及四次通信,需要注意的是,“握手阶段”的所有通信都是明文的。

客户端发出请求(ClientHello)

首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步中,客户端主要向服务器提供以下信息:
1. 支持的协议版本,比如TLS 1.0版
2. 一个客户端生成的随机数,稍后用于生成“对话密钥”。
3. 支持的加密方法,比如RSA公钥加密。
4. 支持的压缩方法。
这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应用向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。

服务器回应(ServerHello)

服务器收到客户端请求后,向客户端发出回应,这叫做ServerHello。服务器的回应包含以下内容:
1. 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
2. 一个服务器生成的随机数,稍后用于生成“对话密钥”。
3. 确认使用的加密方法,比如RSA公钥加密。
4. 服务器证书。
除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供“客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁发,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项消息。
1. 一个随机数。该随机数用服务器公钥加密,防止被窃听。
2. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
3. 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项通常也是前面发送的所有内容的hash值,用来供服务器校验。
上面第一项随机数,是整个握手阶段出现的第三个随机数,又称“pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把“会话密钥”。

服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的“会话密钥”。然后,向客户端最后发送下面信息。
1. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
2. 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发生的所有内容的hash值,用来供客户端校验。

握手结束

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用“会话密钥”加密内容。
 

服务器基于Nginx搭建HTTPS虚拟站点

之前一篇文章详细介绍了在服务器端如何生成SSL证书,并基于Nginx搭建HTTPS服务器,链接:Nginx搭建HTTPS服务器
 

Android实现HTTPS通信

由于各种原因吧,这里使用HttpClicent类讲解一下Android如何建立HTTPS连接。代码demo如下。
MainActivity.java
  1. package com.example.photocrop;
  2. import java.io.BufferedReader;
  3. import java.io.InputStreamReader;
  4. import org.apache.http.HttpResponse;
  5. import org.apache.http.HttpStatus;
  6. import org.apache.http.StatusLine;
  7. import org.apache.http.client.HttpClient;
  8. import org.apache.http.client.methods.HttpPost;
  9. import org.apache.http.client.methods.HttpUriRequest;
  10. import android.app.Activity;
  11. import android.os.AsyncTask;
  12. import android.os.Bundle;
  13. import android.os.AsyncTask.Status;
  14. import android.text.TextUtils;
  15. import android.util.Log;
  16. import android.view.View;
  17. import android.widget.Button;
  18. import android.widget.TextView;
  19. public class MainActivity extends Activity {
  20. private Button httpsButton;
  21. private TextView conTextView;
  22. private CreateHttpsConnTask httpsTask;
  23. @Override
  24. protected void onCreate(Bundle savedInstanceState) {
  25. super.onCreate(savedInstanceState);
  26. setContentView(R.layout.activity_main);
  27. httpsButton = (Button) findViewById(R.id.create_https_button);
  28. httpsButton.setOnClickListener(new View.OnClickListener() {
  29. @Override
  30. public void onClick(View v) {
  31. runHttpsConnection();
  32. }
  33. });
  34. conTextView = (TextView) findViewById(R.id.content_textview);
  35. conTextView.setText("初始为空");
  36. }
  37. private void runHttpsConnection() {
  38. if (httpsTask == null || httpsTask.getStatus() == Status.FINISHED) {
  39. httpsTask = new CreateHttpsConnTask();
  40. httpsTask.execute();
  41. }
  42. }
  43. private class CreateHttpsConnTask extends AsyncTask<Void, Void, Void> {
  44. private static final String HTTPS_EXAMPLE_URL = "自定义";
  45. private StringBuffer sBuffer = new StringBuffer();
  46. @Override
  47. protected Void doInBackground(Void... params) {
  48. HttpUriRequest request = new HttpPost(HTTPS_EXAMPLE_URL);
  49. HttpClient httpClient = HttpUtils.getHttpsClient();
  50. try {
  51. HttpResponse httpResponse = httpClient.execute(request);
  52. if (httpResponse != null) {
  53. StatusLine statusLine = httpResponse.getStatusLine();
  54. if (statusLine != null
  55. && statusLine.getStatusCode() == HttpStatus.SC_OK) {
  56. BufferedReader reader = null;
  57. try {
  58. reader = new BufferedReader(new InputStreamReader(
  59. httpResponse.getEntity().getContent(),
  60. "UTF-8"));
  61. String line = null;
  62. while ((line = reader.readLine()) != null) {
  63. sBuffer.append(line);
  64. }
  65. } catch (Exception e) {
  66. Log.e("https", e.getMessage());
  67. } finally {
  68. if (reader != null) {
  69. reader.close();
  70. reader = null;
  71. }
  72. }
  73. }
  74. }
  75. } catch (Exception e) {
  76. Log.e("https", e.getMessage());
  77. } finally {
  78. }
  79. return null;
  80. }
  81. @Override
  82. protected void onPostExecute(Void result) {
  83. if (!TextUtils.isEmpty(sBuffer.toString())) {
  84. conTextView.setText(sBuffer.toString());
  85. }
  86. }
  87. }
  88. }

HttpUtils.java

  1. package com.example.photocrop;
  2. import org.apache.http.HttpVersion;
  3. import org.apache.http.client.HttpClient;
  4. import org.apache.http.conn.ClientConnectionManager;
  5. import org.apache.http.conn.scheme.PlainSocketFactory;
  6. import org.apache.http.conn.scheme.Scheme;
  7. import org.apache.http.conn.scheme.SchemeRegistry;
  8. import org.apache.http.conn.ssl.SSLSocketFactory;
  9. import org.apache.http.impl.client.DefaultHttpClient;
  10. import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
  11. import org.apache.http.params.BasicHttpParams;
  12. import org.apache.http.params.HttpProtocolParams;
  13. import org.apache.http.protocol.HTTP;
  14. import android.content.Context;
  15. public class HttpUtils {
  16. public static HttpClient getHttpsClient() {
  17. BasicHttpParams params = new BasicHttpParams();
  18. HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
  19. HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
  20. HttpProtocolParams.setUseExpectContinue(params, true);
  21. SchemeRegistry schReg = new SchemeRegistry();
  22. schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
  23. schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
  24. ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
  25. return new DefaultHttpClient(connMgr, params);
  26. }
  27. public static HttpClient getCustomClient() {
  28. BasicHttpParams params = new BasicHttpParams();
  29. HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
  30. HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
  31. HttpProtocolParams.setUseExpectContinue(params, true);
  32. SchemeRegistry schReg = new SchemeRegistry();
  33. schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
  34. schReg.register(new Scheme("https", MySSLSocketFactory.getSocketFactory(), 443));
  35. ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
  36. return new DefaultHttpClient(connMgr, params);
  37. }
  38. public static HttpClient getSpecialKeyStoreClient(Context context) {
  39. BasicHttpParams params = new BasicHttpParams();
  40. HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
  41. HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
  42. HttpProtocolParams.setUseExpectContinue(params, true);
  43. SchemeRegistry schReg = new SchemeRegistry();
  44. schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
  45. schReg.register(new Scheme("https", CustomerSocketFactory.getSocketFactory(context), 443));
  46. ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
  47. return new DefaultHttpClient(connMgr, params);
  48. }
  49. }
activity_main.xml
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical">
  6. <Button
  7. android:id="@+id/create_https_button"
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:text="@string/hello_world"
  11. android:textSize="16sp" />
  12. <TextView
  13. android:id="@+id/content_textview"
  14. android:layout_width="match_parent"
  15. android:layout_height="wrap_content"
  16. android:gravity="center"
  17. android:textSize="16sp" />
  18. </LinearLayout>
Android使用DefaultHttpClient建立HTTPS连接,关键需要加入对HTTPS的支持:
  1. schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

加入对HTTPS的支持,就可以有效的建立HTTPS连接了,例如“https://www.google.com.hk”了,但是访问自己基于Nginx搭建的HTTPS服务器却不行,因为它使用了不被系统承认的自定义证书,会报出如下问题:No peer certificate。

使用自定义证书并忽略验证的HTTPS连接方式

解决证书不被系统承认的方法,就是跳过系统校验。要跳过系统校验,就不能再使用系统标准的SSL SocketFactory了,需要自定义一个。然后为了在这个自定义SSL SocketFactory里跳过校验,还需要自定义一个TrustManager,在其中忽略所有校验,即TrustAll。
MySSLSocketFactory.java实现代码如下:
  1. package com.example.photocrop;
  2. import java.io.IOException;
  3. import java.net.Socket;
  4. import java.net.UnknownHostException;
  5. import java.security.KeyManagementException;
  6. import java.security.KeyStore;
  7. import java.security.KeyStoreException;
  8. import java.security.NoSuchAlgorithmException;
  9. import java.security.UnrecoverableKeyException;
  10. import java.security.cert.CertificateException;
  11. import java.security.cert.X509Certificate;
  12. import javax.net.ssl.SSLContext;
  13. import javax.net.ssl.TrustManager;
  14. import javax.net.ssl.X509TrustManager;
  15. import org.apache.http.conn.ssl.SSLSocketFactory;
  16. public class MySSLSocketFactory extends SSLSocketFactory {
  17. SSLContext sslContext = SSLContext.getInstance("TLS");
  18. public MySSLSocketFactory(KeyStore truststore)
  19. throws NoSuchAlgorithmException, KeyManagementException,
  20. KeyStoreException, UnrecoverableKeyException {
  21. super(truststore);
  22. TrustManager tm = new X509TrustManager() {
  23. @Override
  24. public X509Certificate[] getAcceptedIssuers() {
  25. return null;
  26. }
  27. @Override
  28. public void checkServerTrusted(X509Certificate[] chain,
  29. String authType) throws CertificateException {
  30. }
  31. @Override
  32. public void checkClientTrusted(X509Certificate[] chain,
  33. String authType) throws CertificateException {
  34. }
  35. };
  36. sslContext.init(null, new TrustManager[] { tm }, null);
  37. }
  38. @Override
  39. public Socket createSocket() throws IOException {
  40. return sslContext.getSocketFactory().createSocket();
  41. }
  42. @Override
  43. public Socket createSocket(Socket socket, String host, int port,
  44. boolean autoClose) throws IOException, UnknownHostException {
  45. return sslContext.getSocketFactory().createSocket(socket, host, port,
  46. autoClose);
  47. }
  48. public static SSLSocketFactory getSocketFactory() {
  49. try {
  50. KeyStore trustStore = KeyStore.getInstance(KeyStore
  51. .getDefaultType());
  52. trustStore.load(null, null);
  53. SSLSocketFactory factory = new MySSLSocketFactory(trustStore);
  54. return factory;
  55. } catch (Exception e) {
  56. e.getMessage();
  57. return null;
  58. }
  59. }
  60. }

同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:

  1. public static HttpClient getCustomClient() {
  2. BasicHttpParams params = new BasicHttpParams();
  3. HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
  4. HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
  5. HttpProtocolParams.setUseExpectContinue(params, true);
  6. SchemeRegistry schReg = new SchemeRegistry();
  7. schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
  8. schReg.register(new Scheme("https", MySSLSocketFactory.getSocketFactory(), 443));
  9. ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
  10. return new DefaultHttpClient(connMgr, params);
  11. }

这样就可以成功的访问自己构建的基于Nginx的HTTPS虚拟站点了。

缺陷:

不过,虽然这个方案使用了HTTPS,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。

使用自定义证书建立HTTPS连接

为了防止上面方案可能导致的“中间人攻击”,我们可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中,由应用自己来验证证书。

生成KeyStore

要验证自定义证书,首先要把证书编译到应用中,这需要使用keytool工具生产KeyStore文件。这里的证书就是指目标服务器的公钥,可以从web服务器配置的.crt文件或.pem文件获得。同时,你需要配置bouncycastle,我下载的是bcprov-jdk16-145.jar,至于配置大家自行google就好了。

  1. keytool -importcert -v -trustcacerts -alias example -file www.example.com.crt -keystore example.bks -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath /home/wzy/Downloads/java/jdk1.7.0_60/jre/lib/ext/bcprov-jdk16-145.jar -storepass pw123456

运行后将显示证书内容并提示你是否确认,输入Y回车即可。

生产KeyStore文件成功后,将其放在app应用的res/raw目录下即可。

使用自定义KeyStore实现连接

思路和TrushAll差不多,也是需要一个自定义的SSLSokcetFactory,不过因为还需要验证证书,因此不需要自定义TrustManager了。
  1. package com.example.photocrop;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.security.KeyManagementException;
  5. import java.security.KeyStore;
  6. import java.security.KeyStoreException;
  7. import java.security.NoSuchAlgorithmException;
  8. import java.security.UnrecoverableKeyException;
  9. import org.apache.http.conn.ssl.SSLSocketFactory;
  10. import android.content.Context;
  11. public class CustomerSocketFactory extends SSLSocketFactory {
  12. private static final String PASSWD = "pw123456";
  13. public CustomerSocketFactory(KeyStore truststore)
  14. throws NoSuchAlgorithmException, KeyManagementException,
  15. KeyStoreException, UnrecoverableKeyException {
  16. super(truststore);
  17. }
  18. public static SSLSocketFactory getSocketFactory(Context context) {
  19. InputStream input = null;
  20. try {
  21. input = context.getResources().openRawResource(R.raw.example);
  22. KeyStore trustStore = KeyStore.getInstance(KeyStore
  23. .getDefaultType());
  24. trustStore.load(input, PASSWD.toCharArray());
  25. SSLSocketFactory factory = new CustomerSocketFactory(trustStore);
  26. return factory;
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. return null;
  30. } finally {
  31. if (input != null) {
  32. try {
  33. input.close();
  34. } catch (IOException e) {
  35. e.printStackTrace();
  36. }
  37. input = null;
  38. }
  39. }
  40. }
  41. }

同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:

  1. public static HttpClient getSpecialKeyStoreClient(Context context) {
  2. BasicHttpParams params = new BasicHttpParams();
  3. HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
  4. HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
  5. HttpProtocolParams.setUseExpectContinue(params, true);
  6. SchemeRegistry schReg = new SchemeRegistry();
  7. schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
  8. schReg.register(new Scheme("https", CustomerSocketFactory.getSocketFactory(context), 443));
  9. ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
  10. return new DefaultHttpClient(connMgr, params);
  11. }

转 Android HTTPS详解的更多相关文章

  1. Android ConstraintLayout详解(from jianshu)

    Android ConstraintLayout详解 https://www.jianshu.com/p/a8b49ff64cd3 1. 概述     在本篇文章中,你会学习到有关Constraint ...

  2. 最全面的Android Webview详解

    转自:最全面的Android Webview详解 前言 现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝.京东.聚划算等等,如下图  那么这种该如何实现呢?其实这是 ...

  3. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  4. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

  5. 公钥与私钥,HTTPS详解

    1.公钥与私钥原理1)鲍勃有两把钥匙,一把是公钥,另一把是私钥2)鲍勃把公钥送给他的朋友们----帕蒂.道格.苏珊----每人一把.3)苏珊要给鲍勃写一封保密的信.她写完后用鲍勃的公钥加密,就可以达到 ...

  6. Android ActionBar详解

    Android ActionBar详解 分类: Android2014-04-30 15:23 1094人阅读 评论(0) 收藏 举报 androidActionBar   目录(?)[+]   第4 ...

  7. Android 签名详解

    Android 签名详解 AndroidOPhoneAnt设计模式Eclipse  在Android 系统中,所有安装 到 系统的应用程序都必有一个数字证书,此数字证书用于标识应用程序的作者和在应用程 ...

  8. Android编译系统详解(一)

    ++++++++++++++++++++++++++++++++++++++++++ 本文系本站原创,欢迎转载! 转载请注明出处: http://blog.csdn.net/mr_raptor/art ...

  9. Android布局详解之一:FrameLayout

      原创文章,如有转载,请注明出处:http://blog.csdn.net/yihui823/article/details/6702273 FrameLayout是最简单的布局了.所有放在布局里的 ...

随机推荐

  1. log4net 日志文件占用,不能及时释放

    在appender 下面加 <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />

  2. ExecutorService的submit(Runnable x)和execute(Runnable x) 两个方法的本质区别

    Runnable任务没有返回值,而Callable任务有返回值.并且Callable的call()方法只能通过ExecutorService的submit(Callable <T> tas ...

  3. VBS自编写脚本。(实现批量修改文件名且在执行前,备份原有文件夹中的文件)

    '=========================================================================='' VBScript Source File - ...

  4. 《JS权威指南学习总结--第二章词法结构》

    第二章词法结构 内容要点: 一.注释 1. //表示单行注释 2. /*这里是一段注释*/ 3.一般编辑器里加注释是:选中要加注释的语句,按 ctrl+/ 二.直接量 所谓直接量,就是程序中直接使用的 ...

  5. Adobe Flash CC 2014 下载及破解

    来源 :http://prodesigntools.com/adobe-cc-2014-direct-download-links.html 地址:http://trials3.adobe.com/A ...

  6. dp + 组合数 Codeforces Beta Round #9 (Div. 2 Only) D

    http://codeforces.com/problemset/problem/9/D 题目大意:给你一个二叉树和n个数字,满足左小右大,能形成多少种不同的二叉树 思路:定义dp[i][j]表示目前 ...

  7. Windows下的 Axel下载工具 - 移植自Linux

    Axel 是 CLI (command-line interface) 下的一个多线程下载工具,通常我都用它取代 wget 下载各类文件,适用于 Linux 及 BSD 等 UNIX 类平台. 以下是 ...

  8. SQL中的左连接与右连接有什么区别,点解返回值会不同?(转)

    例子,相信你一看就明白,不需要多说 A表(a1,b1,c1) B表(a2,b2) a1 b1 c1 a2 b2 01 数学 95 01 张三 02 语文 90 02 李四 03 英语 80 04 王五 ...

  9. Css 之 px em %

    在页面整体布局中,页面元素的尺寸大小(长度.宽度.内外边距等)和页面字体的大小也是重要的工作之一.一个合理设置,则会让页面看起来层次分明,重点鲜明,赏心悦目.反之,一个不友好的页面尺寸和字体大小设置, ...

  10. iOS屏幕旋转 浅析

    一.两种orientation 了解屏幕旋转首先需要区分两种orientation 1.device orientation 设备的物理方向,由类型UIDeviceOrientation表示,当前设备 ...