当前位置: 首页 > article >正文

使用Go语言编写一个简单的NTP服务器

NTP服务介绍

NTP服务器【Network Time Protocol(NTP)】是用来使计算机时间同步化的一种协议。

  • 应用场景说明
    为了确保封闭局域网内多个服务器的时间同步,我们计划部署一个网络时间同步服务器(NTP服务器)。这一角色将由一台个人笔记本电脑承担,该笔记本将连接到局域网中,并以其当前时间为基准。我们将利用这台笔记本电脑作为NTP服务器,对局域网内的多个运行CentOS 8的服务器进行时间校准,以保证系统时间的一致性和准确性。

NTP协议

  • NTP通信协议的传输层协议是UDP
  • NTP通信协议的应用层协议是NTP

NTP报文说明

  • NTP的报文是48字节
  1. 第1个字节可以理解为简易的报文头,这8个bit包含Leap Indicator、NTP Version、Mode
    a> LI 占用2个bit
    b> VN 占用3个bit,笔者编写的服务器设置为版本v4.0
    c> Mode 占用3个bit,ntp server时为4,ntp client时为3
  2. 第2个字节为 Peer Clock Stratum
  3. 第3个字节为 Peer Polling Interval
  4. 第4个字节为 Peer Clock Precision
  5. 第5 - 8字节为 Root Delay
  6. 第9 - 12字节为 Root Dispersion
  7. 第13- 16字节为 Reference Identifier
  8. 第17 - 24字节为 Reference Timestamp 参考时间戳
  9. 第25 - 32字节为 Originate Timestamp 起始时间戳
  10. 第33 - 40字节为 Receive Timestamp 接收时间戳
  11. 第 41 - 48字节 Transmit Timestamp 传输时间戳

根据NTP报文编码实现Go语言的结构体

type NtpPacket struct {
	/*
		LI: 2bit      00   Leap Indicator(0)
		VN: 3bit      100  NTP Version(4)
		Mode: 3bit    100  Mode: server(4), client(3)
	*/
	Header    uint8 // 报文头: 包含LI、VN、Mode
	Stratum   uint8 // Peer Clock Stratum: primary reference (1)
	Poll      uint8 // Peer Polling Interval: invalid (0)
	Precision uint8 // Peer Clock Precision: 0.000000 seconds

	RootDelay uint32 // Root Delay
	RootDisp  uint32 // Root Dispersion
	RefID     uint32 // Reference Identifier

	RefTS   uint64 // Reference Timestamp 参考时间戳
	OrigTS  uint64 // Originate Timestamp 起始时间戳
	RecvTS  uint64 // Receive Timestamp   接收时间戳
	TransTS uint64 // Transmit Timestamp  传输时间戳
}

NTP服务器的源码

  • ntpsrv.go
package main

import (
	hldlog "NTPServer/log4go"
	"encoding/binary"
	"fmt"
	"log"
	"net"
	"sync"
	"time"
)

const (
	STANDARD_PACKET_SIZE = 48 // 标准NTP的报文大小
)

type NTPServer struct {
	srvAddress string

	conn *net.UDPConn
	wait sync.WaitGroup

	ntpPack      NtpPacket // NTP协议报文
	requestCount uint64    // 请求计数
}

type NtpPacket struct {
	/*
		LI: 2bit      00   Leap Indicator(0)
		VN: 3bit      100  NTP Version(4)
		Mode: 3bit    100  Mode: server(4), client(3)
	*/
	Header    uint8 // 报文头: 包含LI、VN、Mode
	Stratum   uint8 // Peer Clock Stratum: primary reference (1)
	Poll      uint8 // Peer Polling Interval: invalid (0)
	Precision uint8 // Peer Clock Precision: 0.000000 seconds

	RootDelay uint32 // Root Delay
	RootDisp  uint32 // Root Dispersion
	RefID     uint32 // Reference Identifier

	RefTS   uint64 // Reference Timestamp 参考时间戳
	OrigTS  uint64 // Originate Timestamp 起始时间戳
	RecvTS  uint64 // Receive Timestamp   接收时间戳
	TransTS uint64 // Transmit Timestamp  传输时间戳
}

func (srv *NTPServer) NewNtpPacket() *NtpPacket {
	// 初始化Header字段
	header := uint8(0)
	header |= (0 << 6) // LI: 2bit 00
	header |= (4 << 3) // VN: 3bit 100
	header |= (4 << 0) // Mode: 3bit 100

	// 创建新的NtpPacket实例
	packet := &NtpPacket{
		Header:    header,
		Stratum:   0x01,
		Poll:      0x00,
		Precision: 0x00,
		RootDelay: 0,
		RootDisp:  0,
		RefID:     0,
		RefTS:     0,
		OrigTS:    0,
		RecvTS:    0,
		TransTS:   0,
	}

	return packet
}

func (pack *NtpPacket) SetTimestamp(timestamp time.Time, field string) {
	ntpTime := ToNTPTime(timestamp)
	switch field {
	case "RefTS":
		pack.RefTS = ntpTime
	case "OrigTS":
		pack.OrigTS = ntpTime
	case "RecvTS":
		pack.RecvTS = ntpTime
	case "TransTS":
		pack.TransTS = ntpTime
	}
}

// toNTPTime 将Unix时间转换为NTP时间
func ToNTPTime(t time.Time) uint64 {
	seconds := uint32(t.Unix()) + 2208988800 // NTP时间从1900年开始计算
	fraction := uint32(float64(t.Nanosecond()) * (1 << 32) / 1e9)
	return uint64(seconds)<<32 | uint64(fraction)
}

func NewNTPServer(srvAddr string) *NTPServer {
	return &NTPServer{srvAddress: srvAddr}
}

// 启动NTP服务器
func (srv *NTPServer) Start() error {
	addr, err := net.ResolveUDPAddr("udp", srv.srvAddress)
	if err != nil {
		return err
	}
	hldlog.Info(fmt.Sprintf("<%s:%d>", addr.IP.String(), addr.Port))

	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		return err
	}

	srv.wait.Add(1)
	srv.conn = conn

	go RecvMsg(srv)

	return nil
}

// 关闭NTP服务器
func (srv *NTPServer) Stop() {
	srv.conn.Close()
	srv.wait.Wait()
}

// 接收数据
func RecvMsg(srv *NTPServer) {
	defer srv.wait.Done()
	buffer := make([]byte, 2*1024)

	for {
		n, remoteAddr, err := srv.conn.ReadFromUDP(buffer[0:])
		if err != nil {
			fmt.Println("ReadFromUDP error:", err)
			return
		}
		hldlog.Info(fmt.Sprintf("[Recv] %d bytes from <%s>", n, remoteAddr.String()))
		if n != STANDARD_PACKET_SIZE {
			continue
		}

		// 接收到NTP客户端消息的时间
		recvMsgTime := time.Now().UTC()

		recvHexString := BytesToHex(buffer[:n])
		hldlog.Info(fmt.Sprintf("[Recv] %s", recvHexString))

		udpPacket, err := ParseUDPPacket(buffer[:n])
		if err != nil {
			log.Printf("Error parsing UDP packet: %v", err)
			continue
		}
		ntpPack := srv.NewNtpPacket()
		ntpPack.SetTimestamp(time.Now().UTC(), "RefTS")
		ntpPack.OrigTS = udpPacket.TransTS
		ntpPack.SetTimestamp(recvMsgTime, "RecvTS")
		ntpPack.SetTimestamp(time.Now().UTC(), "TransTS")

		sendPacket := ntpPack.Serialize()

		sendLen, err := srv.conn.WriteToUDP(sendPacket, remoteAddr)
		if err != nil {
			log.Println(err.Error())
			continue
		}

		if sendLen > 0 {
			hldlog.Info(fmt.Sprintf("[Send] %s", BytesToHex(sendPacket)))
		}

		srv.requestCount++
	}
}

func (pack *NtpPacket) Serialize() []byte {
	packet := make([]byte, 48)

	// binary.BigEndian.PutUint32(packet[0:4], pack.Header)
	packet[0] = pack.Header
	packet[1] = pack.Stratum
	packet[2] = pack.Poll
	packet[3] = pack.Precision
	binary.BigEndian.PutUint32(packet[4:8], pack.RootDelay)
	binary.BigEndian.PutUint32(packet[8:12], pack.RootDisp)
	binary.BigEndian.PutUint32(packet[12:16], pack.RefID)
	binary.BigEndian.PutUint64(packet[16:24], pack.RefTS)
	binary.BigEndian.PutUint64(packet[24:32], pack.OrigTS)
	binary.BigEndian.PutUint64(packet[32:40], pack.RecvTS)
	binary.BigEndian.PutUint64(packet[40:48], pack.TransTS)

	return packet
}

// BytesToHex 将字节数组转换为16进制字符串
func BytesToHex(data []byte) string {
	hexString := make([]byte, 3*len(data)-1)
	for i, b := range data {
		high := "0123456789ABCDEF"[(b >> 4)]
		low := "0123456789ABCDEF"[(b & 0x0F)]
		hexString[i*3] = high
		hexString[i*3+1] = low
		if i < len(data)-1 {
			hexString[i*3+2] = ' ' // 每个16进制数据之间加空格
		}
	}
	return string(hexString)
}

func ParseUDPPacket(buf []byte) (*NtpPacket, error) {
	if len(buf) < STANDARD_PACKET_SIZE { // 最小有效长度为48字节
		return nil, fmt.Errorf("Invalid UDP packet length: %d", len(buf))
	}

	packet := &NtpPacket{
		// Header:    binary.BigEndian.Uint32(buf[0:4]),
		Header:    buf[0],
		Stratum:   buf[1],
		Poll:      buf[2],
		Precision: buf[3],
		RootDelay: binary.BigEndian.Uint32(buf[4:8]),
		RootDisp:  binary.BigEndian.Uint32(buf[8:12]),
		RefID:     binary.BigEndian.Uint32(buf[12:16]),
		RefTS:     binary.BigEndian.Uint64(buf[16:24]),
		OrigTS:    binary.BigEndian.Uint64(buf[24:32]),
		RecvTS:    binary.BigEndian.Uint64(buf[32:40]),
		TransTS:   binary.BigEndian.Uint64(buf[40:48]),
	}

	return packet, nil
}
  • main.go
package main

import (
	hldlog "NTPServer/log4go"
	"fmt"
	"gopkg.in/ini.v1"
	"time"
)

type NetAddr struct {
	IP   string
	Port string
}

var LocalHost = NetAddr{IP: "0.0.0.0", Port: "60123"}

func loadConfig() (NetAddr, error) {
	// 读取INI配置文件
	iniConf, err := ini.Load("./config/config.ini")
	if err != nil {
		hldlog.Error(fmt.Sprintf("Fail to read INI file: %v", err))
		return LocalHost, nil
	}

	iniSection := iniConf.Section("LocalHost")
	return NetAddr{
		IP:   iniSection.Key("ip").String(),
		Port: iniSection.Key("port").String(),
	}, nil
}

// 初始化log4go日志库
func init() {
	hldlog.LoadConfiguration("./config/log.xml", "xml")
}

func main() {
	hldlog.Info("===NTP SERVER Start(48 Bytes)===")

	LocalHost, err := loadConfig()
	if err != nil {
		hldlog.Error(fmt.Sprintf("Failed to load configuration: %v", err))
	}

	ntpSrv := NewNTPServer(fmt.Sprintf("%s:%s", LocalHost.IP, LocalHost.Port))
	ntpSrv.Start()

	for {
		time.Sleep(60 * time.Second)
	}
}
  • 代码细节说明
    NTP服务器在回复NTP客户端的消息中其中OrigTS uint64(Originate Timestamp 起始时间戳)必须是NTP客户端发送来的TransTS uint64(Transmit Timestamp 传输时间戳)。

验证GoNTPSrv

上述实现的NTP服务已经过Go语言中开源的NTP Client库 https://github.com/beevik/ntp 验证。

在这里插入图片描述

  • UDP数据包
# 客户端发送的数据
23 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 39 1C 79 9E 83 D3 D5 82

# 服务器返回的数据
24 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EA D9 CF EE AD 4D DC 2B 39 1C 79 9E 83 D3 D5 82 EA D9 CF EE AD 4D DC 2B EA D9 CF EE AD 4D DC 2B
  • NTP Client的简单源码
package main

import (
	hldlog "NTPCli/log4go"
	"fmt"
	"log"
	"os"
	"os/exec"
	"time"

	"github.com/beevik/ntp"
	"gopkg.in/ini.v1"
)

type NetAddr struct {
	IP   string
	Port int
}

var RemoteAddr = NetAddr{IP: "0.0.0.0", Port: 60123}

func init() {
	hldlog.LoadConfiguration("./config/log.xml", "xml")
}

func main() {
	hldlog.Info("===NTP CLIENT Start===")

	currTime := time.Now()
	formattedTime := currTime.Format("2006-01-02 15:04:05.000")
	hldlog.Info(formattedTime)

	// 读取INI配置文件
	iniConf, err := ini.Load("./config/config.ini")
	if err != nil {
		log.Fatalf("Fail to read INI file: %v", err)
	}

	remoteSection := iniConf.Section("NTP_SERVER")
	RemoteAddr.IP = remoteSection.Key("ip").String()
	RemoteAddr.Port, _ = remoteSection.Key("port").Int()
	hldlog.Info(fmt.Sprintf("ntp://%s:%d", RemoteAddr.IP, RemoteAddr.Port))

	// edu.ntp.org.cn
	// resp, err := ntp.Time("edu.ntp.org.cn")
	resp, err := ntp.Time(fmt.Sprintf("%s:%d", RemoteAddr.IP, RemoteAddr.Port))
	if err != nil {
		hldlog.Error(fmt.Sprintf("%v", err))
		os.Exit(-1)
	}
	hldlog.Info(resp.String())

	localTime := resp.Local()
	hldlog.Info(localTime.Format("2006-01-02 15:04:05.000"))

	// setTime(localTime)

	for {
		time.Sleep(60 * time.Second)
	}
}

http://www.kler.cn/a/388443.html

相关文章:

  • 【 CSS 】sass 扩展语言的安装
  • git设置项目远程仓库指向github的一个仓库
  • 从测试服务器手动热部署到生产环境的实现
  • linux命令中cp命令-rf与-a的差别
  • 大恒相机开发(2)—Python软触发调用采集图像
  • 【Rust自学】6.3. 控制流运算符-match
  • 《重学Java设计模式》之 建造者模式
  • 第三十八章 章节练习之面经页面
  • (一)<江科大STM32>——软件环境搭建+新建工程步骤
  • conda 启动时添加执行脚本
  • 优化布线拥塞
  • vue/react做多语言国际化的时候,在语言配置中不同的语言配置不同的字体,动态引入scss里面
  • spring gateway 动态路由
  • Python pandas库:强大的数据处理工具
  • NoSQL大数据存储技术测试(2)NoSQL数据库的基本原理
  • DU模拟器(S5040A Open RAN Studio Player and Capture Appliance)
  • OSS和FastDFS的详细比较
  • 宝塔面板中使用Acme SSL.cn申请的免费HTTPS SSL证书安装步骤
  • 如何进行数据库连接池的参数优化?
  • 默认 iOS 设置使已锁定的 iPhone 容易受到攻击
  • 2024-11-01 - 统一身份认证 - OpenLdap - 中间件 - 流雨声
  • 【SAP FICO】财务三大报表_2-进阶(杜邦分析法、资产负债表-数据表结构、取数逻辑)
  • DOM事件监听 (鼠标事件,键盘事件,表单事件)
  • 【.Net Core/.Net8教程】巧用 C# 8.0 切片语法:高效处理数组和字符串
  • 【微服务】Docker 容器化
  • 两个matlab在线编译网站