无论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,代码如下(可选项不用)

 private const byte NTPDataLength = ;
// NTP 数据包 (基于RFC 2030)
byte[] NTPData = new byte[NTPDataLength]; //NTP数据包初始化
private void Initialize()
{
//版本4,模式客户端(3)
NTPData[] = 0x1B;
//其他初始化为0
for (int i = ; i < ; i++)
{
NTPData[i] = ;
}
//发送端本地时间
TransmitTimestamp = DateTime.Now;
}

   2.2报文发送

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

 //在DNS服务器中查询NTP服务器的IP 地址(这里就不要输入IP地址了,否则报错)
IPHostEntry hostadd = Dns.GetHostEntry(TimeServer);
IPEndPoint EPhost = new IPEndPoint(hostadd.AddressList[], ); //连接NTP服务器
UdpClient TimeSocket = new UdpClient();
TimeSocket.Connect(EPhost); //初始化NTP数据报文
Initialize();
//发送NTP报文
TimeSocket.Send(NTPData, NTPData.Length);

2.3报文接收

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

 NTPData = TimeSocket.Receive(ref EPhost);
//记录接收到报文时的本地时间
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) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 private ulong GetMilliSeconds(byte offset)
{
ulong intpart = , fractpart = ; for (int i = ; i <= ; i++)
{
intpart = * intpart + NTPData[offset + i];
}
for (int i = ; i <= ; i++)
{
fractpart = * fractpart + NTPData[offset + i];
} ulong milliseconds = intpart * + (fractpart * ) / 0x100000000L;
return milliseconds;
}

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

milliseconds = 1000* fraction / 0x100000000

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

 private DateTime ComputeDate(ulong milliseconds)
{
TimeSpan span =TimeSpan.FromMilliseconds((double)milliseconds);
DateTime time = new DateTime(, , );
time += span;
return time;
}

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

  // T1 请求报文客户端时间
public DateTime OriginateTimestamp
{
get
{
return
ComputeDate(GetMilliSeconds(offOriginateTimestamp));
}
} // T2 接收到请求报文时服务器端时间
public DateTime ReceiveTimestamp
{
get
{
DateTime time = ComputeDate(GetMilliSeconds(offReceiveTimestamp));
// 协调世界时转为当地时间
TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
return time + offspan;
}
} // T3 响应报文发送时服务器端时间
public DateTime TransmitTimestamp
{
get
{
DateTime time = ComputeDate(GetMilliSeconds(offTransmitTimestamp));
// 协调世界时转为当地时间
TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
return time + offspan;
}
}

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

 3、代码封装

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

     public class BeijingTime
{
private const string HOST = "ntp.sjtu.edu.cn"; private static BeijingTime _instance = null;
private NTPClient _client; private TimeSpan _tsClock = new TimeSpan(); private bool _IsConnect = false; //没有建立连接 private BeijingTime()
{
_client = new NTPClient(HOST);
} public bool IsConnect
{
get { return _IsConnect; }
} public DateTime BeijingTimeNow
{
get { return DateTime.Now.Add(_tsClock); }
} /// <summary>
/// 设置本地时间,返回失败可能是因为权限不足,请在管理员权限下使用
/// </summary>
/// <param name="dtLocal"></param>
/// <returns></returns>
public bool SetLocalTime(DateTime dtLocal)
{
return _client.SetTime(dtLocal);
} public bool Connect()
{
try
{
_client.Connect();
_IsConnect = true;
_tsClock = new TimeSpan(_client.LocalClockOffset); return true;
}
catch (Exception)
{
_IsConnect = false;
return false;
}
} public static BeijingTime Instance
{
get
{
if (_instance == null)
{
_instance = new BeijingTime();
} return _instance;
}
}
}

4、测试结果

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

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

结果如下:因为本身使用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/

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

  1. 北斗时钟同步系统-GPS卫星授时设备-NTP网络校时服务器

    北斗时钟同步系统-GPS卫星授时设备-NTP网络校时服务器 北斗时钟同步系统-GPS卫星授时设备-NTP网络校时服务器 北斗时钟同步系统-GPS卫星授时设备-NTP网络校时服务器 论述当下网络时间同步 ...

  2. 网络时钟服务器,NTP授时设备,北斗网络校时服务器,GPS时间同步器

    网络时钟服务器,NTP授时设备,北斗网络校时服务器,GPS时间同步器 网络时钟服务器,NTP授时设备,北斗网络校时服务器,GPS时间同步器 论述当下网络时间同步的重要性   北京华人开创科技发展有限公 ...

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

    用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学 ...

  4. Android消息推送(二)--基于MQTT协议实现的推送功能

    国内的Android设备,不能稳定的使用Google GCM(Google Cloud Messageing)消息推送服务. 1. 国内的Android设备,基本上从操作系统底层开始就去掉了Googl ...

  5. 基于UDP协议的网络编程

    UDP协议是一种不可靠的网络协议,它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送.接收数据报的对象. Java使用DatagramSock ...

  6. 通过python基于netconf协议获取网络中网元的配置数据,助力企业网络控制自动化轻松实现!

    摘要:在当今信息化时代,大多数企业都需要网络支撑企业的ICT运行,提升企业运行效率,针对企业网络中的网元设备(包括交换机,路由器,防火墙等),很多企业希望根据自身的业务特点定制网络管理,比如可以实现网 ...

  7. 基于TCP协议的网络通讯流程

    不多说了,先上个图: 从上面的图中可以看出来,基于TCP协议进行通讯可以大致分成以下几个阶段: 1. 首先是在服务器端, TCP Sever调用socket(), bind(), listen()完成 ...

  8. 基于TCP协议的网络编程

    TCP通信协议是一种可靠的传输层协议,它在通信的两端各建立一个Socket,从而在通信的两端之间形成虚拟网络链路.一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信.Java使用Socke ...

  9. UNIX网络编程——基于UDP协议的网络程序

    一.下图是典型的UDP客户端/服务器通讯过程 下面依照通信流程,我们来实现一个UDP回射客户/服务器: #include <sys/types.h> #include <sys/so ...

随机推荐

  1. C#文字转换语音朗读或保存MP3、WAV等格式

    最近遇到一个需求,需要把文字转换语音,参考很多大佬写的方法,最后经过自己改造实现文字在线朗读.保存MP3.WAV等格式. //需要引用System.Speech程序集 //引用using System ...

  2. 关于.NET Core 2.0.2升级到2.1.1版本相关问题

    之前,因日常任务管理比较混乱,所以自己开发了PTager任务管理系统. 当时用了.NET Core 2.0版本. 现在想修改相关功能,但.NET Core已发布到2.1.301了,也即2.1.1. 附 ...

  3. Fiddler4无法抓取HttpWebRequest本地请求的解决办法

    网上很多解决案例是如下方代码设置代理,但在我的Fiddler4环境下无效,后寻得官方处理方法证实与代理无关. HttpWebRequest request= WebRequest.Create(&qu ...

  4. Java面试题(全)--视频系列

    此系列为面试笔试题的视频讲解,以下均为超链接,点击即可进入每个知识点的讲解. Java面试题01.面试的整体流程 Java面试题02.java的垮平台原理 Java面试题03.搭建一个java的开发环 ...

  5. “全栈2019”Java异常第二十二章:try-with-resources语句详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...

  6. Struts2、SpringMVC、Servlet(Jsp)性能对比 测试

    Struts2.SpringMVC.Servlet(Jsp)性能对比 测试 . Servlet的性能应该是最好的,可以做为参考基准,其它测试都要向它看齐,参照它. 做为一个程序员,对于各个框架的性能要 ...

  7. iphone 自学常用网址

    https://www.gitbook.com/book/numbbbbb/-the-swift-programming-language-/details https://github.com/ip ...

  8. 如何在CentOS 7上使用vsftpd设置ftp服务器

    一.前言介绍 FTP(文件传输协议)是一种标准的客户机-服务器网络协议,允许用户在远程网络之间传输文件. 有几个开源的FTP服务器可用于Linux.最受欢迎和广泛使用的是pureftpd.proftp ...

  9. Solr7.4的学习与使用

    学习的原因: 17年的时候有学习使用过lucene和solr,但是后来也遗忘了,最近公司有个项目需要使用到全文检索,正好也顺便跟着学习一下,使用的版本是Solr7.4的,下载地址:http://arc ...

  10. 哈弗曼树的理解和实现(Java)

    哈弗曼树概述 哈弗曼树又称最优树,是一种带权路径长度最短的树,在实际中有广泛的用途.哈弗曼树的定义,涉及路径.路径长度.权等概念.哈弗曼树可以用于哈弗曼编码,用于压缩,用于密码学等. 哈弗曼树的一些定 ...