因?yàn)楣ぷ髦杏玫搅诵枰?code>NTP和服務(wù)端進(jìn)行時間同步,所以在網(wǎng)上找到了ios-ntp這個庫,用起來還是可以解決一部分項(xiàng)目問題;
至此對于一個有著多年工作開發(fā)經(jīng)驗(yàn)的程序員來說,會用并不能滿足我的需求,對此我對源碼及其相關(guān)協(xié)議做了一些研究;
本章可能會預(yù)計(jì)花費(fèi)你 10~20分鐘閱讀時間;
第一章節(jié)對一個高級的iOS開發(fā)來說可能意義不大,重點(diǎn)在第二章節(jié),及源碼中的注視;
本章主要分為2個部分進(jìn)行展開
1.ios-ntp 的使用
1.1 ios-ntp 的使用
1.2. ios-ntp 源碼解析
2.NTP協(xié)議介紹
2.1 ntp 協(xié)議重點(diǎn)分析
1.ios-ntp介紹
1.1 ios-ntp 的使用
https://github.com/jbenet/ios-ntp
1.如果使用NetworkClock獲取NTP網(wǎng)絡(luò)時間,需在項(xiàng)目工程中創(chuàng)建一個ntp.hosts 的文件,并在文件以文本的方式寫入NTP 服務(wù)器地址;
2.因?yàn)?code>NetworkClock需要通過UDP獲取 網(wǎng)絡(luò)時間,所以需要倒入CocoaAsyncSocket 庫
ntp.hosts 配置文件例子
ntp.aliyun.com
time.apple.com
ntp1.ict.ac.cn
NetworkClock 調(diào)用案例
NetworkClock * nc = [NetworkClock sharedNetworkClock];
NSDate * nt = nc.networkTime;
NetAssociation 調(diào)用案例
netAssociation = [[NetAssociation alloc] initWithServerName:@"time.apple.com"];
netAssociation.delegate = self;
netAssociation sendTimeQuery];
///獲取同步后的本地之間 和ntp服務(wù)器之間的offset
- (void) reportFromDelegate {
double timeOffset = netAssociation.offset;
}
1.2. ios-ntp 源碼解析
NetworkClock 是一個NTP服務(wù)的管理者,主要用于主動創(chuàng)建NTP服務(wù)和管理NetAssociation 的一個實(shí)例對象;
/// 單例對象
+ (instancetype) sharedNetworkClock;
/// 開啟NTP 服務(wù)(以 ntp.hosts 文件的NTP服務(wù)器地址獲取最新同步后的時間)
- (void) createAssociations;
/// 以指定的NTP服務(wù)器地址數(shù)組 獲取最新同步后的時間
- (void) createAssociationsWithServers:(NSArray *)servers;
/// 開啟NTP同步
- (void) enableAssociations;
/// 停止NTP同步
- (void) snoozeAssociations;
/// 停止并移除相關(guān)NTP同步
- (void) finishAssociations;
/// 獲取當(dāng)前NSDate 的網(wǎng)絡(luò)時間
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSDate * networkTime;
/// 獲取當(dāng)前設(shè)備和NTP服務(wù)器同步后誤差的偏移量
@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval networkOffset;
NetAssociation 介紹
/////通過IP 解析相應(yīng)的NTP 服務(wù)器地址,并創(chuàng)建UdpSocket 通信
socket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self
delegateQueue:dispatch_queue_create(
[serverName cStringUsingEncoding:NSUTF8StringEncoding], DISPATCH_QUEUE_SERIAL)];
創(chuàng)建請求間隔池,確保剛開始快速同步NTP,后續(xù)穩(wěn)定間隔去同步NTP, 增加隨機(jī)值,確保所有需要和NTP同步的設(shè)備,不會在同一個時間點(diǎn)去請求NTP服務(wù)(防止打爆服務(wù)器)
/// 請求間隔時間池
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服務(wù)的包(重點(diǎn))
- (NSData *) createPacket {
///創(chuàng)建4*12 字節(jié)的數(shù)據(jù)包
uint32_t wireData[12];
///初始值設(shè)置為0
memset(wireData, 0, sizeof wireData);
/// 通過 << 左移多少位,分別給第一個 4字節(jié)填充相應(yīng)的數(shù)據(jù)
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);
///獲取當(dāng)前設(shè)備時間
ntpClientSendTime = ntp_time_now();
///第10和第11 個4字節(jié) 填充本地時鐘 (參考NTP 協(xié)議 )
wireData[10] = htonl(ntpClientSendTime.partials.wholeSeconds); // Transmit Timestamp
wireData[11] = htonl(ntpClientSendTime.partials.fractSeconds);
///數(shù)據(jù)填充完畢,通過UDPSocket 發(fā)送出去
return [NSData dataWithBytes:wireData length:48];
}
收到NTP返回的包(重點(diǎn))
- (void) decodePacket:(NSData *) data {
/*┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ grab the packet arrival time as fast as possible, before computations below ... │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
/// 獲取客戶端收到服務(wù)端響應(yīng)的時間
ntpClientRecvTime = ntp_time_now();
uint32_t wireData[12];
[data getBytes:wireData length:48];
///第一個4字節(jié)的數(shù)組 包含NTP 標(biāo)識器/版本/模式/層級等參數(shù),獲取相應(yīng)字段并解析,此處還有大小端轉(zhuǎn)換
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)
/// 參考標(biāo)識符
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 ... │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘*/
/// 判斷條件,確保發(fā)送的客戶端時間戳 和服務(wù)端反回的一致 (確保是同一個packet的 ACK)
if (ntpClientSendTime.partials.wholeSeconds != ntohl(wireData[6]) ||
ntpClientSendTime.partials.fractSeconds != ntohl(wireData[7])) return; // NO;
/// 獲取服務(wù)端返回的(接受時間戳)
ntpServerRecvTime.partials.wholeSeconds = ntohl(wireData[8]);
ntpServerRecvTime.partials.fractSeconds = ntohl(wireData[9]);
/// 獲取服務(wù)端返回的 (傳送時間戳)
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返回的包 - 客戶端發(fā)送同步請求的時間
// t32服務(wù)端發(fā)送消息報 - 服務(wù)端收到同步請求的時間
double t41 = ntpDiffSeconds(&ntpClientSendTime, &ntpClientRecvTime); // .. (T4-T1)
double t32 = ntpDiffSeconds(&ntpServerRecvTime, &ntpServerSendTime); // .. (T3-T2)
_roundtrip = t41 - t32;
// t21服務(wù)端收到客戶端發(fā)送的包 - 客戶端發(fā)送同步請求的時間
// t34 服務(wù)端發(fā)送消息包 - 客戶端收到同步請求的時間
double t21 = ntpDiffSeconds(&ntpServerSendTime, &ntpClientRecvTime); // .. (T2-T1)
double t34 = ntpDiffSeconds(&ntpServerRecvTime, &ntpClientSendTime); // .. (T3-T4)
// 計(jì)算 偏移量
_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));
}
// 實(shí)例發(fā)送代理通知
dispatch_async(dispatch_get_main_queue(), ^{ [self->_delegate reportFromDelegate]; });// tell delegate we're done
}
2.NTP協(xié)議介紹
2.1 ntp 協(xié)議重點(diǎn)分析
NTP(Network Time Protocol)網(wǎng)絡(luò)時間協(xié)議基于 UDP,用于網(wǎng)絡(luò)時間同步的協(xié)議,使網(wǎng)絡(luò)中的計(jì)算機(jī)時鐘同步到UTC,再配合各個時區(qū)的偏移調(diào)整就能實(shí)現(xiàn)精準(zhǔn)同步對時功能。提供NTP對時的服務(wù)器有很多,比如微軟的NTP對時服務(wù)器,利用NTP服務(wù)器提供的對時功能,可以使我們的設(shè)備時鐘系統(tǒng)能夠正確運(yùn)行。

LI 閏秒標(biāo)識器,占用2個bit
VN 版本號,占用3個bits,表示NTP的版本號,現(xiàn)在為3
Mode 模式,占用3個bits,表示模式
stratum(層),占用8個bits
Poll 測試間隔,占用8個bits,表示連續(xù)信息之間的最大間隔
Precision 精度,占用8個bits,,表示本地時鐘精度
Root Delay根時延,占用8個bits,表示在主參考源之間往返的總共時延
Root Dispersion根離散,占用8個bits,表示在主參考源有關(guān)的名義錯誤
Reference Identifier參考時鐘標(biāo)識符,占用8個bits,用來標(biāo)識特殊的參考源
參考時間戳,64bits時間戳,本地時鐘被修改的最新時間。
原始時間戳,客戶端發(fā)送的時間,64bits。
接受時間戳,服務(wù)端接受到的時間,64bits。
傳送時間戳,服務(wù)端送出應(yīng)答的時間,64bits。

t0是請求數(shù)據(jù)包傳輸?shù)目蛻舳藭r間戳
t1是請求數(shù)據(jù)包回復(fù)的服務(wù)器時間戳
t2是響應(yīng)數(shù)據(jù)包傳輸?shù)姆?wù)器時間戳
t3是響應(yīng)數(shù)據(jù)包回復(fù)的客戶端時間戳
舉例:
(1)Device A發(fā)送一個NTP報文給Device B,該報文帶有它離開Device A時的時間戳,該時間戳為10:00:00am(T1)。
(2)當(dāng)此NTP報文到達(dá)Device B時,Device B加上自己的時間戳,該時間戳為11:00:01am(T2)。
(3)當(dāng)此NTP報文離開Device B時,Device B再加上自己的時間戳,該時間戳為11:00:02am(T3)。
(4) 當(dāng)Device A接收到該響應(yīng)報文時,Device A的本地時間為10:00:03am(T4)
NTP報文的往返時延 Delay=(T4-T1)-(T3-T2)= 2秒。
Device A相對Device B的時間差 offset=((T2-T1)+(T3-T4))/2=1小時 。