网络时间同步机制
网络时间同步机制
系统版本:android7.1
涉及类
//监控网络时间并更新系统时间策略类NetworkTimeUpdateService.java//与远程 NTP 服务器连接,获取网络时间NtpTrustedTime.java//真正接受网络时间的类SntpClient.java
NetworkTimeUpdateService解析
NetworkTimeUpdateService启动流程
NetworkTimeUpdateService是在SystemServer.java里面的startOtherServices方法中创建的,分别调用NetworkTimeUpdateService的构造方法和systemReady方法,最终网络同步系统时间服务启动完成。
private void startOtherServices() { ...NetworkTimeUpdateService networkTimeUpdater = null;...//初始化时间同步服务 networkTimeUpdater = new NetworkTimeUpdateService(context);//添加到binder管理ServiceManager.addService("network_time_update_service", networkTimeUpdater);...final NetworkTimeUpdateService networkTimeUpdaterF = networkTimeUpdater;mActivityManagerService.systemReady(new Runnable() {... try { if (networkStatsF != null) networkStatsF.systemReady(); } catch (Throwable e) { reportWtf("making Network Stats Service ready", e); }...}}
NetworkTimeUpdateService构造方法解析
主要的工作内容
- 创建NtpTrustedTime对象,该对象用于获取网络时间。
- 设置更新时间的广播Intent
- 获取framework/base/core/res/res/value/config.xml中的配置信息
//继承Binder,这样就可以添加到ServiceManager中public class NetworkTimeUpdateService extends Binder { public NetworkTimeUpdateService(Context context) { mContext = context;//用于连接ntp服务器,获取网络时间 mTime = NtpTrustedTime.getInstance(context);//获取AlarmManager对象,用来设置系统时间 mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);//com.android.server.NetworkTimeUpdateService.action.POLL广播 Intent pollIntent = new Intent(ACTION_POLL, null);//设置请求更新时间广播 mPendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, pollIntent, 0);//24小时 mPollingIntervalMs = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpPollingInterval);//1分钟 mPollingIntervalShorterMs = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpPollingIntervalShorter);//3次 mTryAgainTimesMax = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpRetry);//5秒 mTimeErrorThresholdMs = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpThreshold);//持有唤醒锁 mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, TAG); }}
systemReady解析
主要的工作内容
- 创建sim同步运营商同步时间监听。
- 创建com.android.server.NetworkTimeUpdateService.action.POLL广播监听。
- 创建网络变化广播监听
- 创建带handler的子线程,用于后面获取网络时间和同步时间到系统中
- 发送同步时间广播,执行开机后第一次时间同步
- 设置SettingsObserver对象监听Settings.Global.AUTO_TIME属性变化,就是系统设置里面那个是否开启同步时间的设置项修改的值。
/** Initialize the receivers and initiate the first NTP request */ public void systemRunning() { //注册sim同步时间监听 registerForTelephonyIntents();//注册com.android.server.NetworkTimeUpdateService.action.POLL广播 registerForAlarms();//注册网络变化广播 registerForConnectivityIntents();//创建带handler的子线程 HandlerThread thread = new HandlerThread(TAG); thread.start(); mHandler = new MyHandler(thread.getLooper());//同步时间 // Check the network time on the new thread mHandler.obtainMessage(EVENT_POLL_NETWORK_TIME).sendToTarget();//监听Settings.Global.AUTO_TIME变化 mSettingsObserver = new SettingsObserver(mHandler, EVENT_AUTO_TIME_CHANGED); mSettingsObserver.observe(mContext); }
其中registerForTelephonyIntents()要说明下,该方法监听的广播收到会改变mNitzTimeSetTime的值,但是不是更新系统时间,该ACTION_NETWORK_SET_TIME和ACTION_NETWORK_SET_TIMEZONE广播是sim卡同步运营商时间时,设置系统时间后再发出来的,故发送广播之前就同步时间,感兴趣可以在系统源码中搜索下ACTION_NETWORK_SET_TIME广播是哪里发的就可以找到同步的地方了。
private void registerForTelephonyIntents() { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(TelephonyIntents.ACTION_NETWORK_SET_TIME); intentFilter.addAction(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE); mContext.registerReceiver(mNitzReceiver, intentFilter); } /** Receiver for Nitz time events */ private BroadcastReceiver mNitzReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DBG) Log.d(TAG, "Received " + action); if (TelephonyIntents.ACTION_NETWORK_SET_TIME.equals(action)) { mNitzTimeSetTime = SystemClock.elapsedRealtime(); } else if (TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE.equals(action)) { mNitzZoneSetTime = SystemClock.elapsedRealtime(); } } };
除了sim广播外,其他监听触发后都是给handler发送消息同步时间。先看下NetworkTimeUpdateService中的Handler类。
private class MyHandler extends Handler { public MyHandler(Looper l) { super(l); } @Override public void handleMessage(Message msg) { switch (msg.what) { case EVENT_AUTO_TIME_CHANGED: case EVENT_POLL_NETWORK_TIME: case EVENT_NETWORK_CHANGED://msg.what表示触发导致的同步时间 onPollNetworkTime(msg.what); break; } } }
最终调用的的onPollNetworkTime方法
onPollNetworkTime方法的主要工作内容:
- 判断Settings.Global.AUTO_TIME属性是否不为零,即自动同步时间开关是否打开,若没打开就停止同步流程。
- 调用onPollNetworkTimeUnderWakeLock,执行同步时间策略。
private void onPollNetworkTime(int event) { //判断是否启动时间统同步 // If Automatic time is not set, don't bother. if (!isAutomaticTimeRequested()) return;//获取唤醒锁 mWakeLock.acquire(); try { onPollNetworkTimeUnderWakeLock(event); } finally { mWakeLock.release(); } }
onPollNetworkTimeUnderWakeLock的主要工作内容如下:
- 判断是否sim同步过时间,并且同步的时间间隔不超过24h,是的话停止同步,等待24小时候再同步,防止多次同步。
- ntp没有同步过时间或者距离上次同步时间超过24小时,或者Settings.Global.AUTO_TIME改变,则开始时间同步,否则再等24小时同步时间。
- 调用NtpTrustedTime的forceRefresh方法获取ntp时间。
- 调用NtpTrustedTime的currentTimeMillis方法获取从ntp服务上获取的时间。
- 调用SystemClock.setCurrentTimeMillis将同步的时间设置到系统中。
- 若forceRefresh获取时间失败会另外有3次机会从ntp上获取时间,间隔5秒,三次都失败了则再过24小时同步时间。
private void onPollNetworkTimeUnderWakeLock(int event) { final long refTime = SystemClock.elapsedRealtime();//sim卡时间同步过,并且同步时时间不到24小时 // If NITZ time was received less than mPollingIntervalMs time ago, // no need to sync to NTP. if (mNitzTimeSetTime != NOT_SET && refTime - mNitzTimeSetTime = mLastNtpFetchTime + mPollingIntervalMs || event == EVENT_AUTO_TIME_CHANGED) { if (DBG) Log.d(TAG, "Before Ntp fetch");//距上次同步时间过了24小时获取没有同步过 // force refresh NTP cache when outdated if (mTime.getCacheAge() >= mPollingIntervalMs) {//重新获取网络时间 mTime.forceRefresh(); }//距离上次时间小于24小时,说明同步时间成功,24小时内同步过一次 // only update when NTP time is fresh if (mTime.getCacheAge() mTimeErrorThresholdMs || mLastNtpFetchTime == NOT_SET) { // Set the system time if (DBG && mLastNtpFetchTime == NOT_SET&& Math.abs(ntp - currentTime) <= mTimeErrorThresholdMs) { Log.d(TAG, "For initial setup, rtc = " + currentTime); } if (DBG) Log.d(TAG, "Ntp time to be set = " + ntp); // Make sure we don't overflow, since it's going to be converted to an int if (ntp / 1000 < Integer.MAX_VALUE) {//设置系统时间 SystemClock.setCurrentTimeMillis(ntp); } } else { if (DBG) Log.d(TAG, "Ntp time is close enough = " + ntp); }//更新mLastNtpFetchTime属性 mLastNtpFetchTime = SystemClock.elapsedRealtime(); } else {//说明同步时失败,没5秒同步一次,同步三次,三次后再过24小时 // Try again shortly mTryAgainCounter++; if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) { resetAlarm(mPollingIntervalShorterMs); } else { // Try much later mTryAgainCounter = 0; resetAlarm(mPollingIntervalMs); } return; } } resetAlarm(mPollingIntervalMs); }
NtpTrustedTime与SntpClient解析
前面已经研究完同步策略了,下面讲下,如何系统是如何与ntp服务器联系并且获取到网络时间的。先讲下NtpTrustedTime,该类是个单例,所以从它的getInstance方法讲起。主要做如下工作:
- 获取ntp服务器的地址和连接超时时间。
- 创建NtpTrustedTime对象,将ntp服务器的地址和连接超时时间和地址传入进去。
private NtpTrustedTime(String server, long timeout) { if (LOGD) Log.d(TAG, "creating NtpTrustedTime using " + server); mServer = server; mTimeout = timeout; } public static synchronized NtpTrustedTime getInstance(Context context) { if (sSingleton == null) { final Resources res = context.getResources(); final ContentResolver resolver = context.getContentResolver();//默认ntp服务器地址 2.android.pool.ntp.org final String defaultServer = res.getString( com.android.internal.R.string.config_ntpServer);//5秒 final long defaultTimeout = res.getInteger( com.android.internal.R.integer.config_ntpTimeout); final String secureServer = Settings.Global.getString( resolver, Settings.Global.NTP_SERVER); final long timeout = Settings.Global.getLong( resolver, Settings.Global.NTP_TIMEOUT, defaultTimeout); final String server = secureServer != null ? secureServer : defaultServer; sSingleton = new NtpTrustedTime(server, timeout); sContext = context; } return sSingleton; }
解析获取网络时间方法forceRefresh,主要工作如下:
- 判断设备是否有网。
- 创建SntpClient对象,用于获取网络时间的。
- 向ntp请求网络时间。
- 获取网络请求的时间进行保存。
public boolean forceRefresh() { ...//设备是否有网 final NetworkInfo ni = mCM == null ? null : mCM.getActiveNetworkInfo(); if (ni == null || !ni.isConnected()) { if (LOGD) Log.d(TAG, "forceRefresh: no connectivity"); return false; } if (LOGD) Log.d(TAG, "forceRefresh() from cache miss");//创建SntpClient对象,用于获取网络时间的 final SntpClient client = new SntpClient();//向ntp请求网络时间 if (client.requestTime(mServer, (int) mTimeout)) {//表示有缓冲 mHasCache = true;//将SntpClient获取的网络时间同步过来 mCachedNtpTime = client.getNtpTime();//记录相应ntp后的机器开机后的时间 mCachedNtpElapsedRealtime = client.getNtpTimeReference(); mCachedNtpCertainty = client.getRoundTripTime() / 2; return true; } else { return false; } }
NetworkTimeUpdateService中有调用NtpTrustedTime的getCacheAge和currentTimeMillis,我们依次解析下
public long getCacheAge() {//mHasCache为false表示之前没有同步过时间 if (mHasCache) {//获取距离上次同步时间的间隔 return SystemClock.elapsedRealtime() - mCachedNtpElapsedRealtime; } else { return Long.MAX_VALUE; } } public long currentTimeMillis() { if (!mHasCache) { throw new IllegalStateException("Missing authoritative time source"); } if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");//计算出当前时间放回给调用者。 // current time is age after the last ntp cache; callers who // want fresh values will hit makeAuthoritative() first. return mCachedNtpTime + getCacheAge(); }
最后解析下SntpClient对象,真正与ntp服务其对接的逻辑都在这个类里面,才有socket通信。该类构造方法是默认构造方法,没有自己定义,我们直接解析它的requestTime方法。主要内容
- 根据host地址创建InetAddress对象。
- 创建DatagramSocket对象,用于ntp服务器通讯。
- 创建请求对象DatagramPacket的类对象request。
- 调用DatagramSocket对象的send方法,将请求传递给ntp服务器。
- 创建接收ntp服务器信息的DatagramPacket的类对象response。
- 调用socket.receive方法,将ntp服务器里的信息传递给response。
- response里面的信息赋值給SntpClient各个参数,供NtpTrustedTime调用。
public boolean requestTime(String host, int timeout) { InetAddress address = null; try {//根据host创建网络地址 address = InetAddress.getByName(host); } catch (Exception e) { if (DBG) Log.d(TAG, "request time failed: " + e); return false; }//设置地址和端口以及超时时间 return requestTime(address, NTP_PORT, timeout); } public boolean requestTime(InetAddress address, int port, int timeout) { DatagramSocket socket = null; try {//创建数据报socket对象 socket = new DatagramSocket(); socket.setSoTimeout(timeout); byte[] buffer = new byte[NTP_PACKET_SIZE];//创建请求 DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, port); // set mode = 3 (client) and version = 3 // mode is in low 3 bits of first byte // version is in bits 3-5 of first byte buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION <> 6) & 0x3); final byte mode = (byte) (buffer[0] & 0x7); final int stratum = (int) (buffer[1] & 0xff); final long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET); final long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET); final long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET); /* do sanity check according to RFC */ // TODO: validate originateTime == requestTime. checkValidServerReply(leap, mode, stratum, transmitTime); long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime); // receiveTime = originateTime + transit + skew // responseTime = transmitTime + transit - skew // clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2 // = ((originateTime + transit + skew - originateTime) + // (transmitTime - (transmitTime + transit - skew)))/2 // = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2 // = (transit + skew - transit + skew)/2 // = (2 * skew)/2 = skew long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2; if (DBG) { Log.d(TAG, "round trip: " + roundTripTime + "ms, " + "clock offset: " + clockOffset + "ms"); } // save our results - use the times on this side of the network latency // (response rather than request time) mNtpTime = responseTime + clockOffset; mNtpTimeReference = responseTicks; mRoundTripTime = roundTripTime; } catch (Exception e) { if (DBG) Log.d(TAG, "request time failed: " + e); return false; } finally { if (socket != null) { socket.close(); } } return true; }