
5.1 概述
在NTP网络授时系统中,各子网中的路由器可从NTP服务器获取标准时间,并为其它终端用户提供授时服务。同时,其它终端用户也可以直接从NTP服务器获取标准时间。NTP网络授时系统的时钟同步软件,即部署在需通过网络获取精确时间的各windows终端上。时钟同步软件共包括三个功能模块:系统管理、定时器和时钟获取与校正。
系统管理模块,用于设置作为时钟参照的NTP服务器的地址、每次对时发送NTP报文的次数、对时间隔等参数。
定时器模块,用于定时或手工发起时钟同步请求。在通常状况下,系统按用户设置的对时间隔,自动与NTP服务器进行时间同步。在需要的时候,也可以由用户进行手工同步
时间获取与校正模块:用于根据系统指定的参数,构造NTP消息报文,从NTP服务器获取精确的标准时间。系统根据时钟同步的方式(手工或定时)本地时间,使系统内部各个节点的时钟保持一致。任意一个时钟同步客户端发起时钟同步消息,授时服务器返回系统时钟信息,客户端根据时钟信息调整本地时间。
5.2 时钟同步软件静态结构图
5.2.1 时钟同步软件整体结构
时钟同步软件中,定时同步与手工同步的流程基本相同,系统的整体结构如下图所示:
图5-1 时间同步的整体模型结构图
5.2.2 时钟同步软件的类图
分析时钟同步软件的结构,进一步细化系统中各种对象,以完成对系统中的类进行定义。
图5-2 时钟同步模块的类图
5.2.3 系统主要类设计
系统主要类的设计说明如下:
1. SYSTEMTIME类
表5-1 SYSTEMTIME类设计
| 类 | 表示系统时间的类 | |
| 属性 | 访问权限 | 描述 |
| wYear | public | 年 |
| wMonth | pulic | 月 |
| wDayOfWeek | pulic | 一周第几天 |
| wDay | public | 日 |
| wHour | public | 小时 |
| wMinute | pulic | 分 |
| wSecond | pulic | 秒 |
| wMillisecond | public | 毫秒 |
| 操作 | 参数 | 描述 |
| SYSTEMTIME | 构造函数 | |
表5-2 SynClient类设计
| 类 | 时钟同步客户端类 | |
| 属性 | 访问权限 | 描述 |
| offset | private | 时钟偏差 |
| maxoffset | private | 最大时钟偏差 |
| ServerTime | public | 标准时间 |
| BackTime | private | 报文回收时间 |
| 操作 | 参数 | 描述 |
| getoffset | long value | 获取时钟偏差 |
| getmaxoffset | 获取最大时钟偏差 | |
| setmaxoffset | long value | 设置最大时钟偏差 |
| getBackTime | 获取报文回收时间 | |
| setBackTime | SYSTEMTIME systemtime | 设置报文回收时间 |
表5-3 ConstructPacket类设计
| 类 | 构建NTP同步报文 | |
| 属性 | 访问权限 | 描述 |
| LI | public | LI标志 |
| VN | public | VN标志 |
| Mode | public | 模式 |
| Stratum | public | 层级 |
| Poll | public | 轮询间隔 |
| Precision | public | 精度 |
| RootDelay | public | 根延时 |
| RefTime | public | 服务器对时时间 |
| OriTime | private | 报文发送时间 |
| RecTime | private | 报文接收时间 |
| TransTime | private | 报文回复时间 |
| 操作 | 参数 | 描述 |
| ConstructPacket | 构造函数 | |
| getOriTime | 获取OriTime时间戳 | |
| setOriTime | long time | 设置OriTime时间戳 |
| getRecTime | 获取RecTime时间戳 | |
| setRecTime | long time | 设置RecTime时间戳 |
| getTransTime | 获取TransTime时间戳 | |
| setTransTime | long time | 设置TransTime时间戳 |
表5-4 SynTask类设计
| 类 | 时间同步任务类 | |
| 属性 | 访问权限 | 描述 |
| Address | private | 服务器地址 |
| Interval | private | 同步间隔 |
| 操作 | 参数 | 描述 |
| SynTask | 构造函数 | |
| getAddress | 获取服务器地址 | |
| setAddress | IPAddress ip | 设置服务器地址 |
| getInterval | 获取同步间隔 | |
| setInterval | long para | 设置同步间隔 |
| Check | 时钟检查 | |
| Run | 发起时钟同步请求 | |
表5-5 SynServer类设计
| 类 | 同步通信接口类 | |
| 属性 | 访问权限 | 描述 |
| sk | public | socket接口 |
| packet | private | 同步报文 |
| 操作 | 参数 | 描述 |
| SynServer | 构造函数 | |
| getpacket | 获取报文 | |
| setpacket | ConstructPacket pk | 设置报文 |
| sendpacket | ConstructPacket pk, socket sk | 发送报文 |
| recvpacket | ConstructPacket pk, socket sk | 接收报文 |
5.3.1 时钟同步算法
时钟同步最主要的问题是解决同步消息传输延迟的计算问题。由于本系统采用单服务器的方式,解决这一问题的办法,是通过时钟同步的客户端向授时服务器多次发送带有时间戳的同步消息报文,并接收授时服务器返回的时钟同步消息报文,计算并记录每次的时间延迟,并选取值最小的一次记录,用此次的标准时间来校正本地时钟[28]。整个过程如下图所示:
图5-3 时钟同步的活动图
5.3.2 定时同步
定时同步,是指客户端按系统设置的同步时间间隔,定时向授时服务器发送时钟同步请求,保证本地时钟与标准时间保持一致,过程如下:
1. 定时时间到,定时器触发发送时钟同步请求;
2. 时钟同步客户端SynClient向指定的授时服务器发送时钟同步消息报文;
3. 授时服务器接收时钟同步消息报文,将接收时间和响应时间封装在报文中,发回客户端;
4. 客户端接收返回报文,计算出本地时钟与标准时间的偏差,校正本地时钟。
一次定时同步的时序图与协作图如下:
图5-4 定时同步时序图
图5-5 定时同步的协作图
5.3.3 手工同步
手工同步,是指由用户人工触发时钟同步事件,其过程与定时同步基本相同,事件流描述如下:
1. 用户发送时钟同步请求;
2. 时钟同步的客户端向授时服务器发送时钟同步消息报文;
3. 授时服务器接收时钟同步消息报文,将接收时间和响应时间封装在报文中,发回客户端;
4. 客户端接收返回报文,计算出本地时钟与标准时间的偏差,校正本地时钟。
5. 向用户反馈同步信息。
手工同步的时序图与协作图如下:
图5-6 手工同步时序图
图5-7 手工同步的协作图
5.4 时间获取与校正模块的实现
5.4.1 总体实现
时间获取与校正模块是时钟同步软件的核心,其主要任务是向网络中的NTP服务器发送时钟同步请求并接收服务器返回的带有时间戳的NTP报文,计算网络延时和时间偏差,修正本地主机的系统时间。时间获取与校正的主要流程如下所示。
图5-8 时间获取与校正流程图
5.4.2 时钟获取与校正的关键技术点
时钟同步模块与授时模块的运行环境不同,部署在windows操作系统上,在此用C#编程实现。
其实现的要点有:
1. NTP报文的封装
根据NTP报文的格式,客户端首选构建一个NTP报文,其主要字段的设置如下:
表5-1 客户端NTP报文时间戳设置
| 字段名称 | 值 |
| LI | 00 |
| VN | 3 |
| Mode | 3 |
| Stratum | 2 |
| Poll | 忽略 |
| Precision | 忽略 |
| Root Delay | 忽略 |
| 字段名称 | 值 |
| Root Dispersion | 忽略 |
| Reference Identifier | 忽略 |
| Reference Timestamp | 忽略 |
| Originate Timestamp | 忽略 |
| Receive Timestamp | 忽略 |
| Transmit Timestamp | 取报文发送时间 |
| Authenticator | 可选 |
DateTime t1900 = Convert.ToDateTime("1900-01-01 00:00:00");
long tick1900 = t1900.Ticks;
long tick = DateTime.Now.Ticks;
long s1900 = ((tick - tick1900)-36000000000*8)/10000000;
long s1900s = ((tick - tick1900) - 36000000000 * 8) % 10000000;
byte[ ] temp = BitConverter.GetBytes(s1900);
for (int j = 0; j < 4; j++)
{
Packet[40 + j ] = temp[3-j ];
}
temp = BitConverter.GetBytes(s1900s);
for (int k = 4; k < 8; k++)
{
Packet[40 + k] = temp[7 - k];
| } |
2. 与服务器间的socket通讯
网络编程要用到的两个命名空间[29]是System.Net和System.Net.Sockets。System.Net命名空间通常与较高层的操作有关,例如 download或upload,试用HTTP和其他协议进行Web请求等等,而System.Net.Sockets命名空间所包含的类通常与较低层的操作有关。如果要直接使用Sockets或者TCP/IP之类的协议,这个命名空间的类是非常有用的。
在.Net中,System.Net.Sockets 命名空间为需要严密控制网络访问的开发人员提供了 Windows Sockets (Winsock) 接口的托管实现。System.Net 命名空间中的所有其他网络访问类都建立在该套接字Socket实现之上。在应用程序端或者服务器端创建了Socket对象之后,就可以使用Send/SentTo方法将数据发送到连接的Socket,或者使用Receive/ReceiveFrom方法接收来自连接Socket的数据。
针对Socket编程,.NET 框架的Socket 类是 Winsock32 API 提供的套接字服务的托管代码版本[30]。其中为实现网络编程提供了大量的方法,大多数情况下,Socket 类方法只是将数据封送到它们的本机 Win32 副本中并处理任何必要的安全检查。
在使用之前,你需要首先创建Socket对象的实例,这可以通过Socket类的构造方法来实现:
| Public Socket(AddressFamily addressFamily,SocketType socketType,ProtocolType protocolType); |
在本系统中,NTP客户端使用UDP方式与服务器通信,所以在创建Socket实例时,用的是Socket的无连接的数据报形式:
| Socket temp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); |
1.创建一个Socket对象;
2.将创建的Socket对象与本地IPEndPoint进行绑定。
经过这两步,创建的Socket对象就能够在IPEndPoint上接收流入的UDP数据包,或者将流出的UDP数据包发送到网络中任意其他设备上。使用UDP通信时,不能使用标准的Send()和Receive()方法,须使用另外的SendTo()和ReceiveFrom()方法。
在客户端使用Socket通信,因为客户机不需要在指定的UDP端口等待流入的数据,因此,不必要使用Bind()方法,而是使用在数据发送时系统随机指定的一个UDP端口,也使用同一个端口来接收返回的消息。UDP客户机程序首选定义一个IPEndPoint,UDP服务器将发送数据包到这个IPEndPoint。本系统客户端Socket通信功能的代码片断如下:
try
{
myIP = IPAddress.Parse(TBip.Text);
}
catch { MessageBox.Show("IP地址不正确,请重新输入!"); }
try
{
BTsend.Enabled = true;
myServer = new IPEndPoint(myIP, 123);
//创建一个Socket实例sk
sk = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
}
catch (Exception ee) { MessageBox.Show(ee.Message); }
//构造NTP消息包
ConstructPacket pk = new ConstructPacket();
data = pk.Packet;
//发送NTP消息包
sk.SendTo(data, data.Length, SocketFlags.None, myServer);
//接收服务器返回的NTP消息包
sender = new IPEndPoint(IPAddress.Any, 0);
EndPoint Remote = (EndPoint)sender;
byte[] rdata = new byte[256];
| int recv = sk.ReceiveFrom(rdata, ref Remote); |
3. 本地时钟的获取与设置
在网络授时系统的客户端,发起时钟同步请求时,首先要获取本地时钟,在收到服务器返回的NTP报文后,要计算出本地时钟与标准时钟的偏差,然后对本地时钟进行校正,将其时间设置为标准时间。
本系统在 .NET架构的C#环境下开发,对本地时间的获取和设置分别采用了两种不同的方式。
1.获取本地时钟,可以直接创建一个DateTime类的实例,然后用DateTime.Now属性,即可得到本地时间。但是本系统需要的是UTC时间,所以不必直接获取本地时间,而是通过DateTime对象的Ticks属性来获取时间差,并通过必要时间基准和时区的转换,得到NTP报文所需要的UTC时间。这段功能代码在上面Transimit Timestamp时间戳的获取代码中已经包含,在此不再重复。
2.设置本地时钟。在C#中,与时间相关的类中没有合适的直接设置本地时间的方法,本系统设置时间通过调用Win32 API的SetSystemTime函数来实现。
首先,必须定义对SetSystemTime函数的调用声明:
[DllImport("kernel32.dll")]
| private extern static uint SetSystemTime(ref SYSTEMTIME lpSystemTime); |
private struct SYSTEMTIME
{
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
| } |
private void SetTime()
{
SYSTEMTIME systime = new SYSTEMTIME();
//将标准时间赋值到systime对象
……代码略
// 设置本地时间
SetSystemTime(ref systime);
| } |
1. 系统管理和参数设置
为了便于时间同步软件相关参数的设定,如NTP服务器地址、同步时间间隔、每次同步发送的数据包个数等等,必须提供用户与计算机进行交互的手段。本系统通过一个图形用户界面,用户可以方便地调整各项参数的值。系统管理的界面如下图。
图5-9 系统管理界面
2. 定时器
定时器平时在系统后台运行,当到达系统指定的时间间隔时,向时钟同步软件发起同步请求。用户也可以通过定时器的菜单,手工发起时钟同步请求,或者调用系统管理模块进行系统参数的设置。定时器的实现界面如下图。
图5-10 定时器的实现界面
