1、NTP同步时间原理
NTP协议是对网络内所有具有时钟的设备进行时钟同步,使网络内所有设备的时钟保持一致。
系统时钟同步的工作过程如下:
Device A发送一个NTP报文给Device B,该报文带有它离开Device A时的时间戳,该时间戳为10:00:00am(T1)。
当此NTP报文到达Device B时,Device B加上自己的时间戳,该时间戳为11:00:01am(T2)。
当此NTP报文离开Device B时,Device B再加上自己的时间戳,该时间戳为11:00:02am(T3)。
当Device A接收到该响应报文时,Device A的本地时间为10:00:03am(T4)。
至此,Device A已经拥有足够的信息来计算两个重要的参数:
NTP报文的往返时延Delay=(T4-T1)-(T3-T2)=2秒。
Device A相对Device B的时间差offset=((T2-T1)+(T3-T4))/2=1小时。
这样,Device A就能够根据这些信息来设定自己的时钟,使之与Device B的时钟同步。
对于android平台,framework中有关于NTP客户端实现的代码SntpClient,使用其requestTime函数,同步网络时间。
2、android中同步时间代码流程
android代码中,NetworkTimeUpdateService用于同步网络时间。
(1)简单介绍下NetworkTimeUpdateService的代码流程:
onPollNetworkTime(msg.what);是其时间同步的流程所在,可以看到有三种情况会调用到该函数。
/** Handler to do the network accesses on */
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_CONNECTED:
onPollNetworkTime(msg.what);
break;
default :
break;
}
}
}
进入onPollNetworkTime进行分析,主要看更新时间的条件:当网络连接,或者是 mTime.getCacheAge() >= mPollingIntervalMs时,执行mTime.forceRefresh()调用到NtpTrustedTime的requestTime函数去重新同步网络时间。
默认系统设置mPollingIntervalMs为24小时。而设置系统时间的流程则是调用SystemClock.setCurrentTimeMillis(ntp);,前提是设同步的时间和当前时间的时间差在mTimeErrorThresholdMs以内,默认为5s。
private void onPollNetworkTime(int event) {
......
final long currentTime = System.currentTimeMillis();
// Get the NTP time
if (mLastNtpFetchTime == NOT_SET || refTime >= mLastNtpFetchTime + mPollingIntervalMs
|| event == EVENT_AUTO_TIME_CHANGED || event == EVENT_NETWORK_CONNECTED) {
// force refresh NTP cache when outdated
if (mTime.getCacheAge() >= mPollingIntervalMs || event == EVENT_NETWORK_CONNECTED) {
mTime.forceRefresh();
}
// only update when NTP time is fresh
if (mTime.getCacheAge() < mPollingIntervalMs) {
final long ntp = mTime.currentTimeMillis();
mTryAgainCounter = 0;
// If the clock is more than N seconds off or this is the first time it's been
// fetched since boot, set the current time.
if (Math.abs(ntp - currentTime) > 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 = SystemClock.elapsedRealtime();
} else {
// Try again shortly
mTryAgainCounter++;
if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
resetAlarm(mPollingIntervalShorterMs);
} else {
// Try much later
mTryAgainCounter = 0;
resetAlarm(mPollingIntervalMs);
}
return;
}
}
resetAlarm(mPollingIntervalMs);
}
3、鉴权模式修改
问题是这样的,最近有需求需要使用特定的NTP服务器,其服务器进行了鉴权加密处理,我使用Android原生的流程去进行时间同步,却一直请求超时,而使用Linux的ntpdate可以正确同步到时间。
下面是NTP报文格式:
其中最后两个字段,则是用于认证用的,分别是KEY ID和信息认证码。所以对于Android原生代码,则需要实现NTP V4版本的才能支持鉴权。从包的格式来看,需要多发KEY ID和信息认证码即可。
修改也不难,首先会从服务器端得到鉴权的KEY ID和鉴权信息。代码部分只需要将android里的NTP版本号改为4,在原本的包后加发一个KEY ID的4个字节数据,最后加发鉴权信息和包数据一起加密出来16个字节(MD5等,这个也得从服务器端得知)。
这样就试了一下,可以从服务器端同步到时间了,具体代码就不贴了,流程基本就是这样。
4、偶发同步到错误时间
但是后面发现一个bug,在同步的时候,会有概率(大概1/10)同步到错误的时间,看了一下包,其中 Originate Timestamp(NTP请求报文离开发送端时发送端的本地时间)和我请求时发出去的Transmit Timestamp(应答报文离开应答者时应答者的本地时间)不对应,这个时间就是我们在上面介绍的时间戳T1。但是Linux服务器使用ntpdate抓的包有时也这样,同步的时间是对的。。。
我对于SntpClient.java中的一处地方不是很理解,正常来说,这两处时间,应该是一样的才对,但是为什么SntpClient.java中不使用自己本地发出去的时间来计算,而是用了服务器返回的时间戳来计算?这里我不是很明白,希望有高手解答一下。
所以当我发现服务器返回的时间戳和我发出去的差别很大(针对我调试的这个服务器我觉得超过30分钟就是服务器回错了,当然这种方法很挫),我就直接用我自己发出去的时间来计算。
这下子就没出现同步时间错误的情况了。