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") 设置。

相关推荐
后端码匠4 小时前
MySQL 8.0安装(压缩包方式)
android·mysql·adb
梓仁沐白5 小时前
Android清单文件
android
董可伦7 小时前
Dinky 安装部署并配置提交 Flink Yarn 任务
android·adb·flink
每次的天空8 小时前
Android学习总结之Glide自定义三级缓存(面试篇)
android·学习·glide
恋猫de小郭8 小时前
如何查看项目是否支持最新 Android 16K Page Size 一文汇总
android·开发语言·javascript·kotlin
LuXi_foryou10 小时前
错误: 程序包org.junit不存在 import org.junit.Test;
junit·android studio
flying robot10 小时前
小结:Android系统架构
android·系统架构
xiaogai_gai10 小时前
有效的聚水潭数据集成到MySQL案例
android·数据库·mysql
鹅鹅鹅呢11 小时前
mysql 登录报错:ERROR 1045(28000):Access denied for user ‘root‘@‘localhost‘ (using password Yes)
android·数据库·mysql
在人间负债^11 小时前
假装自己是个小白 ---- 重新认识MySQL
android·数据库·mysql