OKhttp是一个处理网络请求的开源项目,由Square公司开发用于替代HttpUrlConnection和Apache HttpClient(Android API23 6.0中已将HttpClient移除),是一个非常适用于Android(Java)的轻量级框架。
为了在客户端使用OKhttp,本文章还将给出服务器端代码,当然这些代码只是为了测试而编写的简单代码。
发送GET请求
使用OKhttp使发送一个请求变得再简单不过,你只需要提供一个URL和一系列请求参数(对于GET请求,可以直接将请求参数添加到URL尾部)即可。
使用OKhttp发送请求之前,需要创建一个OkHttpClient对象,此对象负责发送请求。建议用一个OkHttpClient对象发送请求,而不是在发送每个请求之前都创建一个新的OkHttpClient对象。
private static OkHttpClient client=new OkHttpClient();
一个请求被封装成一个Request对象,这个Request对象可以包含请求头(Headers)和请求体(RequestBody),这里的GET请求不会用到请求头和请求体,它们将在下文介绍。你可以通过Request.Builder来一步步构建需要的请求(而不是直接new一个Request)。下面这个方法封装了构建一个Request的操作。
private static Request getGetRequest(String url, String[] params) {
if(params!=null && params.length>0) {
if(params.length%2!=0)
throw new IllegalArgumentException("The number of request parameters must be even!");
StringBuilder sb=new StringBuilder(url);
for(int i=0; i<params.length-1; i+=2) {
sb.append((i>0) ? "&" : "?").
append(params[i]).
append("=").
append(params[i+1]);
}
url=sb.toString();
}
return new Request.Builder().url(url).build();
}
构建好一个请求后,就可以向服务器发起一个对话(Call)了。OkHttpClient负责发起一个新的Call,这个Call会返回一个响应(Response)。注意,这里使用了同步的方式发起一个Call,最好在一个子线程中执行。
public static void get(String url, String[] params, Callback callback) {
Request request=getGetRequest(url, params);
Call call=client.newCall(request);
try {
Response response=call.execute();
callback.onResponse(call, response);
} catch (IOException e) {
callback.onFailure(call, e);
}
}
另外也可以异步提交一个请求,不过此时不会立即返回一个Response,而是在之后的某个时间点回调Callback对象的onResponse(收到响应)或onFailure(发生异常)方法。
call.enqueue(callback);
现在我们向服务器发送一个GET请求,并提交用户名和密码(user和password),下面是测试代码:
HttpUtils.get(
"http://localhost:8080/HttpServer/zzw/get",
new String[]{"user", "张三", "password", "123456"},
new Callback(){
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
}
});
我们在onResponse方法中简单显示了来自服务器的响应。和Request类似,Reponse也可以有响应头(Headers)和响应体(ResponseBody),响应报文就封装在响应体中。相应的服务端代码如下(如果你不感兴趣可以直接跳过哦):
public class GetAction extends ActionSupport {
private static final long serialVersionUID = -7442519273670774235L;
private String user;
private String password;
public void setUser(String u) { user=u; }
public void setPassword(String p) { password=p; }
public String getUser() { return user; }
public String getPassword() { return password; }
@Override
@Action(value="/zzw/get",
results={@Result(name="success", location="user.jsp")})
public String execute() throws Exception {
return SUCCESS;
}
}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<%@ page language="java" contentType="text/html; charset=GBK"
pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s"%>
<meta http-equiv="Content-Type" content="text/html; charset=GBK">
<title>get page</title>
</head>
<body>
用户: <s:property value="user"/><br>
密码: <s:property value="password"/>
</body>
</html>
这里的Action使用了Convention插件进行了配置,使其可以接收任何发给http://localhost:8080/HttpServer/zzw/get
的请求。
发送POST请求
发送POST请求比GET请求稍微复杂点,不过大体流程还是一致的,先看代码:
private static Request getPostRequest(String url, String[] params) {
FormBody.Builder builder=new FormBody.Builder();
if(params!=null && params.length>0) {
if(params.length%2!=0)
throw new IllegalArgumentException("The number of request parameters must be even!");
for(int i=0; i<params.length-1; i+=2)
builder.add(params[i], params[i+1]);
}
RequestBody requestBody=builder.build();
return new Request.Builder().url(url).post(requestBody).build();
}
由于POST请求的请求参数都是放在报文体中的,因此需要在”build”过程中添加进请求体中(相比于GET请求更安全)。另外还需要调用Builder的post方法指明发送的是一个POST请求。
注意,这里是按照“提交表单”的方式发送一个POST请求,也就是说这里的POST请求和方法为post的form表单一样。除了这种方式之外,还有一种用于上传文件的POST请求将在后面介绍。
public static void post(String url, String[] params, Callback callback) {
Request request=getPostRequest(url, params);
Call call=client.newCall(request);
try {
Response response=call.execute();
callback.onResponse(call, response);
} catch (IOException e) {
callback.onFailure(call, e);
}
}
客户端测试代码如下:
String user=null;
try {
user=URLEncoder.encode("张三", "GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
HttpUtils.post(
"http://localhost:8080/HttpServer/zzw/form_post",
new String[]{"user", user, "password", "123456"},
new Callback(){
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
}
});
相应的服务端代码如下:
public class FormPostAction extends ActionSupport {
private static final long serialVersionUID = -2701435552091943728L;
private String user;
private String password;
private String encoding;
public void setUser(String u) { user=u; }
public void setPassword(String p) { password=p; }
public void setEncoding(String e) { encoding=e; }
public String getUser() { return user; }
public String getPassword() { return password; }
public String getEncoding() { return encoding; }
@Override
@Action(value="/zzw/form_post",
params={"encoding", "GBK"},
results={@Result(name="success", location="user.jsp")})
public String execute() throws Exception {
user=URLDecoder.decode(user, encoding);
return SUCCESS;
}
}
注意到,我们在客户端对提交的字符串进行了编码,而在服务端对接收的字符串进行了解码。这是因为需要保证前端和后端编码一致才不会出现乱码,通常客户端的编码是在jsp、HTML等页面中设置的。
上传文件
当客户端需要向服务端上传文件时,比如在课程网站上提交作业时(让我又想起要在Word里面打各种奇奇怪怪的数学符号),这时再使用上面介绍的POST请求就不行了。
我们先来看个上传文件的例子,先是jsp页面代码:
<form method="post" action="upload" enctype="multipart/form-data">
选择文件: <input type="file" id="file" name="file"><br>
<input type="submit" value="上传"><br>
</form>
这张图片是上传某个文件(图片)时抓获的Request和Response信息。
在Request Headers中有一个Content-Type字段,可以看到这个字段的值包含multipart/form-data
,和form表单的enctype属性值相同;另外boundary指定了多个文件(参数)之间的边界。
再看Request Payload,因为我们只上传了一个文件,所以这里只有两个boundary字符串。重点在于这里有一个Content-Disposition字段,这是我们真正需要的,里面的”name=’file’”和form表单中类型为file的input名字一致。
关于上面的Request Headers和Request Payload,详细的分析可以参考鸿洋大神的文章《从原理角度解析Android (Java) http 文件上传》。
下面是真正用于上传文件的POST请求代码:
private static Request getPostRequest(String url, Part[] parts) {
MultipartBody.Builder builder=new MultipartBody.Builder().
setType(MultipartBody.FORM);
if(parts!=null && parts.length>0) {
for(int i=0; i<parts.length; i++)
builder.addPart(parts[i].getHeaders(), parts[i].getRequestBody());
}
RequestBody requestBody=builder.build();
return new Request.Builder().url(url).post(requestBody).build();
}
上传文件的请求体是MultipartBody而不是FormBody,在MultipartBody中可以添加需要上传的文件和需要提交的请求参数,不管是上传文件还是提交请求参数,都需要提供一个请求头和请求体。
我们提供了一个Part接口来提供请求头和请求体:
public interface Part {
Headers getHeaders();
RequestBody getRequestBody();
}
同时我们也提交了实现Part接口的文件上传类(FilePart)和参数提交类(StringPart):
public static class FilePart implements Part {
private File file;
private MediaType mediaType;
private String formName;
public FilePart(File f, String mt, String fn) {
file=f;
mediaType=MediaType.parse(mt);
formName=fn;
}
@Override
public Headers getHeaders() {
return Headers.of("Content-Disposition",
"form-data; name=\""+formName+
"\"; filename=\""+file.getName()+"\"");
}
@Override
public RequestBody getRequestBody() {
return RequestBody.create(mediaType, file);
}
}
public static class StringPart implements Part {
private String param;
private String formName;
public StringPart(String p, String fn) {
param=p;
formName=fn;
}
@Override
public Headers getHeaders() {
return Headers.of("Content-Disposition",
"form-data; name=\""+formName+"\"");
}
@Override
public RequestBody getRequestBody() {
return RequestBody.create(null, param);
}
}
可以看到,如果想要上传文件,需要在请求头中添加form表单中类型为file的input名字和文件名,还要在请求体中添加文件的media type和File对象。
如果想要提交请求,需要在请求头中添加form表单中类型为text的input名字(对应与这里的StringPart,其实不一定是text类型),还要在请求体中添加请求参数。
下面我们进一步封装此POST请求:
public static void post(String url, Part[] parts, Callback callback) {
Request request=getPostRequest(url, parts);
Call call=client.newCall(request);
try {
Response response=call.execute();
callback.onResponse(call, response);
} catch (IOException e) {
callback.onFailure(call, e);
}
}
好了,现在你只需要提供URL、上传内容和回调对象就行了。实际上,大多数情况下都会上传文件和提交字符串形式的请求参数,因此你可以使用我们提供的FilePart和StringPart,现在你只需要提供URL、文件和文件的media type(或者请求参数)以及回调对象即可。
现在我们马上来测试一下:
HttpUtils.post(
"http://localhost:8080/HttpServer/zzw/upload",
new Part[]{new FilePart(new File("res\\picture.jpg"), "image/jpg", "src")},
new Callback(){
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
}
});
相应的服务端代码如下:
public class UploadAction extends ActionSupport {
private static final long serialVersionUID = -7408370789285125065L;
private File src;
private String srcContentType;
private String srcFileName;
private String destDir;
private String destFileName;
public void setSrc(File f) { src=f; }
public void setSrcContentType(String ct) { srcContentType=ct; }
public void setSrcFileName(String fn) { srcFileName=fn; }
public void setDestDir(String d) { destDir=d; }
public File getSrc() { return src; }
public String getSrcContentType() { return srcContentType; }
public String getSrcFileName() { return srcFileName; }
public String getDestDir() { return destDir; }
public String getDestFileName() { return destFileName; }
@Action(value="/zzw/upload",
params={"destDir", "F:\\workspace\\HttpServer\\res\\dest\\"},
results={@Result(name="success", location="upload.jsp")})
public String upload() throws Exception {
destFileName=UUID.randomUUID().toString().replaceAll("-", "")+
srcFileName.substring(srcFileName.indexOf('.'));
File dest=new File(destDir+destFileName);
FileUtils.copyFile(src, dest);
return SUCCESS;
}
}
上传成功后可以在%SERVER_PROJECT%/res/dest/
目录下找到客户端上传的文件。
下载文件
相信大多数人下载文件的次数都远远超过上传文件的次数,我们上面已经介绍了如何上传文件,那么你可能已经迫不及待地想要知道如何下载文件了。别急,下载文件的方法其实我们已经写好了,只需要稍微修改一下回调方法就OK了。
这里我们先来看看服务端到底是怎样相应一个下载文件的请求的:
public class DownloadAction extends ActionSupport {
private static final long serialVersionUID = 9078680027961237229L;
private String srcPath;
public String getSrcPath() { return srcPath; }
public void setSrcPath(String path) { srcPath=path; }
public InputStream getTarget() throws Exception {
return new FileInputStream(srcPath);
}
}
此Action在struts.xml中的配置如下:
<package name="zzw" extends="struts-default" namespace="/zzw">
<action name="download" class="com.zzw.action.DownloadAction">
<param name="srcPath">F:\workspace\HttpServer\res\src\butterfly.jpg</param>
<result name="success" type="stream">
<param name="contentType">image/jpg</param>
<param name="inputName">target</param>
<param name="contentDisposition">filename="butterfly.jpg"</param>
<param name="bufferSize">4096</param>
</result>
</action>
</package>
可以看到,服务端将返回一个类型为”image/jpg”的图像文件的字节流。我们只需要在客户端接收这个字节流并将其写入一个(图像)文件即可。
那么,你可能会问,到底是发送GET请求好呢,还是发送POST请求好呢?经测试后发现,不管是发送GET请求还是POST请求,都可以成功下载文件,不过发送GET请求更为简单。
下面是测试代码:
HttpUtils.get(
"http://localhost:8080/HttpServer/zzw/download",
null, new Callback(){
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String fileName=response.header("Content-Disposition");
int end=fileName.lastIndexOf('"');
int begin=fileName.lastIndexOf('"', end-1)+1;
fileName=fileName.substring(begin, end);
OutputStream os=new FileOutputStream("res\\"+fileName);
os.write(response.body().bytes());
os.flush();
os.close();
System.out.println("download completed!");
}
});
注意,这里返回的Content-Disposition是filename=”butterfly.jpg”,所以需要从中提取文件名。
下载完成后可以在%CLIENT_PROJECT%/res/
目录下找到下载的文件。
使用json进行通信
在网络通信中,json格式的文本是最常用的一种。既然如此,我们当然也想使用json格式的文本进行通信了。其实,json格式的文本说白了就是按照某个特定的格式编写的文本(字符串),我们只需要对其进行解析就行了(正则表达式)。
说到这,如果你真的自己去写json格式文本的解析器,那我也只能。。。Google提供了一个开源工具GSON,它使得解析json格式的文本变得像操作一个对象一样简单。
下面有一段json格式的文本:
{"name":"张三","age":22}
[{"name":"李四","age":23},{"name":"王五","age":24}]
第一行是一个人,第二行是两个人的集合。我们定义一个Person类来表示这样一个人:
public class Person {
private String name;
private int age;
public Person(String n, int a) {
name=n;
age=a;
}
public void setName(String n) { name=n; }
public void setAge(int a) { age=a; }
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return "{name:"+name+",age:"+age+"}";
}
}
下面来看我们的测试代码:
HttpUtils.get("http://localhost:8080/HttpServer/zzw/json", null, new Callback(){
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String msg=URLDecoder.decode(response.body().string(), "GBK");
String[] msgs=msg.split("\r\n");
System.out.println("response body="+msg);
Gson gson=new Gson();
Person person=gson.fromJson(msgs[0], Person.class);
System.out.println("person="+person);
List<Person> persons=gson.fromJson(msgs[1], new TypeToken<List<Person>>(){}.getType());
System.out.println("persons="+persons);
}
});
相应的服务端代码如下:
public class JsonAction extends ActionSupport {
private static final long serialVersionUID = 1059643779198325028L;
@Action(value="/zzw/json",
results={@Result(name="success", location="json.jsp")})
public String execute() throws Exception {
HttpServletRequest request=ServletActionContext.getRequest();
request.setCharacterEncoding("GBK");
request.getSession(true);
HttpServletResponse response=ServletActionContext.getResponse();
response.setCharacterEncoding("GBK");
PrintWriter writer=response.getWriter();
Gson gson=new Gson();
Person person=new Person("张三", 22);
String single=URLEncoder.encode(gson.toJson(person), "GBK");
List<Person> persons=new ArrayList<>();
persons.add(new Person("李四", 23));
persons.add(new Person("王五", 24));
String list=URLEncoder.encode(gson.toJson(persons), "GBK");
writer.write(single+URLEncoder.encode("\r\n", "GBK")+list);
writer.flush();
writer.close();
return SUCCESS;
}
}
可以看到,我们使用GSON解析这个json格式的文本只用了四行代码!
使用GSON时,需要先创建一个GSON对象"Gson gson=new Gson();"
,如果想把一个对象转换成json字符串,只需调用toJson方法;如果想把一个json字符串转换成对象,只需调用fromJson方法。
另外,注意解析一个对象和解析一个对象集合为json字符串需要传递不同的参数。解析对象只需要对象类型,而解析对象集合需要借助TypeToken类。
源代码
上述所有代码(包括客户端和服务端)都已上传到GitHub:
https://github.com/jzyhywxz/OKhttpTest