实现基于NTP协议的网络校时功能

     无论PC端还是移动端系统都自带时间同步功能,基于的都是NTP协议,这里使用C#来实现基于NTP协议的网络校时功能(也就是实现时间同步)。

1、NTP原理

    NTP【Network Time Protocol】是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化,它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒),且可介由加密确认的方式来防止恶毒的协议攻击。

  先介绍下NTP数据包格式(其标准化文档为RFC2030,NTP版本是第4版本):

                           1                   2                   3
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |LI | VN  |Mode |    Stratum    |     Poll      |   Precision   |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                          Root Delay                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                       Root Dispersion                         |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                     Reference Identifier                      |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                   Reference Timestamp (64)                    |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                   Originate Timestamp (64)                    |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                    Receive Timestamp (64)                     |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                    Transmit Timestamp (64)                    |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                 Key Identifier (optional) (32)                |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      |                                                               |
      |                 Message Digest (optional) (128)               |
      |                                                               |
      |                                                               |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

其中协议字段的含义如下所示:

      LI:跳跃指示器,警告在当月最后一天的最终时刻插入的迫近闺秒(闺秒)。0表示无警告,1表示最后一分钟有61秒,2表示最后一分钟有59秒,3表示告警状态,时钟未被同步。 
      VN:版本号。这里是4。 
     Mode:工作模式。该字段包括以下值:0-预留;1-对称行为;3-客户机;4-服务器;5-广播;6-NTP控制信息。NTP协议具有3种工作模式,分别为主/被动对称模式、客户/服务器模式、广播模式。在主/被动对称模式中,有一对一的连接,双方均可同步对方或被对方同步,先发出申请建立连接的一方工作在主动模式下,另一方工作在被动模式下; 客户/服务器模 式与主/被动模式基本相同,惟一区别在于客户方可被服务器同步,但服务器不能被客户同步;在广播模式中,有一对多的连接,服务器不论客户工作 在何种模式下,都会主动发出时间信息,客户根据此信息调整自己的时间。
     Stratum:对本地时钟级别的整体识别。 
     Poll:有符号整数表示连续信息间的最大间隔。
     Precision:有符号整数表示本地时钟精确度。
     Root Delay:表示到达主参考源的一次往复的总延迟,它是有15~16位小数部分的符号定点小 数。
     Root Dispersion:表示一次到达主参考源的标准误差,它是有15~16位小数部分的无符号 定点小数。
     Reference Identifier:识别特殊参考源。 
     Originate Timestamp:NTP请求报文离开发送端是发送端的本地时间,采用64位时标格式。 
     Receive Timestamp:NTP请求报文接收到时接收端的本地时间,采用64位时标格式。
     Transmit Timestamp:这是应答报文离开应答者时应答者的本地时间,采用64位时标格式。
 
这里采用的是客户端请求服务器的模式,所以只介绍客户端模式报文发送,可选项不需要,如下
    字段名称                   单播                    
                              请求报文    响应报文
      ------------------------------------------------
      LI                      0          0-2          
      VN                      4          3-4                                                   
      Mode                    3          4            
      Stratum                 0          1-14         
      Poll                    0          ignore      
      Precision               0          ignore       
      Root Delay              0          ignore      
      Root Dispersion         0          ignore       
      Reference Identifier    0          ignore      
      Reference Timestamp     0          ignore      
      Originate Timestamp     0          请求报文发送时间(T1)  
      Receive Timestamp       0          请求报文到达服务端时间(T2)   
      Transmit Timestamp    本地时间(T1) 服务端应答报文离开时服务端本地时间(T3)      

   可以看到客户端发送本地时间(T1)过去后,服务端响应报文会将客户端报文发送时间放在字段Originate Timestamp字段中发回来,同时报文中带有请求报文到达服务端的时间(T2)和服务端应答报文离开服务端时的服务端时间(T3),而客户端接收到来自服务端发送的响应报文时的本地时间为T4,根据这四个参数可以计算:

    NTP报文的往返时延delay=(T4-T1)-(T3-T2) 

    客户端与服务端时间差(时钟补偿)offset=((T2-T1)+(T3-T4))/2

    以上时间差计算是假定报文往返相同的情况下,如果请求报文时延和响应报文所花费时间不一致,则计算的时间差offset并不准确(一般来说肯定有误差,误差最大为往返时延的1/2),但这点精度还在容忍范围。如此可以计算服务器端时间ServerTime = LocalTime + offset。

2、代码实现

    2.1 报文构造

    前面已经讲过,发送的报文Mode为3,版本为4,发送时间是本地时间,其余字段为0,代码如下(可选项不用)

 1 private const byte NTPDataLength = 48;
 2 // NTP 数据包 (基于RFC 2030)
 3 byte[] NTPData = new byte[NTPDataLength];
 4 
 5  //NTP数据包初始化
 6 private void Initialize()
 7 {
 8      //版本4,模式客户端(3)
 9      NTPData[0] = 0x1B;
10      //其他初始化为0
11      for (int i = 1; i < 48; i++)
12      {
13             NTPData[i] = 0;
14       }
15       //发送端本地时间
16       TransmitTimestamp = DateTime.Now;
17 }
View Code

   2.2报文发送

    NTP协议基于UDP,端口号为123,报文构造好后则发送报文,需要先获取NTP服务器端地址,百度搜索下第一个就是豆瓣的,笔者使用的是上海交通大学网络中心NTP服务器地址ntp.sjtu.edu.cn,参照国外一位作者的代码(该代码写于2001年,后续笔者会对该代码进行部分改动并封装,后面会放出改动的代码),通过域名解析的方式获得IP地址,然后进行连接。

 1 //在DNS服务器中查询NTP服务器的IP 地址(这里就不要输入IP地址了,否则报错)
 2 IPHostEntry hostadd = Dns.GetHostEntry(TimeServer);
 3 IPEndPoint EPhost = new IPEndPoint(hostadd.AddressList[0], 123);
 4 
 5 //连接NTP服务器
 6 UdpClient TimeSocket = new UdpClient();
 7 TimeSocket.Connect(EPhost);
 8 
 9 //初始化NTP数据报文
10 Initialize();
11 //发送NTP报文
12 TimeSocket.Send(NTPData, NTPData.Length);
View Code

    2.3报文接收

    报文接收后,首先要记录接收报文时的本地时间,代码非常简单,如下

1 NTPData = TimeSocket.Receive(ref EPhost); 
2 //记录接收到报文时的本地时间
3 ReceptionTimestamp = DateTime.Now; 

    2.4报文解析

    首先介绍下时间格式,如下所示,时间分为秒和秒的小数部分,左边是高位,右边是低位,代码如下:

                        1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                           Seconds                             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                  Seconds Fraction (0-padded)                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 1 private ulong GetMilliSeconds(byte offset)
 2 {
 3     ulong intpart = 0, fractpart = 0;
 4 
 5     for (int i = 0; i <= 3; i++)
 6     {
 7         intpart = 256 * intpart + NTPData[offset + i];
 8     }
 9     for (int i = 4; i <= 7; i++)
10     {
11         fractpart = 256 * fractpart + NTPData[offset + i];
12     }
13     
14     ulong milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L;
15     return milliseconds;
16 }
View Code

    主要讲解下秒的小数部分的表示,小数部分由32位整数表示,如果全部为1,并除以以0x100000000,也就是0xFFFFFFFF/0x100000000=0.999999999767(后面的就省略了),可以看到通过换算小数部分最大值可以精确表示到0.999999999,也就是纳秒级别,这里忽略了大约200多皮秒的时间。对我们来说,只要毫秒时间可以了,所以毫秒计算公式为

    milliseconds = 1000* fraction / 0x100000000

    获得总毫秒时间后换算为具体年月日时间,代码如下

1 private DateTime ComputeDate(ulong milliseconds)
2 {
3     TimeSpan span =TimeSpan.FromMilliseconds((double)milliseconds);
4     DateTime time = new DateTime(1900, 1, 1);
5     time += span;
6     return time;
7 }
View Code

   基于此,计算上面所讲的T1、T2、T3

 1  // T1 请求报文客户端时间
 2 public DateTime OriginateTimestamp
 3 {
 4     get
 5     {
 6           return 
 7 ComputeDate(GetMilliSeconds(offOriginateTimestamp));
 8     }
 9 }
10 
11 // T2 接收到请求报文时服务器端时间
12 public DateTime ReceiveTimestamp
13 {
14     get
15     {
16         DateTime time = ComputeDate(GetMilliSeconds(offReceiveTimestamp));
17         // 协调世界时转为当地时间
18         TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
19         return time + offspan;
20     }
21 }
22 
23 // T3 响应报文发送时服务器端时间
24 public DateTime TransmitTimestamp
25 {
26     get
27     {
28         DateTime time = ComputeDate(GetMilliSeconds(offTransmitTimestamp));
29         // 协调世界时转为当地时间
30        TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
31        return time + offspan;
32     }
33 }
View Code

   这样可以计算得到时钟补偿offset = (ReceiveTimestamp - OriginateTimestamp) - (ReceptionTimestamp - TransmitTimestamp)

   3、代码封装

    代码封装基于原国外代码基础之上,重复造车轮意义不大,原代码没有进行时钟补偿,直接使用了服务器端发送时间即 TransmitTimestamp(T3),对其封装后直接获取当前时间就可以了,不用再做修改了,代码如下(比较简单,就没有注释了)

 1     public class BeijingTime
 2     {
 3         private const string HOST = "ntp.sjtu.edu.cn";  
 4 
 5         private static BeijingTime _instance = null;
 6         private NTPClient _client;
 7 
 8         private TimeSpan _tsClock = new TimeSpan(0);
 9 
10         private bool _IsConnect = false;       //没有建立连接
11 
12         private BeijingTime()
13         {
14             _client = new NTPClient(HOST);
15         }
16 
17         public bool IsConnect
18         {
19             get { return _IsConnect; }
20         }
21 
22         public DateTime BeijingTimeNow
23         {
24             get { return DateTime.Now.Add(_tsClock); }
25         }
26 
27         /// <summary>
28         /// 设置本地时间,返回失败可能是因为权限不足,请在管理员权限下使用
29         /// </summary>
30         /// <param name="dtLocal"></param>
31         /// <returns></returns>
32         public bool SetLocalTime(DateTime dtLocal)
33         {
34             return _client.SetTime(dtLocal);
35         }
36 
37         public bool Connect()
38         {
39             try
40             {
41                 _client.Connect();
42                 _IsConnect = true;
43                 _tsClock = new TimeSpan(_client.LocalClockOffset);  
44                         
45                 return true;
46             }
47             catch (Exception)
48             {
49                 _IsConnect = false;
50                 return false;
51             }
52         }
53 
54         public static BeijingTime Instance
55         {
56             get
57             {
58                 if (_instance == null)
59                 {
60                     _instance = new BeijingTime();
61                 }
62 
63                 return _instance;
64             }
65         }      
66     }
View Code

   4、测试结果

  下载封装好的代码,如下方式调用

1 static void Main(string[] args)
2 {
3     Utility.BeijingTime beijing = Utility.BeijingTime.Instance;
4     beijing.Connect();
5     Console.WriteLine(string.Format("时钟补偿:{0:f6}",(beijing.BeijingTimeNow - DateTime.Now).TotalSeconds));
6     Console.WriteLine(string.Format("本地时间:{0}",beijing.BeijingTimeNow.ToString()));
7     Console.ReadLine();
8 }

  结果如下:因为本身使用Windows自带同步功能同步过,所以结果还是蛮精确的

    5、后记

    网上虽然有很多相关介绍的文章,但个别地方讲的并不仔细,大多代码也不能直接拿来用,就参照国外的源代码和RFC2030文档写了这篇文章,并修改了代码,方便不愿意看原理的人直接下载代码就可以使用。NTP协议内容很多,这里只讲了客户端请求服务端的方式。限于笔者个人水平,文章中难免会出现疏漏,还望指正。

   

参考文章

    1、http://blog.sina.com.cn/s/blog_772ee6f30100pbzw.html

    2、http://www.ietf.org/rfc/rfc2030.txt

    3、http://blog.163.com/yzc_5001/blog/static/2061963420121283050787/

posted @ 2015-06-23 22:01  日行一米  阅读(2714)  评论(0编辑  收藏  举报