我们知道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
后面会将更多笔记整理成博客,欢迎关注。
支持原创,转载请注明出处。