本文参考自:C# NTP时间同步类 - 逍遥子k - 博客园
NTP报文结构参考:https://wenku.baidu.com/view/90040c2bb868a98271fe910ef12d2af90342a84b.html
原理:
1.客户端向服务器发送一个报文请求(包含本机发出时间)
2.服务器回复一个报文(包含请求报文发出时间、服务器接收到报文时间、回复报文发出时间以及服务器的其它信息);
3.客户端记录收到回复的时间,然后通过报文信息计算客户端与服务器端的时间偏差
4.计算过程:发出时间A、接收时间B、回复时间C、收到回复时间D、报文从客户端到服务器传输时长L,客户端与服务器偏差时间值X,公式1:A+X+L=B,公式2:C-X+L=D,从公式1得出L=B-A-X,那么公式2可以是C-X+B-A-X=D,推导出2X=C-A+B-D=C-A+(B-D),偏差时间值X= ((C-A)+(B-D)) / 2
/// <summary>
/// NTP客户端类(请先调用Connect方法获取时间数据)
/// </summary>
public class NTPClient
{
/// <summary>
/// SNTP 数据结构长度
/// </summary>
private const byte SNTPDataLength = 48;
/// <summary>
/// SNTP 数据结构 (协议版本 RFC 2030)
/// </summary>
byte[] SNTPData = new byte[SNTPDataLength];
//数据结构中时间戳的偏移常量
/// <summary>
/// 参考标识符偏移
/// </summary>
private const byte offReferenceID = 12;
/// <summary>
/// 最后一次被设定或更新的时间戳偏移
/// </summary>
private const byte offReferenceTimestamp = 16;
/// <summary>
/// 客户端发出时间戳偏移
/// </summary>
private const byte offOriginateTimestamp = 24;
/// <summary>
/// 服务端接收时间戳偏移
/// </summary>
private const byte offReceiveTimestamp = 32;
/// <summary>
/// 服务端发出时间戳偏移
/// </summary>
private const byte offTransmitTimestamp = 40;
/// <summary>
/// Leap Indicator Warns of an impending leap second to be inserted/deleted in the last minute of the current day. 值为“3”时表示告警状态,时钟未被同步。为其他值时NTP本身不做处理
/// </summary>
public _LeapIndicator LeapIndicator
{
get
{
// 选取两个最高有效位
byte val = (byte)(SNTPData[0] >> 6);
switch (val)
{
case 0: return _LeapIndicator.NoWarning;
case 1: return _LeapIndicator.LastMinute61;
case 2: return _LeapIndicator.LastMinute59;
case 3: goto default;
default:
return _LeapIndicator.Alarm;
}
}
}
/// <summary>
/// Version Number Version number of the protocol (3 or 4) NTP的版本号
/// </summary>
public byte VersionNumber
{
get
{
// 选取 3 - 5位
byte val = (byte)((SNTPData[0] & 0x38) >> 3);
return val;
}
}
/// <summary>
/// Mode 长度为3比特,表示NTP的工作模式。不同的值所表示的含义分别是:0未定义、1表示主动对等体模式、2表示被动对等体模式、3表示客户模式、4表示服务器模式、5表示广播模式或组播模式、6表示此报文为NTP控制报文、7预留给内部使用
/// </summary>
public _Mode Mode
{
get
{
// 选取 6 - 8位
byte val = (byte)(SNTPData[0] & 0x7);
switch (val)
{
case 0:
return _Mode.Reserved;
case 1:
return _Mode.SymmetricActive;
case 2:
return _Mode.SymmetricPassive;
case 3:
return _Mode.Client;
case 4:
return _Mode.Server;
case 5:
return _Mode.Broadcast;
case 6:
return _Mode.ReservedForNTPcontrolMessages;
case 7:
return _Mode.ReservedForPrivateUse;
default:
return _Mode.Reserved;
}
}
}
/// <summary>
/// Stratum 系统时钟的层级等级
/// </summary>
public _Stratum Stratum
{
get
{
byte val = (byte)SNTPData[1];
if (val == 0) return _Stratum.Unspecified;
else
if (val == 1) return _Stratum.PrimaryReference;
else
if (val <= 15) return _Stratum.SecondaryReference;
else
return _Stratum.Reserved;
}
}
/// <summary>
/// 系统时钟的层级值,取值范围为1~16,它定义了时钟的准确度。层数为1的时钟准确度最高(0表示基准时钟源硬件),准确度从1到16依次递减,层数为16的时钟处于未同步状态,不能作为参考时钟
/// </summary>
public byte StratumValue
{
get
{
return SNTPData[1];
}
}
/// <summary>
/// Poll Interval (in seconds) Maximum interval between successive messages 轮询时间,即两个连续NTP报文之间的时间间隔
/// </summary>
public uint PollInterval
{
get
{
// Thanks to Jim Hollenhorst <hollenho@attbi.com>
return (uint)(Math.Pow(2, (sbyte)SNTPData[2]));
}
}
/// <summary>
/// Precision (in seconds) Precision of the clock 系统时钟的精度
/// </summary>
public double Precision
{
get
{
// Thanks to Jim Hollenhorst <hollenho@attbi.com>
return (Math.Pow(2, (sbyte)SNTPData[3]));
}
}
/// <summary>
/// Root Delay (in milliseconds) Round trip time to the primary reference source NTP服务器到主参考时钟的往返延迟
/// </summary>
public double RootDelay
{
get
{
int temp = 0;
temp = 256 * (256 * (256 * SNTPData[4] + SNTPData[5]) + SNTPData[6]) + SNTPData[7];
return 1000 * (((double)temp) / 0x10000);
}
}
/// <summary>
/// Root Dispersion (in milliseconds) Nominal error relative to the primary reference source NTP服务器相对于主参考时钟的最大误差
/// </summary>
public double RootDispersion
{
get
{
int temp = 0;
temp = 256 * (256 * (256 * SNTPData[8] + SNTPData[9]) + SNTPData[10]) + SNTPData[11];
return 1000 * (((double)temp) / 0x10000);
}
}
/// <summary>
/// 参考标识符(4个字符的字符串或IP地址)
/// </summary>
public string ReferenceID
{
get
{
string val = "";
switch (Stratum)
{
case _Stratum.Unspecified:
goto case _Stratum.PrimaryReference;
case _Stratum.PrimaryReference:
val += (char)SNTPData[offReferenceID + 0];
val += (char)SNTPData[offReferenceID + 1];
val += (char)SNTPData[offReferenceID + 2];
val += (char)SNTPData[offReferenceID + 3];
break;
case _Stratum.SecondaryReference:
switch (VersionNumber)
{
case 3: // Version 3, Reference ID is an IPv4 address系统时间的来源IP地址(也就是上一级时钟的IP)
string Address = SNTPData[offReferenceID + 0].ToString() + "." +
SNTPData[offReferenceID + 1].ToString() + "." +
SNTPData[offReferenceID + 2].ToString() + "." +
SNTPData[offReferenceID + 3].ToString();
try
{
IPHostEntry Host = Dns.GetHostEntry(Address);
val = Host.HostName + " (" + Address + ")";
}
catch (Exception)
{
val = "N/A" + " (" + Address + ")";
}
break;
case 4: // Version 4, Reference ID is the timestamp of last update
DateTime time = ComputeDate(GetMilliSeconds(offReferenceID));
// Take care of the time zone
TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
val = (time + offspan).ToString();
break;
default:
val = "N/A";
break;
}
break;
}
return val;
}
}
/// <summary>
/// Reference Timestamp The time at which the clock was last set or corrected NTP系统时钟最后一次被设定或更新的时间
/// </summary>
public DateTime ReferenceTimestamp
{
get
{
DateTime time = ComputeDate(GetMilliSeconds(offReferenceTimestamp));
// Take care of the time zone
TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
return time + offspan;
}
}
/// <summary>
/// Originate Timestamp (T1) The time at which the request departed the client for the server. 发送报文时的本机时间
/// </summary>
public DateTime OriginateTimestamp
{
get
{
return ComputeDate(GetMilliSeconds(offOriginateTimestamp));
}
}
/// <summary>
/// Receive Timestamp (T2) The time at which the request arrived at the server. 报文到达NTP服务器时的服务器时间
/// </summary>
public DateTime ReceiveTimestamp
{
get
{
DateTime time = ComputeDate(GetMilliSeconds(offReceiveTimestamp));
// Take care of the time zone
TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
return time + offspan;
}
}
/// <summary>
/// Transmit Timestamp (T3) The time at which the reply departed the server for client. 报文从NTP服务器离开时的服务器时间
/// </summary>
public DateTime TransmitTimestamp
{
get
{
DateTime time = ComputeDate(GetMilliSeconds(offTransmitTimestamp));
// Take care of the time zone
TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
return time + offspan;
}
set
{
SetDate(offTransmitTimestamp, value);
}
}
/// <summary>
/// Destination Timestamp (T4) The time at which the reply arrived at the client. 接收到来自NTP服务器返回报文时的本机时间
/// </summary>
public DateTime DestinationTimestamp;
/// <summary>
/// Round trip delay (in milliseconds) The time between the departure of request and arrival of reply 报文从本地到NTP服务器的往返时间
/// </summary>
public double RoundTripDelay
{
get
{
// Thanks to DNH <dnharris@csrlink.net>
TimeSpan span = (DestinationTimestamp - OriginateTimestamp) - (ReceiveTimestamp - TransmitTimestamp);
return span.TotalMilliseconds;
}
}
/// <summary>
/// Local clock offset (in milliseconds) The offset of the local clock relative to the primary reference source.本机相对于NTP服务器(主时钟)的时间差
/// </summary>
public double LocalClockOffset
{
get
{
// 基于往返数据的网络传输时间相同,Thanks to DNH <dnharris@csrlink.net>
TimeSpan span = (ReceiveTimestamp - OriginateTimestamp) + (TransmitTimestamp - DestinationTimestamp);
return span.TotalMilliseconds / 2;
}
}
/// <summary>
/// 计算日期,给定1900年1月1日以来的毫秒数
/// </summary>
/// <param name="milliseconds">毫秒数</param>
/// <returns></returns>
private DateTime ComputeDate(ulong milliseconds)
{
TimeSpan span = TimeSpan.FromMilliseconds((double)milliseconds);
DateTime time = new DateTime(1900, 1, 1);
time += span;
return time;
}
/// <summary>
/// Compute the number of milliseconds, given the offset of a 8-byte array.计算指定偏移量的毫秒数
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
private ulong GetMilliSeconds(byte offset)
{
ulong intpart = 0, fractpart = 0;
for (int i = 0; i <= 3; i++)
{
intpart = 256 * intpart + SNTPData[offset + i];
}
for (int i = 4; i <= 7; i++)
{
fractpart = 256 * fractpart + SNTPData[offset + i];
}
ulong milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L;
return milliseconds;
}
/// <summary>
/// Compute the 8-byte array, given the date.设置指定偏移量的时间值
/// </summary>
/// <param name="offset"></param>
/// <param name="date"></param>
private void SetDate(byte offset, DateTime date)
{
ulong intpart = 0, fractpart = 0;
DateTime StartOfCentury = new DateTime(1900, 1, 1, 0, 0, 0); // January 1, 1900 12:00 AM
ulong milliseconds = (ulong)(date - StartOfCentury).TotalMilliseconds;
intpart = milliseconds / 1000;
fractpart = ((milliseconds % 1000) * 0x100000000L) / 1000;
ulong temp = intpart;
for (int i = 3; i >= 0; i--)
{
SNTPData[offset + i] = (byte)(temp % 256);
temp = temp / 256;
}
temp = fractpart;
for (int i = 7; i >= 4; i--)
{
SNTPData[offset + i] = (byte)(temp % 256);
temp = temp / 256;
}
}
/// <summary>
/// 初始化客户端请求的数据
/// </summary>
private void Initialize()
{
// Set version number to 4 and Mode to 3 (client)
SNTPData[0] = 0x1B;
// Initialize all other fields with 0
for (int i = 1; i < 48; i++)
{
SNTPData[i] = 0;
}
// Initialize the transmit timestamp
TransmitTimestamp = DateTime.Now;
}
/// <summary>
/// 时间服务器的IP地址
/// </summary>
private IPAddress serverAddress = null;
/// <summary>
/// 通过主机名称构建对象
/// </summary>
/// <param name="host">时间服务器名称</param>
public NTPClient(string host)
{
//string host = "ntp1.aliyun.com";
//string host = "0.asia.pool.ntp.org";
//string host = "1.asia.pool.ntp.org";
//string host = "www.ntp.org/";
// Resolve server address
IPHostEntry hostadd = Dns.GetHostEntry(host);
foreach (IPAddress address in hostadd.AddressList)
{
if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) //只支持IPV4协议的IP地址
{
serverAddress = address;
break;
}
}
if (serverAddress == null)
throw new Exception("Can't get any ipaddress infomation");
}
/// <summary>
/// 通过IP地址构建对象
/// </summary>
/// <param name="address">时间服务器IP地址</param>
public NTPClient(IPAddress address)
{
if (address == null)
throw new Exception("Can't get any ipaddress infomation");
serverAddress = address;
}
/// <summary>
/// 连接时间服务器获取时间信息(每调用一次刷新一次信息)
/// </summary>
/// <param name="updateSystemTime">是否更新本地时间</param>
/// <param name="timeout">网络等待超时(毫秒)</param>
public void Connect(bool updateSystemTime, int timeout = 3000)
{
IPEndPoint EPhost = new IPEndPoint(serverAddress, 123);
//Connect the time server
using (System.Net.Sockets.UdpClient TimeSocket = new System.Net.Sockets.UdpClient())
{
TimeSocket.Connect(EPhost);
// Initialize data structure
Initialize();
TimeSocket.Send(SNTPData, SNTPData.Length);
TimeSocket.Client.ReceiveTimeout = timeout;
SNTPData = TimeSocket.Receive(ref EPhost);
if (!IsResponseValid)
throw new Exception("Invalid response from " + serverAddress.ToString());
}
DestinationTimestamp = DateTime.Now;
if (updateSystemTime)
SetTime();
}
/// <summary>
/// 检查服务器的响应是否有效
/// </summary>
/// <returns></returns>
public bool IsResponseValid
{
get
{
return !(SNTPData.Length < SNTPDataLength || Mode != _Mode.Server);
}
}
/// <summary>
/// 显示数据信息(已重写)
/// </summary>
/// <returns></returns>
public override string ToString()
{
StringBuilder sb = new StringBuilder(512);
sb.AppendFormat("标识符: {0}", ReferenceID.ToString().Replace("\0", string.Empty));
sb.Append("\r\n告警状态: ");
switch (LeapIndicator)
{
case _LeapIndicator.NoWarning:
sb.Append("没有告警");
break;
case _LeapIndicator.LastMinute61:
sb.Append("最后一分钟有61秒");
break;
case _LeapIndicator.LastMinute59:
sb.Append("最后一分钟有59秒");
break;
case _LeapIndicator.Alarm:
sb.Append("报警状态(时钟不同步)");
break;
}
sb.AppendFormat("\r\n版本号: {0}\r\n", VersionNumber);
sb.Append("模式: ");
switch (Mode)
{
case _Mode.Reserved:
sb.Append("保留");
break;
case _Mode.SymmetricActive:
sb.Append("Symmetric Active-主动对等体模式");
break;
case _Mode.SymmetricPassive:
sb.Append("Symmetric Pasive-被动对等体模式");
break;
case _Mode.Client:
sb.Append("Client-客户端模式");
break;
case _Mode.Server:
sb.Append("Server-服务器模式");
break;
case _Mode.Broadcast:
sb.Append("Broadcast-广播模式");
break;
case _Mode.ReservedForNTPcontrolMessages:
sb.Append("NTP控制报文");
break;
case _Mode.ReservedForPrivateUse:
sb.Append("内部使用预留");
break;
}
sb.Append("\r\nStratum-层阶等级: ");
switch (Stratum)
{
case _Stratum.Unspecified:
sb.Append("基准时钟硬件");
break;
case _Stratum.Reserved:
sb.Append("保留");
break;
case _Stratum.PrimaryReference:
sb.Append("主参考时钟");
break;
case _Stratum.SecondaryReference:
sb.Append("一般参考时钟");
break;
}
sb.AppendFormat(",层级: {0}", StratumValue);
sb.AppendFormat("\r\nNTP系统时钟精度: {0} s", Precision);
sb.AppendFormat("\r\nNTP系统时钟到主参考时钟的往返延迟: {0} ms", RootDelay);
sb.AppendFormat("\r\nNTP系统时钟相对于主参考时钟的最大误差: {0} ms", RootDispersion);
sb.AppendFormat("\r\nNTP系统时钟最后一次更新时间: {0:yyyy-MM-dd HH:mm:ss:fff}", ReferenceTimestamp);
sb.AppendFormat("\r\n轮询间隔: {0}", this.PollInterval.ToString());
sb.Append("\r\n");
sb.AppendFormat("\r\n报文开始时间: {0}", this.OriginateTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
sb.AppendFormat("\r\n报文服务端到达时间: {0}", this.ReceiveTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
sb.AppendFormat("\r\n报文服务端发出时间: {0}", this.TransmitTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
sb.AppendFormat("\r\n报文完成时间: {0}", this.DestinationTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
sb.AppendFormat("\r\n报文往返时间: {0} ms", RoundTripDelay);
sb.AppendFormat("\r\n本机相对于服务端的时间差: {0} ms", LocalClockOffset);
sb.Append("\r\n");
return sb.ToString();
}
/// <summary>
/// SYSTEMTIME structure used by SetSystemTime
/// </summary>
[StructLayoutAttribute(LayoutKind.Sequential)]
private struct SYSTEMTIME
{
public short year;
public short month;
public short dayOfWeek;
public short day;
public short hour;
public short minute;
public short second;
public short milliseconds;
}
[DllImport("kernel32.dll")]
static extern bool SetLocalTime(ref SYSTEMTIME time);
[DllImport("Kernel32.dll")]
static extern void GetLocalTime(ref SYSTEMTIME Time);
/// <summary>
/// Set system time according to transmit timestamp 校准本地时间
/// </summary>
public void SetTime()
{
SYSTEMTIME st;
DateTime trts = DateTime.Now.AddMilliseconds(LocalClockOffset);
st.year = (short)trts.Year;
st.month = (short)trts.Month;
st.dayOfWeek = (short)trts.DayOfWeek;
st.day = (short)trts.Day;
st.hour = (short)trts.Hour;
st.minute = (short)trts.Minute;
st.second = (short)trts.Second;
st.milliseconds = (short)trts.Millisecond;
SetLocalTime(ref st);
}
/// <summary>
/// 代码示例:获取NTP网络时间
/// </summary>
/// <returns></returns>
public static DateTime GetDateTime()
{
/*
* 中国科学院国家授时中心:ntp.ntsc.ac.cn [114.118.7.161或114.118.7.163] 层级1,网络延时40+
* 腾讯:ntp.tencent.com [139.199.215.251] 层级2,网络延时15(推荐)
* 阿里云:ntp.aliyun.com [203.107.6.88] 层级2,网络延时40+
*/
NTPClient ntpClient = new NTPClient("ntp.tencent.com");
ntpClient.Connect(false);
return DateTime.Now.AddMilliseconds(ntpClient.LocalClockOffset);
}
}
/// <summary>
/// 告警状态
/// </summary>
public enum _LeapIndicator
{
NoWarning, // 0 - No warning
LastMinute61, // 1 - Last minute has 61 seconds
LastMinute59, // 2 - Last minute has 59 seconds
Alarm // 3 - Alarm condition (clock not synchronized)
}
/// <summary>
/// 模式
/// </summary>
public enum _Mode
{
Reserved,//保留。
SymmetricActive,//主动对等体模式。
SymmetricPassive,//被动对等体模式。
Client,//客户端模式。
Server,//服务器模式。
Broadcast,//广播模式。
ReservedForNTPcontrolMessages,//NTP控制报文。
ReservedForPrivateUse,//内部使用预留。
}
/// <summary>
/// 层级越高离核心时钟源越近,最高为0表示基准时钟源
/// </summary>
public enum _Stratum
{
Unspecified, // 0 - 未指定或不可用,一般用硬件实现,例如原子钟(如铯、铷)、GPS时钟或其他无线电时钟。它们也被称为参考(基准)时钟
PrimaryReference, // 1 - 主参考时钟
SecondaryReference, // 2-15 - 一般参考时钟(通过NTP或SNTP)
Reserved // 16-255 - 保留
}