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

   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);

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 }

主要讲解下秒的小数部分的表示,小数部分由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 }

基于此,计算上面所讲的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 }

这样可以计算得到时钟补偿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     }

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/

时间: 2024-10-07 17:08:14

实现基于NTP协议的网络校时功能的相关文章

ntp网络校时服务器在安防系统的解决方案

网络校时服务器是以校正终端设备时间的一款标准服务器,在市场中的因其标准协议NTP标准符合大多数包括计算机,摄像机,监控,工控机等设备的时间校正协议,使得其在目前时间同步的市场中占据了相当大的比例.安防系统一般包括:监控系统,呼叫系统.办公电脑等组成.其中监控系统是安防中的主要组成部分,并随着现代计算机网络发展水平的提高,已经由最早的模拟系统转变为数字化系统,在现代工业应用中更加智能化,操作集中化,有利于监管配置,目前在政府机关,道路监控,电力电信,监狱公安,军队,商场酒店,超市小区等各个行业场所

三北斗网络校时服务器

北斗网络校时服务器是一款接收北斗卫星标准时钟信号,通过网口及串口输出时间信息的一款性价比极高的时间服务器,北斗授时服务器广泛应用于金融.移动通信.石油.电力.交通.工业以及国防等领域. 产品概述 SYN2136型北斗NTP网络时间服务器是由西安同步电子科技有限公司精心设计.自行研发生产的一款授时设备,接收北斗卫星信号,从北斗地球同步卫星上获取标准时钟信号信息,将这些信息通过TCP/IP网络传输,为网络设备(用户)提供精确.标准.安全.可靠和多功能的时间服务,同时产生1PPS(秒信号)同步脉冲信号

gps网络校时服务器在医院的应用

gps网络校时服务器在医院的应用目前,我国各大医院均运行着数十套或上百套信息系统和服务器,计算机终端约为数千台,此外医院里的医疗设备.数字时钟及电子屏幕等均有数字时钟.由于各种原因致使这些设备或系统的时间不准确,有的甚至相差数小时.医院设备的时钟时间不一致,会导致病患入院.病患入科.医嘱下达.转抄.执行以及检验检查的结果回报单等的时间存在误差,实验室对临床标本采集时间.送达时间.失效时间以及检测时间均有严格的先后时间顺序,一旦时间控制顺序混乱,将导致检验结果不准确,甚至造成临床的误诊,并将有可能

linux通过ntpdate网络校时

目前 Linux 系统上面有两个时间喔,一个是 Linux 系统,另一个则是 BIOS 时间(真正的硬件记录的时间)! 我们可以使用 date 这个指令来手动修正目前主机的时间,不过, date 这个指令仅修正 Linux 时间而已,我们还需要以 hwclock 这个指令来将 BIOS 时间也更新才行! hwclock [-rw]     -r:查看现有BIOS时间     -w:将现在的linux系统时间写入BIOS中 当我们进行完 Linux 时间的校时后,还需要以 hwclock -w 来

Java中的基于Tcp协议的网络编程

一:网络通信的三要素? IP地址     端口号     通信协议 IP地址:是网络中设备的通信地址.由于IP地址不易记忆,故可以使用主机名.本地环回地址,127.0.0.1   本地主机名localhost 端口号:发送端准备的数据要发送到指定的目的应用程序上,为了标识这些应用程序,所以用网络数字来标识这些不同的应用程序,这些数 字称为端口号.端口号是不同进程之间的标识.一般来说,有0~65535的端口可供使用,但是1~1024系统使用,或者称作保留端口. 通信协议:指定义的通信规则,这个规则

用c++开发基于tcp协议的文件上传功能

用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学阅读,比 APUE 讲的更深入 这是某个银行广告项目(p2p传输视频)的一部分 IO模型采用的阻塞模式,文件一打开就直接上传 用vc 2003编译,生成win32 dll 麻雀虽小五脏俱全,CSimpleSocket,CReadStream dll 输出一虚类 extern "C" __d

基于UDP协议的网络编程

UDP协议是一种不可靠的网络协议,它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送.接收数据报的对象. Java使用DatagramSocket代表基于UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报.Java使用DatagramPacket来代表数据报,DatagramSocket接收和发送数据都是通过DatagramPacket对象完成的. Datagr

基于UDP协议的网络程序

下图是基于UDP协议的客户端/服务器程序的一般流程: 图1.1 UDP协议通信流程 UDP套接口是无连接的.不可靠的数据报协议: 既然他不可靠为什么还要用呢?其一:当应用程序使用广播或多播时只能使用UDP协议:其二:由于他是无连接的,所以速度快.因为UDP套接口是无连接的,如果一方的数据报丢失,那另一方将无限等待,解决办法是设置一个超时. 建立UDP套接口时socket函数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口:由于UDP是无连接的,所以服务器端并不需要listen或

基于 UDP 协议的网络编程

类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序 UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达 DatagramPacket 对象封装了 UDP 数据报(<64k),在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号 UDP 协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接 举例: publ