目录
第1章通讯协议
通过访问时间服务器,就可以获得精准的时间。Windows7自带访问时间服务器的功能,如下图所示:
这里就存在一个问题了:如何才能获得时间服务器time.windows.com上的时间?这就需要了解时间服务器的通讯协议了。
1.1 RFC-868
使用RFC-868通讯协议,客户端获取时间的步骤为:
1、客户端使用TCP连接服务器的37号端口;
2、服务器给客户端发送一个时刻;
3、服务器立即断开TCP连接。及时断开TCP连接是为了更快的响应其他用户的时间询问请求。
这里关键的就是第2步——服务器给客户端返回的时刻格式。事实上,这个时间数据简单得就是一个4字节(32位)的无符号整数。如:
D7 C6 3D 77 |
这个整数0xD7C63D77(即高位在前,低位在后)表示1900年1月1日零时到当前UTC时刻的秒数。
1900年1月1日零时过1万秒后的UTC时刻是什么?这个有些困难,如果在Windows下编程,就可借助Windows API了。直接上VC++代码:
/**************************************************************** 转换时间:32位整数(1900年1月1日到当前时刻的秒数)==>年月日时分秒 uTime [in] 1900年1月1日到当前时刻的秒数 tmSys [out] 年月日时分秒 ****************************************************************/ void TIME_UINT32toSYSTEM(ULONG uTime,SYSTEMTIME&tmSys) { SYSTEMTIME tmSys0; memset(&tmSys0,0,sizeof(tmSys0)); tmSys0.wYear = 1900; tmSys0.wMonth = 1; tmSys0.wDay = 1; FILETIME tmFile; SystemTimeToFileTime(&tmSys0,&tmFile); *(unsigned __int64*)&tmFile += uTime * 10000000I64; FileTimeToSystemTime(&tmFile,&tmSys); } |
SystemTimeToFileTime(&tmSys0,&tmFile);将1900年1月1日0时0分0秒转换为FILETIME。FILETIME其实是一个8字节(64位)无符号整数,它表示1601年1月1日0时0分0秒到当前时刻的时间,单位:秒。
代码*(unsigned __int64*)&tmFile += uTime * 10000000I64将时间由1900年1月1日0时0分0秒向后增加了uTime * 10000000I64。乘以10000000I64是为了把单位由秒换算为秒。
代码FileTimeToSystemTime(&tmFile,&tmSys)把FILETIME转换为SYSTEMTIME,现在tmSys就是服务器发来的年月日时分秒格式的UTC时刻。
注意:32位无符号整数最大为0xFFFFFFFF,用它表示秒就意味着最多表示136年。也就是说:RFC-868通讯协议返回的时间数据用来表示1900年至2035年之间的时间是没有问题的,超出这个范围的时间就会有歧义。如:服务器返回时间0,它可以被解释为1900年,也可以被解释为2036年。
1.2 time/UDP
使用time/UDP通讯协议,客户端获取时间的步骤为:
1、客户端给服务器的37号端口发送UDP数据包,即请求时间的数据包(4字节的00H);
2、服务器获得请求时间的数据包后,会给客户端返回一个UDP数据包,该数据包里包含时刻。
这里关键的就是第2步——服务器给客户端返回的时刻格式。事实上,time/UDP协议获得的时刻数据包与RFC-868协议获得的时刻数据包格式完全相同。
注意:UDP通讯没有TCP通讯稳定,有可能会丢包。
1.3 NTP
RFC-868和time/UDP协议中,服务器发来的时刻都是通过网络的,一旦网络延时过大则客户端获得的服务器时刻将与实际会有较大的偏差。NTP可以缓解这一问题(并不能彻底解决)。
使用NTP通讯协议,客户端获取时间的步骤为:
1、客户端给服务器的123号端口发送UDP数据包,即请求时间的数据包;
2、服务器获得请求时间的数据包后,会给客户端返回一个UDP数据包,该数据包里包含时刻。
可见:NTP通讯协议与time/UDP通讯协议的执行步骤完全相同,所不同的仅仅是数据包格式。
1.3.1 客户端给服务器发送请求
涉及到的结构有两个,如下表所示:
#pragma pack(push,1) //NTP数据包里的时刻 struct NtpTime { public: unsigned long coarse; //时间(秒)的整数部分 unsigned long fine; //时间(秒)的小数部分 };
//NTP 数据包。长度为 48 字节 struct NtpPkg { char Flag; char PeerClockStratum; char PeerPollingInterval; char PeerClockPrecision; long RootDelay; unsigned long ClockDispersion; char ReferenceClock[4]; NtpTime ReferenceClockUpdateTime; NtpTime OriginateTimeStamp; NtpTime ReceiveTimeStamp; NtpTime TransmitTimeStamp; }; #pragma pack(pop) |
给服务器发送的数据包,其实就是一个NtpPkg。其发送代码如下
#define STRATUM 0 #define POLL 4 #define PREC -6
NtpPkg np; memset(&np,0,sizeof(np)); int LI = 0; //闰秒表示 int VN = 3; //版本号NTPv1请设置为1,NTPv2设置为2…… int Mode = 3; //3表示客户端发送给服务端 np.Flag = (LI << 6) | (VN << 3) | Mode; SYSTEMTIME tmSys; GetSystemTime(&tmSys); np.OriginateTimeStamp.coarse = TIME_SYSTEMtoUINT32(tmSys); np.PeerClockStratum = STRATUM; np.PeerPollingInterval = POLL; np.PeerClockPrecision = PREC; np.RootDelay = 1<<16; np.ClockDispersion = 1<<16; np.RootDelay = htonl(np.RootDelay); np.ClockDispersion = htonl(np.ClockDispersion); m_pSocketUdp->Write((const char*)&np,sizeof(np),...); |
说明:
1、最关键的代码就是m_pSocketUdp->Write((const char*)&np,sizeof(np),...);这行代码把结构NtpPkg的实例np发送给服务器;
2、NtpPkg里如下成员变量的字节数大于1,要特别注意字节顺序:
RootDelay、ClockDispersion、ReferenceClockUpdateTime、OriginateTimeStamp、ReceiveTimeStamp、TransmitTimeStamp
发送到网络或从网络接收时,它们都是高位在前,低位在后的。而Windows里,字节顺序恰恰相反。因此,发送结构前,需要调用htonl函数,把这些成员变量的字节顺序逆一下序。接收网络数据时,也需要调用ntohl函数再次逆序;
3、OriginateTimeStamp表示客户端发送请求时的本机时刻。这个不需要特别精确。它是NtpTime类型的数据,NtpTime其实是一个定点数,它表示1900年1月1日0时0分0秒至当前UTC时刻的秒数。coarse是秒数的整数部分,fine是秒数的小数部分。秒数的计算可按下式进行:
1.3.2 服务器给客户端的回复
服务器给客户端的回复也是一个NtpPkg结构。注意在Windows下,需要把RootDelay、ClockDispersion、ReferenceClockUpdateTime、OriginateTimeStamp、ReceiveTimeStamp、TransmitTimeStamp的字节顺序逆序。
比较重要的数据是ReceiveTimeStamp。如果客户端对时间的精确度要求不高,可以把它所代表的UTC时刻当做当前时刻,校准本机时间。如果客户端对时间的精确度要求很高,而且需要知道网络延迟时间,则需要进行如下的计算。
1.3.3 网络延时
做如下假定:
1、客户端发送数据包给服务器时,其本机时刻为T1;
2、服务器接收到数据包时的UTC时刻为T2,即NtpPkg里的ReceiveTimeStamp;
3、服务器发送数据包给客户端的UTC时刻为T3,即NtpPkg里的TransmitTimeStamp;
4、客户端接收到服务器传来的数据包时,本机时刻为T4;
5、客户端发送数据至服务器接收到数据,耗时为;
6、服务器发送数据至客户端接收到数据,耗时为;
7、客户端本机时刻与UTC时刻相差,即UTC时刻=本机时刻+。
根据上面的假定,可以列出两个方程:
说明:和用来把客户端本机时刻转换为UTC时刻。
上面有两个方程,但是却有三个未知数:。只有增加一个方程后才能求出未知数。现在,只能假定,即数据在客户端和服务端相互传输时其耗时相同。
求解如下方程组
可得
网络总延时
根据上面的数学公式,可以计算出网络延迟和客户端时间偏差。如下图所示:
客户端可以根据校准本机时间。