当前位置: 首页 > 科技观察

迈向高潮:优秀Android程序员必知的网络基础

时间:2023-03-22 11:59:24 科技观察

1.前言网络通信一直是Android项目中比较重要的一个模块。它只是一些工具类,用于对HttpClient和HttpUrlConnection进行简单的封装和使用。后来Google开源了比较完整丰富的Volley,再到现在比较流行的Okhttp和Retrofit。如果你想了解它们之间的异同(或者具体来说,如果你想更深入地了解Android开发中的网络通信技术),你必须熟悉网络基础知识和Android的基本原理网络框架。只有在关键时刻,你才能找到最适合你APP的网络通信技术实践。事实证明,在Android的日常开发和源码阅读中,经常会遇到相关的知识。掌握这些网络基础知识也是Android程序员在真正走向高阶的过程中必须具备的基本技术素养之一。鉴于此,本文将主要介绍计算机网络的一些基础知识,以及Android开发中的一些使用和遇到的问题及解决方法。本文主要分为以下几个部分:1)计算机网络体系结构;2)Http相关;3)tcp相关;4)插座。2、作者简介舒大飞:携程网Android开发工程师。注意:当包含这篇文章时,为了更容易理解,对内容进行了更仔细的修改。3、计算机网络体系结构计算机网络体系结构,也就是我们经常看到的计算机网络系统的分层结构,这个还是有必要搞清楚的,防止和两个不在同一层的协议Http和Tcp纠缠不清。根据不同的参考模型,例如OSI模型和TCP/IP模型,分层结构有几种不同的版本。我们以比较常见的5层架构为例:如上图所示,五层架构自上而下,最终可以实现端到端的数据传输和通信。他们负责什么?如何实现端到端的通信?应用层:比如http协议,它实际上定义了如何打包和解析数据。如果应用层是http协议,它会根据协议对数据进行打包,比如根据请求行,将requestheader和requestbody进行打包,数据打包后,将数据传给传输层。传输层:传输层有TCP和UDP两种协议,分别对应可靠传输和不可靠传输。比如TCP因为需要提供可靠的传输,所以内部需要解决如何建立连接,如何保证传输可靠,不丢失数据,如何调整流量控制和拥塞控制。关于这一层,我们一般都是和Socket打交道。Socket是一套封装好的编程调用接口。通过它,我们可以操作TCP和UDP建立连接。我们平时使用Socket建立连接时,通常会指定端口号,所以这一层指定了发送数据的对应端口号。网络层:这一层IP协议,还有一些路由协议等,所以这一层规定了数据要传输到哪个IP地址。中间涉及到一些最优路由、路由算法等。数据链路层:印象最深的是ARP协议,负责将IP地址解析成MAC地址,即硬件地址,从而找到对应的唯一机器。物理层:该层为底层,提供二进制流传输服务,即通过传输介质(有线、无线)进行数据传输的真正起点。因此,通过以上五层各司其职,根据应用层协议对物理传输介质——MAC地址——IP地址——端口号——获取的数据进行解析,最终实现网络通信和数据传输。下面会重点介绍HTTP和TCP相关的东西。4.HTTP相关本节主要讲一些关于Http的基础知识,以及Android中的一些实际应用和遇到的问题及解决方法。4.1正确理解HTTP的“无连接”和“无状态”Http是无连接和无状态的。没有连接不代表不需要连接。Http协议只是一个应用层协议,最终要依赖于TCP协议等传输层提供的服务进行连接。无连接的意思是http约定每个连接只处理一个请求,一个请求完成后断开连接。这主要是为了减轻服务器的压力,减少连接对服务器资源的占用。我的理解是连接的建立其实是传输层的事情。对于应用层的HTTP来说,是无连接的,因为上层对下层没有感知。无状态意味着每个请求都是独立的,没有能力记住以前的请求事务。所以就有了cookie之类的东西,用来保存一些状态。4.2请求报文和响应报文这里简单说一下HTTP请求报文和响应报文格式的基础知识。请求消息:响应消息:关于Get和Post,我们都熟悉的Get和Post的区别大致如下:Get会将请求参数拼接在url后面,最后显示在地址栏,而Post会将请求参数数据放入请求体中,地址栏中不会显示;传递的参数的长度是有限的。问题:对于第1)点,在浏览器地址栏暴露隐私数据确实不合适,但如果是在App开发中,地址栏没有概念,这是否还有一个选项?post仍然是get的一个约束;对于第2点),长度限制应该是浏览器的限制,与get本身无关。如果是在App开发中,这一点可以忽略吗?4.3HTTP缓存机制之所以要介绍下面的Http缓存机制,是因为Okhttp使用了Http缓存机制来进行网络请求缓存,而不是像Volley等框架自己玩的那样,完全由客户端自己写一套缓存策略。Http缓存主要由header中的两个字段控制:Cache-control和ETag,下面分别介绍。1)Cache-control主要包括几个字段:private:只有客户端可以缓存;public:客户端和代理服务器都可以缓存;max-age:缓存的过期时间;no-cache:需要使用比对缓存来校验缓存数据;no-store:所有内存都不会被缓存。其实这里设置了一个缓存策略,服务端第一次通过header发送给客户端。可以看到:max-age:缓存过期的时间,稍后再请求。如果缓存没有过期可以直接使用缓存;no-cache:表示需要使用比对缓存来校验缓存数据。如果开启这个字段,即使max-age缓存没有失效,还是需要发起请求跟服务器确认资源是否有更新,是否需要重新请求数据,至于如何对比缓存,就是下面要说的Etag的作用。如果服务端确认资源没有更新,则返回304,取本地缓存。如果有更新,则返回最新的资源;no-store:如果开启该字段,则不缓存也不缓存。2)ETag:用于比较和缓存。Etag是服务器资源的标识码。当客户端发出第一个请求时,服务器会发出当前请求资源的标识码Etag。标识码Etag会通过Header中的If-None-Match携带,服务器会将客户端发送的Etag与最新的资源Etag进行比较。如果相同,则说明资源没有更新,返回304。3)总结:Http缓存机制是通过Cache-control和Etag的配合实现的。4.4HTTPCookie说的是Http协议是无状态的,Cookie是用来在本地缓存中记住一些状态的。一个Cookie一般包含domin(域)、path、Expires(过期时间)等几个属性。.服务端可以通过响应头中的set-cookies将状态写入客户端的Cookie中。下次客户端发起请求时,带上cookie就可以了。Android开发中遇到的问题及解决方法:说到cookie,一般如果只做App开发,是不会经常遇到的,但是如果涉及到WebView的需求,就可能会遇到。先说一个我在项目中遇到的关于WebViewCookie的苦恼往事:需求是这样的,加载的WebView中的H5页面需要登录,所以我们需要手动登录原页面。将ticket写入WebView的Cookie中,然后WebView中加载的H5页面会把Cookie中的ticket带到服务器进行校验。但是遇到了一个问题:通过Chromeinspect调试WebView,手动写的cookie确实已经写入了,但是发起请求的时候没有带cookie,导致请求校验失败,经查是属性默认关闭的WebView原因,通过设置以下代码打开:else{cookieManager.setAcceptCookie(true);}4.5Https我们都知道Https保证了我们数据传输的安全性,Https=Http+Ssl,它能保证安全的主要原因是采用了非对称加密算法,之所以常用的对称加密算法并不安全,因为双方使用统一的密钥进行加密和解密。只要任何一方泄露了密钥,其他人就可以使用密钥来解密数据。非对称加密算法之所以能够实现安全传输的核心本质是,用公钥加密的信息只能用私钥解密,用私钥加密的信息只能用公钥解密。1)简述为什么非对称加密算法是安全的:服务器申请CA组织颁发的证书,然后获得证书的公钥和私钥。私钥只有服务器知道,公钥可以共享给他人。传递给客户端,让客户端用服务器传来的公钥加密自己传输的数据,服务器用私钥解密数据。由于在客户端用公钥加密的数据只能用私钥解密,而且只有服务器端有这个私钥,所以数据传输是安全的。以上只是简单的说说非对称加密算法是如何保证数据安全的。其实Https的工作过程远比这复杂(限于篇幅这里不详述,网上相关文章很多):需要验证服务器发送的CA证书的合法性和有效性,因为CA证书在传输过程中存在被转移的风险,这涉及到客户端如何验证服务器证书的合法性,以保证双方身份合法;一是非对称算法虽然保证了数据的安全性,但相对于对称算法其效率相对较差。如何对其进行优化,既可以保证数据的安全性,又可以提高效率。2)客户端如何验证证书的合法性:首先,CA证书一般包括以下内容:证书的颁发机构和版本;证书的使用者;证书的公钥;证书的有效时间;证书的数字签名哈希值和签名哈希算法(这个数字签名哈希值是用证书的私钥加密的);等等。客户端验证服务器发送的证书的合法性,首先使用得到的公钥解密证书中的数字签名哈希值1(因为是用私钥加密的),然后使用签名哈希算法生成一个散列值2,如果两个值相等,说明证书合法,服务器可以信任。3)Android开发中遇到的问题及解决方法:顺便提一下,在项目开发中,在公司的测试服务器上使用AndroidWebView加载网页证书的问题。解决方法是暂时忽略测试环境中的SSL报错,这样网页就可以加载了。当然,不要在生产中这样做。一是会存在安全问题,二是GooglePlay要审核不通过。最好的方法是覆盖WebViewClient的onReceivedSslError():@OverridepublicvoidonReceivedSslError(WebViewview,SslErrorHandlerhandler,SslErrorerror){if(ContextHolder.sDebug){handler.proceed();return;}super.onReceivedSslError(view,handler,error);}4.6Http2.0Okhttp支持使用Http2.0协议进行配置。Http2.0与Http1.x相比,有了巨大的提升,主要有以下几点。1)二进制格式:http1.x是文本协议,而http2.0是以帧为基本单位的二进制协议。一个帧中不仅包含数据,还包含帧的标识:StreamIdentifier,标识该帧属于哪个请求,使得网络传输非常灵活;2)多路复用:一个很大的改进,原来的http1。比如建立多个连接的消耗和效率。为了解决效率问题,http1.x可能会发起尽可能多的并发请求来加载资源。但是浏览器对同一域名下的并发请求有限制,优化的方法一般是将请求的资源放在不同的域名下。突破这个限制。http2.0支持的多路复用可以很好的解决这个问题。多个请求共享一个TCP连接,多个请求可以同时并发在这个TCP连接上。一是解决建立多个TCP连接的消耗问题,一个也解决了效率问题。那么在一个TCP连接上支持多个请求并发的原理是什么呢?基本原理就是上面的二进制分帧,因为每一帧都有一个标识,所以多个请求的不同帧可以乱序并发发送出去,服务器会根据每一帧的标识将它们排序到对应的请求中。3)Header头部压缩:主要是通过压缩头部来减小请求的大小,减少流量消耗,提高效率。因为之前有一个问题,每次请求都要带一个header,而这个header里面的数据一般都是同一层的。4)支持服务器推送。5、TCP相关TCP是面向连接的,提供可靠的数据传输。在这一层,我们通常使用SocketApi来操作TCP、建立连接等。5.1三次握手建立连接第一次:发送SNY=1表示本次握手是请求建立连接,然后seq生成一个客户端的随机数X第二次:发送SNY=1,ACK=1表示回复建立连接的请求,然后是client的ack=seq+1(这样client收到后可以确认是自己要连接的server),然后是server也会生成一个代表自己的随机数seq=Y发送给客户端。第三次:ACK=1。seq=client随机数+1,ack=server随机数+1(让server知道刚才是client)为什么要建立连接需要三次握手?首先很清楚,两次握手是最基本的。对于第一次握手,C端向S端发送连接请求消息。S端收到后,S端就知道可以和C端成功连接了,但是C端不知道S端有没有收到这个消息。消息,所以S端收到消息后要进行响应,而C端只有收到S端的回复后才能确认可以与S端建立连接。这是第二次握手。C端只有在确认可以和S端连接后才能开始发送数据。所以两次握手一定是最基本的。那么为什么需要第三次握手呢?假设如果没有第三次握手,但是我们认为经过两次握手就建立了连接,会发生什么?第三次握手是为了防止已经过期的连接请求段突然再次发送给服务器,导致出错。具体情况是:C端发送的第一个网络连接请求由于某种原因滞留在网络节点,造成延迟,直到某个时间点释放连接才到达S端。这是一个已经过期的消息,但是此时S端仍然认为这是C端的连接建立请求的第一次握手,所以S端响应C端,第二次握手。如果只有两次握手,那么到这里,连接就建立了,但是此时C端没有任何数据发送,S端就会傻等,造成很大的资源浪费。所以需要第三次握手,只有C端再次响应才能避免这种情况。5.2挥手四次断开连接经过上面连接建立图的分析,这个图应该不难理解了。这里有一个主要的问题:为什么比建立连接时多了一波?可以看到这里服务器的ACK(回复客户端)和FIN(终止)消息并不是同时发送的,而是先发送ACK,再发送FIN。也很容易理解,当client请求断开连接时,此时server可能还有未发送的数据,所以先ACK,然后等待FIN之前发送数据。这变成了四次握手。TCP连接建立和断开的过程上面已经讲过了。TCP的主要特点是提供可靠的传输。那么如何保证数据传输的可靠性呢?这就是下面要讨论的滑动窗口协议。5.3滑动窗口协议滑动窗口协议是保证TCP可靠传输的基础,因为发送窗口只有在收到确认帧后才会向后移动并继续发送其他帧。举个例子:如果发送窗口是3帧,第一个发送窗口在前3帧[1,2,3],那么前3帧可以发送,后面的暂时不能发送,比如[1]帧发送出去收到接收方的确认消息后,此时发送窗口可以后移1帧,发送窗口来到[2,3,4]。同样,只能发送发送窗口中的帧,以此类推。接收窗口接收到帧后将其放入相应位置,然后移动接收窗口。界面窗口与发送窗口大小相同。如果接收窗口为5帧,落在接收窗口之外的帧将被丢弃。发送窗口和接收窗口大小的不同设置扩展了不同的协议:stop-wait协议:每发送一帧,都需要等待确认消息才能发送下一帧。缺点:效率低下。回N帧协议:采取累积确认的方式,接收方在正确接收到N帧后向发送窗口发送累积确认报文,确认这N帧已经正确接收,如果发送方在指定时间,则认为超时或数据丢失,重发确认帧后的所有帧。缺点:错误序号之后的PDU已经发送了,但是还需要重发,比较浪费。选择重传协议:如果发生错误,只重传错误涉及的PDU,提高传输效率,减少不必要的重传。这里还剩下最后一个问题:由于发送窗口和接收窗口的发送效率和接收效率不匹配,会导致拥塞。为了解决这个问题,TCP有一套流量控制和拥塞控制机制。5.4流量控制和拥塞控制1)流量控制:流量控制是对通信路径上的流量进行控制,即发送方通过获取接收方的反馈动态调整发送速率,以达到控制流量的效果。确保发送方的发送速度不超过接收方的接收速度。2)拥塞控制:拥塞控制是对整个通信子网的流量进行控制,属于全局控制。①慢启动+拥塞避免先看一张经典图:一开始使用慢启动,即拥塞窗口设置为1,然后拥塞窗口呈指数增长到慢启动的阈值(ssthresh=16),然后切换到congestionAvoid,也就是加性增长,增加到一定程度就会造成网络拥塞。此时拥塞窗口会再次减小到1,即重新启动慢启动,新的慢启动阈值调整为12,以此类推。②快重传+快恢复快重传:我们上面提到的重传机制是等待接收方超时未收到回复才开始重传。快速重传的设计思路是:如果发送方收到接收方的3个重复ACK,就可以判断一个报文段丢失,然后可以立即重传丢失的报文段,而不用等待设定的超时时间才开始重传过期,提高重传效率。快速恢复:上面的拥塞控制会在网络拥塞时将拥塞窗口减小到1,慢慢重新启动。这种方式的问题之一是网络不能快速恢复到正常状态。快速恢复就是为了优化这个问题。使用fastrecovery,当发生拥塞时,拥塞窗口只会减少到新的慢启动阈值(即12),不会减少到1,然后直接开始进入拥塞,避免加性增长,如图下图:快速重传和快速恢复是对拥塞控制的进一步改进。6.关于SocketSocket是一组操作TCP/UDP的API,比如HttpURLConnection和Okhttp,涉及到发送比较底层的网络请求,当然都是通过Socket发送网络请求连接,而Volley和Retrofit是它是一种更高层的封装,最终依赖于HttpURLConnection或Okhttp来进行最终的连接建立和请求发送。如果你简单的使用Socket,你应该可以做到。每端创建一个Socket,服务器端调用ServerSocket,然后建立连接。7、本文小结当然,以上内容只是我所了解和认为非常重要的计算机网络基础知识。还有很多网络基础知识需要深入理解和探索。写了很多,也算是对自己网络基础的整理,可能会有错误。