ios-ntp库源码分析+ NTP协议分析

因为工作中用到了需要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服务器提供的对时功能,可以使我们的设备时钟系统能够正确运行。

image.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。

image.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小时

参考链接:
NTP(Network Time Protocol)协议详解
TP协议详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,540评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,640评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,643评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,672评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,525评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,387评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,809评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,450评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,743评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,787评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,560评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,406评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,824评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,048评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,335评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,784评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,997评论 2 337

推荐阅读更多精彩内容