2019-07-03

作者: 茧儿_3bda | 来源:发表于2020-07-10 18:19 被阅读0次

      本节的目的只有一个,OKHttp并不神秘,无论用了什么巧妙的设计模式也好,各种封装也罢,在宏观上要清楚它本质就是做了一件什么样的事。
      先整体回顾一下我们一个Android APP整个的数据流走向,在宏观上对OKHttp是个什么东西,做了哪些事情,在整体上有一个清楚的认知,这样在以后的源码分析过程中才有方向,才不会迷路。不跟你们多bb,先来张图再说。

    image.png

      这是我做的一张Android APP 数据流示意图(上传的时候放大可能有点模糊,请各位看官多担待),这张图是我在TCP/IP四层模型的基础上结合Android APP 数据流向进行制作的,在实际的APP开发过程中,我们实际接触的是图中的上3层,即:UI页面,业务逻辑层,数据层,用一句话讲就是请求数据,将数据根据业务逻辑处理,然后显示到页面上。我用红字和箭头标识了步骤,以便逐步说明。

    step1,step2: 进入APP某个页面,页面需要展示数据,如果需要通过网络请求从服务器获得,那么我们会在业务层将获取数据的url(形如:https://www.baidu.com)传到数据层,通过网络来进行数据获取,即将url传给OKHttp框架,然后OKHttp帮我们进行HTTP/HTTPS请求。

    step3:这一步开始就是OKHttp框架帮我们做的事情,也是我们以后进行源码学习的部分,我们重点来来看一下,我们把获取数据的url((形如:https://www.baidu.com)传进来,我们通过url中的http/https字段知道我们要发送一个http请求,既然要发送一个http请求,需要将url处理一下,处理成差不多下图这种形式。这一步其实就是将url根据一定的规则进行转化,而这个规则就是http协议,这里我们先不关心http协议的具体内容,我们现在只需要先知道这一步将url根据规则(HTTP协议)转化成下图的这个样子的字符串。

    image

    step4.重点!!!
    上一步我们已经根据规则(HTTP协议)将要发给服务端的内容组装完毕了,下面要做的事情就是把上面那坨信息发给服务端,怎么发?通过socket~ ~ 我要在这里啰嗦一下socket是什么东西,从图中可以看出,socket横跨在APP的应用层和传输层,严格的说他不属于这两层,它是对传输层各种操作的封装,然后提供给应用层进行使用的一套工具,供应用层使用。
      如果觉得上面这句话没看懂,我举个栗子,在传输层,TCP和UDP是最常用的两种方式,其中HTTP请求用的就是TCP连接方式,那么如果我们想在APP中开启一个TCP连接,我们应该怎么做?没错,你只需创建一个Socket,怕各位看官贵人多忘事,贴一段陈年往事的代码:

    public static void main(String[] args) throws IOException {
           Socket socket = new Socket();
           // 绑定到本地20001端口
           socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), 20001));
           socket.setSoTimeout(2000);
           // 设置接收发送缓冲器大小
           socket.setReceiveBufferSize(64 * 1024 * 1024);
           socket.setSendBufferSize(64 * 1024 * 1024);
           // 链接到本地20000端口,超时时间3秒,超过则抛出超时异常
           socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), 20000), 3000);
           System.out.println("已发起服务器连接,并进入后续流程~");
           System.out.println("客户端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
           System.out.println("服务器信息:" + socket.getInetAddress() + " P:" + socket.getPort());
           // 发送接收数据
           // 得到Socket输出流
           OutputStream outputStream = socket.getOutputStream();
           // 得到Socket输入流
           InputStream inputStream = socket.getInputStream();
           
           // 释放资源
          // outputStream.close();
         //  inputStream.close();
         //  socket.close();
          // System.out.println("客户端已退出~");
       }
    

      当你执行完 socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), 20000), 3000);的时候,你的TCP就已经创建好了,就这么简单,TCP的“三次握手,四次挥手”那些复杂的流程你都不需要做,因为socket都帮你做好了,你只需要通过输入输出流将读写数据就好了哎~

            OutputStream outputStream = socket.getOutputStream();
            // 得到Socket输入流
            InputStream inputStream = socket.getInputStream();
    

      我们需要做的就是把step3得到的那一坨东西通过outputStream write出去就好了,至此我们就完成了发送一次请求了

            byte[] buffer = new byte[3*1024];
            ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
            String str = "符合HTTP协议规范请求信息";
            byteBuffer.put(str.getBytes());
    // 发送到服务器
            outputStream.write(buffer, 0, byteBuffer.position() + 1);
    

    step5,6,7
      这几个步骤都是一个TCP链接建立的逻辑,这些socket都为我们封装好了,我们不需要做什么,静静地看着就好,是不是很贴心,我要是有一个这样的女朋友该多好~这几步我们就不多分析了。

    step 8,9,10
      好了,至此我们的一个HTTP请求就已经发出去了,将那一坨信息发送给了服务器。大家先喝口水。
      如果不出意外,服务器会收到我们发给他的一坨信息,这坨信息是我们根据HTTP协议进行组装的,携带了一些信息,服务端得到之后就会知道我们要什么东西(数据)了,它就会为我们准备好我们想要的数据,然后也按照HTTP协议的规则组装,大概是这个样子。

    HTTP/1.1 200 OK
    Date: Sat, 31 Dec 2005 23:59:59 GMT
    Content-Type: text/html;charset=ISO-8859-1
    Content-Length: 122

    {"data":{
    "name" : "shecw",
    "age" : 18,
    ........
    }}
    step11
    然后通过刚才建立好的TCP将这坨信息返回给我们,这些socket同样为我们做好了,我们需要做的只需要通过调用socket.getInputStream()方法,将服务器给我们的信息读出来就好了。

             byte[] buffer = new byte[3*1024];
             InputStream inputStream = socket.getInputStream();
             int read = inputStream.read(buffer);
             System.out.println("收到数量:" + read);
    
    

    step12,13
      好了,我们拿到了服务端给我们的数据了,我们的任务基本完成了,我们根据HTTP协议取出我们真正想要的数据,就可以交给业务逻辑层了。我们根据我们的需要,将数据处理之后,就可以显示到我们的页面上了。
    上面花了很长的篇幅去从宏观上看明白了OKHttp框架本质上帮我们做了一件什么事情,我们做一下总结:
        1.根据我们传入的url,依据HTTP协议,将url转换成符合HTTP协议规范的信息(request)。
        2.通过创建socket,与服务端建立起TCP连接。
        3.将组装好的信息通过建立好的TCP发送给服务端。
        4.通过socket将服务端返回的数据通过流读出来。
        5.根据HTTP协议,获取我们需要的信息,返回给逻辑层。
    综上所述,我们在分析OKHttp源码的时候要有以下两个重点。
       A. OKHttp框架是怎么根据HTTP协议将url转化成符合规范的信息的。
       B. OKHttp框架在将消息通过socket发送出去的过程中做了哪些优化(连接的复用,缓存)。
      整个框架都在围绕着这两个点进行处理,所以理所当然,这也是我们下面分析,学习的方向。
      在看源码之前我们脑子里有清楚的知道这些,至少我们不会迷失大方向,我在这先举个栗子,具体会在后面细讲,所谓的OKHttp连接connection复用机制,乍一看是不是有点懵,其实就是socket的复用,socket成功建立TCP连接后可以多次利用读,写流进行读写,从而与服务端进行交互,所以当你重复使用同样的url进行数据请求时,没必要再重复的去new Socket(),只需要读写即可。
    OutputStream outputStream = socket.getOutputStream()
    InputStream inputStream = socket.getInputStream();
    现在没看懂没关系,后面我们会具体分析,我要说的是,在分析源码的时候要能够做到透过现象看本质。
      接下来有一个关键的问题需要说清楚,非常重要,HTTP和HTTPS之间的关系和不同,因为现在已经到了9102年了,http已经逐步的被https取代了,在以后的源码分析的过程中,我们也会以HTTPS来进行分析,学习,所以在最一开始我觉得必须把这件事情说清楚。
      结合上面的分析,从下面两点进行归纳
      1.根据我们传入的url,依据HTTP协议,将url转换成符合HTTP协议规范的信息(request)。
    这一点http和https二者是基本相同的,相同点在于二者都是将url根据HTTP协议的格式转化成用于请求的报文的。就像这样。也就是说不管http还是https,都会先做这件事情。
    !

    TCP连接建立过程.png

    2.通过创建socket,与服务端建立起TCP连接。
       重点!!!这里我会补充的知识有点多,各位看官多担待。。。
       首先我们先说http请求
      上面我们分析过,发送一个http请求本质是通过一个socket与服务端建立一个tcp连接,然后通过socket的输出流outputstream将符合http协议格式的请求报文发送出去。因为是建立tcp连接,在建立连接的时候就会涉及到三次握手,在关闭连接的时候会进行四次挥手,当然,这些复杂的过程已经被socket封装好,对应关系如下:
    socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), 20000), 3000) ===> 建立TCP连接的三次握手
    socket.close(); ===> TCP断开连接时的四次挥手
      说到这里我想吐槽一下,本人有幸也旁听过几次招聘新人的面试,发现当涉及到网络层的知识的时候,无论问关于网络层的什么问题,应聘者几乎都会先喊出【三次握手,四次挥手】的响亮口号,非常有趣,不知道各位看官是否有过类似的经历。。。好了,我们回到正题,虽然我们知道这些被封装在socket的方法中,但我要在细致说明一下,以便下面讲解https,先上图


    TCP连接建立过程.png

      翻来翻去网上相关的图很多,但感觉用了并不能把事情说清楚,所以自己做了一张图
      上图画出了TCP建立连接的过程。假定主机A运行的是我们的APP,B运行的是服务器程序。最初两端的TCP进程都处于CLOSED状态。图中在主机下面的是TCP进程所处的状态。A是主动打开连接,B是被动打开连接。

      连接前状态:B(服务端)的TCP服务器进创建传输控制模块,准备接受客户进程的连接请求,然后服务器进程就处于LISTEN(监听)状态,等待客户的连接请求。为了防止你不知道我在说什么,来两行代码,不要说你没见过这段代码...... 你要真的没有见过。。。我也不能把你怎么着,实际上服务器会在此基础上进行大量的封装, 但不管如何封装和优化,应该知道,它本质上就是做了这些事情。

            ServerSocket serverSocket = new ServerSocket();
            // 是否复用未完全关闭的地址端口
            serverSocket.setReuseAddress(true);
            // 等效Socket#setReceiveBufferSize
            serverSocket.setReceiveBufferSize(64 * 1024 * 1024);
           // 绑定到本地端口上
            server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 50);
            System.out.println("服务器准备就绪~");
            System.out.println("服务器信息:" + server.getInetAddress() + " P:" + server.getLocalPort());
            // 等待客户端连接
            for (; ; ) {
                // 得到客户端
                Socket client = server.accept();
                // 客户端构建异步线程
                ClientHandler clientHandler = new ClientHandler(client);
                // 启动线程
                clientHandler.start();
            }
    

    step1.
      首先A的TCP客户进程向B发出连接请求报文段,这时首部中的同步位SYN=1,同时选择一个初始序号(seq)并在客户端保存其值,为了方便记忆,我们将其值临时记作(seqAInit)。TCP规定,SYN报文段(即SYN=1的报文段)不能携带数据,但要消耗掉一个序号。将报文发给服务端B,A的客户进程就进入SYN-SENT(同步已发送)状态。为了更清楚,结合下图理解。

    发送给服务端的信息内容.png

    step2
       B(服务端)收到连接请求报文段后,先队报文中标记位SYN=1进行确认,确认成功后将客户端A发送过来的序列号(seq)进行存储,以防混乱,其值我们临时记做变量seqFromA(即seqFromA=seqAInit)。新建一份报文,把报文段段中把SYN和ACK位都置为1,确认号(ack)的值设置为seqFromA+1,同时也为自己选择一个初始序号(seq),同样为了防止混乱,我们将这个B(服务端)生成的序列号的值记做seqBInit。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。将数据报文发送给A,这时B的TCP服务器进程就进入SYN-RCVD(同步已收到)状态。

    step3
      A的收到B的数据后,要进行信息校验。
      1)确认报文段的ACK==1,标记位SYN=1
      2)来自B的信息的 确认号(ack)与第一步保存的 seqInitA+1 二者的值相等
      (注:因为seqFromA==seqAInit,上一步在服务端做了seqFromA+1,然后将其作为确认号(ack)返回给客户端A,所以如果通信正常,二者一定相等)。
    这两个条件都满足后,将B服务端传递的序列号(seq)记录下来,为了方便理解,其值记做seqFromB, 创建新的报文,将ACK位置为1,序列号(seq)设置为seqAInit+1 ,确认号(ack)设置为seqFromB+1,将报文发送给服务端B。

    image.png

    当B收到A发来的信息后,
    1)会将第二次挥手生成的序列号(seq)的值进行seqBInit+=1然后 和从A收到的确认号(seq)的值进行对比,如果二者相等,第一步确认完成。
    2)会将第一次挥手客户端A传来的确认号的值进行seqAInit+1操作然后和客户端传递过来的序列号(seq)的值进行比较,如果相等,第二步确认完成。
    当两部校验都完成,会进入ESTABLISHED状态,即握手确认成果,TCP连接可以建立。

      其实这是一个很巧妙也很有趣的机制,不知道各位看客有没有同感,好了,到这里我觉得已经把TCP连接建立时进行三次握手这件事情苦口婆心的讲的非常清楚了,下面我面要对HTTPS的TCP连接进行说明讲解。
      首先从名字上理解HTTPS,
      HTTPS=HTTP+SSL ;
      SSL=Secure Sockets Layer [安全套接层]
      HTTPS = HTTP+Secure Sockets Layer
      其实https协议就是依旧遵从HTTP协议将请求的url转换成报文格式通过socket将消息发送出去,只不过这里用的socket不是我们刚才的socket,而是SSLSocket,我们知道通过socket发送网络数据,发送出去什么信息,在网络中传递的就是什么信息,这样做是非常不安全的,因为你的请求很容易被别人窃取,所以为了避免这一点,SSLSockett就应运而生了,SSLSocket在传输的过程中,会对你发送的消息进行加密,这样即便其他人获取了你的请求,因为报文内容是加密过的,所以其他人也不知道发送的内容,增加了网络数据传输的安全性。
      HTTP与HTTPS的本质区别归结成一句话:HTTP用socket发送报文,而HTTPS用Secure Socket发送报文。
    看下面代码,并不是什么神秘的东西,都给你封装好了,是不是很贴心

     public static void sslSocket() throws UnknownHostException, IOException {  
            SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory  
                    .getDefault();  
            SSLSocket s = (SSLSocket) factory.createSocket("localhost", 10002);  
            System.out.println("ok");  
          
            OutputStream output = s.getOutputStream();  
            InputStream input = s.getInputStream();  
          
            output.write("alert".getBytes());  
            System.out.println("sent: alert");  
            output.flush();  
          
            byte[] buf = new byte[1024];  
            int len = input.read(buf);  
            System.out.println("received:" + new String(buf, 0, len));  
        } 
    
    

    下面我们看一下在SSLSocket建立TCP连接的过程中都做了些什么事情,先啰嗦几个概念。
    对称加密:
      即加密和解密使用同一个密钥,虽然对称加密破解难度很大,但由于对称加密需要在网络上传输密钥和密文,一旦被黑客截取很容就能被破解,因此对称加密并不是一个较好的选择。常用的对称加密算法有:AES、DES、3DES.我们来点实在的,来段代码看下怎么对称加密

    private void secret() {
            //1,获取cipher 对象
            Cipher cipher = null;
            String content = "我想要个像socket一样铁心的女盆友";
            Log.e("she","加密前信息:"+content.toString());
            try {
                //1,得到cipher 对象(可翻译为密码器或密码系统)
                 cipher = Cipher.getInstance("DES");
                //2,创建秘钥
                SecretKey key = KeyGenerator.getInstance("DES").generateKey();
                //3,设置操作模式(加密/解密)
                cipher.init(Cipher.ENCRYPT_MODE, key);
                //4,执行操作
                byte[] result = cipher.doFinal(content.getBytes());
                
                Log.e("she","加密后:"+new String(result));
                //使用私钥初始化密码器
                cipher.init(Cipher.DECRYPT_MODE, key);
                //执行解密操作
                byte[] deResult = cipher.doFinal(result);
                Log.e("she","解密后:"+new String(deResult));
            } catch (Exception e) {
                e.printStackTrace();
            } 
        }
    

    非对称加密:


    image.png

      即加密和解密使用不同的密钥,分别称为公钥和私钥。我们可以用公钥对数据进行加密,但必须要用私钥才能解密。在网络上只需要传送公钥,私钥保存在服务端用于解密公钥加密后的密文。但是非对称加密消耗的CPU资源非常大,效率很低,严重影响HTTPS的性能和速度。因此非对称加密也不是HTTPS的理想选择。
    常见的非对称加密算法有:RSA、Elgamal
    我们还是来段代码体会下

    private void secret() {
            //1,获取cipher 对象
            Cipher cipher = null;
            String content = "我想要个像socket一样铁心的女盆友";
            Log.e("she","加密前信息:"+content.toString());
            try {
                cipher = Cipher.getInstance("RSA");
                //2,通过秘钥对生成器KeyPairGenerator 生成公钥和私钥
                KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
                //使用公钥进行加密,私钥进行解密(也可以反过来使用)
                PublicKey publicKey = keyPair.getPublic();
                PrivateKey privateKey = keyPair.getPrivate();
                //3,使用公钥初始化密码器
                cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    
    
                //4,执行加密操作
                byte[] result = cipher.doFinal(content.getBytes());
                Log.e("she","加密后:"+new String(result));
                //使用私钥初始化密码器
                cipher.init(Cipher.DECRYPT_MODE, privateKey);
                //执行解密操作
                byte[] deResult = cipher.doFinal(result);
                Log.e("she","解密后:"+new String(deResult));
            } catch (Exception e) {
                e.printStackTrace();
            } 
        }
    

    那么HTTPS采用了怎样的加密方式呢?其实为了提高安全性和效率HTTPS结合了对称和非对称两种加密方式。
    1)服务端持有用于非对称加解密的一对公钥(pub_key)和私钥(pri_key)。服务端先把公钥pub_key发送给客户端
    2)客户端收到公钥pub_key后,生成一个用于对称加解密的秘钥key,客户端通过pub_key对key进行非对称加密后发给服务端
    3)服务端用私钥pri_key解密出用于对称加密的key
    4)双方同key对交互的数据进行加解密。
    我们来张图


    SSL连接建立.png

      因为我目前暂时没有精力去亲自分析SSLSocket源码,所以上面这张图是我结合几篇前辈的博客进行整理总结而成的,可以看到客户端A与服务端B的第一次交互式一样的,不同的是后面生成和传递秘钥的过程。
      最后要提到的是https的数字证书(Certificate),这里我不打算展开讲,只需要知道证书起了什么作用,证书包含了:
       1.上图中传递的用于非对称加密的公钥pub_key
       2.服务器的host身份信息
       3.上面两项信息的数字签名
    所以实际上,上图中服务端发送给客户端的不是pub_key,而是数字证书。相关知识点我推荐一篇文章,讲的非常通俗易懂:

    《https 加密那点事》

      到这里各位应该对HTTP和HTTPS请求上有了一个大体地概念了,至少我们 对这件事不再感到神秘,OKHttp源码其实就是在这个基础上进行的各种封装。

      我们来做一下本节总结:
      这一节主要弄清了两个事情:
      1.OKHttp本质上做了啥?
        1)将请求url依据http协议转化报文
        2)将转化好的报文用socket发送出去
      2.HTTP与HTTPS的本质上的区别在哪
        HTTP的请求用socket发送报文,明文,而HTTPS用SSLSocket发送报文,加密

    相关文章

      网友评论

        本文标题:2019-07-03

        本文链接:https://www.haomeiwen.com/subject/epmqhctx.html