因为工作中用到了需要NTP
和服务端进行时间同步,所以在网上找到了ios-ntp
这个库,用起来还是可以解决一部分项目问题;
至此对于一个有着多年工作开发经验的程序员来说,会用并不能满足我的需求,对此我对源码及其相关协议做了一些研究;
本章可能会预计花费你 10~20分钟
阅读时间;
第一章节对一个高级的iOS开发来说可能意义不大,重点在第二章节,及源码中的注视;
本章主要分为2个部分进行展开
1.ios-ntp 的使用
1.1 ios-ntp 的使用
1.2. ios-ntp 源码解析
2.NTP协议介绍
2.1 ntp 协议重点分析
1.ios-ntp介绍
1.1 ios-ntp 的使用
https://github.com/jbenet/ios-ntp
1.如果使用NetworkClock
获取NTP网络时间
,需在项目工程中创建一个ntp.hosts
的文件,并在文件以文本的方式写入NTP 服务器地址
;
2.因为NetworkClock
需要通过UDP
获取 网络时间,所以需要倒入CocoaAsyncSocket
库
ntp.hosts
配置文件例子
ntp.aliyun.com
time.apple.com
ntp1.ict.ac.cn
NetworkClock
调用案例
NetworkClock * nc = [NetworkClock sharedNetworkClock];
NSDate * nt = nc.networkTime;
NetAssociation
调用案例
netAssociation = [[NetAssociation alloc] initWithServerName:@"time.apple.com"];
netAssociation.delegate = self;
netAssociation sendTimeQuery];
///获取同步后的本地之间 和ntp服务器之间的offset
- (void) reportFromDelegate {
double timeOffset = netAssociation.offset;
}
1.2. ios-ntp 源码解析
NetworkClock
是一个NTP
服务的管理者,主要用于主动创建NTP
服务和管理NetAssociation
的一个实例对象;
/// 单例对象
+ (instancetype) sharedNetworkClock;
/// 开启NTP 服务(以 ntp.hosts 文件的NTP服务器地址获取最新同步后的时间)
- (void) createAssociations;
/// 以指定的NTP服务器地址数组 获取最新同步后的时间
- (void) createAssociationsWithServers:(NSArray *)servers;
/// 开启NTP同步
- (void) enableAssociations;
/// 停止NTP同步
- (void) snoozeAssociations;
/// 停止并移除相关NTP同步
- (void) finishAssociations;
/// 获取当前NSDate 的网络时间
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSDate * networkTime;
/// 获取当前设备和NTP服务器同步后误差的偏移量
@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval networkOffset;
NetAssociation 介绍
/////通过IP 解析相应的NTP 服务器地址,并创建UdpSocket 通信
socket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self
delegateQueue:dispatch_queue_create(
[serverName cStringUsingEncoding:NSUTF8StringEncoding], DISPATCH_QUEUE_SERIAL)];
创建请求间隔池,确保刚开始快速同步NTP
,后续稳定间隔去同步NTP,
增加随机值,确保所有需要和NTP
同步的设备,不会在同一个时间点去请求NTP
服务(防止打爆服务器)
/// 请求间隔时间池
static double pollIntervals[18] = {
2.0, 16.0, 16.0, 16.0, 16.0, 35.0, 72.0, 127.0, 258.0,
511.0, 1024.0, 2048.0, 4096.0, 8192.0, 16384.0, 32768.0, 65536.0, 131072.0
};
repeatingTimer = [NSTimer timerWithTimeInterval:MAXFLOAT
target:self selector:@selector(queryTimeServer)
userInfo:nil repeats:YES];
repeatingTimer.tolerance = 1.0; // it can be up to 1 second late
[[NSRunLoop mainRunLoop] addTimer:repeatingTimer forMode:NSDefaultRunLoopMode];
timerWobbleFactor = ((float)rand()/(float)RAND_MAX / 2.0) + 0.75; // 0.75 .. 1.25
NSTimeInterval interval = pollIntervals[pollingIntervalIndex] * timerWobbleFactor;
repeatingTimer.tolerance = 5.0; // it can be up to 5 seconds late
repeatingTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:interval];
请求NTP
服务的包(重点)
- (NSData *) createPacket {
///创建4*12 字节的数据包
uint32_t wireData[12];
///初始值设置为0
memset(wireData, 0, sizeof wireData);
/// 通过 << 左移多少位,分别给第一个 4字节填充相应的数据
wireData[0] = htonl((0 << 30) | // no Leap Indicator
(4 << 27) | // NTP v4
(3 << 24) | // mode = client sending
(0 << 16) | // stratum (n/a)
(4 << 8) | // polling rate (16 secs)
(-6 & 0xff)); // precision (~15 mSecs)
wireData[1] = htonl(1<<16);
wireData[2] = htonl(1<<16);
///获取当前设备时间
ntpClientSendTime = ntp_time_now();
///第10和第11 个4字节 填充本地时钟 (参考NTP 协议 )
wireData[10] = htonl(ntpClientSendTime.partials.wholeSeconds); // Transmit Timestamp
wireData[11] = htonl(ntpClientSendTime.partials.fractSeconds);
///数据填充完毕,通过UDPSocket 发送出去
return [NSData dataWithBytes:wireData length:48];
}
收到NTP
返回的包(重点)
- (void) decodePacket:(NSData *) data {
/*┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ grab the packet arrival time as fast as possible, before computations below ... │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
/// 获取客户端收到服务端响应的时间
ntpClientRecvTime = ntp_time_now();
uint32_t wireData[12];
[data getBytes:wireData length:48];
///第一个4字节的数组 包含NTP 标识器/版本/模式/层级等参数,获取相应字段并解析,此处还有大小端转换
li = ntohl(wireData[0]) >> 30 & 0x03;
vn = ntohl(wireData[0]) >> 27 & 0x07;
mode = ntohl(wireData[0]) >> 24 & 0x07;
stratum = ntohl(wireData[0]) >> 16 & 0xff;
/*┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Poll: 8-bit signed integer representing the maximum interval between successive messages, │
│ in log2 seconds. Suggested default limits for minimum and maximum poll intervals are 6 and 10. │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
poll = ntohl(wireData[0]) >> 8 & 0xff;
/*┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Precision: 8-bit signed integer representing the precision of the system clock, in log2 seconds.│
│ (-10 corresponds to about 1 millisecond, -20 to about 1 microSecond) │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
prec = ntohl(wireData[0]) & 0xff;
if (prec & 0x80) prec |= 0xffffff00; // -ve byte --> -ve int
/// 根延迟
_root_delay = ntohl(wireData[1]) * 0.0152587890625; // delay (mS) [1000.0/2**16].
/// 根误差
_dispersion = ntohl(wireData[2]) * 0.0152587890625; // error (mS)
/// 参考标识符
refid = ntohl(wireData[3]);
/// 参考时间戳
ntpServerBaseTime.partials.wholeSeconds = ntohl(wireData[4]); // when server clock was wound
ntpServerBaseTime.partials.fractSeconds = ntohl(wireData[5]);
/*┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ if the send time in the packet isn't the same as the remembered send time, ditch it ... │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
/// 判断条件,确保发送的客户端时间戳 和服务端反回的一致 (确保是同一个packet的 ACK)
if (ntpClientSendTime.partials.wholeSeconds != ntohl(wireData[6]) ||
ntpClientSendTime.partials.fractSeconds != ntohl(wireData[7])) return; // NO;
/// 获取服务端返回的(接受时间戳)
ntpServerRecvTime.partials.wholeSeconds = ntohl(wireData[8]);
ntpServerRecvTime.partials.fractSeconds = ntohl(wireData[9]);
/// 获取服务端返回的 (传送时间戳)
ntpServerSendTime.partials.wholeSeconds = ntohl(wireData[10]);
ntpServerSendTime.partials.fractSeconds = ntohl(wireData[11]);
// NTP_Logging(@"%@", [self prettyPrintPacket]);
/*┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ determine the quality of this particular time .. │
│ .. if max_error is less than 50mS (and not zero) AND │
│ .. stratum > 0 AND │
│ .. the mode is 4 (packet came from server) AND │
│ .. the server clock was set less than 1 minute ago │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
_offset = INFINITY; // clock meaningless
if ((_dispersion < 100.0) &&
(stratum > 0) &&
(mode == 4) &&
(ntpDiffSeconds(&ntpServerBaseTime, &ntpServerSendTime) < 3600.0)) {
// t41客户端收到server返回的包 - 客户端发送同步请求的时间
// t32服务端发送消息报 - 服务端收到同步请求的时间
double t41 = ntpDiffSeconds(&ntpClientSendTime, &ntpClientRecvTime); // .. (T4-T1)
double t32 = ntpDiffSeconds(&ntpServerRecvTime, &ntpServerSendTime); // .. (T3-T2)
_roundtrip = t41 - t32;
// t21服务端收到客户端发送的包 - 客户端发送同步请求的时间
// t34 服务端发送消息包 - 客户端收到同步请求的时间
double t21 = ntpDiffSeconds(&ntpServerSendTime, &ntpClientRecvTime); // .. (T2-T1)
double t34 = ntpDiffSeconds(&ntpServerRecvTime, &ntpClientSendTime); // .. (T3-T4)
// 计算 偏移量
_offset = (t21 + t34) / 2.0; // calculate offset
// NSLog(@"t21=%.6f t34=%.6f delta=%.6f offset=%.6f", t21, t34, _roundtrip, _offset);
_active = TRUE;
// NTP_Logging(@"%@", [self prettyPrintTimers]);
}
else {
NTP_Logging(@" [%@] : bad data .. %7.1f", _server, ntpDiffSeconds(&ntpServerBaseTime, &ntpServerSendTime));
}
// 实例发送代理通知
dispatch_async(dispatch_get_main_queue(), ^{ [self->_delegate reportFromDelegate]; });// tell delegate we're done
}
2.NTP协议介绍
2.1 ntp 协议重点分析
NTP(Network Time Protocol)
网络时间协议基于 UDP
,用于网络时间同步的协议,使网络中的计算机时钟同步到UTC
,再配合各个时区的偏移调整就能实现精准同步对时功能。提供NTP
对时的服务器有很多,比如微软的NTP
对时服务器,利用NTP
服务器提供的对时功能,可以使我们的设备时钟系统能够正确运行。
LI 闰秒标识器,占用2个bit
VN 版本号,占用3个bits,表示NTP的版本号,现在为3
Mode 模式,占用3个bits,表示模式
stratum(层),占用8个bits
Poll 测试间隔,占用8个bits,表示连续信息之间的最大间隔
Precision 精度,占用8个bits,,表示本地时钟精度
Root Delay根时延,占用8个bits,表示在主参考源之间往返的总共时延
Root Dispersion根离散,占用8个bits,表示在主参考源有关的名义错误
Reference Identifier参考时钟标识符,占用8个bits,用来标识特殊的参考源
参考时间戳,64bits时间戳,本地时钟被修改的最新时间。
原始时间戳,客户端发送的时间,64bits。
接受时间戳,服务端接受到的时间,64bits。
传送时间戳,服务端送出应答的时间,64bits。
t0
是请求数据包传输的客户端时间戳
t1
是请求数据包回复的服务器时间戳
t2
是响应数据包传输的服务器时间戳
t3
是响应数据包回复的客户端时间戳
举例:
(1)Device A发送一个NTP报文给Device B,该报文带有它离开Device A时的时间戳,该时间戳为10:00:00am(T1)。
(2)当此NTP报文到达Device B时,Device B加上自己的时间戳,该时间戳为11:00:01am(T2)。
(3)当此NTP报文离开Device B时,Device B再加上自己的时间戳,该时间戳为11:00:02am(T3)。
(4) 当Device A接收到该响应报文时,Device A的本地时间为10:00:03am(T4)
NTP报文
的往返时延 Delay=(T4-T1)-(T3-T2)= 2秒
。
Device A
相对Device B
的时间差 offset=((T2-T1)+(T3-T4))/2=1小时
。