TCP协议
TCP概述
传输控制协议(TCP,Transmission Control Protocol) 是一种面向连接的、可拷的、基于字节流的传输层通讯协议
核心特性
- 面向连接:通讯前必须建立连接
- 可靠传输:确保数据完整、有序到达
- 双全工通讯:支持双向同时传输
- 流量控制:防止发送方过快淹没接收方
- 拥塞控制:避免网络过载
TCP在协议栈中的位置
应用层 → HTTP, FTP, SMTP等
传输层 → TCP, UDP
网络层 → IP
链路层 → Ethernet, WiFi等
TCP报文格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口号 | 目的端口号 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号(SEQ) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号(ACK) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移 | 保留 |标志位| 窗口大小 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 | 紧急指针 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项(可选) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段说明
字段 长度 说明
源端口号 16位 发送方端口
目的端口号 16位 接收方端口
序列号(SEQ) 32位 数据字节流的编号
确认号(ACK) 32位 期望收到的下一个序号
数据偏移 4位 TCP首部长度
保留 6位 保留将来使用
标志位 6位 URG、ACK、PSH、RST、SYN、FIN
窗口大小 16位 接收窗口大小
校验和 16位 校验首部和数据
紧急指针 16位 紧急数据的末尾位置
标志位详解
- URG:紧急指针有效
- ACK:确认号有效
- PSH:接收方应尽快将数据交给应用层
- RST:重置连接
- SYN:同步序号,用于建立连接
- FIN:发送方完成数据发送
TCP连接管理
三次握手
目的是确认双方的收发能力都是正常的
客户端 服务器
| |
|----SYN, SEQ=x---------->| (1) 客户端发送SYN
| |
|<---SYN, ACK, SEQ=y, ACK=x+1--| (2) 服务器回应SYN+ACK
| |
|----ACK, SEQ=x+1, ACK=y+1--->| (3) 客户端发送ACK
| |
|<===== 连接建立成功 =====>|
握手过程:
- 第一次握手:客户端发送SYN包(SEQ=x),进入SYN_SENT状态;表示:我想和你建立连接,此时客户端止到:我发信号的能力是没有问题的
- 第二次握手:服务器收到SYN包,回复SYN+ACK包(SEQ=y, ACK=x+1),进入SYN_RCVD状态;表示:收到请求了,我也准备好了,你确认一下?;此时服务端止到:您发信号正常,我接收信号和发信号也正常
- 第三次握手:客户端收到SYN+ACK包,回复ACK包(ACK=y+1),进入ESTABLISHED状态;服务器收到ACK后也进入ESTABLISHED状态;表示:收到,那我们开始聊吧;此时客户端止到:我接收信号也正常
为什么需要三次握手?
- 防止已失效的连接请求突然传到服务器
- 确保双方都有发送和接收能力
- 同步双方的初始序列号
四次挥手(释放连接)
由于TCP是双开工的(双方都可以同时发送数据),所以关闭连接时,双方都需要单独确认自己不再发送数据了
客户端 服务器
| |
|----FIN, SEQ=u---------->| (1) 客户端发送FIN
| |
|<---ACK, SEQ=v, ACK=u+1---| (2) 服务器回复ACK
| |
|<---FIN, SEQ=w, ACK=u+1---| (3) 服务器发送FIN
| |
|----ACK, SEQ=u+1, ACK=w+1--->| (4) 客户端发送ACK
| |
| 连接关闭 |
挥手过程:
- 第一次挥手:客户端发送FIN包(SEQ=u),进入FIN_WAIT_1状态;表述:告诉服务端“我的数据发完了,我要关闭发送通道了”
- 第二次挥手:服务器收到FIN,回复ACK包(ACK=u+1),进入CLOSE_WAIT状态;客户端收到ACK后进入FIN_WAIT_2状态;表示:知道了,但我还有点数据还没发完,你等我一下;此时客户端进入半开工状态,只收不发
- 第三次挥手:服务器发送FIN包(SEQ=w),进入LAST_ACK状态;表示:我也发完了,咱们散伙吧
- 第四次挥手:客户端收到FIN,回复ACK包(ACK=w+1),进入TIME_WAIT状态;服务器收到ACK后关闭连接;表示:行,拜拜;注意:客户端发完这个包后会等待2MSL(最大报文生存时间)才会正式关闭。这是为了防止最后的ACK丢了,导致服务端没法正常关闭
为什么需要TIME_WAIT状态?
- 确保最后一个ACK能到达服务器
- 防止旧连接的报文影响新连接
- 通常持续2MSL(最大报文生存时间)
超时重传
流量控制
拥塞控制
HTTP协议
版本演进
- HTTP/0.9 只有 GET 方法,仅传输 HTML,无头部。过于简陋
- HTTP/1.0 引入版本号、状态码、头部概念,支持多种文件类型。每个 TCP 连接只能发送一个请求,效率低。
- HTTP/1.1 主流版本引入持久连接 (Keep-Alive)、管道化 (Pipelining)、分块传输编码、更多的缓存控制。队头阻塞 (HOL blocking) 问题未完全解决。
- HTTP/2.0 二进制分帧、多路复用 (Multiplexing)、头部压缩 (HPACK)、服务器推送 (Server Push)。基于 TCP,TCP 的队头阻塞问题依然存在。
- HTTP/3.0 基于 QUIC 协议 (Quick UDP Internet Connections),使用 UDP 而非 TCP。解决 TCP 队头阻塞问题,连接建立更快。
主要方法
GET 请求指定的资源。用于获取数据,不应产生副作用。
HEAD 类似于 GET,但服务器只返回响应头,不返回响应体。用于检查资源是否存在或获取元信息。
POST 将数据提交给指定的资源,通常用于创建新资源或提交表单。
PUT 将数据发送到服务器以创建或替换指定的资源。通常用于更新整个资源。
DELETE 请求服务器删除指定的资源。
PATCH 对资源进行部分修改。
OPTIONS 描述目标资源的通信选项,允许客户端查看服务器支持的 HTTP 方法。
常见状态码
200 OK 成功
201 Created 请求成功并创建了新资源。
301 Moved Permanently 请求的资源已被永久移动到新 URL。将来应使用新 URL 访问。
302 Found 请求的资源临时从不同的 URL 响应。客户端应继续使用原 URL。
304 Not Modified 资源未修改,客户端可以使用缓存的版本。
400 Bad Request 服务器无法理解请求的格式(参数错误、语义错误等)。
401 Unauthorized 请求需要用户身份验证。
403 Forbidden 服务器理解请求,但拒绝执行。权限不足。
404 Not Found 服务器找不到请求的资源。
405 Method Not Allowed 请求方法对指定的资源不适用。
500 Internal Server Error 服务器内部错误,服务器遇到意外情况,无法完成请求。
502 Bad Gateway 错误网关,作为网关或代理工作的服务器,从上游服务器收到无效响应。
503 Service Unavailable 服务不可用,服务器当前无法处理请求(超载、停机维护)
常见请求/响应头
Host 请求头 指定请求资源的主机名和端口号。
User-Agent 请求头 客户端程序的信息(如浏览器类型)。
Accept 请求头 告诉服务器,客户端能接受哪些媒体类型。
Content-Type 请求/响应头 告诉对方,当前发送的数据是什么类型。
Content-Lenght 请求/响应头 请求体或响应体的数据长度。
Referer 请求头 表示当前请求是从哪个页面链接过来的。
Cookie 请求头 客户端将存储的 Cookie 发送给服务器。
Set-Cookie 响应头 服务器要求客户端保存 Cookie。
Location 响应头 通常与 3xx 状态码一起使用,告诉客户端要重定向到哪里。
Cache-Control 请求/响应头 指定缓存机制
Authorization 请求头 用于向服务器发送身份验证凭证
HTTPS协议
概述
HTTPS (Hypertext Transfer Protocol Secure) 是 HTTP 的安全版本,通过在 HTTP 和 TCP 之间加入 SSL/TLS 协议层来实现加密传输。
为什么需要 HTTPS?
HTTP 存在三大安全隐患:
- 窃听风险:通信使用明文,内容可以被第三方截获。
- 篡改风险:无法验证报文的完整性,内容可能被修改。
- 冒充风险:不验证通信方的身份,可能遭遇伪装服务器。
HTTPS 解决的问题
- 机密性:加密数据,防止被窃听。
- 完整性:防止数据被篡改。
- 身份验证:验证网站的真实性,防止钓鱼网站。
subgraph HTTP
A[客户端] -->|明文传输| B[服务器]
end
subgraph HTTPS
C[客户端] -->|SSL/TLS 加密| D{SSL/TLS层}
D -->|解密| E[服务器]
end
SSL/TLS 协议
什么是 SSL/TLS?
- SSL:Secure Sockets Layer,安全套接层。
- TLS:Transport Layer Security,传输层安全协议(SSL 的继任者)。
核心功能
| 功能 | 描述 | 解决的安全问题 |
|---|---|---|
| 加密 | 对传输的数据进行加密,即使被截获也无法读取。 | 窃听风险 |
| 认证 | 通过数字证书验证通信方的身份。 | 冒充风险 |
| 完整性校验 | 通过消息认证码(MAC)确保数据未被篡改。 | 篡改风险 |
工作原理
HTTPS 握手过程主要分为三个阶段:
- TCP 连接建立:三次握手。
- TLS 握手:协商加密算法、验证证书、生成会话密钥。
- 加密通信:使用会话密钥进行对称加密通信。
OKHttp介绍
引用
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// 如果需要日志拦截器
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
}
//添加权限
<uses-permission android:name="android.permission.INTERNET" />
使用步骤
第一步:创建 OkHttpClient 实例
通常建议在 Application 中创建一个全局单例,以便复用连接池和线程池
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时
.readTimeout(10, TimeUnit.SECONDS) // 读取超时
.addInterceptor(new HttpLoggingInterceptor()) // 添加日志拦截器
.build();
第二步:构建 Request 对象
使用建造者模式构建请求,可以指定 URL、请求方法(GET/POST)、请求头等
// GET 请求
Request request = new Request.Builder()
.url("https://api.github.com/users/octocat")
.header("User-Agent", "MyAwesomeApp")
.build();
// POST 请求 (JSON)
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = "{\"name\": \"John\"}";
RequestBody body = RequestBody.create(json, JSON);
Request postRequest = new Request.Builder()
.url("https://api.example.com/users")
.post(body)
.build();
//POST 请求 (表单)
RequestBody formBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
//文件上传
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), imageFile);
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("description", "A beautiful photo")
.addFormDataPart("photo", "profile.png", fileBody)
.build();
//下载 和 GET请求一模一样
Request request = new Request.Builder()
.url("https://example.com/image.jpg") // 你的文件下载链接
.build();
第三步:发送请求并处理响应
OkHttp 支持同步和异步两种方式。在 Android 的主线程(UI 线程)中,必须使用异步请求,避免阻塞界面
Call newCall = client.newCall(request)
//异步
newCall.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 请求失败(例如:网络不通),在子线程中回调
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 请求成功,在子线程中回调
if (response.isSuccessful()) {
// 注意:response.body().string() 只能调用一次
String responseData = response.body().string();
// 回到主线程更新 UI
runOnUiThread(() -> {
// 更新你的 TextView 或 RecyclerView
});
}
}
});
//同步
Response response = newCall.execute()
// 只有收到响应后,才会执行到这里
if (response.isSuccessful()) {
String data = response.body().string();
// 处理数据...
}
// 取消请求
if (call != null) {
call.cancel();
}
插值器
OkHttp 有两种拦截器,它们在执行时机和作用域上有重要区别
| 特性 | 应用拦截器 (Application Interceptor) | 网络拦截器 (Network Interceptor) |
|---|---|---|
| 添加方式 | addInterceptor() | addNetworkInterceptor() |
| 执行时机 | 在重试和重定向之前 | 在重试和重定向之后 |
| 调用次数 | 每个请求只调用1次 | 可能调用多次(重定向时) |
| 能看到什么 | 原始请求、最终响应 | 中间请求和响应(包括重定向) |
| 能访问 Cache | 可以(能看到缓存响应) | 不能(看不到缓存) |
| 适用场景 | 通用日志、添加Header、Token刷新 | 监控重定向、跟踪网络层细节 |
拦截器的工作位置
请求开始
↓
[应用拦截器] ← 只能看到原始请求和最终响应
↓
OkHttp 内部(重试、重定向)
↓ ↺ 如果重定向
[网络拦截器] ← 能看到每次重定向的请求/响应
↓
服务器
常见的拦截器实战
统一添加Header
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
// 创建新的请求,添加通用 Header
Request request = original.newBuilder()
.header("User-Agent", "MyApp/1.0 (Android)")
.header("Accept-Language", Locale.getDefault().getLanguage())
.header("Authorization", "Bearer " + getToken())
.header("App-Version", BuildConfig.VERSION_NAME)
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
private String getToken() {
// 从 SharedPreferences 获取 token
return "your-token";
}
}
// 使用
client = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor())
.build();
Token自动刷新
不要在拦截器中做耗时操作,同一时间多个请求,会串行执行多次耗时操作,用户体验差
- 异步非阻塞方案【推荐】
public class AsyncTokenInterceptor implements Interceptor {
private static final String TAG = "AsyncTokenInterceptor";
// Token刷新状态管理
private final AtomicBoolean isRefreshing = new AtomicBoolean(false);
private final Object lock = new Object();
private String newToken;
private int refreshErrorCount = 0;
private static final int MAX_RETRY = 3;
// 缓存刷新Token的请求
private final Map<String, List<Call>> pendingCalls = new ConcurrentHashMap<>();
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 1. 添加当前Token
String currentToken = getTokenFromPrefs();
if (!TextUtils.isEmpty(currentToken)) {
request = request.newBuilder()
.header("Authorization", "Bearer " + currentToken)
.build();
}
// 2. 执行请求
Response response = chain.proceed(request);
// 3. 处理401
if (response.code() == 401) {
response.close();
// 检查是否是刷新Token的请求本身
if (isRefreshTokenRequest(request)) {
// 刷新Token的请求也失败了,直接抛出异常
throw new IOException("刷新Token失败,请重新登录");
}
// 非阻塞刷新Token并重试
return handleTokenExpired(chain, request);
}
return response;
}
/**
* 处理Token过期(非阻塞方式)
*/
private Response handleTokenExpired(Chain chain, Request originalRequest) throws IOException {
String urlKey = originalRequest.url().host();
// 如果已经在刷新中,则等待
if (isRefreshing.get()) {
Log.d(TAG, "Token正在刷新中,等待...");
// 等待刷新完成(最多5秒)
String refreshedToken = waitForTokenRefresh();
if (refreshedToken != null) {
// 使用新Token重试
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + refreshedToken)
.build();
return chain.proceed(newRequest);
} else {
throw new IOException("Token刷新失败");
}
}
// 尝试刷新Token
synchronized (lock) {
// 双重检查
if (isRefreshing.get()) {
String refreshedToken = waitForTokenRefresh();
if (refreshedToken != null) {
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + refreshedToken)
.build();
return chain.proceed(newRequest);
} else {
throw new IOException("Token刷新失败");
}
}
// 开始刷新Token
isRefreshing.set(true);
newToken = null;
}
// ⚡ 异步刷新Token(不阻塞当前线程)
refreshTokenAsync(new TokenRefreshCallback() {
@Override
public void onSuccess(String token) {
Log.d(TAG, "Token刷新成功");
newToken = token;
saveTokenToPrefs(token);
isRefreshing.set(false);
// 唤醒所有等待的请求
synchronized (lock) {
lock.notifyAll();
}
}
@Override
public void onFailure(String error) {
Log.e(TAG, "Token刷新失败: " + error);
refreshErrorCount++;
newToken = null;
isRefreshing.set(false);
synchronized (lock) {
lock.notifyAll();
}
}
});
// 等待异步刷新完成(但这里会阻塞,怎么办?)
// ⚠️ 问题:仍然会阻塞!
String refreshedToken = waitForTokenRefresh();
if (refreshedToken != null) {
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + refreshedToken)
.build();
return chain.proceed(newRequest);
} else {
throw new IOException("Token刷新失败");
}
}
private String waitForTokenRefresh() {
synchronized (lock) {
try {
// 最多等待5秒
lock.wait(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return newToken;
}
private void refreshTokenAsync(TokenRefreshCallback callback) {
// 使用异步任务刷新Token
new Thread(() -> {
try {
// 模拟网络请求
Thread.sleep(1500);
// 成功
callback.onSuccess("new-token-" + System.currentTimeMillis());
} catch (Exception e) {
callback.onFailure(e.getMessage());
}
}).start();
}
private boolean isRefreshTokenRequest(Request request) {
return request.url().encodedPath().contains("/refresh-token");
}
private String getTokenFromPrefs() { /* ... */ }
private void saveTokenToPrefs(String token) { /* ... */ }
interface TokenRefreshCallback {
void onSuccess(String token);
void onFailure(String error);
}
}
- 使用 OkHttp 的异步特性【最佳实践】
public class NonBlockingTokenInterceptor implements Interceptor {
private static final String TAG = "NonBlockingTokenInterceptor";
// 使用 CountDownLatch 实现等待/通知
private final AtomicReference<CountDownLatch> refreshLatch = new AtomicReference<>();
private volatile String refreshedToken;
private volatile boolean refreshFailed = false;
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
// 1. 添加Token
Request request = addTokenToRequest(originalRequest);
// 2. 执行请求
Response response = chain.proceed(request);
// 3. 处理401
if (response.code() == 401) {
response.close();
// 检查是否是刷新Token的请求
if (isTokenRefreshRequest(originalRequest)) {
throw new IOException("刷新Token失败,请重新登录");
}
// 非阻塞方式处理Token过期
return handleTokenExpiration(chain, originalRequest);
}
return response;
}
/**
* 非阻塞处理Token过期
*/
private Response handleTokenExpiration(Chain chain, Request originalRequest) throws IOException {
// 尝试获取新Token(非阻塞)
String newToken = getRefreshedToken();
if (newToken != null) {
// 成功获取新Token,重试原请求
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
return chain.proceed(newRequest);
} else {
throw new IOException("无法刷新Token,请求失败");
}
}
/**
* 核心方法:非阻塞获取刷新后的Token
*/
private String getRefreshedToken() {
// 如果已经有新Token,直接返回
if (refreshedToken != null) {
return refreshedToken;
}
// 如果正在刷新,等待但不阻塞当前线程?
// ⚠️ 关键点:这里不能等待,必须立即返回
// ✅ 解决方案:启动异步刷新,但立即返回null
// 这样当前请求会失败,但后续请求会使用新Token
if (refreshLatch.get() == null) {
// 第一次遇到401,触发异步刷新
startAsyncTokenRefresh();
}
// 立即返回null,让当前请求失败
// 但Token会在后台刷新,后续请求会成功
return null;
}
/**
* 异步刷新Token(完全不阻塞)
*/
private void startAsyncTokenRefresh() {
// 确保只启动一次刷新
if (refreshLatch.compareAndSet(null, new CountDownLatch(1))) {
Log.d(TAG, "启动异步Token刷新");
// 使用单独的线程池执行刷新
Executors.newSingleThreadExecutor().execute(() -> {
try {
// 模拟网络请求刷新Token
Thread.sleep(1500);
// 成功
refreshedToken = "new-token-" + System.currentTimeMillis();
refreshFailed = false;
Log.d(TAG, "异步刷新成功: " + refreshedToken);
} catch (Exception e) {
refreshFailed = true;
Log.e(TAG, "异步刷新失败", e);
} finally {
// 释放所有等待的请求
refreshLatch.get().countDown();
}
});
}
}
private Request addTokenToRequest(Request request) {
String token = getTokenFromPrefs();
if (TextUtils.isEmpty(token)) {
return request;
}
return request.newBuilder()
.header("Authorization", "Bearer " + token)
.build();
}
private boolean isTokenRefreshRequest(Request request) {
return request.url().encodedPath().contains("/auth/refresh");
}
private String getTokenFromPrefs() {
// 从SharedPreferences获取
return "your-token";
}
}
缓存
下载容易导致OOM
建议使用现成的库,自己实现不稳定
根本原因:错误地使用了 bytes() 方法
// ⚠️ 危险代码!小文件没问题,大文件必 OOM
Response response = call.execute();
byte[] fileBytes = response.body().bytes(); // 整个文件加载到内存!
问题所在:
bytes()方法会将整个响应体读取到内存中的字节数组- 如果下载 100MB 的文件,内存就会瞬间增加 100MB
- Android 应用堆内存一般只有 128MB-512MB,很容易耗尽
正确的流式下载方案
方案1:基础流式下载(最安全)
public void downloadFile(String url, File destinationFile) {
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
return;
}
// ✅ 关键:使用 byteStream() 获取输入流,而不是 bytes()
try (InputStream inputStream = response.body().byteStream();
FileOutputStream outputStream = new FileOutputStream(destinationFile)) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区,固定大小
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
// 内存始终只占用 8KB + 一些开销
}
outputStream.flush();
}
}
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
});
}
方案2:带进度监听的流式下载
public interface DownloadListener {
void onProgress(int progress, long downloaded, long total);
void onSuccess(File file);
void onFailure(String error);
}
public void downloadWithProgress(String url, File destinationFile, DownloadListener listener) {
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
listener.onFailure("Download failed: " + response.code());
return;
}
long contentLength = response.body().contentLength(); // 文件总大小
long downloaded = 0;
try (InputStream inputStream = response.body().byteStream();
FileOutputStream outputStream = new FileOutputStream(destinationFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
downloaded += bytesRead;
// 计算并回调进度
// 这个地方要优化,比如1s计算一个进度,不能一直计算进度,太耗能
if (contentLength > 0) {
int progress = (int) ((downloaded * 100) / contentLength);
listener.onProgress(progress, downloaded, contentLength);
}
}
outputStream.flush();
// 下载完成
listener.onSuccess(destinationFile);
} catch (IOException e) {
listener.onFailure("IO Error: " + e.getMessage());
}
}
@Override
public void onFailure(Call call, IOException e) {
listener.onFailure("Network Error: " + e.getMessage());
}
});
}