上一篇文章已经对http
协议和整体框架做了一个大致的介绍:
手写Android网络框架——CatHttp(一)
这篇文章我们主要就分析下具体子类是如何实现,以什么方式构建成可以被服务器识别并接受的数据类型提交上去的。所以这篇文章我们主要讨论正文的数据类型和格式。
并不是每种请求方式都能携带正文,如post
和put
可以携带正文,而get
和delete
不能携带正文,有参数的话直接拼接在url
后面。而不同的正文类型对应的一个请求头(Content-Type
)是不同的,Http
协议根据这个请求头按照对应的类型去解析正文。
如果正文传入的是表单,那么请求头声明的ContentType
为:
application/x-www-form-urlencoded; charset=UTF-8
表单组织的结构格
username=zhangsan&pwd=12345&date=2015
当两者均满足该要求时,服务器可以正常解析表单里的数据。
在最初的 http
协议中,没有上传文件方面的功能。 rfc1867
为 http
协议添加了这个功能。
如果想传输文件或者一组二进制字节,则请求头声明的Content-Type
声明为:
"multipart/from-data; boundary=" + boundary;
其中boundary
是一个随机数,在下面的正文格式中会使用该boundary
作为标识:
--AaB03x
Content-Disposition: form-data; name="field1"
Joe Blow
--AaB03x
Content-Disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
可以看到,其实Multipart
的方式也同时支持传输键值对,只是构造的方式不一样,每一个(部分)
,姑且称为部分,都是以--boundary
开始的,并且后面跟着类似请求头的键值对,用来说明数据类型,每部分的数据正文跟这种“请求头”分隔开。
了解了不同正文的格式,我们就可以实现我们的子类了。
针对于文件这种格式会比较复杂,但是观察会有一个共同点,也就是我们说的“部分”,这里用Part
表示,下面先看具体的http
任务是怎么执行的
可以看到,实际的Call实际不管是同步还是异步,都是调用了HttpThreadPool
提供的结构来执行Task
,从而调度任务的执行的。
public class HttpCall implements Call {
final Request request;
final CatHttpClient.Config config;
private IRequestHandler requestHandler = new RequestHandler();
public HttpCall(CatHttpClient.Config config, Request request) {
this.config = config;
this.request = request;
}
@Override
public Response execute() {
Callable task = new SyncTask();
Response response;
try {
response = HttpThreadPool.getInstance().submit(task);
return response;
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return new Response.Builder()
.code(400)
.message("线程异常中断")
.body(new ResponseBody(null))
.build();
}
@Override
public void enqueue(Callback callback) {
Runnable runnable = new HttpTask(this, callback, requestHandler);
HttpThreadPool.getInstance().execute(new FutureTask<>(runnable, null));
}
/**
* 同步提交Callable
*/
class SyncTask implements Callable {
@Override
public Response call() throws Exception {
Response response = requestHandler.handlerRequest(HttpCall.this);
return response;
}
}
}
RequestHandler
是网络请求的处理者,将请求的Request
解析成标准的Http
格式的请求提交给服务器并获取服务器返回的内容。
public class RequestHandler implements IRequestHandler {
@Override
public Response handlerRequest(HttpCall call) throws IOException {
HttpURLConnection connection = mangeConfig(call);
if (!call.request.heads.isEmpty()) addHeaders(connection, call.request);
if (call.request.body != null) writeContent(connection, call.request.body);
if (!connection.getDoOutput()) connection.connect();
//解析返回内容
int responseCode = connection.getResponseCode();
if (responseCode >= 200 && responseCode < 400) {
byte[] bytes = new byte[1024];
int len;
InputStream ins = connection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = ins.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
Response response = new Response
.Builder()
.code(responseCode)
.message(connection.getResponseMessage())
.body(new ResponseBody(baos.toByteArray()))
.build();
try {
ins.close();
connection.disconnect();
} finally {
if (ins != null) ins.close();
if (connection != null) connection.disconnect();
}
return response;
}
throw new IOException(String.valueOf(connection.getResponseCode()));
}
/**
* 用
*
* @param connection
* @param body
* @throws IOException
*/
private void writeContent(HttpURLConnection connection, RequestBody body) throws IOException {
OutputStream ous = connection.getOutputStream();
body.writeTo(ous);
}
/**
* HttpUrlConnection基本参数的配置
*
* @param call
* @return
* @throws IOException
*/
private HttpURLConnection mangeConfig(HttpCall call) throws IOException {
URL url = new URL(call.request.url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(call.config.connTimeout);
connection.setReadTimeout(call.config.readTimeout);
connection.setDoInput(true);
if (call.request.body != null && Request.HttpMethod.checkNeedBody(call.request.method)) {
connection.setDoOutput(true);
}
return connection;
}
/**
* 给对象添加请求头
*
* @param connection
* @param request
*/
private void addHeaders(HttpURLConnection connection, Request request) {
Set keys = request.heads.keySet();
for (String key : keys) {
connection.addRequestProperty(key, request.heads.get(key));
}
}
}
public class Util {
public static void checkMap(String key, String value) {
if (key == null) throw new NullPointerException("key == null");
if (key.isEmpty()) throw new NullPointerException("key is empty");
if (value == null) throw new NullPointerException("value == null");
if (value.isEmpty()) throw new NullPointerException("value is empty");
}
public static void checkMethod(Request.HttpMethod method, RequestBody body) {
if (method == null)
throw new NullPointerException("method == null");
if (body != null && Request.HttpMethod.checkNoBody(method))
throw new IllegalStateException("方法" + method + "不能有请求体");
if (body == null && Request.HttpMethod.checkNeedBody(method))
throw new IllegalStateException("方法" + method + "必须有请求体");
}
/**
* 转换成file的头
*
* @param key
* @param fileName
* @return
*/
public static String trans2FileHead(String key, String fileName) {
StringBuilder sb = new StringBuilder();
sb.append(MultipartBody.disposition)
.append("name=")//name=
.append("\"").append(key).append("\"").append(";").append(" ")//"key";
.append("filename=")//filename
.append("\"").append(fileName).append("\"")//"filename"
.append("\r\n");
return sb.toString();
}
/**
* 转换成表单形式
*
* @param key
* @return
*/
public static String trans2FormHead(String key) {
StringBuilder sb = new StringBuilder();
sb.append(MultipartBody.disposition)
.append("name=")//name=
.append("\"").append(key).append("\"") //"key"
.append("\r\n");//next line
return sb.toString();
}
public static byte[] getUTF8Bytes(String str) throws UnsupportedEncodingException {
return str.getBytes("UTF-8");
}
}
既然表单这种格式比较简单,我们就先构建表单,可以看到,我们可以通过建造者的方式可以直接传入键值对或者map
,内部用一个ArrayMap
来存储键值对,写出的时候将map
里的键值对按照表单的方式构建好再写出去。
public class FormBody extends RequestBody {
// 限制参数不要过多(ArrayMap效率,而且很少需要破k的参数)
public static final int MAX_FROM = 1000;
final Map map;
public FormBody(Builder builder) {
this.map = builder.map;
}
@Override
public String contentType() {
return "application/x-www-form-urlencoded; charset=UTF-8";
}
@Override
public void writeTo(OutputStream ous) throws IOException {
try {
ous.write(transToString(map).getBytes("UTF-8"));
ous.flush();
} finally {
if (ous != null) {
ous.close();
}
}
}
/**
* 拼接请求参数
*
* @param map
* @return
*/
private String transToString(Map map) throws UnsupportedEncodingException {
StringBuilder sb = new StringBuilder();
Set keys = map.keySet();
for (String key : keys) {
if (!TextUtils.isEmpty(sb)) {
sb.append("&");
}
sb.append(URLEncoder.encode(key, "UTF-8"));
sb.append("=");
sb.append(URLEncoder.encode(map.get(key), "UTF-8"));
}
return sb.toString();
}
public static class Builder {
private Map map;
public Builder() {
map = new ArrayMap<>();
}
public Builder add(String key, String value) {
if (map.size() > MAX_FROM) throw new IndexOutOfBoundsException(" 请求参数过多");
Util.checkMap(key, value);
map.put(key, value);
return this;
}
public Builder map(Map map) {
if (map.size() > MAX_FROM) throw new IndexOutOfBoundsException(" 请求参数过多");
this.map = map;
return this;
}
public FormBody build() {
return new FormBody(this);
}
}
}
Part
作为一个抽象类提供了两个静态方法用来创建不同的对象,一种是键值对,另一种是文件。其中写出正文的方法按照标准格式来就行了。
public abstract class Part {
private Part() {
}
public abstract String contentType();
public abstract String heads();
public abstract void write(OutputStream ous) throws IOException;
/**
* 创建构建form的part
*
* @param key
* @param value
* @return
*/
public static Part create(final String key, final String value) {
return new Part() {
@Override
public String contentType() {
return null;
}
@Override
public String heads() {
return Util.trans2FormHead(key);
}
@Override
public void write(OutputStream ous) throws IOException {
ous.write(heads().getBytes("UTF-8"));
ous.write(END_LINE);
ous.write(value.getBytes("UTF-8"));
ous.write(END_LINE);
}
};
}
public static Part create(final String type, final String key, final File file) {
if (file == null) throw new NullPointerException("file 为空");
if (!file.exists()) throw new IllegalStateException("file 不存在");
return new Part() {
@Override
public String contentType() {
return type;
}
@Override
public String heads() {
return Util.trans2FileHead(key, file.getName());
}
@Override
public void write(OutputStream ous) throws IOException {
ous.write(heads().getBytes());
ous.write("Content-Type: ".getBytes());
ous.write(Util.getUTF8Bytes(contentType()));
ous.write(END_LINE);
ous.write(END_LINE);
writeFile(ous, file);
ous.write(END_LINE);
ous.flush();
}
/**
* 写出文件
* @param ous 输出流
* @param file 文件
*/
private void writeFile(OutputStream ous, File file) throws IOException {
FileInputStream ins = null;
try {
ins = new FileInputStream(file);
int len;
byte[] bytes = new byte[2048];
while ((len = ins.read(bytes)) != -1) {
ous.write(bytes, 0, len);
}
} finally {
if (ins != null) {
ins.close();
}
}
}
};
}
}
MultipartBody
存储了一组Part
对象,对外提供了两个接口——传入键值对和传入文件,同时按照上面Multipart
的格式写出body
存储的所有内容。
public class MultipartBody extends RequestBody {
public static final String disposition = "content-disposition: form-data; ";
public static final byte[] END_LINE = {'\r', '\n'};
public static final byte[] PREFIX = {'-', '-'};
final List parts;
final String boundary;
public MultipartBody(Builder builder) {
this.parts = builder.parts;
this.boundary = builder.boundary;
}
@Override
public String contentType() {
return "multipart/from-data; boundary=" + boundary;
}
@Override
public void writeTo(OutputStream ous) throws IOException {
try {
for (Part part : parts) {
ous.write(PREFIX);
ous.write(boundary.getBytes("UTF-8"));
ous.write(END_LINE);
part.write(ous);
}
ous.write(PREFIX);
ous.write(boundary.getBytes("UTF-8"));
ous.write(PREFIX);
ous.write(END_LINE);
ous.flush();
} finally {
if (ous != null) {
ous.close();
}
}
}
public static class Builder {
private String boundary;
private List parts;
public Builder() {
this(UUID.randomUUID().toString());
}
private Builder(String boundary) {
this.parts = new ArrayList<>();
this.boundary = boundary;
}
public Builder addPart(String type, String key, File file) {
if (key == null) throw new NullPointerException("part name == null");
parts.add(Part.create(type, key, file));
return this;
}
public Builder addForm(String key, String value) {
if (key == null) throw new NullPointerException("part name == null");
parts.add(Part.create(key, value));
return this;
}
public MultipartBody build() {
if (parts.isEmpty()) throw new NullPointerException("part list == null");
return new MultipartBody(this);
}
}
}
具体实现类基本如上,这两篇就是CatHttp
的全部内容了,源码已经放在github
上——传送门,如果有什么不足之处,欢迎大家指正,如果觉得我写的还不错,就关注我吧~