近距离接触OkHttp

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容