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 对象​

java 复制代码
OkHttpClient client = new OkHttpClient();

作用​​:初始化一个 OkHttp 客户端实例,用于发起 HTTP 请求。

​​说明​​:OkHttp 会自动管理连接池、线程池和缓存,确保网络请求高效。

2. 构建 HTTP 请求结构(Request)​

java 复制代码
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. 发起异步请求​

java 复制代码
Call call = client.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 处理失败(如网络不可用、URL 错误)
        runOnUiThread(() -> tv_result.setText("调用股指接口报错:" + e.getMessage()));
    }

    @Override
    public 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,...";)。

​​解析数据​​:实际开发中需解析文本(如拆分字符串或使用正则表达式提取关键数值)

java 复制代码
String data = resp.split("\"")[1];  // 示例:提取引号内的数据
String[] fields = data.split(",");   // 按逗号分割字段
String indexName = fields[0];        // 指数名称
String currentPrice = fields[1];     // 当前价格

全部代码如下:

Netconst 类

java 复制代码
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服务的ip
    public 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服务的ip
    public final static int CHAT_PORT = 9011;
}

Okhttp.activity.java:

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; // 当前选中的单选按钮资源编号

    @Override
    protected 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() {
            @Override
            public void onFailure(Call call, IOException e) { // 请求失败
                // 回到主线程操纵界面
                runOnUiThread(() -> tv_result.setText("调用股指接口报错:"+e.getMessage()));
            }

            @Override
            public 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() {
            @Override
            public void onFailure(Call call, IOException e) { // 请求失败
                // 回到主线程操纵界面
                runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));
            }

            @Override
            public 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() {
            @Override
            public void onFailure(Call call, IOException e) { // 请求失败
                // 回到主线程操纵界面
                runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException { // 请求成功
                String resp = response.body().string();
                // 回到主线程操纵界面
                runOnUiThread(() -> tv_result.setText("调用登录接口返回:\n"+resp));
            }
        });
    }
}

xml:

xml 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <RadioGroup
        android:id="@+id/rg_method"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:orientation="horizontal">

        <RadioButton
            android: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" />

        <RadioButton
            android: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" />

        <RadioButton
            android: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>

    <LinearLayout
        android: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">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="用户名:"
                android:textColor="@color/black"
                android:textSize="17sp" />

            <EditText
                android: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>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginTop="10dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="密 码:"
                android:textColor="@color/black"
                android:textSize="17sp" />

            <EditText
                android: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>

    <Button
        android:id="@+id/btn_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起接口调用"
        android:textColor="@color/black"
        android:textSize="17sp" />

    <TextView
        android: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>

效果图:

注意项:

  1. 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
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.cnhttps://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,可通过变量动态设置:

java 复制代码
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") 设置。

相关推荐
二J1 小时前
管理100个小程序-很难吗
android·小程序
未来之窗软件服务1 小时前
声音分离人声和配乐-从头设计数字生命第5课, demucs——仙盟创梦IDE
ide·macos·xcode·仙盟创梦ide·数字生命
s11show_1631 小时前
hz修改后台新增keyword功能
android·java·前端
綦枫Maple2 小时前
Vue实战(08)解决 Vue 项目中路径别名 `@` 在 IDE 中报错无法识别的问题
前端·ide·vue.js
IT技术图谱2 小时前
【绝非标题党】网络监听新姿势:APT编译时注解实现高扩展框架
android·面试·架构
RichardLai882 小时前
[Flutter 基础] - Flutter基础组件 - Text
android·flutter
姜行运3 小时前
数据结构【树和二叉树】
android·数据结构·算法·c#
东风西巷3 小时前
Control Center安卓版:自定义控制中心,提升手机操作体验
android·智能手机·性能优化·软件需求
布拉德很帅3 小时前
Android如何通过aspectj打造一个无侵入式动态权限申请框架
android
程序员沉梦听雨4 小时前
OkHttp入门
okhttp