【linux】适用于linux环境的NTP客户端校时程序

本文介绍了如何使用C语言实现一个简单的NTP客户端,通过抓包和解析NTP数据包获取服务器时间,计算客户端与服务器之间的时钟偏移,并进行时间同步。
摘要由CSDN通过智能技术生成

参考文档简单的NTP客户端-C语言实现_c语言 实现ntp-CSDN博客

原文中代码较为混乱,且貌似不能运行。自己修改了一下,增加了一些注释,重写了获取服务器时间的部分。

原文通过将从服务器获取的buf数据直接转为结构体从中取数据,我没看懂怎么转换的(菜),所以直接抓包找到buf中相关数据的偏移量直接获取并转换为UTC时间戳。抓包参考数据包捕获!网络时间协议(NTP)讲解_ntp数据包-CSDN博客 具体换算参考NTP时间戳转换成UTC时间的过程-CSDN博客

抓包和时间获取截图:

懒得整理变量名,原文变量定义方法和我的不一样,我未进行统一,存在部分较为混乱的宏定义,指针未进行传入判空操作,部分变量未进行初始化。调试时近确认t1-t4以及最后print出的时间无问题,未确认是否进行校时。本代码只作为个人学习使用,如果读者想拿去交作业或者进行开发,记得把上述问题修改或确认一下。如有问题请评论,看到后尽量回复。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <iostream>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <endian.h>

#define VERSION_3           3
#define VERSION_4           4

#define MODE_CLIENT         3
#define MODE_SERVER         4

#define NTP_LI              0			//闰秒指示符
#define NTP_VN              VERSION_3   //版本号
#define NTP_MODE            MODE_CLIENT	//NTP模式
#define NTP_STRATUM         0			//本地时钟层级
#define NTP_POLL            4			//轮询间隔
#define NTP_PRECISION       -6			//本地时钟精度

#define NTP_HLEN            48

#define NTP_PORT            123
#define NTP_SERVER          "182.92.12.11"

#define TIMEOUT             10

#define BUFSIZE             1500

#define JAN_1970 2208988800 //自1900-1-1到1970-1-1经过的秒数

//用于64位时间戳,即小数部分为32位
//NTP_CONV_FRAC32(x)将x转换为NTP时间戳中的小数部分值
//NTP_REVE_FRAC32(x)将NTP时间戳中的小数部分值x解析成具体值
#define NTP_CONV_FRAC32(x)  (uint64_t) ((x) * ((uint64_t)1<<32))    
#define NTP_REVE_FRAC32(x)  ((double) ((double) (x) / ((uint64_t)1<<32)))   

//用于32位时间戳,即小数部分为16位
//NTP_CONV_FRAC16(x)将x转换为NTP时间戳中的小数部分值
//NTP_REVE_FRAC16(x)将NTP时间戳中的小数部分值x解析成具体值
#define NTP_CONV_FRAC16(x)  (uint32_t) ((x) * ((uint32_t)1<<16))    
#define NTP_REVE_FRAC16(x)  ((double)((double) (x) / ((uint32_t)1<<16)))    

//timeval结构中tv_usec字段和NTP时间戳小数部分互转
#define USEC2FRAC(x)        ((uint32_t) NTP_CONV_FRAC32( (x) / 1000000.0 )) 
#define FRAC2USEC(x)        ((uint32_t) NTP_REVE_FRAC32( (x) * 1000000.0 )) 

#define ZERO 0

struct s_fixedpt     //32位NTP时间戳
{	
    uint16_t    intpart;	//整数部分
    uint16_t    fracpart;	//小数部分
};

struct l_fixedpt //64位NTP时间戳
{	
    uint32_t    intpart;
    uint32_t    fracpart;
};

//此结构体按位定义字段,所以需要解决字节序问题
struct ntphdr 
{
#if __BYTE_ORDER == __BID_ENDIAN	//大端
    unsigned int    ntp_li : 2;
    unsigned int    ntp_vn : VERSION_3;
    unsigned int    ntp_mode : MODE_CLIENT;
#elif __BYTE_ORDER == __LITTLE_ENDIAN	//小端
    unsigned int    ntp_mode : MODE_CLIENT;
    unsigned int    ntp_vn : VERSION_3;
    unsigned int    ntp_li : 2;
#endif
    uint8_t         ntp_stratum = 0;	//本地时钟层级
    uint8_t         ntp_poll = 0;		//轮询间隔
    int8_t          ntp_precision = 0;	//时钟精度
    struct l_fixedpt    ntp_orits;	//客户端发送时间请求的时间
    struct l_fixedpt    ntp_recvts;	//服务器收到时间请求的时间
    struct l_fixedpt    ntp_transts;	//服务器发送时间回复的时间
};


in_addr_t inet_host(const char* host)
{
    in_addr_t saddr;
    struct hostent* hostent;

    if ((saddr = inet_addr(host)) == INADDR_NONE) {
        if ((hostent = gethostbyname(host)) == NULL)
            return INADDR_NONE;
		
        memmove(&saddr, hostent->h_addr, hostent->h_length);
    }
    return saddr;
}

//构建NTP请求报文
//入参:	buf:指向存放NTP报文的缓冲区
//		size:传入的是缓冲区长度,返回的是NTP报文长度
int get_ntp_packet(void* buf, struct timeval* orignTv, size_t* size)  
{
    struct ntphdr* ntp;

    if (!size || *size < NTP_HLEN)
        return -1;

    memset(buf, 0, *size);

    ntp = (struct ntphdr*)buf;
    ntp->ntp_li = NTP_LI;
    ntp->ntp_vn = NTP_VN;
    ntp->ntp_mode = NTP_MODE;
    ntp->ntp_stratum = NTP_STRATUM;
    ntp->ntp_poll = NTP_POLL;
    ntp->ntp_precision = NTP_PRECISION;

    gettimeofday(orignTv, NULL);  //把目前的时间用orignTv 结构体返回, 用作t1
	ntp->ntp_orits.intpart = htonl(orignTv->tv_sec + JAN_1970);	//将UTC时间戳改为NTP时间戳
	ntp->ntp_orits.fracpart = htonl(USEC2FRAC(orignTv->tv_usec));

    *size = NTP_HLEN;

    return 0;
}

//将L_fixedpt结构(64位时间戳)换算成自1970-1-1所经过的秒数
double lfixedToDouble(struct l_fixedpt time)
{
	double tempIntpart = 0;
	double tempFracpart = 0;
	tempIntpart = ntohl(time.intpart - JAN_1970);
	tempFracpart = ntohl(FRAC2USEC(time.fracpart));

	//此处直接使用NTP_REVE_FRAC32(tempFracpart)会损失10^-7数量级精度
	tempFracpart = FRAC2USEC(tempFracpart) / 1000000.0;
	return tempIntpart + tempFracpart;
}

void getRecvTimeFromNTP(char* buf, struct l_fixedpt* recvTime, struct l_fixedpt* transTime)
{
	double tempIntpart;
	double tempFracpart;
	tempIntpart = ntohl(*(int*)(&buf[32]));
	tempFracpart = ntohl(*(int*)(&buf[36]));
	recvTime->intpart = tempIntpart - JAN_1970;
	recvTime->fracpart = int (tempFracpart * 232 / 1000000.0);

	tempIntpart = ntohl(*(int*)(&buf[40]));
	tempFracpart = ntohl(*(int*)(&buf[44]));
	transTime->intpart = tempIntpart - JAN_1970;
	transTime->fracpart = int (tempFracpart * 232 / 1000000.0);
}

double get_offset(char* buf, struct timeval recvtv, struct timeval orignTv)  //本地时钟偏移量
{
	struct l_fixedpt recvTime;
	struct l_fixedpt transTime;
    double t1 = ZERO;
	double t2 = ZERO;
	double t3 = ZERO;
	double t4 = ZERO;

	getRecvTimeFromNTP(buf, &recvTime, &transTime);
	
	t1 = orignTv.tv_sec + orignTv.tv_usec / 1000000.0;	//客户端发送时间请求的时间
	t2 = recvTime.intpart + recvTime.fracpart / 1000000.0;	//服务器收到时间请求的时间
	t3 = transTime.intpart + transTime.fracpart / 1000000.0;	//服务器发送时间回复的时间
    t4 = recvtv.tv_sec + recvtv.tv_usec / 1000000.0;	//客户端收到时间回复的时间

    return ((t2 - t1) + (t3 - t4)) / 2;

}



int main()
{
    char dateBuf[64] = {0};
    char cmd[128] = {0};
    tm* local;
    char buf[BUFSIZE] = {0};
    size_t nbytes = ZERO;
    int sockfd = ZERO;
	int maxfd1 = ZERO;
    struct sockaddr_in servaddr;
    fd_set readfds;
    struct timeval timeout;	//超时时间
	struct timeval recvtv;
	struct timeval tv;		//1970.1.1至今偏移秒数和微秒数
	struct timeval orignTv; //客户端发送时间请求的时间
    double offset = ZERO;

    // if (argc != 2) {
        // usage();
        // exit(-1);
        
	//构建服务器地址套接字结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(NTP_PORT);
    servaddr.sin_addr.s_addr = inet_host("119.28.183.184");

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) 
	{
        perror("socket error");
        exit(-1);
    }

    if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)) != 0) 
	{
        perror("connect error");
        exit(-1);
    }

	//构建并发送NTP报文
    nbytes = BUFSIZE;
    if (get_ntp_packet(buf, &orignTv, &nbytes) != 0) 
	{
        fprintf(stderr, "construct ntp request error \n");
        exit(-1);
    }
    send(sockfd, buf, nbytes, 0);

    FD_ZERO(&readfds);	//清空fd_set集合	
    FD_SET(sockfd, &readfds);	//将给定的文件描述符加入到集合中
    maxfd1 = sockfd + 1;

	//设置select超时时间
    timeout.tv_sec = TIMEOUT;
    timeout.tv_usec = 0;

    if (select(maxfd1, &readfds, NULL, NULL, &timeout) > 0) 
	{
        if (FD_ISSET(sockfd, &readfds))
        {
            if ((nbytes = recv(sockfd, buf, BUFSIZE, 0)) < 0)
            {
                perror("recv error");
                exit(-1);
            }

            //接收到服务器数据后记录一次时间,用以计算C/S之间时间偏移量
            gettimeofday(&recvtv, NULL);
            offset = get_offset(buf, recvtv, orignTv);
            //再次系统时间,用以进行校时
            gettimeofday(&tv, NULL);	//1970.1.1至今偏移时间的秒数和微秒数
			
			//针对tv.tv_usec向上溢出的处理,并进行校时
            if(tv.tv_usec + (offset - (int)offset) * 1000000 >= 1000000)
            {
            	tv.tv_usec +=(offset - (int)offset) * 1000000 - 1000000;
				tv.tv_sec += (int)offset + 1;
            }
			else
			{
				tv.tv_usec +=(offset - (int)offset) * 1000000;
				tv.tv_sec += (int)offset;
			}
            local = localtime((time_t*)&tv.tv_sec);	//1970.1.1到目前的偏移秒数和微秒数变为本地时间

            strftime(dateBuf, 64, "%Y-%m-%d %H:%M:%S", local);	//格式化本地时间
            sprintf(cmd, "system busybox date -s \"%s\"", dateBuf);
            printf("%s \n", ctime((time_t*)&tv.tv_sec));

        }
    }
    close(sockfd);

    return 0;

}

  • 10
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值