安卓网络编程学习之HTTP(S)

这里学习使用的API为Android原生API:HttpUrlConnection。我们平常使用中更多的是使用第三方框架例如OKHttp,Volley、Retrofit等,但是像Volley也是封装了访问网络的一些操作,底层用的还是HttpUrlConnection,而OkHttp和HttpUrlConnection则使用socket实现了网络连接,只是OkHttp比HttpUrlConnection来说功能更强大。当然这里先不说其他的第三方框架如何,先了解http一些基础知识,使用原生API进行学习。

Http 概念:hypertext transfer protocol 超文本传输协议,TCP/IP协议的一个应用层协议,用于定义WEB浏览器与WEB服务器之间交换数据的过程。客户端连上web服务器后,若想获得web服务器中的某个web资源,需遵守一定的通讯格式,Http协议用于定义客户端与web服务器通讯的格式。

1.Http通信流程

这里就以经常会问到的浏览器输入url:www.baidu.com后会发生什么为例,当然现在百度的请求都改为了Https,这里考虑最简单的情况。

1.1DNS解析

DNS解析的最终目的是查找域名对应的ip地址。

  • 1.请求发起后,浏览器有自己的缓存,因此先查找浏览器有没有相应的DNS缓存记录,没有的话,调用系统函数获取系统缓存记录。
  • 2.接上一步,查看系统的hosts文件里有没有对应的域名规则,有的话就直接使用hosts文件里面的ip地址。以前常用的fq方法大致就是修改hosts文件加上google.com的对应规则。
  • 3.如果第2步没有找到,会继续到路由器上,路由器也有DNS缓存。
  • 4.如果上面都没找到,那么这个解析url的DNS请求会先发送到本地DNS(域名分布系统)服务器,一般由运营商提供。本地DNS服务器去查询自己的缓存,有的话返回。
  • 5.第4步没有找到的话,本地DNS服务器将这个DNS请求继续向根DNS服务器发送,根DNS服务器本身并没有记录域名与ip的对应关系,而是告诉本地DNS服务器,你到.com域服务器上查询,并给出.com域服务器的地址。
  • 6.本地DNS服务器就把这个DNS请求发送到.com域服务器上,.com域服务器收到DNS请求之后,和根DNS服务器一样也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,去www.baidu.com域名的域服务器上查询。
  • 7.最后,本地DNS服务器向www.baidu.com域名的解析服务器发出请求,就可以收到这个域名对应的IP地址。本地DNS服务器收到后把IP地址返回给用户,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

之后的步骤可以参考下图(图片来源),主机A相当于我们的PC,浏览器处于应用层,主机B为百度的服务器,总的来说就是我们的请求从A的应用层到A的物理层,通过物理线路传到B的物理层,这中间会涉及到各种路由协议等等,B收到之后又从下至上层层解包,接着响应数据又顺着应用层->传输层->网络层->网络层->传输层->应用层的顺序返回到A,A应用层的浏览器根据获得的响应渲染响应页面。 重要的几个步骤如下:
安卓网络编程学习之HTTP(S)_第1张图片

1.2建立TCP连接

Http本质上是通过TCP传送数据的(原因就是我们常说的TCP是面向连接的可靠的,可以进行差错控制等,而UDP是无连接的,不可靠的),因此在发送接收http请求之前,先要建立TCP连接。这里就用的是上篇文章里面提到的三次握手。

1.3Http请求

TCP连接建立后,应用层的http请求加上TCP头部-包括源端口号、目的端口号(http为80)和用于校验数据完整性的序号等,继续向下层传输到网络层,网络层在数据包上加上IP头部(见下图),包含了原IP与目的IP,之后就是加上数据链路层的帧头,中间的细节省略掉,继续向下传输到底层物理层,底层通过物理线路传输到百度服务器。服务器收到后又层层解包,服务器网络层收到数据包后,解析出IP头部,识别数据部分,并将解开的数据包向上传输到传输层,传输层获得数据包后,就解析出TCP头部,识别端口,将解开的数据包向上传输到应用层,应用层HTTP解析请求头和请求体,根据用户请求返回相应资源接着响应数据又顺着应用层->传输层->网络层->网络层->传输层->应用层的顺序返回到PC,PC应用层的浏览器根据获得的响应渲染响应页面。
在这里插入图片描述
例如请求百度的网络请求报文如下:安卓网络编程学习之HTTP(S)_第2张图片
HTTP请求报文一般由3部分组成(请求行+请求头+请求体):

  • 请求行:包括请求方法(例如get/post等)、请求url、http协议及版本
  • 请求头:包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。常见的HTTP报文头属性如下:
    • Accept:告诉服务端 客户端接受什么类型的响应。例如Accept: / 代表浏览器(客户端)可以处理所有类型。
    • Accept-Encoding:指定浏览器可以支持的web服务器返回内容压缩编码类型。
    • Accept-Language:浏览器可接受的语言。
    • Connection:表示是否需要持久连接。(HTTP 1.1默认进行持久连接)。例如Connection: keep-alive代表保活。
    • Cookie:存储一些用户信息以便让服务器辨别用户身份的,比如cookie会存储sessionid等。
    • Referer:表示这个请求是从哪个URL过来的。
    • User-Agent:告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。
    • Cache-Control:对缓存进行控制。
  • 请求体:将一个页面表单中的组件值通过param1=value1¶m2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。
1.4断开TCP连接

数据传输完成后,为了避免客户端与服务器资源占用和损耗,当双方没有请求或响应传递了,任意一方都可以发起关闭请求,用到的就是四次挥手。Http 1.1版本增加了长连接功能,在Http数据头部加上Connection:Keep-Alive,表示TCP一直保持连接,可以提高资源加载速度。

1.5浏览器渲染

这里考虑简单的HTML、CSS资源,浏览器通过解析HTML,生成DOM树,解析CSS,生成CSS规则树,然后将DOM树和CSS规则树结合生成渲染树,之后通过Layout可以计算出每个元素具体的宽高颜色位置,结合起来开始绘制,最后显示在屏幕新页面中。

Http协议在通信的过程中是以明文方式发送数据,不提供任何方式的数据加密,如果攻击者截取了浏览器和服务器之间的传输报文,就可以直接读懂其中的信息,因此,Http协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。在这样的情况下,Https应用而生,Https可以将数据加密传输,保证数据传输的安全。这里学习一下Https的通信过程,参考iispring博文:

2.Https

Https协议简单来说由Http协议和SSL/TLS协议构成。Http协议前面已经说过,它扮演的角色就是传输数据,而SSL/TLS是负责加密解密等安全处理的模块,需要用它对数据进行加密和解密,加密后的数据也是通过Http传输的,所以Https的核心在SSL/TLS上面。

SSL的全称是Secure Sockets Layer,即安全套接层协议,是为网络通信提供安全及数据完整性的一种安全协议。SSL协议在1994年被Netscape发明,后来各个浏览器均支持SSL,其最新的版本是3.0 。
TLS的全称是Transport Layer Security,即安全传输层协议,最新版本的TLS(Transport Layer Security,传输层安全协议)是IETF(Internet Engineering Task Force,Internet工程任务组)制定的一种新的协议,它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本。在TLS与SSL3.0之间存在着显著的差别,主要是它们所支持的加密算法不同,所以TLS与SSL3.0不能互操作。虽然TLS与SSL3.0在加密算法上不同,但是在我们理解HTTPS的过程中,我们可以把SSL和TLS看做是同一个协议。

通信的过程图如下图所示:
安卓网络编程学习之HTTP(S)_第3张图片
借用iispring博文中的一句简短的介绍就是:HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。数据是被对称加密传输的,对称加密过程需要客户端的一个密钥,为了确保能把该密钥安全传输到服务器端,采用非对称加密对对称加密所使用的密钥进行加密传输。具体步骤如下:

  • 1.客户端或者这里说浏览器向服务器发起HTTPS请求,连接到百度服务器的443端口,请求携带了浏览器支持的加密算法和哈希算法。
  • 2.服务器收到请求,选择浏览器支持的加密算法和哈希算法。
  • 3.服务器将数字证书返回给浏览器,这里的数字证书可以是向某个权威机构申请的,也可以是自制的。
    这里解释一下数字证书:第3步的实质其实是服务器将自己的公钥发送给浏览器以方便之后非对称加密用。我们知道服务器端有自己的公钥和私钥,服务器端着私钥自己保密不泄漏,但是公钥在发送给浏览器的时候很可能会被黑客中途篡改,这就存在安全问题即浏览器不知道这个公钥是自己想请求的服务器的还是黑客的,于是数字证书就应运而生。数字证书简单点说就是找了个可靠的权威的第三方担保人CA,CA也有自己的私钥和公钥,它用自己的私钥对服务器的公钥进行非对称加密,只有用CA公开的公钥才能解密,这样就能保证服务器公钥的可靠性。加密完之后,得到的密文再加上证书的有效期、颁发给、颁发者等信息,构成了了数字证书。当然如果是自制的认证机构,可能浏览器就认为是不可靠的,得让客户端去信任相应的跟证书。简单来说,数字证书就是为了解决公钥的可靠性问题。
  • 4.浏览器验证服务器数字证书的合法性,这一部分是浏览器内置的TLS完成的,如果发现发现数字证书有问题,那么HTTPS传输就无法继续,具体的验证方法如下:
  • 4.1. 首先浏览器会从内置的证书列表中索引,找到服务器下发证书对应的机构,如果没有找到相应机构,此时就会提示用户该证书是不是由权威机构颁发,无法信任。如果查到了对应的机构,则取出该机构颁发的公钥。
  • 4.2. 用这个对应机构的证书公钥去解密,能够解密成功的话说明该数字证书就是由该CA的私钥签发的。解密的内容包括网站的网址、网站的公钥、证书的有效期等。浏览器会先验证证书签名的合法性,还要验证浏览器当前访问的服务器的域名是与数字证书中提供的“颁发给”这一项吻合,还要检查数字证书是否过期等。这些都通过认证时,浏览器就可以安全使用证书中的网站公钥进行之后的非对称加密了。
  • 4.3. 已经验证服务器公钥是可靠的之后,浏览器生成一个随机值R,作为传输数据时的对称加密密钥,并利用4.2中得到的服务器公钥对这个R进行非对称加密。因为传输数据量一般较大,非对称加密计算复杂耗时太多,肯定会造成延迟,因此量大数据适合采用对称加密。
  • 5.浏览器将非对称加密后的密钥密文发送给服务器。到这里就已经完成了Https的握手过程。
  • 6.服务器接收到客户端发来的密文之后,用自己的私钥对其进行非对称解密,解密之后的明文就是共享的对称密钥R,然后用对称密钥R对数据例如网页内容进行对称加密,这样数据就变成了密文。
  • 7.然后服务器将加密后的密文发送给浏览器。
  • 8.浏览器收到密文后,用共享的对称密钥R对密文进行对称解密,得到服务器发送的数据。

https与http的主要区别如下:

  • https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
  • http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  • http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

3.Http版本

这里简单学习下Http 1.0、1.1和最新的2.0三个版本的区别。参考一只好奇的茂博文。

先看下Http 1.0和1.1的区别

这里挑几个重要的点来说:

  • 1.节约宽带
  • Http1.0中不支持断点续传,例如客户端只是需要某个资源的一部分,但是服务器却会将整个资源送过来。Http1.1则在请求头引入了RANGE头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。这点我们在前面AsyncTask实现文件下载的时候学习过,计算出已存在文件的长度,之后再下载的话就直接请求这个长度之后的文件资源,而不用从头再下载整个文件。
    安卓网络编程学习之HTTP(S)_第4张图片
  • 2.长连接
    Http 1.0规定客户端与服务器只能保持短暂的连接,客户端的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个用户也不记录过去的请求。举个例子比如一个网页中包含很多图像url地址或者包含JS、CSS规则等,那么每次请求和响应都需要通过TCP三次握手建立一个单独的连接(耗时),每次连接传输一个文档或者图像等,传输完成后就四次挥手断开连接,上一次和下一次请求完全是分离的,这必不可免的会造成耗时。为了解决这个问题,Http1.0需要使用connection:keep-alive参数来告知服务器端要建立一个TCP长连接,一次请求完成后不会断开连接。Http 1.1则默认支持长连接,默认开启Connection: keep-alive,如果使用http1.1协议不希望建立长连接,就则需要在request或者response的header中指明connection的值为close。
    http1.0不指明keep-alive时,每次请求都要建立一次连接:安卓网络编程学习之HTTP(S)_第5张图片
    http1.1默认开启长连接:
    安卓网络编程学习之HTTP(S)_第6张图片
  • 3.Host头处理
    在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此不支持Host请求头字段,客户端无法使用主机头名来明确表示要访问服务器上的哪个WEB站点,这样就无法使用WEB服务器在同一个IP地址和端口号上配置多个虚拟WEB站点。在HTTP 1.1中增加Host请求头字段后,客户端可以使用主机头名来明确表示要访问服务器上的哪个WEB站点,实现了一台服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点。
Http 2.0相比Http1.x

Http 2.0相比Http1.x多出一些新的特性:

  • 1.多路复用 (Multiplexing)
    Http 2.0使用了多路复用,允许同一个连接并发处理多个请求即连接共享,一个请求对应一个id,这样一个连接上可以有多个请求,每个连接的请求可以随机的混杂在一起,接收方可以根据请求的 id将请求再归属到各自不同的服务端请求里面。连接共享如下图所示:
    安卓网络编程学习之HTTP(S)_第7张图片

  • 2.二进制分帧
    Http 1.x的解析是基于文本,基于文本协议的格式解析存在缺陷,因为文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只有0和1组合。基于这种考虑Http 2.0的协议解析采用二进制格式,在应用层和传输层之间增加一个二进制分帧层,在二进制分帧层里面将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,实现方便且健壮。

  • 3.服务端推送
    服务端推送可以把客户端可能会用到的资源伴随着网页内容一起发送到客户端,省去了客户端重复请求的步骤,非常合适加载静态资源,可以极大地提升静态资源的加载速度。与普通的客户端请求对比图如下:
    普通客户端请求:
    安卓网络编程学习之HTTP(S)_第8张图片

采用服务端推送:
安卓网络编程学习之HTTP(S)_第9张图片

  • 4.头部(Header)压缩
    Http 1.x不支持header数据的压缩,但是header中带有大量信息,每次都要重复发送会耗时。Http 2.0使用HPACK算法对header的数据进行压缩,数据体积变小,在网络上传输就会更快。

4.GET与POST

GET与POST是我们最常见的两种数据请求方式,都是用来发送数据的,只是发送机制不一样,GET安全性非常低,Post安全性较高, 但是执行效率却比Post方法好,一般获取查询数据的时候用GET,数据增删改的时候用POST。主要区别如下:

  • 1.GET把参数包含在URL中,对所有人可见,POST通过request body传递参数,数据不会显示在 URL 中。
  • 2.受URL长度限制,Get传送的数据量较小(2k);Post传送的数据量较大,一般被默认为不受限制。
  • 3.后退或者刷新页面时,GET请求无害,而POST请求数据会被重新提交。
  • 4.GET请求会缓存数据,而POST请求不会缓存数据。
  • 5.GET请求限制数据类型为ASCII 字符,POST请求无限制,允许二进制数据。
  • 6.GET请求会把http header和data一并发送出去,而POST请求根据浏览器或者框架的不同,会将header和data分开发送,先发送header,服务器响应100 continue,再发送data。

从上面也可以看到GET请求的速度比POST请求块,主要的几点原因也在上面提到了:

  • 1.POST需要在请求的body部分包含数据,所以会多了几个数据描述部分的首部字段(如content-type)。
  • 2.POST请求可能会将header和data分开发送。
  • 3.get会将数据缓存起来,而post不会

5.Session和Cookie

参考知乎:

Session: 由于Http是无状态的协议,所以服务端需要记录用户的状态时就需要用某种机制来识具体的用户,这个机制就是Session。典型的场景比如购物车,当你点击下单按钮时,由于Http协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识并且跟踪用户,这样才知道用户进行了什么操作例如购物车里面有几本书。Session是保存在服务端的,有一个唯一标识。

Cookie是实现Session机制的一种常用形式(为什么说是之一呢?因为如果客户端的浏览器禁用了 Cookie ,还可以使用做URL重写的技术来进行会话跟踪,即每次Http交互,URL后面都会被附加上一个诸如 SessionID=xxxxx 这样的参数,服务端据此来识别用户;也可以使用Token 机制,当用户第一次登录后,服务器根据提交的用户信息生成一个 Token,响应时将 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次登录验证,服务端也不必像session那样存储session,只负责生成token并验证即可)。举例来说: 一般发送登陆请求后,服务器通过Set-Cookie响应头,返回一个Cookie,浏览器默认保存这个Cookie, 后续访问相关页面的时候会带上这个Cookie,通过Cookie请求头标识用户信息完成访问,如果没Cookie或者 Cookie过期,就提示用户没登陆,登陆超时,访问需要登陆之类的信息。

用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

如果只用cookie不用session,那么账户信息全部保存在客户端,一旦被劫持,全部信息都会泄露。并且客户端数据量变大,网络传输的数据量也会变大。

Cookie 和 Session 区别

  • 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
  • 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
  • 有效期不同,Cookie 可设置为长时间保持(持久性Cookie),比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者Session 超时都会失效。
  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取;Session 存储在服务端,安全性相对 Cookie 要好一些。
  • 存储大小不同, 单个 Cookie保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。

app实现持久登录方式
平常第一次登录app成功后,以后每次启动时都是登录状态,不需要每次启动时再次登录。其具体实现就可以用token:

第一次登录时APP将用户输入的账号和密码提交给服务器;服务器对其进行校验,若账号和密码对得上则校验通过,说明登录成功。并生成一个token值,将其保存在数据库,同时也返回给客户端;客户端拿到返回的token值后,可将其保存在本地。作为公共参数,即以后每次请求服务器时都携带该token,提交给服务器,让服务器校验。服务器接收到请求后,会取出请求头里的token值与数据库存储的token进行对比校验。若两个token值相同,则说明用户登录成功过,且当前正处于登录状态,此时正常返回数据,让APP显示数据。若两个值不一致,则说明原来的的登录已经失效,此时返回错误状态码,提示用户跳转至登录界面重新登录。并且用户每进行一次登录,登录成功后服务器都会更新个token新值返回给客户端。

6.Demo练习

前面学习了相关知识,这里使用原生API进行练习,请求方式为GET。

GET请求获取数据

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <TextView
        android:id="@+id/tvmenu"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="加载菜单"
        android:clickable="true"
        android:gravity="center"
        android:textSize="20sp" />

    <ImageView
        android:id="@+id/mpicture"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"/>

    <ScrollView
        android:id="@+id/scroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone">
        <TextView
            android:id="@+id/scrolltxt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </ScrollView>

    <WebView
        android:id="@+id/mwebview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"/>

</LinearLayout>

menu文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- checkableBehavior的可选值由三个:single设置为单选,all为多选,none为普通选项 -->
    <group android:checkableBehavior="none">
        <item android:id="@+id/pic" android:title="@string/query_pic"/>
        <item android:id="@+id/code" android:title="@string/query_code"/>
        <item android:id="@+id/loadview" android:title="@string/loadview"/>
    </group>

</menu>

数据请求类:

public class GetData {
    //定义类方法
    public static byte[] getImage(String param) throws Exception {
        URL url = new URL(param);
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        conn.setConnectTimeout(5000);//超时时间为5s
        conn.setRequestMethod("GET");
        if(conn.getResponseCode() == 200) {
            InputStream is = conn.getInputStream();
            byte[] bt = StreamTobyte.read(is);
            is.close();
            return bt;
        }else {
            throw new RuntimeException("请求失败");
        }
    }

    public static String getHtml(String param) throws Exception {
        URL url = new URL(param);
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        conn.setConnectTimeout(5000);//超时时间为5s
        conn.setRequestMethod("GET");
        if(conn.getResponseCode()==200){
            InputStream is = conn.getInputStream();
            byte[] bt = StreamTobyte.read(is);
            String html = new String(bt,"UTF-8");
            return html;
        }else {
            throw new RuntimeException("请求失败");
        }
    }

}

输入流转换为二进制数组类:

public class StreamTobyte {
    //定义类方法 将输入流转换为二进制数组
    public static byte[] read(InputStream inStream) throws Exception{
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while((len = inStream.read(buffer)) != -1)
        {
            outStream.write(buffer,0,len);
        }
        inStream.close();
        return outStream.toByteArray();
    }
}

MainActivity:

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    private ImageView imageView;
    private ScrollView scrollView;
    private TextView scrolltextview;
    private WebView webView;

    private Bitmap bitmap;
    private String html;
    private long exitTime = 0;
    private final static String picurl = "https://ww2.sinaimg.cn/large/7a8aed7bgw1evshgr5z3oj20hs0qo0vq.jpg";
    private final static String htmlurl = "https://www.baidu.com";

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 0x001:
                    hideAllWidget();
                    imageView.setVisibility(View.VISIBLE);
                    imageView.setImageBitmap(bitmap);
                    Toast.makeText(MainActivity.this,"图片加载完成",Toast.LENGTH_SHORT).show();
                    break;
                case 0x002:
                    hideAllWidget();
                    scrollView.setVisibility(View.VISIBLE);
                    scrolltextview.setText(html);
                    Toast.makeText(MainActivity.this,"html代码加载完成",Toast.LENGTH_SHORT).show();
                    break;
                case 0x003:
                    hideAllWidget();
                    webView.setVisibility(View.VISIBLE);
                    webView.setWebViewClient(new WebViewClient() {
                        //设置在webView点击打开的新网页在当前界面显示,而不跳转到新的浏览器中
                        @Override
                        public boolean shouldOverrideUrlLoading(WebView view, String url) {
                            view.loadUrl(url);
                            return true;
                        }
                    });
                    webView.getSettings().setJavaScriptEnabled(true);  //设置WebView属性,运行执行js脚本
                    webView.loadUrl(htmlurl); //调用loadUrl方法为WebView加入链接
                    Toast.makeText(MainActivity.this,"网页加载完成",Toast.LENGTH_SHORT).show();
                    break;

            }
        }
    };

    private void hideAllWidget() {
        //INVISIBLE属性界面会保留view控件所占有的空间,GONE属性界面则不保留view控件所占有的空间
        imageView.setVisibility(View.GONE);
        scrollView.setVisibility(View.GONE);
        webView.setVisibility(View.GONE);
    }

    @Override
    public void onBackPressed() {
        //重写回退按钮的时间,当用户点击回退按钮,webView.canGoBack()判断网页是否能后退,可以则goback()
        //如果不可以连续点击两次退出App,否则弹出提示Toast
        if (webView.canGoBack()) {
            webView.goBack();
        } else {
            if ((System.currentTimeMillis() - exitTime) > 2000) {
                Toast.makeText(getApplicationContext(), "再按一次退出程序",
                        Toast.LENGTH_SHORT).show();
                exitTime = System.currentTimeMillis();
            } else {
                super.onBackPressed();
            }

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindviews();
        registerForContextMenu(textView);
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        MenuInflater inflater = new MenuInflater(this);
        inflater.inflate(R.menu.menu_context,menu);
    }

    @Override
    public boolean onContextItemSelected(@NonNull MenuItem item) {
        //菜单的点击事件
        switch (item.getItemId()){
            case R.id.pic:
                //UI线程中不能进行网络操作
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                         try{
                             byte[] pic = GetData.getImage(picurl);
                             bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.length);
                         }catch (Exception e){
                             e.printStackTrace();
                         }
                         handler.sendEmptyMessage(0x001);
                    }
                }).start();
                break;
            case R.id.code:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            html = GetData.getHtml(htmlurl);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        handler.sendEmptyMessage(0x002);
                    }
                }).start();
                break;
            case R.id.loadview:
                handler.sendEmptyMessage(0x003);
                break;
        }
        return true;
    }

    private void bindviews() {
        textView = findViewById(R.id.tvmenu);
        imageView = findViewById(R.id.mpicture);
        scrollView = findViewById(R.id.scroll);
        scrolltextview = findViewById(R.id.scrolltxt);
        webView = findViewById(R.id.mwebview);
    }
}

效果图如下,长按加载菜单文本:
安卓网络编程学习之HTTP(S)_第10张图片

点击请求图片:
安卓网络编程学习之HTTP(S)_第11张图片

点击请求html代码:
安卓网络编程学习之HTTP(S)_第12张图片

点击加载webview:
安卓网络编程学习之HTTP(S)_第13张图片

搜索后:
安卓网络编程学习之HTTP(S)_第14张图片

回退:
安卓网络编程学习之HTTP(S)_第15张图片

再次回退:
安卓网络编程学习之HTTP(S)_第16张图片

7.Retrofit

Retrofit是Square公司推出的一个HTTP网络框架,它实际上是对OkHttp网络请求框架的二次封装,本质仍是OkHttp。即网络请求的工作本质上是由OkHttp完成,而Retrofit仅负责网络请求接口的封装。Retrofit可以使我们用面向对象的思维进行网络操作:
在这里插入图片描述
如上图所示,App通过Retrofit请求网络,实际上是使用Retrofit接口层封装请求参数、Header、Url等信息,之后由OkHttp完成后续的请求操作。在服务器返回数据之后,OkHttp将原始的结果交给Retrofit,Retrofit会根据用户的需求对结果进行解析。

通过Demo来学习一下,Demo参考《Android第一行代码》:

首先添加依赖库:

dependencies {
    ...
    implementation 'com.squareup.retrofit2:retrofit:2.6.1'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
    ...
}

由于Retrofit是基于OkHttp开发的,因此第一条依赖会自定将Retrofit、OkHttp等一起下载,第二条依赖会将GSON库下载下来,可以使用GSON库解析json文件。

这里搭建了一个简单的Web服务器,并新建了一个get_data.json的文件:
安卓网络编程学习之HTTP(S)_第17张图片
由于GSON库解析json数据的时候用的就是面向对象的思维,因此这里要新建对应的实体类:

public class App {
     public String id;
     public String version;
     public String name;
}

接下来创建用于描述网络请求的接口,接口函数里要定义URL路径、请求参数、返回类型。其中,需要使用注解来描述请求类型和请求参数:

public interface AppService {
    @GET("get_data.json")
    Call<List<App>> getAppData();
}

上述代码中getAppData上面有个 @GET注解代表调用getAppData时Retrofit会发起一条GET请求,请求地址就是@GET注解的参数,这里只需要传入相对路径即可,跟路径会另行设置。并且getAppData方法的返回类型必须为Retrofit内置的Call类型,并要通过泛型来指定服务器响应的数据应该转换成什么对象,这里有一返回的是包含App数据的JSON数组,因此将泛型声明为List

接下来修改布局文件添加按钮,并添加点击事件:

public class MainActivity extends AppCompatActivity {
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Retrofit retrofit = new Retrofit.Builder().baseUrl("http://10.0.2.2/").addConverterFactory(GsonConverterFactory.create()).build();
                AppService appService = retrofit.create(AppService.class);
                appService.getAppData().enqueue(new Callback<List<App>>() {
                    @Override
                    public void onResponse(Call<List<App>> call, Response<List<App>> response) {
                          List<App> list = response.body();
                          if(list!=null){
                              for(int i=0;i<list.size();i++){
                                  App app = list.get(i);
                                  Log.d("MainActivity",app.id+","+app.version+","+app.name);
                              }
                          }
                    }

                    @Override
                    public void onFailure(Call<List<App>> call, Throwable t) {
                          t.printStackTrace();
                    }
                });
            }
        });
    }
}

点击事件中,首先使用Retrofit.Builder()创建Retrofit对象,并设置跟路径以及数据解析器,之后调用了Retrofit对象的create方法,并传入具体Service接口所对应的Class类型以创建该接口的动态代理对象,这样就可以通过这个动态代理对象随意调用接口中定义的方法了。

接着调用之前接口中定义的getAppData方法,返回一个Call>对象,接着调用该对象的enqueue()方法,Retrofit就会根据注解中配置的服务器接口地址进行网络请求(http://10.0.2.2/get_data.json),响应的数据回调到enqueue()方法中传入的Callback实现中。注意这里的网络请求是异步的,也就是发起请求的时候,Retrofit就会自动在内部开启子线程,回调到Callback中后,Retrofit又会自动切回主线程中,接着在Callback的onResponse调用response.body()方法得到Retrofit解析后的对象。完成后,在Manifest.xml文件中添加网络请求权限:

 <uses-permission android:name="android.permission.INTERNET"/>

效果如下图所示:
在这里插入图片描述

当然上面的请求只是最简单的情况,因为接口不可能永远是静态不改变的,例如以下情况:

GET http://example.com/<page>/get_data.json

在这种情况下,随着page的变化,服务器返回的数据也不相同,那么接口应该有如下写法:

public interface ExampleService {
    @GET("{page}/get_data.json")
    Call<Data> getData(@Path("page") int page);
}

在@GET注解中,使用了一个{page}占位符,然后在getAppData中添加了一个page参数,并使用@Path(“page”)注解来声明这个参数,这样当调用getAppData方法发起请求时,Retrofit就会自动将page参数的值替换到占位符的位置,从而组成一个合法的请求地址。

另一种情况下,服务器还会要求我们传入一系列参数,例如:

GET http://example.com/get_data.json?u=<user>&p=<password>

那么我们可以使用@Query:

public interface ExampleService {
    @GET("get_data.json")
    Call<Data> getData(@Query("u") String user,@Query("p") String password);
}

这里在getAppData方法中添加了user和password两个参数,并使用@Query注解对它们进行声明,这样当发起网络请求的时候,Retrofit就会自动按照参数GET请求的格式将这两个参数构建到请求地址中。

上面的例子都是GET请求,那POST请求该怎么写?例如:

POST http://example.com/data/create 
{"id":1,"content":"hello world!"}

我们可以借助@Body注解实现:

public interface ExampleService {
    @POST("data/create")
    Call<ResponseBody> createData(@Body Data data);
}

在createAppData方法中声明了一个Data类型的参数,并加上了@Body注解,这样当Retrofit发出POST请求时,就会自动将Data对象中的数据转换成JSON格式的文本,并放到POST请求的body部分,服务器接收到请求后只需要将这部分数据解析出来即可。

最后,有时候发送网络请求的时候需要我们在请求的header中指定参数,例如:

GET http://example.com/get_data.json
User-Agent: okhttp
Cache-Control: max-age=0

这些header参数其实就是一个个的键值对,可以使用@Headers注解进行声明:

public interface ExampleService {
    @Headers("User-Agent: okhttp","Cache-Control: max-age=0")
    @GET("get_data.json")
    Call<Data> getData();
}

如果想要动态指定,使用@Header注解:

public interface ExampleService {
    @GET("get_data.json")
    Call<Data> getData(@Header("User-Agent") String userAgent,@Header("Cache-Control") String cacheControl);
}

当发起网络请求的时候,Retrofit就会自动将参数中传入的值设置到User-Agent和Cache-Control这两个header中,从而实现动态指定header值的功能。

8. Okhttp3

这里列举异步GET请求步骤如下:new OkHttpClient、构造Request对象、通过前两步中的对象构建Call对象、通过Call#enqueue(Callback)方法来提交异步请求,同步请求的方式和异步是相似的,只不过只是最后一部是通过 Call#execute() 来提交请求。

示例:点击按钮后显示图片,由于是从网络上下载图片,因此采用了异步消息机制

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button button;
    private ImageView imageView;
    private static String image_path = "http://ww4.sinaimg.cn/bmiddle/786013a5jw1e7akotp4bcj20c80i3aao.jpg";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindViews();
        button.setOnClickListener(this);
    }

    private void bindViews() {
        button = findViewById(R.id.button);
        imageView = findViewById(R.id.imageview);
    }

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            Bitmap bitmap = (Bitmap) msg.obj;
            imageView.setImageBitmap(bitmap);
        }
    };
    public class dlThread implements Runnable{

        @Override
        public void run() {
            OkHttpClient okHttpClient = new OkHttpClient();//单例模式
            Request request = new Request.Builder().url(image_path).build();//建造者模式
            okHttpClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {}

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    InputStream inputStream = response.body().byteStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    Message msg = Message.obtain();//推荐用obtain()获取Message
                    msg.obj = bitmap;
                    handler.sendMessage(msg);
                }
            });
        }
    }

    @Override
    public void onClick(View v) {
        switch(v.getId()){
            case R.id.button:
                new Thread(new dlThread()).start();
                break;
        }

    }
}

原理大致如下:
安卓网络编程学习之HTTP(S)_第18张图片
创建好okHttpClient(单例模式)以及Request对象(建造者模式)后,调用okHttpClient的newcall(request)的enqueue方法后,以异步为例,会调用分发器Dispather的enqueue方法,Dispather的enqueue方法会将AsyncCall入队并提交给线程池执行,线程池中的线程又调用Call的execute()方法,Call的execute()方法又会调用getResponseWithInterceptorChain()方法,通过一系列拦截器(责任链模式)对请求进行处理之后发出该请求并读取响应。重要的几个拦截器如下:

  • RetryAndFollowUpInterceptor:检验返回的 Response ,如果没有异常(包括请求失败、重定向等),那么执行 return Response, return 会直接结束循环操作,将结果返回到下一个拦截器中进行处理。检验返回的 Response ,如果出现异常情况,那么会根据 Response 新建 Request,并且执行一些必要的检查(是否为同一个 connnetion ,是的话抛出异常,不是的话是否旧的 connection 的资源,并新建一个 connection),进入死循环的下一次循环,那么此时将进行新一轮的拦截器的处理。
  • BridgeInterceptor:将用户构建的 Request 请求转换为能够进行网络访问的请求。
  • CacheInterceptor:实现缓存功能的拦截器。
  • ConnectInterceptor:打开一个面向指定服务器的连接,并且执行下一个拦截器。

之后有时间再学习。

参考:

  • OkHttp原理解析
  • OkHttp3 中几个拦截器基本功能介绍

你可能感兴趣的:(安卓开发)