网络(一):httpURlconnection

参考:
https://blog.csdn.net/fightingXia/article/details/71775516
https://www.cnblogs.com/jeffen/p/6937788.html
https://blog.csdn.net/zz153417230/article/details/80271155

HttpUrlConnection使用详解

一,HttpURLconnection的介绍

在Android开发中网络请求是最常用的操作之一, Android SDK中对HTTP(超文本传输协议)也提供了很好的支持,这里包括两种接口:
1、标准Java接口(java.NET) —-HttpURLConnection,可以实现简单的基于URL请求、响应功能;
2、Apache接口(org.appache.http)—-HttpClient,使用起来更方面更强大。

但在android API23的SDK中Google将HttpClient移除了。Google建议使用httpURLconnection进行网络访问操作。

HttpURLconnection是基于http协议的,支持get,post,put,delete等各种请求方式,最常用的就是get和post,下面针对这两种请求方式进行讲解。

二,get请求的使用方法
HttpURLconnection是同步的请求,所以必须放在子线程中。使用示例如下:

new Thread(new Runnable() {@Overridepublic void run() {try {String url = "https://www.baidu.com/";URL url = new URL(url);//得到connection对象。HttpURLConnection connection = (HttpURLConnection) url.openConnection();//设置请求方式connection.setRequestMethod("GET");//连接connection.connect();//得到响应码int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){//得到响应流InputStream inputStream = connection.getInputStream();//将响应流转换成字符串String result = is2String(inputStream);//将流转换为字符串。Log.d("kwwl","result============="+result);}} catch (Exception e) {e.printStackTrace();}}
}).start();

get请求的使用方法如上。如果需要传递参数,则直接把参数拼接到url后面,其他完全相同,如下:

String url = "https://www.baidu.com/?userName=zhangsan&password=123456";

注意点:

1,url与参数之间用?隔开。
2,键值对中键与值用=连接。
3,两个键值对之间用&连接。

分析:
1, 使用connection.setRequestMethod(“GET”);设置请求方式。
2, 使用connection.connect();连接网络。请求行,请求头的设置必须放在网络连接前。
3, connection.getInputStream()只是得到一个流对象,并不是数据,不过我们可以从流中读出数据,从流中读取数据的操作必须放在子线程。
4, connection.getInputStream()得到一个流对象,从这个流对象中只能读取一次数据,第二次读取时将会得到空数据。

三,post请求的使用方法

1,post的基本用法如下:

使用示例如下:

new Thread(new Runnable() {@Overridepublic void run() {try {URL url = new URL(getUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST");//设置请求方式为POSTconnection.setDoOutput(true);//允许写出connection.setDoInput(true);//允许读入connection.setUseCaches(false);//不使用缓存connection.connect();//连接int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){InputStream inputStream = connection.getInputStream();String result = is2String(inputStream);//将流转换为字符串。Log.d("kwwl","result============="+result);}} catch (Exception e) {e.printStackTrace();}}
}).start();

注:post请求与get请求有很多相似,只是在连接之前多了一些设置,两者可以对比学习使用。

2,使用post请求传递键值对参数

使用示例如下:

new Thread(new Runnable() {@Overridepublic void run() {try {URL url = new URL(getUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST"); connection.setDoOutput(true);connection.setDoInput(true);connection.setUseCaches(false);connection.connect();String body = "userName=zhangsan&password=123456";BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8"));writer.write(body);writer.close();int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){InputStream inputStream = connection.getInputStream();String result = is2String(inputStream);//将流转换为字符串。Log.d("kwwl","result============="+result);}} catch (Exception e) {e.printStackTrace();}}
}).start();

分析:
1,post方式传递参数的本质是:从连接中得到一个输出流,通过输出流把数据写到服务器。
2,数据的拼接采用键值对格式,键与值之间用=连接。每个键值对之间用&连接。

3,使用post请求传递json格式参数

post请求也可以传递json格式的参数,使用示例如下:

new Thread(new Runnable() {@Overridepublic void run() {try {URL url = new URL(getUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST"); connection.setDoOutput(true);connection.setDoInput(true);connection.setUseCaches(false);connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");//设置参数类型是json格式connection.connect();String body = "{userName:zhangsan,password:123456}";BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8"));writer.write(body);writer.close();int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){InputStream inputStream = connection.getInputStream();String result = is2String(inputStream);//将流转换为字符串。Log.d("kwwl","result============="+result);}} catch (Exception e) {e.printStackTrace();}}
}).start();

传递json格式的参数与传递键值对参数不同点有两个:
1,传递json格式数据时需要在请求头中设置参数类型是json格式。
2,body是json格式的字符串。

四,设置请求头

Get请求与post请求都可以设置请求头,设置请求头的方式也是相同的。为了节约篇幅,重复的代码不再列出,核心代码如下:

connection.setRequestMethod("POST");
connection.setRequestProperty("version", "1.2.3");//设置请求头
connection.setRequestProperty("token", token);//设置请求头
connection.connect();

注意:
1,请求头必须在connection.connect();代码前设置。
2,可以设置多个请求头参数。

五,上传文件

在post请求传递参数时知道,可以从连接中得到一个输出流,输出流可以像服务器写数据。同理,可以使用这个输出流将文件写到服务器。代码如下:

try {URL url = new URL(getUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST");connection.setDoOutput(true);connection.setDoInput(true);connection.setUseCaches(false);connection.setRequestProperty("Content-Type", "file/*");//设置数据类型connection.connect();OutputStream outputStream = connection.getOutputStream();FileInputStream fileInputStream = new FileInputStream("file");//把文件封装成一个流int length = -1;byte[] bytes = new byte[1024];while ((length = fileInputStream.read(bytes)) != -1){outputStream.write(bytes,0,length);//写的具体操作}fileInputStream.close();outputStream.close();int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){InputStream inputStream = connection.getInputStream();String result = is2String(inputStream);//将流转换为字符串。Log.d("kwwl","result============="+result);}} catch (Exception e) {e.printStackTrace();
}

注:
1,上传文件使用的是post请求方式。
2,使用的原理类似于post请求中上传参数

六,同时上传参数和文件

在实际应用时,上传文件的同时也常常需要上传键值对参数。比如在微信中发朋友圈时,不仅有图片,还有有文字。此时就需要同时上传参数和文件。

在httpURLconnection中并没有提供直接上传参数和文件的API,需要我们自己去探索。我们知道在Web页面上传参数和文件很简单,只需要在form标签写上contentype=”multipart/form-data”即可,剩余工作便都交给浏览器去完成数据收集并发送Http请求。但是如果没有页面的话要怎么上传文件呢?

由于脱离了浏览器的环境,我们就要自己去完成数据的封装并发送。首先我们来看web页面上传参数和文件是什么样子的?

我们写一个web表单,上传两个键值对参数和一个文件。使用抓包工具抓取的数据结果如下:

经过分析可知,上传到服务器的数据除了键值对数据和文件数据外,还有其他字符串,使用这些这些字符串来拼接一定的格式。

那么我们只要模拟这个数据,并写入到Http请求中便能实现同时传递参数和文件。

代码如下:

try {String BOUNDARY = java.util.UUID.randomUUID().toString();String TWO_HYPHENS = "--";String LINE_END = "\r\n";URL url = new URL(URLContant.CHAT_ROOM_SUBJECT_IMAGE);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST");connection.setDoOutput(true);connection.setDoInput(true);connection.setUseCaches(false);//设置请求头connection.setRequestProperty("Connection", "Keep-Alive");connection.setRequestProperty("Charset", "UTF-8");connection.setRequestProperty("Content-Type","multipart/form-data; BOUNDARY=" + BOUNDARY);connection.setRequestProperty("Authorization","Bearer "+UserInfoConfigure.authToken);connection.connect();DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());StringBuffer strBufparam = new StringBuffer();//封装键值对数据一strBufparam.append(TWO_HYPHENS);strBufparam.append(BOUNDARY);strBufparam.append(LINE_END);strBufparam.append("Content-Disposition: form-data; name=\"" + "groupId" + "\"");strBufparam.append(LINE_END);strBufparam.append("Content-Type: " + "text/plain" );strBufparam.append(LINE_END);strBufparam.append("Content-Lenght: "+(""+groupId).length());strBufparam.append(LINE_END);strBufparam.append(LINE_END);strBufparam.append(""+groupId);strBufparam.append(LINE_END);//封装键值对数据二strBufparam.append(TWO_HYPHENS);strBufparam.append(BOUNDARY);strBufparam.append(LINE_END);strBufparam.append("Content-Disposition: form-data; name=\"" + "title" + "\"");strBufparam.append(LINE_END);strBufparam.append("Content-Type: " + "text/plain" );strBufparam.append(LINE_END);strBufparam.append("Content-Lenght: "+"kwwl".length());strBufparam.append(LINE_END);strBufparam.append(LINE_END);strBufparam.append("kwwl");strBufparam.append(LINE_END);//拼接完成后,一块写入outputStream.write(strBufparam.toString().getBytes());//拼接文件的参数StringBuffer strBufFile = new StringBuffer();strBufFile.append(LINE_END);strBufFile.append(TWO_HYPHENS);strBufFile.append(BOUNDARY);strBufFile.append(LINE_END);strBufFile.append("Content-Disposition: form-data; name=\"" + "image" + "\"; filename=\"" + file.getName() + "\"");strBufFile.append(LINE_END);strBufFile.append("Content-Type: " + "image/*" );strBufFile.append(LINE_END);strBufFile.append("Content-Lenght: "+file.length());strBufFile.append(LINE_END);strBufFile.append(LINE_END);outputStream.write(strBufFile.toString().getBytes());//写入文件FileInputStream fileInputStream = new FileInputStream(file);byte[] buffer = new byte[1024*2];int length = -1;while ((length = fileInputStream.read(buffer)) != -1){outputStream.write(buffer,0,length);}outputStream.flush();fileInputStream.close();//写入标记结束位byte[] endData = (LINE_END + TWO_HYPHENS + BOUNDARY + TWO_HYPHENS + LINE_END).getBytes();//写结束标记位outputStream.write(endData);outputStream.flush();//得到响应int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){InputStream inputStream = connection.getInputStream();String result = is2String(inputStream);//将流转换为字符串。Log.d("kwwl","result============="+result);}} catch (Exception e) {e.printStackTrace();
}

注:http最早出现时就是为了浏览器与服务器之间的数据传输,所以有固定的协议,协议规范了一定的数据格式,所以在浏览器中传递数据时会自动按照一定的格式封装。在android中不能自动封装,所以这些操作需要手动操作。

七,下载文件
从服务器下载文件是比较简单的操作,只要得到输入流,就可以从流中读出数据。使用示例如下:

try {String urlPath = "https://www.baidu.com/";URL url = new URL(urlPath);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");connection.connect();int responseCode = connection.getResponseCode();if(responseCode == HttpURLConnection.HTTP_OK){InputStream inputStream = connection.getInputStream();File dir = new File("fileDir");if (!dir.exists()){dir.mkdirs();}File file = new File(dir, "fileName");//根据目录和文件名得到file对象FileOutputStream fos = new FileOutputStream(file);byte[] buf = new byte[1024*8];int len = -1;while ((len = inputStream.read(buf)) != -1){fos.write(buf, 0, len);}fos.flush();}} catch (Exception e) {e.printStackTrace();}

八,对httpURLconnection的封装

经过上面的学习可知,httpURLconnection的使用非常麻烦,每次访问网络都需要写大量的代码,尤其在同时上传参数和文件时更加繁琐,一不小心就容易出现错误。而且httpURLconnection请求是同步的,使用时必须开启子线程,修改UI时需要跳转到UI线程。等等导致不得不对httpURLconnection封装后再使用。Google也提供了网络请求封装类volley,熟悉volley的小伙伴都知道,volley在操作文件时性能并不好,而且没有提供同时上传参数和文件的方法。所以我们必须自己封装一套httpURLconnection的工具类。

我个人研究httpURLconnection的用法后封装了一套httpURLconnection的工具类,叫UrlHttpUtils。这套UrlHttpUtils最大的优点是简单和便于使用,这是我项目中实际用的网络请求工具类,完全可以说拿来即用。而且代码简单,可供学习使用。

UrlHttpUtils在github上的地址是:https://github.com/guozhengXia/UrlHttpUtils

封装的功能有:

  • 一般的get请求
  • 一般的post请求
  • 上传单个文件(包含进度)
  • 上传list集合文件
  • 上传map集合文件
  • 文件下载(包含进度)
  • 图片下载(实现了图片的压缩)

使用HttpURLConnection访问https协议请求时.对SSL信任

此篇只涉及到如果访问https链接, 具体的原理不做深究.

当我们使用HttpURLConnection访问http请求的时候没有任何困难, 但是当访问https协议的链接时, 由于证书的问题, 就涉及到此链接的证书验证. 可以保证安全的通信, 但是对爬虫来说, 会变得非常的麻烦. 所以我们需要对https协议的链接在代码层实现信任此链接.

第一步: 实现X509TrustManager接口

package util;import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;import javax.net.ssl.X509TrustManager;public class MyX509TrustManager implements X509TrustManager{@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {// TODO Auto-generated method stub}@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {// TODO Auto-generated method stub}@Overridepublic X509Certificate[] getAcceptedIssuers() {// TODO Auto-generated method stubreturn null;}}

这一步非常简单, 不需要实现任何方法, 也就是没有做任何的验证, 相当于所有的https链接都设为信任.

第二步: 请求类, 可以使用HttpURLConnection或者HttpsURLConnection

(1)给HttpURLConnection类设置默认的SSL

导入的包, 都是自带的

import java.net.URL;
import java.security.SecureRandom;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
SSLContext sslcontext = SSLContext.getInstance("SSL", "SunJSSE");//第一个参数为协议,第二个参数为提供者(可以缺省)
TrustManager[] tm = {new MyX509TrustManager()};
sslcontext.init(null, tm, new SecureRandom());
HostnameVerifier ignoreHostnameVerifier = new HostnameVerifier() {public boolean verify(String s, SSLSession sslsession) {System.out.println("WARNING: Hostname is not matched for cert.");return true;}
};
HttpsURLConnection.setDefaultHostnameVerifier(ignoreHostnameVerifier);
HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

然后就可以用conn去访问https协议的链接了.

(2)对一个HttpsURLConnection是对HttpURLConnection的扩展, 支持各种特定于 https 功能 .

这里我们用到了其中设计SSL的方法

public SSLSocketFactory getSSLSocketFactory()
获取为安全 https URL 连接创建套接字时使用的 SSL 套接字工厂。
返回:
SSLSocketFactory

具体的代码和设置默认的SSL比较类似, 导入的包不变:

SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");//第一个参数为 返回实现指定安全套接字协议的SSLContext对象。第二个为提供者
TrustManager[] tm = {new MyX509TrustManager()};
sslContext.init(null, tm, new SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();  URL url = new URL(path);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);

然后就可以用此conn来访问https协议的链接了.

网上看到的关于证书详解, 没有找到原出处, 对原作者抱歉

X509证书信任管理器类的详解

在JSSE中,证书信任管理器类就是实现了接口X509TrustManager的类。我们可以自己实现该接口,让它信任我们指定的证书。

接口X509TrustManager有下述三个公有的方法需要我们实现:

    ⑴ oid checkClientTrusted(X509Certificate[] chain, String authType)

throws CertificateException

该方法检查客户端的证书,若不信任该证书则抛出异常。由于我们不需要对客户端进行认证,因此我们只需要执行默认的信任管理器的这个方法。JSSE中,默认的信任管理器类为TrustManager。

⑵ oid checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
  该方法检查服务器的证书,若不信任该证书同样抛出异常。通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。

⑶ X509Certificate[] getAcceptedIssuers()
返回受信任的X509证书数组。

自己实现了信任管理器类,如何使用呢?类HttpsURLConnection似乎并没有提供方法设置信任管理器。其实,HttpsURLConnection通过SSLSocket来建立与HTTPS的安全连接,SSLSocket对象是由SSLSocketFactory生成的。HttpsURLConnection提供了方法setSSLSocketFactory(SSLSocketFactory)设置它使用的SSLSocketFactory对象。SSLSocketFactory通过SSLContext对象来获得,在初始化SSLContext对象时,可指定信任管理器对象。下面用一个图简单表示这几个JSSE类的关系:

假设自己实现的X509TrustManager类的类名为:MyX509TrustManager,下面的代码片断说明了如何使用MyX509TrustManager:

//创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = {new MyX509TrustManager ()};
SSLContext sslContext = SSLContext.getInstance("SSL","SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());

//从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
//创建HttpsURLConnection对象,并设置其SSLSocketFactory对象
HttpsURLConnection httpsConn = (HttpsURLConnection)myURL.openConnection();
httpsConn.setSSLSocketFactory(ssf);

这样,HttpsURLConnection对象就可以正常连接HTTPS了,无论其证书是否经权威机构的验证,只要实现了接口X509TrustManager的类MyX509TrustManager信任该证书。

小结

本文主要介绍了在HTTPS的证书未经权威机构认证的情况下,访问HTTPS站点的两种方法,一种方法是把该证书导入到Java的TrustStore文件中,另一种是自己实现并覆盖JSSE缺省的证书信任管理器类。两种方法各有优缺点,第一种方法不会影响JSSE的安全性,但需要手工导入证书;第二种方法虽然不用手工导入证书,但需要小心使用,否则会带来一些安全隐患

网络(一):httpURlconnection相关推荐

  1. 用HTTP协议连接网络(HttpURLConnection)

    2019独角兽企业重金招聘Python工程师标准>>> 使用HttpURLConnection访问网络, 1.首先需要获得一个HttpURLConnection实例,一般只需要new ...

  2. Android 网络请求HttpURLConnection 和 HttpClient详解

    Android一般通过http协议向服务端接口发送请求,常用有POST和GET传输方式.这种请求通常借助于HttpClient,HttpClient 是 Apache Jakarta Common 下 ...

  3. 网络传输---HttpURLConnection

    HttpURLConnection是java做网络传输的一种,一般用于做数据的传输如xml数据传输 1.创建及配置: 1.1创建一个url对象,并指定url的地址 URL url = new URL( ...

  4. 网络编程-HttpURLConnection

    java.lang.Object –java.net.URLConnection –java.net.HttpURLConnection 1.Socket 应该是 TCP 协议层的概念,如果要使用 S ...

  5. Android初级开发(九)——网络交互—HttpURLConnection

    一.使用HttpURLConnection的步骤 1.获取到HttpURLConnection的实例,并传入目标的网络地址,然后调用openConnection()方法 URL url = new U ...

  6. Android网络编程 HttpUrlConnection HttpClient AsyncTask

    前面几篇文章介绍了Android中常用的http框架:volley,所谓框架者无非就是对我们所需的一系列操作进行了封装,简化了我们的工作. 但万变不离其宗,我们还是需要掌握Android原生的网络操作 ...

  7. Android网络请求归纳 HttpUrlConnection| Vollety|OKHttp3|Retrofit2

    1.网络基础知识: Android网络API库有哪些?      HttpUrlConnection: jdk内置      HttpClient:android提供,6.0被删除      Voll ...

  8. 使用HttpURLConnection进行网络访问及代码实战

    使用HttpURLConnection进行网络访问 HttpURLConnection是一种多用途.轻量极的HTTP客户端,使用它来进行HTTP操作可以适用于大多数的应用程序.继承至URLConnec ...

  9. 叮!快收好这份Android网络性能监控方案

    简介:移动互联网时代,移动端极大部分业务都需要通过App和Server之间的数据交互来实现,所以大部分App提供的业务功能都需要使用网络请求.如果因为网络请求慢或者请求失败,导致用户无法顺畅的使用业务 ...

  10. Android网络性能监控方案

    背景 移动互联网时代,移动端极大部分业务都需要通过App和Server之间的数据交互来实现,所以大部分App提供的业务功能都需要使用网络请求.如果因为网络请求慢或者请求失败,导致用户无法顺畅的使用业务 ...

最新文章

  1. LeetCode 252. Meeting Rooms
  2. Linux kernel 3.10内核源码分析--slab原理及相关代码
  3. python编写安装脚本_LAMP一键安装脚本(Python编写)
  4. 当才华还撑不起梦想时,你应该静下心来看这些
  5. [Leedcode][第215题][JAVA][数组中的第K个最大元素][快排][优先队列]
  6. 容器编排技术 -- Kubernetes 调试 Service
  7. oracle拼接空格错误,oracle运维故事 一个空格引发的血案
  8. 自己动手写Docker系列 -- 5.5实现容器停止
  9. Spark自定义排序
  10. HDU 6390 GuGuFishtion(莫比乌斯反演 + 欧拉函数性质 + 积性函数)题解
  11. Elasticsearch 系列4 --- Windows10安装Kibana
  12. docker搭建sonarqube做代码审计
  13. 阿里云宗志刚:云网一体,新一代洛神云网络平台
  14. 计算机模拟仿真技术的功能,浅谈虚拟仿真技术
  15. Keep It Simple and Stupid是什么意思
  16. java判断图片地址链接是否有效
  17. 联想e570c固态接口支持协议_thinkpad e570c15.6英寸笔记本电脑支持什么固态硬盘接口...
  18. 计算机如何添加新用户,笔记本电脑怎么样快速添加新用户帐号
  19. 测试人员如何做到前置
  20. 一篇文章带你理清宽带、带宽、网速、吞吐量与宽带上下行

热门文章

  1. 集成测试之增式集成测试和非增式集成测试
  2. ESP8266MOD、刷可以使用AT指令的固件、作为客户端向贝壳云端发送固定数据
  3. LabVIEW数据采集:配套视频教程第2集(2.1.1节-2.1.13节)
  4. 服务器千兆网络显示10,win10系统如何查看网卡是千兆还是百兆
  5. 写一篇组织生活会对照检查材料
  6. 使用OZMTool制作 ozmosis BIOS
  7. 创建现成的四种方式 多线程与并发的基本概念:
  8. android编译v8引擎,V8引擎 Android库构建
  9. 财务金额转换(最大单位为亿,最小单位分)
  10. 【D1N910】正则表达式30分钟入门教程 (二)-学习笔记 实践