Android知识Android开发Android开发经验谈

近距离接触OkHttp

2016-10-02  本文已影响305人  gatsby_dhn

我们知道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

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

上一篇下一篇

猜你喜欢

热点阅读