因为工作中用到了需要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
服务器提供的对时功能,可以使我们的设备时钟系统能够正确运行。
![](https://img.haomeiwen.com/i1716313/d21eabc11e41d190.png)
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。
![](https://img.haomeiwen.com/i1716313/6157d2453febde07.png)
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小时
。
网友评论