使用Go語(yǔ)言編寫(xiě)一個(gè)NTP服務(wù)器的流程步驟
NTP服務(wù)介紹
NTP服務(wù)器【Network Time Protocol(NTP)】是用來(lái)使計(jì)算機(jī)時(shí)間同步化的一種協(xié)議。
- 應(yīng)用場(chǎng)景說(shuō)明
為了確保封閉局域網(wǎng)內(nèi)多個(gè)服務(wù)器的時(shí)間同步,我們計(jì)劃部署一個(gè)網(wǎng)絡(luò)時(shí)間同步服務(wù)器(NTP服務(wù)器)。這一角色將由一臺(tái)個(gè)人筆記本電腦承擔(dān),該筆記本將連接到局域網(wǎng)中,并以其當(dāng)前時(shí)間為基準(zhǔn)。我們將利用這臺(tái)筆記本電腦作為NTP服務(wù)器,對(duì)局域網(wǎng)內(nèi)的多個(gè)運(yùn)行CentOS 8的服務(wù)器進(jìn)行時(shí)間校準(zhǔn),以保證系統(tǒng)時(shí)間的一致性和準(zhǔn)確性。
NTP協(xié)議
- NTP通信協(xié)議的傳輸層協(xié)議是UDP
- NTP通信協(xié)議的應(yīng)用層協(xié)議是NTP
NTP報(bào)文說(shuō)明
- NTP的報(bào)文是48字節(jié)
- 第1個(gè)字節(jié)可以理解為簡(jiǎn)易的報(bào)文頭,這8個(gè)bit包含Leap Indicator、NTP Version、Mode
a> LI 占用2個(gè)bit
b> VN 占用3個(gè)bit,筆者編寫(xiě)的服務(wù)器設(shè)置為版本v4.0
c> Mode 占用3個(gè)bit,ntp server時(shí)為4,ntp client時(shí)為3 - 第2個(gè)字節(jié)為 Peer Clock Stratum
- 第3個(gè)字節(jié)為 Peer Polling Interval
- 第4個(gè)字節(jié)為 Peer Clock Precision
- 第5 - 8字節(jié)為 Root Delay
- 第9 - 12字節(jié)為 Root Dispersion
- 第13- 16字節(jié)為 Reference Identifier
- 第17 - 24字節(jié)為 Reference Timestamp 參考時(shí)間戳
- 第25 - 32字節(jié)為 Originate Timestamp 起始時(shí)間戳
- 第33 - 40字節(jié)為 Receive Timestamp 接收時(shí)間戳
- 第 41 - 48字節(jié) Transmit Timestamp 傳輸時(shí)間戳
根據(jù)NTP報(bào)文編碼實(shí)現(xiàn)Go語(yǔ)言的結(jié)構(gòu)體
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 // 報(bào)文頭: 包含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 參考時(shí)間戳
OrigTS uint64 // Originate Timestamp 起始時(shí)間戳
RecvTS uint64 // Receive Timestamp 接收時(shí)間戳
TransTS uint64 // Transmit Timestamp 傳輸時(shí)間戳
}
NTP服務(wù)器的源碼
- ntpsrv.go
package main
import (
hldlog "NTPServer/log4go"
"encoding/binary"
"fmt"
"log"
"net"
"sync"
"time"
)
const (
STANDARD_PACKET_SIZE = 48 // 標(biāo)準(zhǔn)NTP的報(bào)文大小
)
type NTPServer struct {
srvAddress string
conn *net.UDPConn
wait sync.WaitGroup
ntpPack NtpPacket // NTP協(xié)議報(bào)文
requestCount uint64 // 請(qǐng)求計(jì)數(shù)
}
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 // 報(bào)文頭: 包含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 參考時(shí)間戳
OrigTS uint64 // Originate Timestamp 起始時(shí)間戳
RecvTS uint64 // Receive Timestamp 接收時(shí)間戳
TransTS uint64 // Transmit Timestamp 傳輸時(shí)間戳
}
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
// 創(chuàng)建新的NtpPacket實(shí)例
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時(shí)間轉(zhuǎn)換為NTP時(shí)間
func ToNTPTime(t time.Time) uint64 {
seconds := uint32(t.Unix()) + 2208988800 // NTP時(shí)間從1900年開(kāi)始計(jì)算
fraction := uint32(float64(t.Nanosecond()) * (1 << 32) / 1e9)
return uint64(seconds)<<32 | uint64(fraction)
}
func NewNTPServer(srvAddr string) *NTPServer {
return &NTPServer{srvAddress: srvAddr}
}
// 啟動(dòng)NTP服務(wù)器
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
}
// 關(guān)閉NTP服務(wù)器
func (srv *NTPServer) Stop() {
srv.conn.Close()
srv.wait.Wait()
}
// 接收數(shù)據(jù)
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客戶(hù)端消息的時(shí)間
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 將字節(jié)數(shù)組轉(zhuǎn)換為16進(jìn)制字符串
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] = ' ' // 每個(gè)16進(jìn)制數(shù)據(jù)之間加空格
}
}
return string(hexString)
}
func ParseUDPPacket(buf []byte) (*NtpPacket, error) {
if len(buf) < STANDARD_PACKET_SIZE { // 最小有效長(zhǎng)度為48字節(jié)
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日志庫(kù)
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)
}
}
- 代碼細(xì)節(jié)說(shuō)明
NTP服務(wù)器在回復(fù)NTP客戶(hù)端的消息中其中OrigTS uint64(Originate Timestamp 起始時(shí)間戳)必須是NTP客戶(hù)端發(fā)送來(lái)的TransTS uint64(Transmit Timestamp 傳輸時(shí)間戳)。
驗(yàn)證GoNTPSrv
上述實(shí)現(xiàn)的NTP服務(wù)已經(jīng)過(guò)Go語(yǔ)言中開(kāi)源的NTP Client庫(kù) https://github.com/beevik/ntp 驗(yàn)證。

- UDP數(shù)據(jù)包
# 客戶(hù)端發(fā)送的數(shù)據(jù) 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 # 服務(wù)器返回的數(shù)據(jù) 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的簡(jiǎn)單源碼
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)
}
}
到此這篇關(guān)于使用Go語(yǔ)言編寫(xiě)一個(gè)NTP服務(wù)器的流程步驟的文章就介紹到這了,更多相關(guān)Go編寫(xiě)NTP服務(wù)器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
源碼剖析Golang中map擴(kuò)容底層的實(shí)現(xiàn)
之前的文章詳細(xì)介紹過(guò)Go切片和map的基本使用,以及切片的擴(kuò)容機(jī)制。本文針對(duì)map的擴(kuò)容,會(huì)從源碼的角度全面的剖析一下map擴(kuò)容的底層實(shí)現(xiàn),需要的可以參考一下2023-03-03
go語(yǔ)言中json數(shù)據(jù)的讀取和寫(xiě)出操作
這篇文章主要介紹了go語(yǔ)言中json數(shù)據(jù)的讀取和寫(xiě)出操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
淺析在Go語(yǔ)言中如何實(shí)現(xiàn)協(xié)程池
gammazero/workerpool?就是用來(lái)實(shí)現(xiàn)協(xié)程池的?Go?包,本文我們將一起來(lái)學(xué)習(xí)一下其使用方法,并深入其源碼來(lái)探究下如何實(shí)現(xiàn)一個(gè)?Go?協(xié)程池,需要的可以了解下2025-06-06
淺析Go語(yǔ)言如何避免數(shù)據(jù)競(jìng)爭(zhēng)Data?Race和競(jìng)態(tài)條件Race?Condition
在并發(fā)編程中,數(shù)據(jù)競(jìng)爭(zhēng)?(Data?Race)?和?競(jìng)態(tài)條件?(Race?Condition)?是兩個(gè)常見(jiàn)的問(wèn)題,本文將簡(jiǎn)單介紹一下二者如何避免,有需要的可以了解下2025-01-01

