使用 HttpURLConnection 获取服务器头时间(推荐)
这是最常用且最可靠的方法,我们不是直接获取服务器的时间字符串,而是通过 HTTP 协议的一个特性:服务器在响应头中会包含一个 Date 字段,这个字段就是服务器响应时的时间。

优点:
- 准确可靠:直接获取服务器自身的时间,不受 NTP(网络时间协议)同步延迟的影响。
- 简单高效:只需一次简单的网络请求,无需额外依赖。
- 跨平台:不依赖 Android 系统特定的 API。
缺点:
- 需要网络权限:必须进行一次网络请求。
- 有延迟:网络请求本身会带来毫秒级的延迟,但对于绝大多数业务场景,这个延迟可以忽略不计。
实现代码
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
public class ServerTimeSync {
private static final String TAG = "ServerTimeSync";
private static long sServerTimeOffset = 0; // 服务器时间与本地时间的偏移量(毫秒)
/**
* 从服务器同步时间
* @param serverUrl 服务器URL,"https://www.yourserver.com/api/time"
* @return true 同步成功,false 同步失败
*/
public static boolean syncServerTime(String serverUrl) {
HttpURLConnection connection = null;
try {
URL url = new URL(serverUrl);
connection = (HttpURLConnection) url.openConnection();
// 设置请求方法为 HEAD,因为我们只需要响应头,不需要响应体,更高效
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(10000); // 10秒连接超时
connection.setReadTimeout(10000); // 10秒读取超时
connection.connect();
// 获取服务器响应头中的 Date 字段
long serverTime = connection.getHeaderFieldDate("Date", 0);
if (serverTime != 0) {
long localTime = System.currentTimeMillis();
// 计算偏移量:服务器时间 - 本地时间
sServerTimeOffset = serverTime - localTime;
Log.d(TAG, "Server time synced. Offset: " + sServerTimeOffset + "ms");
return true;
}
} catch (IOException e) {
Log.e(TAG, "Failed to sync server time", e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
return false;
}
/**
* 获取当前的服务器时间
* @return 当前服务器时间(如果同步过)或本地时间
*/
public static long getServerTime() {
// 本地时间 + 偏移量 = 服务器时间
return System.currentTimeMillis() + sServerTimeOffset;
}
/**
* 获取当前的服务器时间 Date 对象
* @return 当前服务器时间(如果同步过)或本地时间
*/
public static Date getServerTimeDate() {
return new Date(getServerTime());
}
}
如何使用
-
在应用启动时调用同步方法: 你可以在
Application类的onCreate()方法中,或者在你业务逻辑开始的地方(例如登录成功后)调用syncServerTime。public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // 在后台线程执行,避免阻塞主线程 new Thread(() -> { boolean success = ServerTimeSync.syncServerTime("https://api.yourserver.com"); if (success) { Log.i("MyApp", "Server time sync successful."); } else { Log.w("MyApp", "Server time sync failed."); } }).start(); } } -
在需要获取精确时间的地方调用
getServerTime(): 创建订单时:
(图片来源网络,侵删)long orderTimestamp = ServerTimeSync.getServerTime(); // 使用 orderTimestamp 作为订单创建时间
使用 NTP 客户端库
如果你的服务器本身就是一个权威的时间源,或者你希望使用标准的 NTP 协议来同步时间,可以使用现成的 NTP 客户端库。
优点:
- 协议标准:NTP 是专门用于时间同步的协议,精度很高。
- 功能强大:一些库可以处理复杂的 NTP 逻辑,如多个服务器、时钟过滤等。
缺点:
- 依赖库:需要引入第三方库。
- 可能受限:某些网络环境可能会限制 NTP 端口(如 123 端口)的访问。
- 相对复杂:比简单的 HTTP HEAD 请求要复杂。
实现代码(使用 org.apache.commons:commons-net 库)
-
添加依赖: 在你的
build.gradle文件中添加:
(图片来源网络,侵删)implementation 'org.apache.commons:commons-net:3.9.0'
-
编写同步代码:
import org.apache.commons.net.ntp.NTPUDPClient; org.apache.commons.net.ntp.TimeInfo; import java.net.InetAddress; java.util.Date; public class NtpTimeSync { private static final String TAG = "NtpTimeSync"; private static long sServerTimeOffset = 0; public static boolean syncNtpTime(String ntpHost) { NTPUDPClient client = new NTPUDPClient(); try { // 设置超时时间 client.setDefaultTimeout(10000); client.open(); // 发送请求到 NTP 服务器 InetAddress inetAddress = InetAddress.getByName(ntpHost); TimeInfo info = client.getTime(inetAddress); // 获取服务器时间信息 info.computeDetails(); // 必须调用此方法才能获取详细信息 long serverTime = info.getMessage().getTransmitTimeStamp().getTime(); long localTime = System.currentTimeMillis(); sServerTimeOffset = serverTime - localTime; Log.d(TAG, "NTP time synced. Offset: " + sServerTimeOffset + "ms"); return true; } catch (Exception e) { Log.e(TAG, "Failed to sync NTP time", e); } finally { client.close(); } return false; } // getServerTime() 和 getServerTimeDate() 方法与第一种方法完全相同 public static long getServerTime() { return System.currentTimeMillis() + sServerTimeOffset; } }使用方法:与第一种方法类似,只需调用
NtpTimeSync.syncNtpTime("time.windows.com")即可。
使用 Android 系统的 Settings.Global(不推荐)
这种方法是直接修改 Android 系统的全局时间。强烈不推荐在普通 App 中使用,因为它需要 android.permission.SET_TIME 权限,这是一个 signature 级别的权限,只有系统应用或用系统签名签名的应用才能获取。
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.SET_TIME" />
为什么不推荐?
- 权限极高:滥用此权限会严重影响整个设备的系统时间,可能导致系统混乱、其他应用异常。
- 非用户意图:修改系统时间是一个高风险操作,应由用户手动在系统设置中完成,或由系统级安全应用(如家长控制)执行。
- 副作用大:会影响到所有依赖系统时间的系统服务和应用。
最佳实践与注意事项
- 缓存偏移量:服务器时间的偏移量在短时间内是相对稳定的,你只需要在应用启动或特定事件(如登录)时同步一次,然后将这个偏移量保存在内存中即可,不必每次使用都去请求。
- 处理网络异常:网络请求可能会失败,要做好异常处理,如果同步失败,你的应用应该有一个降级方案,比如使用本地时间,并记录日志。
- 使用后台线程:网络操作不能在主线程(UI线程)中进行,否则会抛出
NetworkOnMainThreadException异常,务必使用AsyncTask,Thread,ExecutorService或Coroutine等方式在后台执行。 - 选择合适的 API 端点:
- 用于同步时间的 API 应该是轻量级的,不执行复杂的业务逻辑。
- 最好使用
HEAD方法,而不是GET,因为它不返回响应体,更节省流量。 - 这个 API 应该放在一个稳定、响应快的机器上,最好是负载均衡器或网关层,而不是你的应用业务服务器。
- HTTPS:确保你的 API 使用
HTTPS协议,防止中间人攻击篡改响应头中的Date字段。
| 方法 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| HTTP HEAD 请求 | 准确、简单、高效、无额外依赖 | 需要网络请求,有微小延迟 | ⭐⭐⭐⭐⭐ (首选) |
| NTP 客户端库 | 协议标准,精度高 | 依赖库,可能受限,相对复杂 | ⭐⭐⭐ (特定场景) |
| 修改系统时间 | 直接修改系统时间 | 权限极高,风险巨大,不推荐 | ⭐☆☆☆☆ (绝对不推荐) |
对于绝大多数 Android 应用开发场景,使用 HttpURLConnection 发送 HEAD 请求来获取服务器时间是最佳选择,它兼顾了准确性、简单性和性能。
