這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在網路編程做了一些研究之後,我邂逅了一篇題目為《Let's Make a NTP Client in C》,由 David Lettier(Lettier) 編寫的文章。這篇文章鼓舞了我用 Go 去做相似的事。> 這篇博文提到的代碼都在這裡 [https://github.com/vladimirvivien/go-ntp-client](https://github.com/vladimirvivien/go-ntp-client)。這篇博文描述了一個(真正的) NTP 用戶端的結構,使用 Go 語言編寫。它通過 encoding/binary 庫去封裝,解鎖裝,發送和接收來自遠端 NTP 伺服器基於 UDP 協議的 NTP 包。你能通過[這裡](http://www.ntp.org/)學到更多關於 NTP 協議的內容,或者閱讀 [RFC5905](https://tools.ietf.org/html/rfc5905) 規範、研究一個實現了更多的功能,(可能)比 Go NTP 用戶端更好的用戶端 [https://github.com/beevik/ntp](https://github.com/beevik/ntp)。## NTP 包結構時間同步的概念是非常複雜的,我還不能完全理解,也超過這篇博文的範圍。但幸運的是,NTP 使用的資料包格式很簡單,對於用戶端來說也小而足夠了。下面的圖展示了 NTP v4 的包格式。關於這篇博文,我們只關注前 48 個位元組,忽略掉 v4 版本的擴充部分。![NTP v4 data format (abbreviated) — https://tools.ietf.org/html/rfc5905](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-ntp/NTP-v4-data-format.png)NTP v4 data format (abbreviated) — https://tools.ietf.org/html/rfc5905## NTP 包用戶端和對應的服務端都使用上面提到的相同的包格式。下面的結構體定義了 NTP 包和它的屬性,跟上面提到的格式一一對應。```gotype packet struct {Settings uint8 // leap yr indicator, ver number, and modeStratum uint8 // stratum of local clockPoll int8 // poll exponentPrecision int8 // precision exponentRootDelay uint32 // root delayRootDispersion uint32 // root dispersionReferenceID uint32 // reference idRefTimeSec uint32 // reference timestamp secRefTimeFrac uint32 // reference timestamp fractionalOrigTimeSec uint32 // origin time secsOrigTimeFrac uint32 // origin time fractionalRxTimeSec uint32 // receive time secsRxTimeFrac uint32 // receive time fracTxTimeSec uint32 // transmit time secsTxTimeFrac uint32 // transmit time frac}```## 啟動 UDP 串連接下來,我們通過 UDP 協議,使用 net.Dial 函數去啟動一個 socket,與 NTP 伺服器聯絡,並設定 15 秒的逾時時間。```goconn, err := net.Dial("udp", host)if err != nil {log.Fatal("failed to connect:", err)}defer conn.Close()if err := conn.SetDeadline(time.Now().Add(15 * time.Second)); err != nil {log.Fatal("failed to set deadline: ", err)}```## 從服務端擷取時間在發送請求包給服務端前,第一個位元組是用來設定通訊的配置,我們這裡用 0x1B(或者二進位 00011011),代表用戶端模式為 3,NTP版本為 3,潤年為 0,如下所示:```go// configure request settings by specifying the first byte as// 00 011 011 (or 0x1B)// | | +-- client mode (3)// | + ----- version (3)// + -------- leap year indicator, 0 no warningreq := &packet{Settings: 0x1B}```接下來,我們使用 binary 庫去自動地將 packet 結構體封裝成位元組流,並以大端格式發送出去。```goif err := binary.Write(conn, binary.BigEndian, req); err != nil {log.Fatalf("failed to send request: %v", err)}```## 從服務端讀取時間接下來,我們使用 binary 包再次將從服務端讀取的位元組流自動地解鎖裝成對應的 packet 結構體。```gorsp := &packet{}if err := binary.Read(conn, binary.BigEndian, rsp); err != nil {log.Fatalf("failed to read server response: %v", err)}```## 解析時間在這個超普通的例子裡面,我們只對 Transmit Time 欄位 (rsp.TxTimeSec 和 rspTxTimeFrac) 感興趣,它們是從服務端發出時的時間。但我們不能直接使用它們,必須先轉成 Unix 時間。Unix 時間是一個開始於 1970 年的紀元(或者說從 1970 年開始的秒數)。然而 NTP 使用的是另外一個紀元,從 1900 年開始的秒數。因此,從 NTP 服務端擷取到的值要正確地轉成 Unix 時間必須減掉這 70 年間的秒數 (1970-1900),或者說 2208988800 秒。```goconst ntpEpochOffset = 2208988800...secs := float64(rsp.TxTimeSec) - ntpEpochOffsetnanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32```NTP 值的分數部分轉成納秒。在這個平凡的例子裡,這裡是可選的,展示只是為了完整性。## 顯示時間最後,函數 time.Unix 被用來建立一個秒數部分使用 secs,分數部分使用 nanos 值的時間。然後這個時間會被列印到終端。```gofmt.Printf("%v\n", time.Unix(int64(secs), nanos))```## 結論這篇博文展示了一個關於 NTP 用戶端的普通的例子。描述了如何利用 encoding/binary 庫,非常容易地將一個結構體轉成位元組形式。相反,我們使用 binary 庫將一個位元組流轉成對應的結構體值。這個 NTP 用戶端還不是一個可用於生產環境的產品,畢竟它缺少了 NTP 規範指定的很多功能。從服務端返回的大部分欄位都被忽略了。你可以從[這裡](https://github.com/beevik/ntp)擷取到一個用 Go 寫的更完整的 NTP 用戶端。
via: https://medium.com/learning-the-go-programming-language/lets-make-an-ntp-client-in-go-287c4b9a969f
作者:Vladimir Vivien 譯者:gogeof 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
584 次點擊 ∙ 2 贊