HTTP报文及Retrofit基本使用
1 前言
每次使用 Retrofit 做网络请求都要到网上去搜索代码,然后复制、粘贴;有时候某个注解的使用方式忘记了,也会查查这个注解的使用例子。查的多了就觉得烦了,所以想着自己总结一下,下次使用直接查自己的博客就好了。
从本质来看,Retrofit 网络请求工作都是 OkHttp 做的,它只是对 OkHttp 的又一次封装。
Retrofit 通过注解的方式,封装了网络请求的接口,所以学习 Retrofit 的关键是学会注解。那注解是用来干啥使的呢?注解其实是用来拼接网络请求接口用的,也可以说是用来拼接报文的。所以在学习注解之前,应该先学习一下报文的知识。学完了报文再学注解,理解起注解的意思来就会非常容易了。
2 报文
2.1 报文的格式
为什么要在学习 Retrofit 之前,要先学习报文呢?
因为每一次网络交互,其实都是报文的交互。App 发送请求报文给服务器,服务器收到请求后发送响应报文给App,App根据响应报文再解析出自己需要的数据进行显示,这就完成了一次网络交互。
所以,只要知道了报文的格式,再熟悉一下 Retrofit 的注解,就能够很容易理解、记忆 Retrofit 的使用方式了。
2.1.1 请求报文的格式
先说一下 HTTP 请求报文的组成吧,一个HTTP请求报文由请求行、请求头、空行和请求体四部分组成,其一般格式如图一所示。
2.1.1.1 请求行
请求行由请求方法、路径和 HTTP 协议版本这三个字段组成,字段间使用空格间隔。图一中的 POST
就是请求方法,/user/register
就是请求路径,HTTP/1.1
就是 HTTP 协议版本。
HTTP 协议从版本角度分为 HTTP 1.0 和 HTTP1.1。
HTTP 1.0 请求方法:GET,POST 和 HEAD 。
HTTP1.1 新增的请求方法:OPTIONS,PUT, DELETE,TRACE和 CONNECT。
所以 ,HTTP 协议总共有 8 种请求方法。这里说的 HTTP 1.0 和 HTTP1.1 就是HTTP 协议的版本。
下面记录下各个请求方法的作用。
【GET】 请求数据。请求指定的URL,并返回实体主体(常见的是返回 Json 串)。
【POST】 新增或修改。向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
【HEAD】 获取响应头。类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
【PUT】 修改。从客户端向服务器传送的数据取代指定的文档的内容。
【DELETE】 删除。请求服务器删除指定的页面。
【CONNECT】 HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
【OPTIONS】 允许客户端查看服务器的性能
【TRACE】 回显服务器收到的请求,主要用于测试或诊断
虽然我们有这么多种请求方法,但常用的还是 GET、POST、HEAD 、PUT 和 DELETE 这五种方法。
GET 请求用于从服务器获取数据,比如获取 Json 格式的数据,一般来说,GET 请求报文是没有请求体的,
POST 方法用于新增或修改资源,典型的应用就是用户注册,注册一个新用户,就是向服务器新增一个资源,POST 方法是有请求体的。
HEAD 方法和 GET 方法类似,他们的区别就是 HEAD 用于获取响应头,它不返回响应体,而 GET 方法既会返回响应头,也会返回响应体。
PUT 方法用于修改服务器资源,典型应用就是修改昵称,它和 POST 方法的区别是 POST 方法既可以修改资源,也可以新增资源,但 PUT 方法只能新增资源,不能修改资源。
DELETE 方法用于删除资源,没什么可说的。
说完了请求行中的请求方法,接着说说请求行中的路径。那请求行中的路径是怎么来的呢?我举例说一下吧,如图二所示,我有 https://www.wanandroid.com/user/register
这样一个接口,那这个接口中的 https
就表示协议类型,www.wanandroid.com
就表示主机地址,/user/register
就是路径了,这个解释一目了然,我就不多说了。
请求行中的协议版本前面已经提过了,就不赘述了。
图二 URL.png
2.1.1.2 请求头
请求头:请求头由键值对组成,每一行就是一组键值对,键和值之间用英文冒号分隔,像图一中的 Host: www.wanandroid.com
这样就是一组请求头。
下面记录一些比较常见的请求头。
User-Agent: 用户代理,即是谁实际发送请求、接受响应的,例如⼿机浏览器、某款⼿机 App
Host:主机名
Content-Type: 指定 Body 的类型,比如 Content-Type: text/html; charset=utf-8、
Content-Type: application/x-www-form-urlencoded、Content-Type: multipart/form-data; boundary=----
Content-Type: application/json; charset=utf-8等等。
Content-Encoding:压缩类型。如 gzip
Content-Length:请求体或响应体的长度 指定 Body 的长度(字节)
Accept:表示客户端希望服务器返回什么类型的响应
Accept-Charset: 客户端接受的字符集。如 utf-8
Accept-Encoding: 客户端接受的压缩编码类型。如 gzip
Accept-Charset:表示客户端希望服务器返回的内容的编码格式
Location 指定重定向的⽬标 URL
2.1.2 响应报文的格式
响应报文和请求报文的格式差不多,响应报文由状态行、响应头和响应体组成,响应报文格式如图三所示。状态行由 Http协议版本,响应码和响应信息组成。
响应头和响应体的意思和请求报文一样,所以也没有什么可多说的了。
下面着重说一下状态码。状态码由三位数字组成,第一个数字定义了响应的类别。
1xx:指示信息--表示请求已接收,继续处理。
2xx:成功--表示请求已被成功接收、理解、接受。
3xx:重定向--要完成请求必须进行更进一步的操作。
4xx:客户端错误--请求有语法错误或请求无法实现。
5xx:服务器端错误--服务器未能实现合法的请求。
常见状态代码、状态描述的说明如下。
200 OK:客户端请求成功。
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常。
2.2 报文的实例
现在我有这样一个接口:https://www.wanandroid.com/user/register(该接口来自于鸿洋老师的玩 Android 开放 API),使用 Postman 发送一下 POST请求,如图四所示,我们看到了该接口返回了 Json 数据,说明我们请求成功了。
接下来,通过点击如图四所示的红框圈出来的“Code”,即可看到如图五所示的网络请求的报文,这时我们就看到了之前提到的那些东西,比如 Content-Type、Host 等。其实我们平时在用 OkHttp 做网络请求的时候,也可以通过拦截器拿到这些东西。
图五 请求报文.png看完请求报文我们再看一下响应报文。不过,Postman将响应报文分开显示了,没有像请求报文一样集中在一起。
图六 Postman 响应.png如图六所示,状态行、响应头和响应体是分开的。
图六中红框圈出来的 Body 代表响应体,我们 Android 端见得最多的响应体格式就是 Json 格式了吧,哈哈。
图六中红框圈出来的 Headers 代表响应头,比较常见的响应头部有 Content-Type、Date等。
图六中红框圈出来的 Status:200 OK 是状态行的一部分,200是响应码,OK 是响应信息,还缺一个没显示的是 Http 协议版本,它显示不显示对我们来说也不重要,反正用不到。
3 使用
说了半天,终于开始写 Retrofit 的使用了,真不容易啊。现在如果我要请求这个接口的数据:https://wanandroid.com/wxarticle/chapters/json ,我该怎么做呢?
第一步,添加依赖
新建一个 Project,在 app 的 build.gradle (不是 project 的 build.gradle)添加如下依赖:
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
第二步,在 AndroidManifest.xml 里添加网络权限。
<uses-permission android:name="android.permission.INTERNET" />
第三步,新建实体类
通过 Postman 请求接口,获取返回的 Json 数据,使用 GsonFormat(Android Studio 的插件) 生成实体类 ArticleModel 。
package isuperred.com.github.retrofit;
import java.util.List;
public class ArticleModel {
private int errorCode;
private String errorMsg;
private List<DataBean> data;
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public List<DataBean> getData() {
return data;
}
public void setData(List<DataBean> data) {
this.data = data;
}
public static class DataBean {
private int courseId;
private int id;
private String name;
private int order;
private int parentChapterId;
private boolean userControlSetTop;
private int visible;
private List<?> children;
public int getCourseId() {
return courseId;
}
public void setCourseId(int courseId) {
this.courseId = courseId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public int getParentChapterId() {
return parentChapterId;
}
public void setParentChapterId(int parentChapterId) {
this.parentChapterId = parentChapterId;
}
public boolean isUserControlSetTop() {
return userControlSetTop;
}
public void setUserControlSetTop(boolean userControlSetTop) {
this.userControlSetTop = userControlSetTop;
}
public int getVisible() {
return visible;
}
public void setVisible(int visible) {
this.visible = visible;
}
public List<?> getChildren() {
return children;
}
public void setChildren(List<?> children) {
this.children = children;
}
}
}
第四步,添加接口
新建存放请求数据的接口的类 ApiService。
package isuperred.com.github.retrofit;
import retrofit2.Call;
import retrofit2.http.GET;
public interface ApiService {
@GET("wxarticle/chapters/json")
Call<ArticleModel> getArticle();
}
上面的 @GET
注解表示该接口使用 GET 请求方法,"wxarticle/chapters/json"
就是请求报文中请求行里 路径
这个字段,Call
是固定不变的(如果使用 Rxjava,Call 会被替换为 Observable),泛型 ArticleModel
就是第三步新建的实体类,getArticle
是方法名。
第五步,创建 Retrofit 管理类 RetrofitManager。下面的 BASE_URL
就是https://wanandroid.com/wxarticle/chapters/json
这个接口的协议和域名。注意一下,BASE_URL
最后是有“/”
的,ApiService 接口的注解里的 wxarticle/chapters/json
最前面是没有“/”
的。
package isuperred.com.github.retrofit;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitManager {
private static RetrofitManager instance;
private static final String BASE_URL = "https://www.wanandroid.com/";
private Retrofit mRetrofit;
public RetrofitManager() {
mRetrofit = new Retrofit
.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
public static RetrofitManager getInstance() {
if (instance == null) {
synchronized (RetrofitManager.class) {
instance = new RetrofitManager();
}
}
return instance;
}
public <T> T create(final Class<T> service) {
return mRetrofit.create(service);
}
}
第六步,在 Activity 里调用 Retrofit 方法进行异步或同步请求网络数据。注意同步请求会阻塞 UI 线程,所以同步请求应在子线程中执行。
package isuperred.com.github.retrofit;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import java.io.IOException;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnEnqueue = findViewById(R.id.btn_enqueue);
Button btnExecute = findViewById(R.id.btn_execute);
btnEnqueue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//创建ApiService对象
ApiService apiService = RetrofitManager.getInstance().create(ApiService.class);
Call<ArticleModel> article = apiService.getArticle();
//异步请求数据
article.enqueue(new Callback<ArticleModel>() {
@Override
public void onResponse(Call<ArticleModel> call, Response<ArticleModel> response) {
ArticleModel articleModel = response.body();
if (articleModel == null) {
return;
}
List<ArticleModel.DataBean> dataBeans = articleModel.getData();
for (int i = 0; i < dataBeans.size(); i++) {
Log.e(TAG, "onResponse Name: " + dataBeans.get(i).getName());
}
}
@Override
public void onFailure(Call<ArticleModel> call, Throwable t) {
t.printStackTrace();
}
});
}
});
btnExecute.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
//创建ApiService对象
ApiService apiService = RetrofitManager.getInstance().create(ApiService.class);
Call<ArticleModel> article = apiService.getArticle();
ArticleModel articleModel = null;
try {
//同步请求数据
articleModel = article.execute().body();
} catch (IOException e) {
e.printStackTrace();
}
if (articleModel == null) {
return;
}
List<ArticleModel.DataBean> dataBeans = articleModel.getData();
for (int i = 0; i < dataBeans.size(); i++) {
Log.e(TAG, "onResponse Name: " + dataBeans.get(i).getName());
}
}
}).start();
}
});
}
}
其对应的布局文件 activity_main.xml 如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_enqueue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="异步请求"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_execute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="同步请求"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_enqueue" />
</android.support.constraint.ConstraintLayout>
4 总结
先写这么多吧,注解下次再写了。写东西真麻烦啊,越写越多,越写越多,我都没想到报文的内容写了这么久。关键我都写烦了,还觉得自己没写清楚呢。真的是觉得把一个东西写清楚是需要功力的,我还没有功力啊。。。我就不瞎感慨了,早点回去洗衣服了!