我们之前学过了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>
效果图:

注意项:
- 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.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,可通过变量动态设置:
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") 设置。