近距离接触OkHttp

作者: gatsby_dhn | 来源:发表于2016-10-02 06:23 被阅读305次

    我们知道HTTP是建立在TCP之上的应用层协议,客户端和服务器建立一条TCP连接,通过该连接发送字节流。无论是OKHttp还是HttpUrlConnection构造的请求报文的格式都是一致的。我们就看看发送的请求到底长什么样?

    支持原创,转载请注明出处。

    工具准备

    抓包工具Fiddler:Fiddler是位于客户端和服务器端的HTTP代理,也是目前最常用的http抓包工具之一。具体使用不细说,移步Fiddler教程。废话不多说直接开始。

    1.使用POST上传字符串

    MediaType MEDIA_TYPE_MARKDOWN
               = MediaType . parse( "text/x-markdown; charset=utf-8") ;
    
    String postBody = ""
                 + "Releases\n"
                 + "--------\n"
                 + "\n"
                 + " * _1.0_ May 6, 2013\n"
                 + " * _1.1_ June 15, 2013\n"
                 + " * _1.2_ August 11, 2013\n" ;
    
    Request request = new Request. Builder ()
                 . url( "http://www.nowcoder.com/" )
                 . post( RequestBody .create ( MEDIA_TYPE_MARKDOWN, postBody ))    //创建RequestBody对象
                 . build() ;
    
    ———————————————————————————————————————抓包获得的HTTP请求格式—————————————————————————————————————————
    POST http://www.nowcoder.com/ HTTP/1.1
    Content-Type: text/x-markdown; charset=utf-8
    Content-Length: 88
    Host: www.nowcoder.com
    Connection: Keep-Alive
    Accept-Encoding: gzip
    User-Agent: okhttp/3.4.1
    
    Releases
    --------
    
     * _1.0_ May 6, 2013
     * _1.1_ June 15, 2013
     * _1.2_ August 11, 2013
    

    因为只需要看发送的请求,为了方便下面我都以www.nowcoder.com作为访问的URL,当然POST到这个URL肯定是无效的。可以看到通过OkHttp发送的请求是符合HTTP请求报文格式的。

    2.使用POST上传请求参数

    //构建RequestBody
    RequestBody formBody = new FormBody. Builder ()
               . add( "search" , "Jurassic Park" )
               . build() ;
    
    Request request = new Request. Builder ()
               . url( "http://www.nowcoder.com/" )
               . post( formBody )
               . build() ;
    Response response = client. newCall( request ). execute ();
    
    -----------------------------请求-----------------------------------------
    POST http://www.nowcoder.com/ HTTP/1.1
    Content-Type: application/x-www-form-urlencoded          //注意类型
    Content-Length: 22
    Host: www.nowcoder.com
    Connection: Keep-Alive
    Accept-Encoding: gzip
    User-Agent: okhttp/3.4.1
    
    search=Jurassic%20Park                     请求实体
    

    我创建了一个RequestBody对象作为HTTP请求实体,可以看到"Jurassic Park"被编码成了Jurassic%20Park。

    3.使用POST上传文件

    MediaType MEDIA_TYPE_MARKDOWN
               = MediaType . parse( "image/png; charset=utf-8" );
               File file = new File( "test.png" );     //文件
    
    Request request = new Request. Builder ()
            . url( "http://www.nowcoder.com/" )
            . post( RequestBody .create ( MEDIA_TYPE_MARKDOWN, file))       //使用文件创建RequestBody对象
            . build() ;
    
    ------------------------------------------请求--------------------------------------------------
    POST http://www.nowcoder.com/ HTTP/1.1
    Content-Type: image/png; charset=utf-8
    Content-Length: 48377
    Host:www.nowcoder.com
    Connection: Keep-Alive
    Accept-Encoding: gzip
    User-Agent: okhttp/3.4.1
    
    PNG
    IHDR  H      q T2   gAMA   。。。二进制数据。。。
    

    可以看到上传一张图片HTTP请求中实体部分就是图片的二进制数据。

    4.使用POST上传multipart数据

    MediaType MEDIA_TYPE_PNG
               = MediaType . parse( "image/png" );
    //构建请求体
    RequestBody requestBody = new MultipartBody .Builder ()
                 . setType( MultipartBody .FORM )
                 . addFormDataPart( "title" , "Square Logo" )
                 . addFormDataPart( "image" , "logo-square.png" , RequestBody .create ( MEDIA_TYPE_PNG, new File ("test.png" )))
                 . build() ;
    
    Request request = new Request. Builder ()
                 . url( "http://www.nowcoder.com/" )
                 . post( requestBody )
                 . build() ;
    
    ------------------------------------请求--------------------------------------
    POST http://www.nowcoder.com/ HTTP/1.1
    Content-Type: multipart/form-data; boundary=5012952a-215b-4a28-beed-3258fda78bab   //注意类型:表单类型
    Content-Length: 48706
    Host: www.nowcoder.com
    Connection: Keep-Alive
    Accept-Encoding: gzip
    User-Agent: okhttp/3.4.1
    
    --5012952a-215b-4a28-beed-3258fda78bab               //自动生成的分隔符
    Content-Disposition: form-data; name="title"         //类型:表单类型,名字
    Content-Length: 11
    
    Square Logo                                          //值
    --5012952a-215b-4a28-beed-3258fda78bab               //分隔符
    Content-Disposition: form-data; name="image"; filename="logo-square.png"   //filename指明默认文件名
    Content-Type: image/png
    Content-Length: 48377
    
     PNG
    ...二进制数据...
    

    我们使用MultipartBody.Builder来构造一个RequestBody对象,我们添加了一个字符串和一张图片。可以看到生成的HTTP请求实体中有两部分,第一部分

    --5012952a-215b-4a28-beed-3258fda78bab               //自动生成的分隔符
    Content-Disposition: form-data; name="title"         //类型:表单类型,名字
    Content-Length: 11
    
    Square Logo                                          //值
    

    开头是随机生成的一串分隔符在Content-Type: multipart/form-data; boundary=5012952a-215b-4a28-beed-3258fda78bab中声明。第二部分是

    --5012952a-215b-4a28-beed-3258fda78bab               //分隔符
    Content-Disposition: form-data; name="image"; filename="logo-square.png"   //filename指明默认文件名
    Content-Type: image/png
    Content-Length: 48377
    
     PNG
    ...二进制数据...
    

    5.使用Gson解析Json

    private final Gson gson = new Gson();         //创建Gson对象
    
      public void run() throws Exception {
        Request request = new Request.Builder()     //请求
            .url("https://api.github.com/gists/c2a7c39532239ff261be")
            .build();
    
        Response response = client.newCall(request).execute();     //响应
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
    
        Gist gist = gson.fromJson(response.body().charStream(), Gist.class);     //将字符串映射到对象
        for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
          System.out.println(entry.getKey());
          System.out.println(entry.getValue().content);
        }
      }
    
      static class Gist {
        Map<String, GistFile> files;
      }
    
      static class GistFile {
        String content;
      }
    

    这个没什么好说的。

    7.添加缓存

          public void testCache() throws IOException {
               //缓存大小
               int cacheSize = 10 * 1024 * 1024;                // 10 MiB
               File cacheDirectory = new File( "cache" );     //缓存文件
             Cache cache = new Cache( cacheDirectory , cacheSize) ;
    
             OkHttpClient client = new OkHttpClient. Builder ()
               . cache( cache )                          //提供缓存
               . build() ;
    
             //创建请求
             Request request = new Request. Builder ()
               . url( "http://publicobject.com/helloworld.txt" )
               . build() ;
    
             Response response1 = client. newCall( request ). execute ();
             if ( ! response1. isSuccessful ()) throw new IOException ("Unexpected code " + response1 ) ;
    
             String response1Body = response1. body() . string() ;
             System .out . println( "Response 1 response:          " + response1 ) ;               //非空
             System .out . println( "Response 1 cache response:    " + response1 . cacheResponse()) ;    //首次加载为空
             System .out . println( "Response 1 network response:  " + response1 . networkResponse()) ; //非空
    
             Response response2 = client. newCall( request ). execute ();
             if ( ! response2. isSuccessful ()) throw new IOException ("Unexpected code " + response2 ) ;
    
             String response2Body = response2. body() . string() ;
             System .out . println( "Response 2 response:          " + response2 ) ;               //非空
             System .out . println( "Response 2 cache response:    " + response2 . cacheResponse()) ;     //非空
             System .out . println( "Response 2 network response:  " + response2 . networkResponse()) ;   //没有走网络,所以为空
    
             System .out . println( "Response 2 equals Response 1? " + response1Body . equals( response2Body ));
    
          }
    --------------------------------------结果--------------------------------------------------
    Response 1 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
    Response 1 cache response:    null
    Response 1 network response:  Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
    Response 2 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
    Response 2 cache response:    Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
    Response 2 network response:  null
    Response 2 equals Response 1? true
    

    我们可以为OKHttp指定一个缓存目录,首次请求时 response1 . cacheResponse()为空,response1 . networkResponse()非空,因为执行了网络请求。第二次请求时会从缓存获取数据,所以response1. cacheResponse()非空,response1 . networkResponse()为空。

    8.取消操作

          public void testCancelCall() {
                ScheduledExecutorService executor = Executors .newScheduledThreadPool ( 1) ;
                OkHttpClient client = new OkHttpClient() ;
                Request request = new Request. Builder ()
                 . url( "http://httpbin.org/delay/2" ) //这个URL会延迟2秒返回数据
                 . build() ;
    
                final long startNanos = System. nanoTime ();
                final Call call = client .newCall ( request) ;
    
                //1秒后开启子线程,取消请求
                executor . schedule( new Runnable () {
                      @Override public void run () {
                        System .out . printf( "%.2f Canceling call.%n", ( System .nanoTime () - startNanos ) / 1e9f ) ;   //开始取消请求
                        call . cancel() ;
                        System .out . printf( "%.2f Canceled call.%n", ( System .nanoTime () - startNanos ) / 1e9f ) ; //取消完毕
                      }
               } , 1 , TimeUnit . SECONDS) ;
    
               try {
                    System .out . printf( "%.2f Executing call.%n", ( System .nanoTime () - startNanos ) / 1e9f ) ;   //开始发起请求
                    //执行请求,一秒后将在子线程被取消
                    Response response = call. execute ();                                                         //执行请求,会阻塞2秒
                    System .out . printf( "%.2f Call was expected to fail, but completed: %s%n",               //不会被执行
                        ( System. nanoTime () - startNanos) / 1e9f , response ) ;
               } catch (IOException e ) {
    
                    System .out . printf( "%.2f Call failed as expected: %s%n",                    //请求被取消将抛出IOException异常
                        ( System. nanoTime () - startNanos) / 1e9f , e ) ;
               }
          }
    
    ------------------------------------输出结果-------------------------------------------
    0.01 Executing call.
    1.01 Canceling call.
    1.01 Canceled call.
    1.02 Call failed as expected: java.net.SocketException : Socket Closed
    

    我们请求的http://httpbin.org/delay/2 会延迟两秒响应请求,我们在发出请求1秒后取消请求会抛出java.net.SocketException 异常。

    9.超时设置

    public void testTimeOuts() throws IOException {
               OkHttpClient client = new OkHttpClient. Builder ()
               . connectTimeout( 10 , TimeUnit . SECONDS)             //连接超时
               . writeTimeout( 10 , TimeUnit . SECONDS)                    //写超时
               . readTimeout( 1 , TimeUnit . SECONDS)                 //设置读超时间为1秒,超时会抛出SocketTimeoutException
               . build() ;
    
             Request request = new Request. Builder ()
               . url( "http://httpbin.org/delay/2" )                    //延迟2秒响应
               . build() ;
    
             Response response = client. newCall( request ). execute ();      //这里读取操作会超时
             System .out . println( "Response completed: " + response) ;
          }
    

    10.为每个请求定制OkHttpClient

    public void testCustomizeClient() {
               OkHttpClient client = new OkHttpClient() ;
             Request request = new Request. Builder ()
               . url( "http://httpbin.org/delay/1" ) // This URL is served with a 1 second delay.
               . build() ;
    
             try {
                 // Copy to customize OkHttp for this request.
                 OkHttpClient copy = client. newBuilder ()                //返回一个OkHttp拷贝,仅用于此次请求
                     . readTimeout( 500 , TimeUnit . MILLISECONDS)            //设置0.5秒超时
                     . build() ;
    
                 Response response = copy. newCall( request ). execute ();    //抛出异常
                 System .out . println( "Response 1 succeeded: " + response) ;
               } catch (IOException e ) {
                 System .out . println( "Response 1 failed: " + e) ;
               }
    
             try {
               // Copy to customize OkHttp for this request.
               OkHttpClient copy = client. newBuilder ()
                   . readTimeout( 3000 , TimeUnit . MILLISECONDS)
                   . build() ;
    
               Response response = copy. newCall( request ). execute ();
               System .out . println( "Response 2 succeeded: " + response) ;
             } catch (IOException e ) {
               System .out . println( "Response 2 failed: " + e );
             }
          }
    

    11.基础认证

    public void testBaseAuthentication() throws IOException {
    //        OkHttpClient client = new OkHttpClient();//没有提供账号密码时,抛出异常,服务器返回401
              OkHttpClient client = new OkHttpClient. Builder ()      //提供基础认证,可以响应请求
                   . authenticator( new okhttp3. Authenticator () {
                        @Override
                        public Request authenticate( Route route , Response response ) throws IOException {
                             System.out.println( "Authenticating for response: " + response );
                          System .out . println( "Challenges: " + response .challenges ()) ;
                          String credential = Credentials .basic ("jesse","password1");//计算账号密码的base64编码
                          return response .request () .newBuilder ()
                              . header( "Authorization" , credential)     //增加Authorization首部
                              . build() ;
                        }
                   })
                   . build() ;
    
    
             Request request = new Request. Builder ()
                 . url ("http://publicobject.com/secrets/hellosecret.txt" )
               . build() ;
    
             Response response = client. newCall( request ). execute ();
             if ( ! response. isSuccessful ()) throw new IOException ("Unexpected code " + response ) ;
    
             System .out . println( response .body () .string ()) ;
    
          }
    
    

    基础认证是一种简单的认证方式,安全性也不高。一次典型的访问场景是:
    浏览器发送http请求(没有Authorization header)
    服务器端返回401页面
    浏览器弹出认证对话框
    用户输入帐号密码,并点确认
    浏览器再次发出http请求(带着Authorization header)
    服务器端认证通过,并返回页面
    浏览器显示页面
    使用http auth的场景不会用cookie,也就是说每次都会送帐号密码信息过去。然后我们都知道base64编码基本上等于明文。这削弱了安全。
    由于种种缺点,http auth现在用的并不多。不过在路由器等场合还是有应用的,原因是http auth最简单,使用起来几乎是零成本。
    在你需要做访问控制,又不想拖上SSO、数据库之类的东西的时候,http auth不失为一个简洁的选项。

    参考资料

    https://github.com/square/okhttp/wiki/Recipes

    后面会将更多笔记整理成博客,欢迎关注。
    支持原创,转载请注明出处。

    相关文章

      网友评论

        本文标题:近距离接触OkHttp

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