凌峰创科服务平台

Android如何同步服务器时间?

使用 HttpURLConnection 获取服务器头时间(推荐)

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

Android如何同步服务器时间?-图1
(图片来源网络,侵删)

优点:

  • 准确可靠:直接获取服务器自身的时间,不受 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());
    }
}

如何使用

  1. 在应用启动时调用同步方法: 你可以在 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();
        }
    }
  2. 在需要获取精确时间的地方调用 getServerTime(): 创建订单时:

    Android如何同步服务器时间?-图2
    (图片来源网络,侵删)
    long orderTimestamp = ServerTimeSync.getServerTime();
    // 使用 orderTimestamp 作为订单创建时间

使用 NTP 客户端库

如果你的服务器本身就是一个权威的时间源,或者你希望使用标准的 NTP 协议来同步时间,可以使用现成的 NTP 客户端库。

优点:

  • 协议标准:NTP 是专门用于时间同步的协议,精度很高。
  • 功能强大:一些库可以处理复杂的 NTP 逻辑,如多个服务器、时钟过滤等。

缺点:

  • 依赖库:需要引入第三方库。
  • 可能受限:某些网络环境可能会限制 NTP 端口(如 123 端口)的访问。
  • 相对复杂:比简单的 HTTP HEAD 请求要复杂。

实现代码(使用 org.apache.commons:commons-net 库)

  1. 添加依赖: 在你的 build.gradle 文件中添加:

    Android如何同步服务器时间?-图3
    (图片来源网络,侵删)
    implementation 'org.apache.commons:commons-net:3.9.0'
  2. 编写同步代码

    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" />

为什么不推荐?

  1. 权限极高:滥用此权限会严重影响整个设备的系统时间,可能导致系统混乱、其他应用异常。
  2. 非用户意图:修改系统时间是一个高风险操作,应由用户手动在系统设置中完成,或由系统级安全应用(如家长控制)执行。
  3. 副作用大:会影响到所有依赖系统时间的系统服务和应用。

最佳实践与注意事项

  1. 缓存偏移量:服务器时间的偏移量在短时间内是相对稳定的,你只需要在应用启动或特定事件(如登录)时同步一次,然后将这个偏移量保存在内存中即可,不必每次使用都去请求。
  2. 处理网络异常:网络请求可能会失败,要做好异常处理,如果同步失败,你的应用应该有一个降级方案,比如使用本地时间,并记录日志。
  3. 使用后台线程:网络操作不能在主线程(UI线程)中进行,否则会抛出 NetworkOnMainThreadException 异常,务必使用 AsyncTask, Thread, ExecutorServiceCoroutine 等方式在后台执行。
  4. 选择合适的 API 端点
    • 用于同步时间的 API 应该是轻量级的,不执行复杂的业务逻辑。
    • 最好使用 HEAD 方法,而不是 GET,因为它不返回响应体,更节省流量。
    • 这个 API 应该放在一个稳定、响应快的机器上,最好是负载均衡器或网关层,而不是你的应用业务服务器。
  5. HTTPS:确保你的 API 使用 HTTPS 协议,防止中间人攻击篡改响应头中的 Date 字段。
方法 优点 缺点 推荐度
HTTP HEAD 请求 准确、简单、高效、无额外依赖 需要网络请求,有微小延迟 ⭐⭐⭐⭐⭐ (首选)
NTP 客户端库 协议标准,精度高 依赖库,可能受限,相对复杂 ⭐⭐⭐ (特定场景)
修改系统时间 直接修改系统时间 权限极高,风险巨大,不推荐 ⭐☆☆☆☆ (绝对不推荐)

对于绝大多数 Android 应用开发场景,使用 HttpURLConnection 发送 HEAD 请求来获取服务器时间是最佳选择,它兼顾了准确性、简单性和性能。

分享:
扫描分享到社交APP
上一篇
下一篇