• 实现基于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/

  • 相关阅读:
    图片和xml文件的转换
    WPF的样式(Style)继承
    .NET的序列化和反序列化
    WPF中的画板InkCanvas
    找到网页的源文件并找到歌曲文件的路径
    How to check if a ctrl + enter is pressed on a control?
    计算两个日期相差的天数
    图片保存到数据库以及从数据库中Load图片
    设计模式Command(命令模式)
    一个强大而且好用的UML设计工具
  • 原文地址:https://www.cnblogs.com/gguuoojun/p/4588370.html
Copyright © 2020-2023  润新知