最近发现手机的时间不是很准了,便到网上下了一个同步时间的小程序,简单了看了一下它的原理,是通过NTP协议来实现校时的,就顺便学习了一下NTP协议,用C#写了个简单的实现。
NTP(Network Time Protocol,网络时间协议)是由RFC 1305定义的时间同步协议,用来在分布式时间服务器和客户端之间进行时间同步。
NTP的基本工作原理如下图所示。Device A和Device B通过网络相连,它们都有自己独立的系统时钟,需要通过NTP实现各自系统时钟的自动同步。为便于理解,作如下假设:
至此,Device A已经拥有足够的信息来计算两个重要的参数:
NTP有两种不同类型的报文,一种是时钟同步报文,另一种是控制报文(仅用于需要网络管理的场合,与本文无关,这里不做介绍)。
NTP基于UDP报文进行传输,使用的UDP端口号为123;时钟同步报文封装在UDP报文中,其格式如下图所示。
主要字段的解释如下:
有了上述基础知识后,我们就可以实现自己的时间同步工具了,下文附了一个简单的C#的实现。
class NptClient
{
IPAddress ntpServer;
public NptClient(IPAddress ntpServer)
{
this.ntpServer = ntpServer;
}
public DateTime GetServerTime()
{
var startTime = DateTime.Now;
var ntpTime = NTPData.Test(ntpServer);
var recvTime = DateTime.Now;
var offset = ((ntpTime.ReceiveTimestamp - startTime) + (ntpTime.TransmitTimestamp - recvTime));
offset = offset.Subtract(TimeSpan.FromSeconds(offset.TotalSeconds / 2));
return recvTime + offset;
}
}
[StructLayout(LayoutKind.Sequential)]
class NTPData
{
byte header = 0;
byte Stratum = 1; //系统时钟的层数,取值范围为1~16,它定义了时钟的准确度
byte Poll = 1; //轮询时间,即两个连续NTP报文之间的时间间隔
byte Precision = 1; //系统时钟的精度
BigEndianUInt32 rootDelay;
BigEndianUInt32 referenceIdentifier;
BigEndianUInt32 ReferenceIdentifier;
public NtpTime ReferenceTimestamp { get; private set; }
public NtpTime OriginateTimestamp { get; private set; }
public NtpTime ReceiveTimestamp { get; private set; }
public NtpTime TransmitTimestamp { get; private set; }
public NTPData()
{
this.header = GetHeader();
}
byte GetHeader()
{
var LI = "00";
var VN = "011"; //NTP的版本号为3
var Mode = "011"; //客户模式
return Convert.ToByte(LI + VN + Mode, 2);
}
public static NTPData Test(IPAddress ntpServer)
{
var data = MarshalExtend.GetData(new NTPData());
var udp = new System.Net.Sockets.UdpClient();
udp.Send(data, data.Length, new IPEndPoint(ntpServer, 123));
var ep = new IPEndPoint(IPAddress.Any, 0);
var replyData = udp.Receive(ref ep);
return MarshalExtend.GetStruct<NTPData>(replyData, replyData.Length);
}
}
[StructLayout(LayoutKind.Sequential)]
class NtpTime
{
BigEndianUInt32 seconds;
BigEndianUInt32 fraction;
static readonly DateTime baseTime = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static implicit operator DateTime(NtpTime time)
{
/* rfc1305的ntp时间中,时间是用64bit来表示的,记录的是1900年后的秒数(utc格式)
* 高32位是整数部分,低32位是小数部分 */
var milliseconds = (int)(((double)time.fraction / uint.MaxValue) * 1000);
return baseTime.AddSeconds(time.seconds).AddMilliseconds(milliseconds).ToLocalTime();
}
public override string ToString()
{
return ((DateTime)this).ToString("o");
}
}
当然,我这里只是在造重复轮子,网上是有不少功能完整的开源项目的。另外,如果对SNTPv4(RFC 2030)感兴趣的,可以参考一下这个页面上的实现——Simple Network Time (NTP) Protocol Client。
最后,附上几个可以使用(不保证,具体能用否还得看电信和方校长的心情)的NTP服务器: