使用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客戶端消息的時(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客戶端的消息中其中OrigTS uint64(Originate Timestamp 起始時(shí)間戳)必須是NTP客戶端發(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ù)包
# 客戶端發(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中的數(shù)組(array)、切片(slice)和map
這篇文章主要介紹了理解Golang中的數(shù)組(array)、切片(slice)和map,本文先是給出代碼,然后一一分解,并給出一張內(nèi)圖加深理解,需要的朋友可以參考下2014-10-10golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)示例代碼
在?Go?語(yǔ)言中,您可以使用?os/exec?包來(lái)執(zhí)行外部命令,不通過(guò)調(diào)用?shell,并且能夠獲得進(jìn)程的退出碼、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤輸出,下面給大家分享golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)的方法,感興趣的朋友跟隨小編一起看看吧2024-06-06golang使用iconv報(bào)undefined:XXX的問(wèn)題處理方案
這篇文章主要介紹了golang使用iconv報(bào)undefined:XXX的問(wèn)題處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03golang 定時(shí)任務(wù)方面time.Sleep和time.Tick的優(yōu)劣對(duì)比分析
這篇文章主要介紹了golang 定時(shí)任務(wù)方面time.Sleep和time.Tick的優(yōu)劣對(duì)比分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05