15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > 关于安卓开发的一些你必须要掌握的网络知识(一):网络基础与网络框架Ok

关于安卓开发的一些你必须要掌握的网络知识(一):网络基础与网络框架Ok

时间:2023-05-28 09:30:01 | 来源:网站运营

时间:2023-05-28 09:30:01 来源:网站运营

关于安卓开发的一些你必须要掌握的网络知识(一):网络基础与网络框架OkHttp: 为了能把OkHttp以及作为安卓开发必须掌握的关于网络知识能写清楚,本文从网络知识讲起,然后会手动仿照OkHttp的源码结构来写一个它的架构,然后结合OkHttp的源码来为大家讲解。相信大家看完本系列后,除了能搞透OkHttp之外,还会对网络框架有一个更深的认识。

一、HTTP协议

说到HTTP协议,其实有很多安卓开发者对这个HTTP协议也是一知半解,所以在做网络请求这块时,解决问题时都是凭着网上搜的答案加上自己的感觉来操作,就算问题最后解决了,也只是刚好运气好让你撞对了。因此很有必要讲一下这个HTTP协议。

其实不外乎就是两点:1)浏览器上地址栏输入网址,然后就打开网站 2)开发时候你使用网络框架去请求后端,然后后端返回数据给你

以上这两个地方其实就是使用了HTTP协议。

HTTP全称超文本传输协议,也就是说可以指向其他文本的链接,那么我们输入的网址以及我们对服务器发送的请求链接,其实就是超文本链接了,那么整个传输过程就是进行HTTP协议的过程,而文本你可以理解就是一段html文本,用来展示页面。而在安卓开发中,服务器返回的一般是json字符串,或者xml文件等。

1.工作方式

如图所示,当你输入了网址之后,点击回车键,浏览器会把这串URL字符串会被封装成HTTP报文,传到服务器上去。我们来分析一下URL字符串:

可以看到,假设我们要访问的网址是http://www.baidu.com/user?username=James,URL则分为三个部分,而这三部分被封装成报文,它的结构是这样的:

虽然说GET方法也有Body,但一般请求时,都不会加的,而POST才会有Body。而这些请求行、Header和Body又是什么意思,接下来会一一为大家讲解。而现在继续看回下一个知识点:服务器收到请求之后会发送响应报文给客户端,响应报文的结构如下图所示:

跟请求报文是有区别的,状态行是协议版本、状态码和状态信息,响应头部Header也有,而Body里就是响应数据,这些Header里的数据等概念同意会在接下来的段落里讲清楚。

2.报文结构

2.1请求方法

在请求报文中的请求行里,首先是一个请求方法,也就是我们平时看到的GET或者POST,它其实是有四种:

1)GET:获取资源,没有Body,直接通过URL字符串请求发送给服务器

2)POST:增加或修改资源,有Body。Body里存放的就是要修改或者增加的数据是什么

可以看到,上面这个请求报文,从请求行中可以看到它的请求方法是POST,然后就是Headers,再到最后的Body里的数据是一个类似key-value形式连起来的字符串数据就是表示要增加或修改资源的参数。

3)PUT:修改资源,有Body。它跟POST很像,区别在于PUT幂等,也就是请求多次,它都是按回你最终那次为准,而POST则不幂等,每发送一次,就会修改一次,而GET跟PUT也是一样,每次发送同样的请求,也都是以最后那次为最终结果。

4)DELETE:删除资源,没有Body。很好理解,就是请求服务器删除该资源,也是幂等。

5)HEAD:跟GET很像,不过唯一区别就是HEAD发送请求后,服务器返回的响应报文中是没有Body,一般HEAD请求都是用来作试探性请求,提前先探测来获取到一些前期准备工作相关的信息。

2.2状态码

这是响应报文中的状态行里出现的,对结果作出的一个描述,比如200表示获取成功,而它其实可以按照开头数字进行划分:

1)2XX:一般表示请求以及响应成功

2)4XX:客户端错误,可能发送的请求或者请求参数有错等

3)5XX:服务器错误,可能服务器那边在处理请求时出错等

4)3XX:重定向。比如301(当用户以URL1进行请求时,服务器会返回301,然后客户端接收到之后会自动以URL2来二次请求)

5)1XX:临时性消息。比如当客户端要使用HTTP2协议来请求时,会先在Headers头里加一个数据用来询问服务器是否支持,然后服务器收到这个请求报文后,如果支持的话,就会在响应报文里发送的状态码变为101发给客户端。还有一种情况,当客户端要进行分段请求,那么也会在Header里加入这个数据表示分段请求,然后发给服务器后,服务器如果表示可以,就会返回状态码100,客户端接收到之后会继续进行请求。

所以这些状态码都能帮助客户端和服务端了解网络情况以及方便调试网络请求。

2.3Header

HTTP消息里的元数据,好像很抽象,其实就是HTTP消息里掺杂另外一些消息,你可以理解为进一步向服务器(或客户端)说明一些情况。比如上文中我们介绍状态码时的100和101,客户端当要对服务器说明自己要发送的请求是使用HTTP2或者分段请求时,就会在Header里添加对应的数据,这个Header就是元数据了。

常见的Header

1)Host:服务器主机地址。其实服务器寻址在发送请求那刻起(DNS--IP层做的事)的时候已经寻到了要给哪个服务器发送请求,但还是需要发送这个Header里的Host给服务器是因为一个IP地址可能会映射好几个服务器主机地址(因为虚拟主机地址的存在),因此还是要添加这个说明来让双方进一步参考。

2)Content-Length:内容的长度(字节),可以让服务器在长长的数据流里知道真正的请求数据从哪里开始,到哪里结束

3)Content-Type:内容的类型。这个header很重要,因为它表示发送(或返回)的内容是什么类型,这个内容的类型非常重要:

--text/html:html文本,发送(或返回)的数据是浏览器页面

--application/x-www-form-urlencoded:普通表单,encoded URL格式。就是Post的时候body里的要发送的参数,它是key1=value1&key2=value2的形式

--multipart/form-data:多部分形式,一般用于传输包含二进制内容(图片、文件等)的多项内容

该请求报文的格式从上到下,请求行,然后到Header,Header里的Content-Type指定了multipart/form-data,表示要发送的形式是多部分的,而这里设置了一个boundary属性,它起到一个边界线作用,用来分割要发送的各个部分的数据,而我们这里要发送的数据,首先有普通表单数据的参数name,它的值这里是James,而接下来要发送的数据则是一个头像图片,名字叫James.jpg,所以FEA45V....这一连串的二进制数据就是这张头像图片,每部分要发送的数据都用boundary来限定好,这样看起来简洁清晰,而且能被服务器识别与分界。当要传文件或者图片的同时,也要把普通的表单参数发送给服务器,就可以使用这张形式。

--application/json:json形式,用于Web API的响应或者POST/PUT请求

很好理解,请求的时候,传的参数是json字符串。

--image/jpeg/application/zip...:单文件,用于Web API响应或POST/PUT请求。它是用来传输文件等最高效的方式,不过需要服务端学习与了解此种方式。

非常简洁,Content-Type里设置image/jpeg,然后body里传的是图片数据。

4)Transfer-Encoding:chunked,服务器分块响应,有时服务器可能不能一下子就把一段数据完整传输给客户端,因此把已经算好的数据先分块传输给客户端,剩下的数据段继续分块发送给客户端。此时Body长度不能确定,Content-Length不能使用。

0表示内容结束,已经传完。

5)Location:重定向的目标URL,在重定向的时候它的值就是那个最终要跳转的URL值

6)User-Agent:用户代理,不同的代理可能会导致解析的内容渲染都不一样。常见的代理有:

7)Range/Accept-Range:指定Body的内容范围。当服务器支持客户端分段接收数据时,就可以设置这个header里的Accept-Range的值来指定客户端要接收的body数据要接收多少。Accept-Range的值是bytes字节,设置Accept-Range:bytes=1024,这样客户端发送给服务器之后,服务器就会返回1024字节的数据给客户端。

8)Cookie/Set-Cookie:发送Cookie/设置Cookie(下面会讲解)

9)Authorization:授权信息(下面会讲解)

10)Accept:客户端能接收的数据类型,比如text/html

11)Accept-Charset:客户端接收数据的字符集,比如utf-8

12)Accept-Encoding:客户端接收数据的压缩编码类型,比如gzip

以上便是一些常见的以及重要的header,还有一些header如果感兴趣的可自行搜索,这里就不作讲解了。

2.4Cache

cache是缓存,第一次请求该接口获取到数据后,将这些数据缓存起来,等到下次如果再需要该接口的数据时,可以直接使用缓存起来的数据,而不用再耗费性能重复请求了,而缓存的数据根据缓存时的时间的新鲜程度来分等级,优先级高的缓存数据则可以不被清除,而时间比较旧的缓存数据则要清除,再次请求接口,然后更新为新鲜数据。

那设置缓存的方法就是设置Header,有以下几个:

--Cache-Control:

1)设置no-cache:表示要缓存数据,然后下次需要使用该接口的数据时,需要请求询问服务器这些缓存数据是否失效

2)设置no-store:不需缓存数据

3)设置max-age:当要再次使用该接口数据时,此时的时间如果是在max-age这个时间值之前,缓存的数据无需询问服务器是否失效,直接使用便可。当超过了这个时间后,缓存的数据就表示失效。

--Last-Modified if-Modified-Since :表示服务器发送给客户端响应时告诉客户端下次如果需要使用这个接口数据时要询问缓存数据在这个时间段前是否改过这个数据的内容,没有改过,则继续使用缓存数据。

--Etag if-None-Match:相当于一个服务器针对这个数据而创建的一个对应指纹数据,发给客户端,而客户端下次需要使用这个接口数据时,拿这个指纹数据询问服务器这个数据对应的这个etag判断是否相等,不相等则证明缓存数据失效,重新请求接口;相等则表示缓存数据没失效,可继续使用缓存数据。

--Cache-Control:private/public:表示请求发送给服务器这个过程的时候,一些中间节点是否可访问和操作这些数据,private则不能,public则可以。

二、加密

加密与解密也是网络请求中涉及到的知识点,非常重要。加密的两个因素:算法和密钥。算法即要怎么去算(比如使用加减乘除),密钥即用什么来算(比如用一串数字564846),这样也就是对564846进行加减乘除后变成另一个密文字符串。

1.对称加密

使用密钥和加密算法对数据进行转换,得到的无意义数据则为密文,使用密钥和解密算法对密文进行逆向转换(解密),可以得到原数据。

A对数据进行加密后,然后把密文发给了B,B拿到密文后,用同样的密钥使用解密算法进行解密,就得到原数据了。而对称加密的经典算法有DES和AES等。

2.非对称加密

使用公钥(对外公开的)对数据进行加密得到密文,使用私钥(私有的,只有自己拥有)对数据进行解密得到原数据。

是不是觉得很神奇,为什么加密和解密用的都是同一种算法,但是经过两种密钥之后,又能还原,其实非对称加密其中一个原理是利用了计算机的进制计算会溢出,然后取有效的位数,从而能达到还原,如果感兴趣的可以自行去搜搜,这是算法,不是本文的重点。

这样的话,非对称加密要比对称加密安全得多,因为即使第三方截取到发送方和接收方的各自的公钥也没用,因为解密必须是自己的私钥才能解密。

3.数字签名

如果用私钥加密数据,然后用公钥解密,可以把密文还原为原数据吗?

如图所示,假设我们把中间红色定为原数据,那么此时只看右边的话,就是用私钥进行加密原数据的过程,而右边的数据为密文,那么如果想解密,根据上文讲的非对称加密与解密原理,是不是就应该用该密文使用公钥(因为要两把不一样的密钥)配合(相同的)加密算法去解密,那这个过程不就是左边的数据到红色数据的过程吗,因此用私钥加密的密文,是可以用公钥还原为原数据的。不过正常情况下,我们应该是使用公钥加密,私钥还原,这样才是正确的非对称加密做法。

不过用私钥加密,公钥还原的这一特性,被用来应用在数字签名上。数字签名是用来验证该数据是否是发送方发送的,而不是其他第三者发送的。

因为能用公钥还原到的这个数据肯定是这个发送方发送的,因为这个签名数据是发送方用它自己的私钥加密而得的,私钥只有这个发送方才有的,任何第三者都没有的。

当然,你怎么知道你用公钥还原的这个数据就是发送方发的呢,即使也是看起来是很正常的数据,也保不齐是第三方用它的私钥加密而得的,因此可以用加密+签名的方式:对原数据先使用加密算法,然后用对方公钥加密成密文,然后再用自己私钥加密原数据为数字签名,这样对方要得到原数据就要先使用自己的私钥来解密,而要验证这个数据是否是发送方发送的,那就使用发送方的公钥进行验证

这个数字签名能让第三方不能伪造成发送方发送数据了,因为多了验证数字签名这一步,签名只能是用私钥来加密的,而私钥只有发送方才有的,第三方用它自己的私钥加密一个签名数据发给接收方,而接收方用发送方的公钥进行验证时,得到的数据就跟第三方伪造成发送方时发的原数据不一样,也就能识别出来。

非对称加密的经典算法有RSA和DSA。

补充:可能会有人将密钥和登录密码两者搞混,其实很好理解,密钥(key)相当于一把钥匙,有它才能开门(加密或者解密),而登录密码只是一个用来证明“你是你”的通过码(password)。

4.Base64

它是一种编码,编码,就是按照某种算法把一个字符换算为另一个字符。而Base64就是将二进制数据转换成由64个字符组成的字符串的编码算法。它有个码表:

总共64个二进制数字,分别对应A-Z,a-z以及一些其他标点字符等64个,然后根据这个码表来算:

以6位为一个字符来进行分割,因此原先三个二进制,就变成四个6位的二进制数字,也就把原来只有三个字符变成如今的四个字符,由此可见,转成Base64的数据会变长。Base64主要用于一定要上传字符串的场景,Base64其实不高效也不安全。人人都知道怎么计算,而且会将原数据变长。

5.URL encoding

也是其中一种编码,将URL中的一些字符使用百分号“%”等特殊字符进行编码,为的是消除歧义和避免解析错误。

可以看到,地址栏输入的是一个中文,然后实则发送过去时会将中文字符编码为一串带着百分号的字符串。

6.压缩与解压缩

压缩是把数据换一种方式来存储,以减少存储空间,而解压缩则是把压缩后的数据还原为原先的数据的形式再以使用。常见的压缩算法有DEFLATE、JPEG和MP3等。

压缩属于编码,因为编码的本质是将A数据转换成B数据,然后B数据通过逆运算后能还原回A数据,而压缩符合这个特点。其中运用编码最多就是媒体数据,比如音频和视频等。

图片、音频和视频经常会用到编码来转换成另一种数据,然后再对该数据进行解码来还原回原数据。比如把图像数据(argb)写出为JPG、PNG等编码格式。

而经典的视频数据原始数据是yum数据,然后经过编码后变成h264码流,然后再进行封装编码成mp4格式数据。其中,h264变成mp4这个过程其实是有压缩的,因为一堆码流,肯定是有重复性的字符的,因此可以使用压缩算法来进行压缩,这样能减少冗余重复的字符,从而达到减少存储空间的目的。而要把编码后的数据还原为原数据,则进行逆运算,也就是视频解码。

7.序列化与反序列化

序列化与反序列化也是编码,序列化是指把数据对象(一般内存中,比如JVM中的对象)转换成字节序列(方便网络传输)的过程,而反序列化就是把字节序列重新转换回内存中的对象

8.Hash

把任意数据转换成指定大小范围(通常很小)的数据。用于摘要、数字指纹等。经典算法有MD5、SHA1、SHA256等。

Hash算法要求算出的值碰撞率要低,也就是要具有唯一性。当要重写某个类的equals方法时,也要重写hashCode方法,因为hashCode相当于一个对象的内存地址,唯一标识,一定要碰撞率小,这样才能保证不会出现明明equals方法是不相等,而由于碰撞率高两个对象的hashCode都一样,这样就出问题了,所以用到hashMap时会出错,hashMap就是用到了hashCode的。

当然也可以用hash让数据具有隐秘性,比如让“James”去作MD5算一下,这样就会变成“sdads...”类似的无意义字符串。不过它也是可以被人“撞库”,意思就是现在很多人会专门记录一些词的MD5之后的字符串是多少,记录起来,形成一张字典,然后拿着这张字典,一个个地去试错,这样有可能被它找到正确的数据。

因此有种措施,就是“加盐”,也就是定义好另一个字符串,比如这个盐是“jjj”,那么在对“James”进行MD5计算时,可以把“James”加上“jjj”后再进行MD5计算,这样安全性会高很多,而这个盐“jjj”只有服务器自己存着,不能公开出去。到了客户端把数据加盐然后再MD5之后,传给服务器时,服务器就可以从自己本地把盐拿出来跟“James”进行MD5后得到的值跟客户端这个值做对比,如果一样,则证明的确是客户端传过来的,而不是第三方伪造的。

Hash不是编码,它是抽取原数据的一些特征经过计算而得出的一个很小的数据,它不能逆运算后转换回原数据的,因此它不是编码。严格上来说,它也不是加密,因为加密也是可逆转的,有加密就会有解密。

Hash可以用到非对称加密当中去,原非对称加密中数字签名是跟原数据一样大,这样就会造成数据容量很大,而为了减少数据量,可以把数字签名用原数据进行hash,变成摘要,这样数字签名的数据大小就瞬间减少很多:

以上过程是在原数据没有进行非对称加密的情况下进行签名验证,如果原数据是非对称加密了的话,则用私钥对密文进行解密,得到原数据之后进行Hash,得到摘要数据,然后再用对方公钥对签名后的摘要进行解密,得到待验证的原摘要,最后就拿着它跟摘要进行对比,两者如果相同,那就验证成功。

三、登录授权

登录其实也是授权,把属于你自己的权限授予你自己,然后就可以获取某些信息,可以操作某些事情,而授权就是把权限授予给你,让你能获取某些信息,可以操作某些事情。

登录与授权的使用方式:Cookie与Authorization

1.Cookie

Cookie的起源是来源于早期电商网站的购物车功能,因为购物车是属于用户收藏的功能,并未真正购买,因此电商平台认为购物车的数据不应该存在他们自己本地上管理,而是交由浏览器开发者保存与管理。而这一功能就叫Cookie。

首先当客户端往购物车存一本书,则往服务端发送请求,服务器收到请求后,就在返回包的header里也就是Set-Cookie:shop="book=1"(shop="book=1"只是一个例子,可根据实际开发自己定值)设置好后给客户端:

就这样,客户端的Cookie就会存下shop="book=1",下次再访问该网站http://shop.com时,客户端(浏览器)根据这个shop="book=1"的Cookie设置更新购物车,显示已经收藏了一本书,而接着如果想继续发送一个请求是在购物车里添加一部计算机,除了在body把计算机post过去之外,还在header里设置Cookie:shop="book=1"发送给服务器:

然后服务器接收到请求后,再次去设置header的Set-Cookie:shop="book=1&computer=1"返回给客户端:

客户端接收到后就把Cookie更新为shop="book=1&computer=1"。后续要对账购物车里的书和计算机做操作,就可以继续把Cookie传给服务器,让服务器作出相关响应。

Cookie除了可以做购物车之外,还可以用它来管理登录状态

当客户端发送登录请求给服务端后,服务器便记录该用户以及设置一个会话id给Set-Cookie,Set-Cookie:sessionId=James&123456(这里的值也是根据实际开发要求自己定义),表示跟客户端此时的用户建立里一个会话,返回给客户端:

客户端把这个Cookie记录为sessionId=James&123456,然后客户端再对服务器进行其他请求时,也把这个header里的Cookie值也发送给服务器:

服务器拿到这个请求后,便会对比自己记录的关于该用户的Cookie值,匹配一致后就搜寻该Cookie对应的用户名,就可以根据该用户名去返回对应的数据

而此时客户端如果想继续发送购物车请求时,可以再添加Cookie值,两个Cookie值可以叠加,互不干扰:

这样之后每次请求,客户端的header里的Cookie值就可以有多个,然后服务器就根据这些Cookie值来作出相应的返回。

Cookie还可以用来管理用户偏好以及分析用户行为等,原理跟上面的登录会话一样

服务器设置了一个Cookie值,里面包含一个clientId和一个链接,该链接其实是一个第三方网站(专门用来记录用户的浏览网址信息),然后服务器把Cookie返回给客户端,这样客户端的Cookie就记录这些数据,等下次再访问别的网站时,因为客户端的Cookie里是有那个网址的,因此会跳转到那个网址(但这里因为我们设置的是一张图片网址,因此对于用户来说只会弹出一张图片),然后触发到图片的网址也就是第三方服务器记录是从哪个网站触发图片的(因为Cookie记录的信息),这样也就知道你上一个浏览的网址是什么,进而逐渐形成了一条用户行为链,进而可以精准推荐相关广告给用户。

Cookie的安全问题:

XSS(跨站脚本攻击),拿到客户端的Cookie,从而获取你的用户信息。在cookie后加上HttpOnly可防止。

XSRF(跨站请求伪造),伪造成真实访问网站。也就是让用户跳转到别的网站(比如你前些天登录过的银行网站),去进行取钱的操作请求,因为你有cookie,会记录银行取钱时这一过程,因为通过这个cookie值就可以让用户进行这个操作。防止这个漏洞的方法可以在请求网址时在cookie后加上Referer,它的值是你要跳转的网址,这就可以让浏览器知道你是从哪个网站跳转来的,进而判断是否信任。

2.Authorization

它比Cookie要安全,有两种方式:

(1)Basic方式, 直接设置header里的Authorization值, Authorization:Basic<<username:password(Base64ed)>> ,意思就是把用户名和密码加在一起然后进行Base64转换,变成一串无意义的字符串,然后设置为Authorization:Basic sbMidasd......可以了。然后服务器接收到后匹配成功就表示授权成功。

(2)Bearer方式, 其实就是持有Token令牌的人 。 Authorization:Bearer<< bearer token>> 这个token就是授权方给客户端的。

Bearer方式还有OAuth2,比如第三方请求对方授权:

然后http://csdn.com返回个授权码给第三方网站,然后第三方网站把这个授权码即Authorization code传给这个第三方网站的服务器:

服务器会把Authorization code以及当初http://csdn.com很早那次请求就给第三方的client_secret(证明我是我)一并发给csdn.com:

然后http://csdn.com确定后没问题就返回token给第三方的服务器:

就这样,OAuth2流程到这里就结束了,接下来如果第三方要进行一些后续操作,就通过它的服务器去请求csdn.com,把令牌设置在header的Authorization:Bearer里,然后进行请求。

http://csdn.com就会返回相应的信息给第三方的服务器。

微信登录属于使用第三方登录,它也是OAuth2的流程,比如我现在在我的软件上点击使用微信登录,那么微信就会跳出授权页面,点击确认,微信就会返回Authorization code给我的软件,然后接下去的步骤就跟上面一样的步骤,这里就不再重复说了。

所以其实按照严格来说,客户端不应该持有token的,持有token的事应该是客户端的服务器持有的,如果由客户端持有token,那就浪费了OAuth2的流程,token由客户端持有也不安全。

如果你不使用OAuth2,那就是自己软件访问自家服务器的时候可以不用按照OAuth2的流程,服务器直接返回token给客户端,客户端持有token,然后以bearer token方式去请求服务器后续操作和数据。

四、TCP/IP协议族

一系列协议组成的一个网络分层模型。当我们把一个数据从网络中发送给接收方时,因为网络是不稳定的,不能保证传输一定成功,所以当传输过程中数据丢包,则就要重新发送,但这样的话,如果传输的数据很大,因为重传的原因,导致最终传输的时间就变长,那这样网速就变差了,一系列问题也会产生。

因此,把传输的数据分块来传输

假设第二块B传输失败,那就重新传第二块,其他数据块成功传输,这样速度上也就是只多传了一次B,而且数据量还是1/4,比起原来的一整块传输失败后要重新传就快很多了。

不过,还可以优化,那就是分层,因为网络传输是一个很复杂的过程,需要做很多事,比如设置配置,确定各种地址和选择那条传输路径等,如果所有事情都交给一层来做,那也是很耗费时间以及很混乱,因此,可以分层,每一层负责做其对应的事情,然后把数据块一层一层往下传,直到所有的准备工作都做好了,数据块也就封装好了,就交给最后的线路去传给接收方,然后接收方把原始数据一层一层往上解封装,最后传到服务器或者接收方上使用。这样一个网络传输过程就变得简单和高效。

分层模型:也就是常见的OSI模型,总共有7层

1)应用层:应用层是进行传输工作,针对软件层面来传输的,比如浏览器进行HTTP

2)表示层:对传输数据进一步封装,比如**加密**,压缩等

3)会话层:机器与机器之间通过端口建立会话

4)传输层:TCP、UDP协议使得两台机器通信

5)网络层:由上面各层传下来的数据打包成数据包,里面包含IP地址等信息,然后通过路由器寻找最优路径进行传输

6)数据链路层:进一步封装数据包,里面包含MAC物理地址,通过交换机进行传输

7)物理层:数据包转换成比特流数据,通过网线或光纤进行传输

而基于以上模型又演化出TCP/IP模型:应用层、传输层、网络层和网络接口层

它把原先OSI模型中的应用层、表示层和会话层合并为应用层,而数据链路层和物理层则合并为网络接口层,其他层不变,而各层负责的职责仍然不变,合并的层则融合了之前的功能。

1.TCP连接

TCP连接是传输层协议,实现双方进行联通。

TCP连接的建立是一个称为“三次握手”的过程,客户端先将标志位SYN设为1,随机产生一个值Seq=X(自定义值),然后将该数据包发送给服务器,等待服务器响应;服务器接收到数据后将标志位SYN设为1,ACK=X+1,然后随机产生一个值Seq=Y(自定义值),并将该数据包发送给客户端确认连接请求;客户端收到服务器的确认后,检查数据以及序号,如果正确则将标志位ACK设为Y+1,Seq=Z(自定义值),发送给服务器,服务器检查各项值,如果都正确则连接建立成功,客户端与服务端之间就可以开始传输数据了。

每一次的箭头表示一次握手过程。

TCP连接的关闭(TCP连接很耗费资源,既然不用需要再通信的话,就要关闭连接):四次挥手,关闭连接的时候,客户端此时会发送一个断开连接的请求给服务端,服务端接收该请求后,则表示同意断开,然后就返回一个消息给客户端,客户端拿到确认消息后,则就再一次发送真的断开消息给服务端,表示断开,而服务器拿到该消息后则就返回消息给客户端,表示关闭成功,具体过程还需要双方设置各种值以及确认这些值是否正确,如图所示:

也是跟三次握手一样,一个箭头表示一次挥手过程。

当使用的是HTTP1.1协议时,客户端跟服务端连接成功后,往后的网络请求中,都不用再重新进行三次握手,直接进行会话便可,而等到不再连接的时候,则会四次挥手断开连接。这样就不用像1.0协议时那样,每次都要重新进行三次握手。

长连接就是强制这个连接不被关闭,由于服务器在一段时间如果发现某个端口没有消息传送了,就认为不再连接,因此会关闭它,因此长连接需要心跳方式来实现,每过一段时间会发送心跳包消息给服务端,这样就能一直保持连接。

2.HTTPS

就是HTTP加SSL,SSL就是一个安全层的意思,就是在HTTP之下增加的一个安全层,用于保障HTTP的加密传输。它的本质是在客户端和服务器之间协商出一个对称密钥,每次发送信息之前内容加密,收到之后解密,达到内容的加密传输。这里不用非对称加密是因为它没对称加密快。

HTTPS连接过程:

1)客户端请求建立TLS连接(属于TCP连接)

2)服务器发回证书

3)客户端验证服务器证书

4)客户端信任服务器后,和服务器协商对称密钥

5)使用对称密钥开始通信

这里的服务器证书是包含服务器的公钥以及服务器地址等相关服务器信息,里面有一个很重要的数据,就是证书的签名,结合我们前面讲到的知识可以知道,这里的证书签名就是我们客户端用来验证这个服务器证书的真实性,也就是证明服务器是服务器的证据。那客户端怎么验证这个证书签名,那就要用这个证书里另一个数据,也就是证书机构公钥来验证,用公钥去计算这个证书,得到一个原数据,然后客户端本身也有该证书的,因此也用那个公钥去计算客户端的这个证书,得到的数据跟服务器这个数据进行对比,如果两者是一样,则证明这个服务器证书的真实性。当然,这样也是有局限性,因为这也只能证明是签名是来自于这个机构,但如果这个机构也是第三方伪造的呢,那这个证书机构也需要提供它的签发方的公钥和其他相关信息来验证,因为签发方证书是属于根证书,根证书是比较安全和权威的,它是操作系统被创建时一并存有的,属于微软等权威机构的。

客户端信任服务器后,就会发送Pre-master Secret给服务器,然后再通过Pre-master Secret和随机数生成Master Secret,然后再通过Master Secret计算出客户端加密密钥和服务器加密密钥以及客户端和服务端的Mac Secret,这四个数据都是让数据真实性变强,防止被伪造。

最后要注意的是,在Android中,有时不能使用https是因为证书信息不全或者系统没有及时更新根证书等,也有可能是因为用的是自签名证书,因此可以自写证书验证过程去解决。

五、OkHttp

OkHttp是对于Socket(基于TCP通信,对TCP协议的封装)的封装的网络框架。所以,使用方便,效率也好。

补充:作为面试时被问得最多的就是,为什么要使用OkHttp时,可以这样作答:Xutil能支持网络请求,又可以加载图片,也可以操作数据库,但就我个人而言,我觉得一个网络框架应该就只专注于网络请求就好了。Retrofit的确是使用方便,但底层是对OkHttp的封装,所以我觉得既然如此,我也就没去用了。Volley对于图片的支持不是很友好,所以就没去了解。OkHttp是对Socket的封装,更贴近底层,所以使用它也意味着我可以更好去了解整个网络底层,这样也能提高我解决网络问题的能力。

1.源码分析

OkHttpClient和Request对象分别构建好之后,便交由Call实现类RealCall的enqueue方法去请求并得到返回数据。所以重点跟踪RealCall的enqueue方法:

然后调用了dispatcher的enqueue方法:

可以看到,首先进行判断,如果当前运行队列(存储的是要执行的请求)中的任务数小于64(maxRequests),并且同时访问同一台服务器的请求小于5(maxRequestsPerHost),就将call请求放入运行队列,否则就放入准备队列(存储的是待执行的请求),加入到运行队列(runningAsyncCalls)中后,则通过线程池执行任务,即AsyncCall的execute方法:

重点是Response response = getResponseWithInterceptorChain()。就是这句,获取请求返回的数据。在讲getResponseWithInterceptorChain()前,有些知识点应该要先了解清楚。

2.构建者模式

什么是构建者模式,它是典型的设计模式之一,使用流式的方法去构建对象,在此过程中可以让用户自己选择构建过程中对属性设置默认值,当然还有另一个优点,就是没有设置默认值的属性其实在底层里可以由架构师去给它设置好,这样用户即便没有设置,也有默认值,这也是为什么构建者模式在创建架构的时候会被大家经常使用。

现在有个例子,那就是房主人要建造一个房子,找建筑师建,而建筑师则找建筑工人去建,建筑师会先在图纸上画好一些默认值,然后给房主人看,房主人则可以根据自己的意见来修改这些默认值,而不修改的话,则可以用回默认值,这样确认好之后,建筑师则可以拿图纸给建筑工人去建房子了。最开始当我(作为房主人)要在构建对象的时候,设置属性的话,就要频繁写些冗余的代码了

当构建好对象时,第一次设置好了值,但又想改动,则又要set多一次,所以,需要进一步进行改善,在设置的方法里返回this,即该对象本身:

这样在调用的时候就可以流式调用:

比起之前要一个一个去set,现在则可以一路set下去,要再次改的话,直接流式接下去继续set。

接下来我们要理清房子跟图纸的关系,因为在new HttpClient.Builder().build()这句代码中点进去看源码可知,HttpClient的内部类Builder里的属性跟HttpClient的全局变量属性是一样的:

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory { ... final Dispatcher dispatcher; final @Nullable Proxy proxy; final List<Protocol> protocols; final List<ConnectionSpec> connectionSpecs; final List<Interceptor> interceptors; final List<Interceptor> networkInterceptors; final EventListener.Factory eventListenerFactory; final ProxySelector proxySelector; final CookieJar cookieJar; final @Nullable Cache cache; final @Nullable InternalCache internalCache; final SocketFactory socketFactory; final @Nullable SSLSocketFactory sslSocketFactory; final @Nullable CertificateChainCleaner certificateChainCleaner; final HostnameVerifier hostnameVerifier; final CertificatePinner certificatePinner; final Authenticator proxyAuthenticator; final Authenticator authenticator; final ConnectionPool connectionPool; ... public OkHttpClient() { this(new Builder()); } OkHttpClient(Builder builder) { this.dispatcher = builder.dispatcher; this.proxy = builder.proxy; this.protocols = builder.protocols; this.connectionSpecs = builder.connectionSpecs; this.interceptors = Util.immutableList(builder.interceptors); this.networkInterceptors = Util.immutableList(builder.networkInterceptors); this.eventListenerFactory = builder.eventListenerFactory; this.proxySelector = builder.proxySelector; this.cookieJar = builder.cookieJar; this.cache = builder.cache; this.internalCache = builder.internalCache; this.socketFactory = builder.socketFactory; ... public static final class Builder { Dispatcher dispatcher; @Nullable Proxy proxy; List<Protocol> protocols; List<ConnectionSpec> connectionSpecs; final List<Interceptor> interceptors = new ArrayList<>(); final List<Interceptor> networkInterceptors = new ArrayList<>(); EventListener.Factory eventListenerFactory; ProxySelector proxySelector; CookieJar cookieJar; @Nullable Cache cache; @Nullable InternalCache internalCache; SocketFactory socketFactory; @Nullable SSLSocketFactory sslSocketFactory; @Nullable CertificateChainCleaner certificateChainCleaner; HostnameVerifier hostnameVerifier; CertificatePinner certificatePinner; Authenticator proxyAuthenticator; Authenticator authenticator; ConnectionPool connectionPool; ... public OkHttpClient build() { return new OkHttpClient(this); } 如代码所示,OkHttpClient的内部类Builder里的属性跟OkHttpClient的全局变量属性是一样的,在new OkHttpClient.Builder().build()调用后,先调用Builder的构造方法构建Builder对象,这时其实在给Builder对象里的属性赋值了,然后就是调用OkHttpClient的构造方法,把Builder里的属性赋值给OkHttpClient的属性,这样就等于把图纸的属性就能赋值给房子的属性了,所以Builder类其实就是图纸,而HttpClient则是房子。而在构造方法里最终返回的就是对象this,所以我们在new的时候不仅仅可以以流式去设置这些属性值,还能不设置,因为不设置的话就可以使用默认值。最后设置好属性值之后,就调用builder的build()方法把Builder里的属性赋值给OkHttpClient的属性,然后返回房子对象,即OkHttpClient对象。

总结一下,构建者模式就是一步一步地去设置,去添加,最终变成一个复杂的对象,这样内部数据过于复杂的时候,用户去创建该对象的时候,就显得简洁方便,而且拿OkHttpClient来说,它里面有各种复杂的对象和属性,涉及到网络知识的,所以用户在创建的时候并不会对所有属性都了解,从而也就不知道该设什么值好,而这样,就可以通过构建者模式很友好地给用户提前在底层里设置好默认值,这样用户创建OkHttpClient时就会很简单方便。可以传自己自定义的属性,也可以不设置而选择用默认值。当然,构建者模式如果说有缺点,那就是在属性这里,会显得冗余重复,因为房子定义一套属性,图纸上也定义了同意一套属性。不过相对于它的优点来说,还是可以忽略的。

3.责任链模式

现在我们再看回getResponseWithInterceptorChain()方法,点进去看它的详情:

代码不算很多,最明显可以看到有一个Interceptor的集合添加多个不同类型的Interceptor,最后返回Interceptor.Chain chain的proceed()方法。

其实这个Interceptor是拦截器,依次添加的拦截器是retryAndFollowUpInterceptor(请求重试和请求重定向);桥拦截器bridgeInterceptor(进行添加header,比如加密和压缩等);缓存拦截器(设置缓存策略的,假若满足条件,则使用缓存,然后就不走下一步拦截器逻辑,直接返回缓存回去);链接拦截器ConnectInterceptor(管理Socket的,去建立连接);最后还有一个拦截器就是callServerInterceptor(读取和封装返回的数据等)。

责任链的好处就是每个拦截器负责对应的工作,而一旦某个拦截器处理结果不成功的话,则可以把结果直接往前面一个一个返回,而后面拦截器不用继续工作。

这里的添加的拦截器的方式其实是用到了责任链模式,下面重点来说一下责任链模式。先定义一个父节点:

在里面定义了isIntercept变量,当isIntercept为true则自己处理,为false则让子节点处理。而addInterceptor()方法就是添加下一个节点作为自己的nextInterceptor节点,代码很好理解。然后依次创建子节点:

而此时依次调用它们:

可以看到现在是比较麻烦的,因为节点1要添加节点2,节点2要添加节点3,就要依次添加,然后从节点1开始执行。为了进一步优化,接下来定义一个父节点接口类:

两个参数的作用已经解释得很清楚,然后仿照OkHttp那样,也创建一个管理者类去管理节点并且实现IBaseInterceptor接口:

如上图所示,逻辑很清晰,用集合iBaseInterceptors添加一个个节点,然后doProcess()方法则通过每次index的自增,来获取到下一个节点,从而执行它的doProcess()方法,同时,把父节点也传过去。然后接着定义节点:

当子节点要处理则表示它要拦截处理,不拦截则调用传进来的父节点的doProcess()方法,这样就可以再调用父节点的doAction方法,此时因为index已经+1,导致调用的是下一个子节点的doProcess(),依次类推下去。

如图所示,直接调用chainManager(作为父节点)的doProcess()方法,第二个参数传的是chainManager,这样就可以一直遍历节点集合,取出下一个节点,执行它的doAction()方法,以此类推。

以上便是一个责任链模式,搞懂之后就可以分析OkHttp的拦截器了。

1)重试/重定向拦截器:要自定义的其实并不多,主要还是用于重定向和重连这块。

2)桥拦截器:是设置header属性等。

3)缓存拦截器:就是设置缓存策略,服务器会在某个时间段前才会去更新数据,那么这就意味着当客户端第二次再去请求服务器这段数据时,那服务器可以只返回一个时间戳给客户端,客户端可通过这个时间戳或者字段来跟自己本地缓存的作判断,如果符合,则可以继续使用本地缓存数据,不符合的才去请求服务器返回更新过的数据,然后又再一次缓存该数据和时间戳字段,进行下一轮的缓存策略。(这个就是上文中说到Cache时的缓存机制,忘记的可以回到上文去)

4)连接拦截器:初始化和准备好要Socket(就是进行TCP连接的框架,属于Java),供CallServerInterceptor使用。

5)CallServerInterceptor拦截器:跟服务器进行真正的交互逻辑,读取和封装数据在这里面进行。

getResponseWithInterceptorChain方法里最后调用了chain.proceed(),调用了它的实现类RealInterceptorChain的proceed方法:

可以看到,通过索引不断自增,然后获取下一个拦截器,调用它的intercept()方法,因为是父类引用指向子类对象,因此我们看看RetryAndFollowUpInterceptor的intercept()方法:

把父类chain赋值给realChain,所以中间调用的realChain的proceed方法其实就是上文讲解的那样它调用了父管理类节点的proceed()方法,然后索引值+1,调用下一个拦截器的拦截方法来做它对应的操作,然后把最终的response返回出去,中途如果某个拦截器不处理或者处理失败,则中途返回response,这就是责任链模式了。

4.线程池

现在可以知道,通过建造者模式去设置请求,然后通过责任链模式去发送请求,那么我再次回到enqueue方法,可以看到最终是ExecutorService线程池执行每个请求任务的:

可以看到SynchronousQueue队列是ExecutorService线程池它的内部队列,作用是当线程池的任务数已经满了,那么此时如果再有任务数进来,那就将它放入该内部队列里进行等待。该队列的特点是没有容量,要么0要么1,就是当把一个任务放入它里面时,别的任务就不能再添加了。要先把里面的任务移除掉,才能再添加。而这样就可以保证添加进来的任务可以马上执行,而不用再等待别的任务。而真正用来添加需要等待的任务则已经定义好了一个专门用来管理等待任务的等待队列,也就是上文提到的readyAsyncCalls。

继续看回execute()方法:

最后一句代码client.dispatcher().finished(),点进去看详情:

可以看到,它将已经处理完的call从运行队列中remove掉,然后执行promoteCalls()方法:

可以看到,它是在遍历等待队列readyAsyncCalls,然后取出任务,调用runningAsyncCalls.add()方法,将该任务添加到运行队列里,然后执行任务,即executorService().execute(call),然后就是这样循环下去,直到所有任务都执行完。这就是一个请求与得到响应的循环过程。

5.跟服务器交互过程

而真正与服务器进行交互是在ConnectInterceptor拦截器里,Socket的初始化在里面进行的。首先看它的拦截方法:

点击查看streamAllocation的newStream()方法:

可以看到一行关键代码findHealthyConnection方法返回一个RealConnection对象,我们看看这个findHealthyConnection()方法:

可以看到这里有一个类似于线程池的类对象connectionPool,再结合官方文档可以知道OkHttp在跟服务器进行连接交互的时候,其实也是启动了一个连接池,那么我们就去看看是不是就是这个connectionPool:

public final class ConnectionPool { /** * Background threads are used to cleanup expired connections. There will be at most a single * thread running per connection pool. The thread pool executor permits the pool itself to be * garbage collected. */ private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */, Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true)); /** The maximum number of idle connections for each address. */ private final int maxIdleConnections; private final long keepAliveDurationNs; private final Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { try { ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } }; private final Deque<RealConnection> connections = new ArrayDeque<>(); final RouteDatabase routeDatabase = new RouteDatabase(); boolean cleanupRunning; .... 可以看到,该类里有线程池executor,以及队列connections,里面装的是RealConnection,那我们再看看RealConnection类:

终于看到Socket了,该类果然封装了Socket。那么接下来就是看看connectPool是什么时候初始化的。通过查看它的构造方法可以查到,是在构造Builder类时初始化的:

所以当你想自定义该连接池时,可以在此步骤中进行。

那么到目前为止,可以总结两个部分:第一部分,就是OkHttp对于请求它是用线程池去执行。而第二部分,则是用Socket去跟服务器进行连接,也是用了一个线程池去处理。从而就达成两个轮询的闭环。

总算是讲完了,下一篇会继续连载关于网络优化以及抓包、统计网络流量等一些网络知识。







扫一扫 关注我的公众号

这里有你感兴趣的技术文与深度文

欢迎大家来投稿,分享你的文章!



关键词:网络,掌握,知识,基础

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭