中国DOS联盟论坛

中国DOS联盟

-- 联合DOS 推动DOS 发展DOS --

联盟域名:www.cn-dos.net  论坛域名:www.cn-dos.net/forum
DOS,代表着自由开放与发展,我们努力起来,学习FreeDOS和Linux的自由开放与GNU精神,共同创造和发展美好的自由与GNU GPL世界吧!

游客:  注册 | 登录 | 命令行 | 会员 | 搜索 | 上传 | 帮助 »
中国DOS联盟论坛 » DOS开发编程 & 发展交流 (开发室) » [原创]如何与时间服务器进行时间同步
作者:
标题: [原创]如何与时间服务器进行时间同步 上一主题 | 下一主题
whowin
初级用户





积分 134
发帖 37
注册 2006-9-28
状态 离线
『楼 主』:  [原创]如何与时间服务器进行时间同步

文章是从我的网志中贴过来的,其中的图片可能过不来,看完整内容,请访问我的网志:
点击进入《DOS编程技术》

在互联网上发布精确时间,要依靠一个协议,叫NTP(Network Time Protocol)协议,中文一般称为网络时间协议,互联网上有许多时间服务器,负责提供精确时间。

1、NTP协议的基本原理

    假定时间服务器是A,我们的机器是B,则同步过程如下进行:

B向A发送一个消息包,记录发出消息包时的时间戳t1(以B机时间为基准)
A收到消息包立即记录时间戳t2(以A机时间为基准)
A向B返回一个消息包,返回消息包时记录时间戳t3(以A机时间为基准)
B收到A返回的消息包,此时记录时间戳t4(以B机时间为基准)
    t4和t1是以B的时间标准记录的时间戳,其差t4-t1表示整个消息传递过程所需要的时间间隔;t3和t2是以A机的时间标准记录的时间戳,其差t3-t2表明消息传递过程在A机逗留的时间,那么(t4-t1)-(t3-t2)应该就是信息包从B到A,再从A传回B的时间(中间刨去了在A机的逗留时间),如果假定信息包从A到B和从B到A所用的时间一样,那么,从A到B或者从B到A信息包的传送时间d为:

    d = ((t4 - t1) - (t3 - t2))/2

    假定B机相对于A机的时间误差是c,则有下列等式:

    t2 = t1 + c + d
    t4 = t3 - c + d

    从以上三个等式可以解出B机的时间误差c为:

    c = ((t2 - t1) + (t3 - t4)) / 2

    如果一时没有转过来,可以自己在纸上画个图,在细细地琢磨一下,应该没有问题。

2、简单网络时间协议SNTPv4(Simple Network Time Protocol version 4)

  SNTPv4由NTP改编而来,主要支持同步网络计算机时钟机制。SNTPv4没有改变NTP规范和原有实现过程,它是对NTP的进一步改进,支持以一种简单、无状态远程过程调用模式执行精确而可靠的操作,这类似于UDP/TIME 协议。

3、SNTP(NTP)的协议结构

                           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(Leap Indicator):闰秒指示,2 bits,bit 0和bit 1
表示是否警告在当天的最后一分钟插入/删除一个闰秒。通常填0表示不需要警告。
VN(Version Number):NTP/SNTP版本号,3 bits
这里我们填3,表示版本3(IPv4)
Mode:工作模式,3 bits,这里我们要填3,表示是客户端方式,时间服务器返回时将把这个字段改为4,表示是服务器工作模式
Stratum:表示所处的层,8 bits。这是因为NTP的网络体系是分层结构,按照离外部UTC(国际标准时间)源的远近将所有服务器归入不同的Stratum(层)中,Stratum-0层为官方时钟所保留,Stratum-1在顶层,有外部UTC接入,而Stratum-2则从Stratum-1获取时间,Stratum-3从Stratum-2获取时间,以此类推,但Stratum层的总数限制在15以内。所有这些服务器在逻辑上形成阶梯式的架构相互连接,而Stratum-1的时间服务器是整个系统的基础。
这个字段一般填0即可。

Poll Interval:表示连续信息间的最大间隔,8 bits,以2的x次幂秒的形式表示。实际填0即可。
Precision:时间精度,以2的负x次幂秒表示,8 bits。实际填0,服务器返回时会填写实际精度。
Root Delay:到主参考时间源的往返总延迟。32 bits固定小数点数,小数点在bit 15和bit 16之间。
Root Dispersion:相对于主参考时间源的正常离差。32 bits固定小数点数,小数点在bit 15和bit 16之间。
Reference Identifier:用于识别特殊的参考源。
Reference Timestamp:本地时中最后一次设置或修正时的时间,64bits,timestamp格式。
Originate Timestamp:前面原理部分说到的t1
Receive Timestamp:前面原理部分说到的t2
Transmit Timestamp:前面原理部分说到的t3
Authenticator(可选):可选项。一般不填。
    有对协议感兴趣的读者,可以在下面地址下载SNTP协议进行深入的研究。

    http://blog.hengch.com/specification/sntp.pdf

4、SNTP的实际实现

    说起来一大堆,但实现起来其实并不像说的那么复杂。

    根据SNTP协议第5节《SNTP Client Operations》的说明,如果采用unicast模式,其目的就是为了获得准确时间的话,向服务器发送的请求包中,除了第一个字节以外,其他的SNTP字段都可以设为0,这正好与我们的目的相符,下面我们给出实现本机与时间服务器同步的源程序。

    还有两个细节要说明,一是NTP实用的端口号是123,二是NTP的header中的字符顺序是big-endian,也就是Network Byte Order(详见另一篇博文:《在DOS下进行网络编程(下)》中的描述),而我们本机的字符顺序是little-endian,所以在收到服务器的回应后要注意转换。

    为方便说明,源程序前面加了行号。

  001  #include <stdio.h>
  002  #include <dos.h>
  003  #include <sys/socket.h>
  004  #include <arpa/inet.h>

  005  #define  SNTP_PORT       123
  006  #define  SNTP_SERVER     "210.72.145.44"     // China
  007  //#define  SNTP_SERVER     "192.43.244.18"     // NIST : sometimes ok
  008  //#define  SNTP_SERVER     "192.5.41.40"       // U.S Naval Observatory : ok
  009  //#define  SNTP_SERVER     "128.102.16.2"
  010  #define  SNTP_EPOCH        86400U * (365U * 70U + 17U)
  011  #define  SNTP_8HOUR        3600U * 8U

  012  struct Sntp_Header {
  013    unsigned char LiVnMode;
  014    unsigned char Stratum;
  015    unsigned char Poll;
  016    unsigned char Precision;
  017    long int RootDelay;
  018    long int RootDispersion;
  019    char RefID[4];
  020    long int RefTimeInt;
  021    long int RefTimeFraction;
  022    long int OriTimeInt;
  023    long int OriTimeFraction;
  024    long int RecvTimeInt;
  025    long int RecvTimeFraction;
  026    long int TranTimeInt;
  027    long int TranTimeFraction;
  028  };

  029  int main() {
  030    int retValue;
  031    fd_set readfds;
  032    // Structure of SNTP
  033    struct Sntp_Header sntpHeader;
  034    struct Sntp_Header sntpHeader1;
  035    struct Sntp_Header *p;
  036    char *p1;
  037    // vars of network
  038    int sendSock;
  039    struct sockaddr_in toAddr;
  040    int addrLen;
  041    char *pBuf;
  042    long int OriTimeInt;
  043    long int DestTimeInt;
  044    long int difference;
  045    unsigned char tempChar;

  046    struct timeval tv;


  047    struct date dateNow;
  048    struct time timeNow;

  049    sendSock = socket(AF_INET, SOCK_DGRAM, 0);


  050    if (sendSock < 0) {
  051      printf("\nsendSocket Creation Fail!");
  052      return -1;
  053    }


  054    toAddr.sin_family      = AF_INET;
  055    toAddr.sin_port        = htons(SNTP_PORT);
  056    toAddr.sin_addr.s_addr = inet_addr(SNTP_SERVER);
  057    bzero(&(toAddr.sin_zero), 8);
  058    addrLen = sizeof(struct sockaddr);

  059    bzero(&sntpHeader, sizeof(struct Sntp_Header));
  060    sntpHeader.LiVnMode = 0x1b;

  061    OriTimeInt = time(0) + SNTP_EPOCH - SNTP_8HOUR;
  062    retValue = sendto(sendSock, &sntpHeader, sizeof(struct Sntp_Header), 0,
                           (struct sockaddr *)&toAddr, addrLen);
  063    printf("\nSend %d chars.", retValue);
  064    p1 = (char *)&sntpHeader;
  065    pBuf = (char *)&sntpHeader1;

  066    printf("\n\tSent...\t\t\t\t\tReceived...");
  067    for (int j = 0; j < 12; j++) {
  068      FD_ZERO(&readfds);
  069      FD_SET(sendSock, &readfds);
  070      tv.tv_sec = 10;
  071      tv.tv_usec = 0;
  072      select(sendSock + 1, &readfds, NULL, NULL, &tv);
  073      if (FD_ISSET(sendSock, &readfds)) {
  074        retValue = recvfrom(sendSock,
                                 &pBuf[j * 4],
                                 4,
                                 0,
                                 (struct sockaddr *)&toAddr,
                                 &addrLen);
  075      } else {
  076        printf("\nDid not Get information from time server");
  077        return -1;
  078      }

  079      if (retValue <= 0 ) {
  080        printf("\nReceiving Fail");
  081        return -1;
  082      }
  083      printf("\n");
  084      for (int i = 0; i < retValue; i++) {
  085        printf("\t%02x", (unsigned char)p1[i + j * 4]);
  086      }
  087      printf("\t");
  088      for (int i = 0; i < retValue; i++) {
  089        printf("\t%02x", (unsigned char)pBuf[i + j * 4]);
  090      }
  091    }

  092    for (int j =  4; j < 12; j++) {
  093      tempChar = *(pBuf + j * 4);
  094      *(pBuf + j * 4) = *(pBuf + j * 4 + 3);
  095      *(pBuf + j * 4 + 3) = tempChar;
  096      tempChar = *(pBuf + j * 4 + 1);
  097      *(pBuf + j * 4 + 1) = *(pBuf + j * 4 + 2);
  098      *(pBuf + j * 4 + 2) = tempChar;
  099    }

  100    p = (struct Sntp_Header *)pBuf;

  101    DestTimeInt = time((time_t *)NULL) + SNTP_EPOCH - SNTP_8HOUR;
  102    printf("\nLocal TimeStamp = %lu", DestTimeInt);
  103    printf("\tRefTimeInt = %lu", p->RefTimeInt);
  104    printf("\nOriTimeInt = %lu", OriTimeInt);
  105    printf("\tRecvTimeInt = %lu", p->RecvTimeInt);
  106    printf("\nTranTimeInt = %lu", p->TranTimeInt);
  107    difference = (p->RecvTimeInt - OriTimeInt) + (p->TranTimeInt - DestTimeInt);
  108    difference = difference / 2;
  109    printf("\tdifference = %ld", difference);

  110    tv.tv_usec = 0;
  111    tv.tv_sec = time(0) + difference;
  112    getdate(&dateNow);
  113    gettime(&timeNow);
  114    printf("\nDate and Time Before adjusting : %04d-%02d-%02d %02d:%02d:%02d",
                dateNow.da_year, dateNow.da_mon, dateNow.da_day,
                timeNow.ti_hour, timeNow.ti_min, timeNow.ti_sec);
  115    retValue = settimeofday(&tv);
  116    if (retValue == 0) {
  117      printf("\nSetting Time ok!");
  118    } else {
  119      printf("\nSetting Time Fail!");
  120    }
  121    getdate(&dateNow);
  122    gettime(&timeNow);
  123    printf("\nDate and Time after adjusting : %04d-%02d-%02d %02d:%02d:%02d\n",
                dateNow.da_year, dateNow.da_mon, dateNow.da_day,
                timeNow.ti_hour, timeNow.ti_min, timeNow.ti_sec);
  124    return 0;
  125  }

     下面就程序的某些部分作一些说明。

第5行是NTP的端口号
第6行是我们要连接的时间服务器的IP地址,这是中科院国家授时中心的服务器,这几个时间服务器还是我在一年前某项目中使用NTP时找的时间服务器,在完成本篇文章之前,我又对这几台服务器做了测试,除第9行的服务器没有连接成功外,其余都没有问题。
第10行的这个常数SNTP_EPOCH是计算了1970年1月1日0点0分0秒到1900年1月1日0点0分0秒之间的秒数,其中86400是每天的秒数,365是一年的天数,70是1970年到1900年一共70年,17是在这70年中的闰年次数(每4年一次)。为什么要计算这个数呢?这是因为本机中的时间戳timestamp是以1970年1月1日为基准的,而NTP中的时间戳timestamp是以1900年1月1日为基准的,所以我们要使用这个常数进行转换。
第11行的常数SNTP_8HOUR是因为北京的时区是GMT+8,有8个小时的时差,而时间服务器发布的时间都是
国际标准时间,所以在转换时间戳时要用这个常数。
第12--28行定义的结构是SNTP的请求头(Request Header),发送信息包和接收信息包将以这个结构为基础。
进入主程序main后,第30--48行定义了一些变量和结构,其中结构包括:sntpHeader和sntpHeader1在前面已经有说明;sockaddr_in在上一篇博文《在DOS下进行网络编程(下)》中有说明;timeval、date和time请在rhide中查阅libc的在线文档,包括使用这几个结构的函数可以一起查;fd_set通常叫做文件描述符集,在本文中是为了解决网络编程的阻塞问题而使用的,关于fd_set、相关的宏以及select()的使用方法可以参考《Beej's Guide to Network Programming Using Internet Sockets》中的介绍,在以前的博文中介绍过如何获得这篇文章,或者以后有机会我写文章介绍。
49--58行的程序我们在上一篇博文《在DOS下进行网络编程(下)》中有说明。
59、60行,我们填了一个SNTP的请求头,只把第一个字节填上了0x1b,其余字节均为0,0x1b展开为二进制为:00011011b,根据字段定义,LI=00b(无警告),VN=011b(版本3),mode=011b(client模式)。
61行我们计算了发送时的时间戳,因为最后要用于计算本地时间的偏差,要与服务器返回的时间戳配合计算,所以我们给它转换成国际标准时间的时间戳。
62行,我们采用UDP方式将SNTP的请求头发出。
68--78行是一种无阻塞接收信息的方式,70、71行设定了一个10秒的超时,72行的select()最多等待10秒钟,73行检查是否有数据可读,如果有,才执行recvfrom()读取信息,这样就避开了阻塞得问题,有于UPD不会像TCP那样在通讯之前要进行connect(),所以,无法收到信息的可能性更大,如果使用阻塞得方式,程序锁死的可能性相对要大一些。
第67行的for语句告诉我们,我们并不是一下收取所有的信息,而是每次只收4个字节,共收12次,完成信息包的接收,这样做并没有什么特别的目的,只是为了打印数据方便。
第84--86行我们在屏幕上打印了我们发出的信息包,第88--90行,我们打印了服务器返回的信息包,在屏幕上非常直观,可以很方便地进行比较。
前面说过,服务器返回的数据字符顺序是big-endian,本机的字符顺序是little-endian,第92--99把收到的Receive Timestamp和Transmit Timestamp两个字段的字符顺序转换成little-endian。
101行计算了收到服务器返回包的时间戳。
107、108行根据前面描述的原理计算了本机时间的偏差。
102--109行之间有一些printf语句,打印了一些我们计算所需要的数据。
115行重新设置了本机的日期、时间,在设置前后分别打印了本机的时间。
    至此,程序解释完毕,该程序在DOS6.22,DJGPP v2,WATT-32下编译通过并运行良好。

    下一篇文章计划写如何在DOS对AC'97的声卡进行编程,会举一个实例,敬请期待。


更多关于DOS编程的文章看我的网志

点击进入《DOS编程技术》

[ Last edited by whowin on 2008-5-9 at 11:51 AM ]

2008-5-9 11:37
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复

请注意:您目前尚未注册或登录,请您注册登录以使用论坛的各项功能,例如发表和回复帖子等。


可打印版本 | 推荐给朋友 | 订阅主题 | 收藏主题



论坛跳转: