Android studio进阶开发(四)--okhttp的网络通信的使用
我们之前学过了socket服务器,这次我们继续来学习网络热门编程http/https的使用与交互
1)什么是Http协议?
答:hypertext transfer protocol(超文本传输协议),TCP/IP协议的一个应用层协议,用于 定义WEB浏览器与WEB服务器之间交换数据的过程。客户端连上web服务器后,若想获得web服务器 中的某个web资源,需遵守一定的通讯格式,HTTP协议用于定义客户端与web服务器通迅的格式。
2)Http 1.0 与 Http 1.1的区别
答:1.0协议,客户端与web服务器建立连接后,只能获得一个web资源! 而1.1协议,允许客户端与web服务器建立连接后,在一个连接上获取多个web资源!
3)Http协议的底层工作流程:
答:我们先要知道两个名词:
SYN(synchronous):TCP/IP建立连接时使用的握手信号
ACK(Acknowledgement):确认字符,确认发来的数据已经接受无误
接着就到TCP/IP三次握手的概念:
客户端发送syn包(syn = j)到服务器,进入SYN_SEND状态,然后等待服务器确认
服务器收到syn包,确认客户的syn(ack = j + 1),同时在自己也发送一个SYN包(syn=k), 即SYN + ACK包,服务器进入SYN_RECV状态
客户端收到SYN + ACK包,向服务器发送确认包ACK(ack = k +1),发送完毕后,客户端与服务端 进入ESTABLISHED状态,完成三次握手,然后两者开始传送数据。
实际开发中我们用得较多的方式是Get和Post,但是实际开发可能还会用到其他请求方式,比如PUT, 小猪的实际项目中就用到了,下面为了方便大家,就把所有的请求方式列出来吧:
Get:请求获取Request-URI所标识的资源
POST:在Request-URI所标识的资源后附加新的数据
HEAD 请求获取由Request-URI所标识的资源的响应信息报头
PUT:请求服务器存储一个资源,并用Request-URI作为其标识
DELETE:请求服务器删除Request-URI所标识的资源
TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT:保留将来使用
OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项
GET:在请求的URL地址后以?的形式带上交给服务器的数据,多个数据之间以&进行分隔, 但数据容量通常不能超过2K,比如:http://xxx?username=…&pawd=…这种就是GET
POST: 这个则可以在请求的实体内容中向服务器发送数据,传输没有数量限制
另外要说一点,这两个玩意都是发送数据的,只是发送机制不一样,不要相信网上说的 “GET获得服务器数据,POST向服务器发送数据”!!另外GET安全性非常低,Post安全性较高, 但是执行效率却比Post方法好,一般查询的时候我们用GET,数据增删改的时候用POST!!
代码解析
1. 创建 OkHttpClient 对象
OkHttpClient client = new OkHttpClient();
作用:初始化一个 OkHttp 客户端实例,用于发起 HTTP 请求。
说明:OkHttp 会自动管理连接池、线程池和缓存,确保网络请求高效。
2. 构建 HTTP 请求结构(Request)
Request request = new Request.Builder().header("Accept-Language", "zh-CN") // 设置请求头:语言为中文.header("Referer", "https://finance.sina.com.cn") // 设置来源页.url(URL_STOCK) // 指定请求的 URL(如新浪股票接口).build();
关键参数:
url():目标接口地址(例如 https://hq.sinajs.cn/list=s_sh000001)。
header():添加 HTTP 请求头(如语言、来源页),某些接口依赖这些头信息验证请求合法性
3. 发起异步请求
Call call = client.newCall(request);
call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {// 处理失败(如网络不可用、URL 错误)runOnUiThread(() -> tv_result.setText("调用股指接口报错:" + e.getMessage()));}@Overridepublic void onResponse(Call call, Response response) throws IOException {String resp = response.body().string();// 处理成功响应runOnUiThread(() -> tv_result.setText("调用股指接口返回:\n" + resp));}
});
关键步骤:
client.newCall(request):将请求封装为一个 Call 对象。
call.enqueue():将请求加入队列,异步执行(后台线程发起请求,不阻塞主线程)。
onResponse():当服务器返回响应时,通过 response.body().string() 读取响应体内容。
runOnUiThread():将结果显示到 UI 线程(Android 禁止在非 UI 线程更新界面)。
4. 处理响应数据
响应内容:假设新浪股票接口返回文本数据(如 var hq_str_s_sh000001=“上证指数,3094.67,…”;)。
解析数据:实际开发中需解析文本(如拆分字符串或使用正则表达式提取关键数值)
String data = resp.split("\"")[1]; // 示例:提取引号内的数据
String[] fields = data.split(","); // 按逗号分割字段
String indexName = fields[0]; // 指数名称
String currentPrice = fields[1]; // 当前价格
全部代码如下:
Netconst 类
package com.example.tttplean;public class Netconst {// HTTP地址的前缀// HTTP地址的前缀public final static String HTTP_PREFIX = "https://192.168.1.7:8080/HttpServer/";// WebSocket服务的前缀public final static String WEBSOCKET_PREFIX = "ws://192.168.1.7:8080/HttpServer/";//public final static String BASE_IP = "192.168.1.7"; // 基础Socket服务的ippublic final static String BASE_IP = "192.168.43.9"; // 基础Socket服务的ip(这里要改为前面获取的IPv4的地址)public final static int BASE_PORT = 9010; // 基础Socket服务的端口public final static String CHAT_IP = "192.168.1.7"; // 聊天Socket服务的ippublic final static int CHAT_PORT = 9011;
}
Okhttp.activity.java:
package com.example.tttplean;import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
import android.widget.TextView;import androidx.appcompat.app.AppCompatActivity;import com.example.tttplean.Netconst;import org.json.JSONObject;import java.io.IOException;import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;public class Okhttp extends AppCompatActivity {private final static String TAG = "OkhttpCallActivity";private final static String URL_STOCK = "https://hq.sinajs.cn/list=s_sh000001";private final static String URL_LOGIN = Netconst.HTTP_PREFIX + "login";private LinearLayout ll_login; // 声明一个线性布局对象private EditText et_username; // 声明一个编辑框对象private EditText et_password; // 声明一个编辑框对象private TextView tv_result; // 声明一个文本视图对象private int mCheckedId = R.id.rb_get; // 当前选中的单选按钮资源编号@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_okhttp);ll_login = findViewById(R.id.ll_login);et_username = findViewById(R.id.et_username);et_password = findViewById(R.id.et_password);tv_result = findViewById(R.id.tv_result);RadioGroup rg_method = findViewById(R.id.rg_method);rg_method.setOnCheckedChangeListener((group, checkedId) -> {mCheckedId = checkedId;int visibility = mCheckedId == R.id.rb_get ? View.GONE : View.VISIBLE;ll_login.setVisibility(visibility);});findViewById(R.id.btn_send).setOnClickListener(v -> {if (mCheckedId == R.id.rb_get) {doGet(); // 发起GET方式的HTTP请求} else if (mCheckedId == R.id.rb_post_form) {postForm(); // 发起POST方式的HTTP请求(报文为表单格式)} else if (mCheckedId == R.id.rb_post_json) {postJson(); // 发起POST方式的HTTP请求(报文为JSON格式)}});}// 发起GET方式的HTTP请求private void doGet() {OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request = new Request.Builder()//.get() // 因为OkHttp默认采用get方式,所以这里可以不调get方法.header("Accept-Language", "zh-CN") // 给http请求添加头部信息.header("Referer", "https://finance.sina.com.cn") // 给http请求添加头部信息.url(URL_STOCK) // 指定http请求的调用地址.build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用股指接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用股指接口返回:\n"+resp));}});}// 发起POST方式的HTTP请求(报文为表单格式)private void postForm() {String username = et_username.getText().toString();String password = et_password.getText().toString();// 创建一个表单对象FormBody body = new FormBody.Builder().add("username", username).add("password", password).build();OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个POST方式的请求结构Request request = new Request.Builder().post(body).url(URL_LOGIN).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口返回:\n"+resp));}});}// 发起POST方式的HTTP请求(报文为JSON格式)private void postJson() {String username = et_username.getText().toString();String password = et_password.getText().toString();String jsonString = "";try {JSONObject jsonObject = new JSONObject();jsonObject.put("username", username);jsonObject.put("password", password);jsonString = jsonObject.toString();} catch (Exception e) {e.printStackTrace();}// 创建一个POST方式的请求结构RequestBody body = RequestBody.create(jsonString, MediaType.parse("text/plain;charset=utf-8"));OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象Request request = new Request.Builder().post(body).url(URL_LOGIN).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口返回:\n"+resp));}});}
}
xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><RadioGroupandroid:id="@+id/rg_method"android:layout_width="match_parent"android:layout_height="30dp"android:orientation="horizontal"><RadioButtonandroid:id="@+id/rb_get"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:checked="true"android:gravity="left|center"android:text="GET方式"android:textColor="@color/black"android:textSize="16sp" /><RadioButtonandroid:id="@+id/rb_post_form"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:checked="false"android:gravity="left|center"android:text="表单POST"android:textColor="@color/black"android:textSize="16sp" /><RadioButtonandroid:id="@+id/rb_post_json"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:checked="false"android:gravity="left|center"android:text="JSON POST"android:textColor="@color/black"android:textSize="16sp" /></RadioGroup><LinearLayoutandroid:id="@+id/ll_login"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingLeft="5dp"android:paddingRight="5dp"android:orientation="vertical"android:visibility="gone"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="40dp"android:orientation="horizontal"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center"android:text="用户名:"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:id="@+id/et_username"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:background="@drawable/editext_selector"android:gravity="left|center"android:hint="请输入用户名"android:maxLength="11"android:textColor="@color/black"android:textSize="17sp" /></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="40dp"android:layout_marginTop="10dp"android:orientation="horizontal"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center"android:text="密 码:"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:id="@+id/et_password"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:background="@drawable/editext_selector"android:gravity="left|center"android:hint="请输入密码"android:inputType="numberPassword"android:maxLength="6"android:textColor="@color/black"android:textSize="17sp" /></LinearLayout></LinearLayout><Buttonandroid:id="@+id/btn_send"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="发起接口调用"android:textColor="@color/black"android:textSize="17sp" /><TextViewandroid:id="@+id/tv_result"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingLeft="5dp"android:textColor="@color/black"android:textSize="17sp" />
</LinearLayout>
效果图:
注意项:
- Referer 头部的作用
定义:Referer(注意拼写是 Referer,而非正确的英文单词 “Referrer”)表示当前请求的来源页面 URL。
用途:
反爬虫:服务器可能检查 Referer 是否来自合法页面,否则拒绝响应(如新浪股票接口)。
防盗链:防止其他网站直接引用资源(如图片、API)。
流量统计:分析用户从哪个页面跳转而来。
2. 代码中 Referer 为何是 https://finance.sina.com.cn
接口要求:新浪股票接口(hq.sinajs.cn)的服务端会校验 Referer,必须来自新浪财经域名(如 finance.sina.com.cn),否则返回 403 Forbidden。
代码示例:
java
.header("Referer", "https://finance.sina.com.cn") // 模拟浏览器从新浪财经页面发起的请求
3. Referer 网址的具体要求
(1) 合法性要求
域名匹配:Referer 的域名需与目标接口的域名一致或属于其信任的白名单。
✅ 合法案例:访问 hq.sinajs.cn 接口时,Referer 设置为 https://finance.sina.com.cn。
❌ 非法案例:若设置为 https://example.com,新浪服务器会拒绝请求。
(2) 协议一致性
HTTP/HTTPS:如果目标接口是 HTTPS,Referer 也应尽量使用 HTTPS(避免混合协议问题)。
(3) 路径要求
允许根域名或子路径:服务器可能接受根域名或特定子路径。
✅ 可接受:https://finance.sina.com.cn 或 https://finance.sina.com.cn/stock/
❌ 不可接受:随意编造的不相关路径(如 https://finance.sina.com.cn/fake-path)。
4. 常见场景及调试方法
(1) 服务器不校验 Referer
如果接口未校验 Referer,可不设置该头部,或设为 null:
java
// 显式移除 Referer(OkHttp 默认不发送)
.header(“Referer”, “”)
(2) 服务器严格校验 Referer
通过浏览器开发者工具分析:
在浏览器中打开目标页面(如新浪财经)。
按 F12 → Network 查看实际请求的 Referer 值。
在代码中复制该值。
接口文档:查阅官方文档是否明确要求 Referer。
(3) 动态 Referer
如果不同页面需设置不同 Referer,可通过变量动态设置:
String referer = "https://finance.sina.com.cn"; // 根据场景调整
Request request = new Request.Builder().header("Referer", referer).url(URL_STOCK).build();
5. 注意事项
拼写错误:确保拼写为 Referer(非 Referrer)。
隐私限制:部分浏览器或安全设置会禁用 Referer 头(需测试兼容性)。
反爬策略:频繁调用接口时,即使 Referer 合法,也可能触发 IP 封禁。
总结
核心要求:Referer 必须指向服务器信任的域名(如新浪财经域名)。
验证方式:通过浏览器开发者工具或接口文档确定合法值。
代码实现:在 OkHttp 请求中通过 .header(“Referer”, “信任的URL”) 设置。