使用Go语言编写一个简单的NTP服务器
NTP服务介绍
NTP服务器【Network Time Protocol(NTP)】是用来使计算机时间同步化的一种协议。
- 应用场景说明
为了确保封闭局域网内多个服务器的时间同步,我们计划部署一个网络时间同步服务器(NTP服务器)。这一角色将由一台个人笔记本电脑承担,该笔记本将连接到局域网中,并以其当前时间为基准。我们将利用这台笔记本电脑作为NTP服务器,对局域网内的多个运行CentOS 8的服务器进行时间校准,以保证系统时间的一致性和准确性。
NTP协议
- NTP通信协议的传输层协议是UDP
- NTP通信协议的应用层协议是NTP
NTP报文说明
- NTP的报文是48字节
- 第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个字节为 Peer Clock Stratum
- 第3个字节为 Peer Polling Interval
- 第4个字节为 Peer Clock Precision
- 第5 - 8字节为 Root Delay
- 第9 - 12字节为 Root Dispersion
- 第13- 16字节为 Reference Identifier
- 第17 - 24字节为 Reference Timestamp 参考时间戳
- 第25 - 32字节为 Originate Timestamp 起始时间戳
- 第33 - 40字节为 Receive Timestamp 接收时间戳
- 第 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、ModeStratum uint8 // Peer Clock Stratum: primary reference (1)Poll uint8 // Peer Polling Interval: invalid (0)Precision uint8 // Peer Clock Precision: 0.000000 secondsRootDelay uint32 // Root DelayRootDisp uint32 // Root DispersionRefID uint32 // Reference IdentifierRefTS uint64 // Reference Timestamp 参考时间戳OrigTS uint64 // Originate Timestamp 起始时间戳RecvTS uint64 // Receive Timestamp 接收时间戳TransTS uint64 // Transmit Timestamp 传输时间戳
}
NTP服务器的源码
- ntpsrv.go
package mainimport (hldlog "NTPServer/log4go""encoding/binary""fmt""log""net""sync""time"
)const (STANDARD_PACKET_SIZE = 48 // 标准NTP的报文大小
)type NTPServer struct {srvAddress stringconn *net.UDPConnwait sync.WaitGroupntpPack 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、ModeStratum uint8 // Peer Clock Stratum: primary reference (1)Poll uint8 // Peer Polling Interval: invalid (0)Precision uint8 // Peer Clock Precision: 0.000000 secondsRootDelay uint32 // Root DelayRootDisp uint32 // Root DispersionRefID uint32 // Reference IdentifierRefTS 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 00header |= (4 << 3) // VN: 3bit 100header |= (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 = ntpTimecase "OrigTS":pack.OrigTS = ntpTimecase "RecvTS":pack.RecvTS = ntpTimecase "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 = conngo 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.TransTSntpPack.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.Headerpacket[1] = pack.Stratumpacket[2] = pack.Pollpacket[3] = pack.Precisionbinary.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] = highhexString[i*3+1] = lowif 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 mainimport (hldlog "NTPServer/log4go""fmt""gopkg.in/ini.v1""time"
)type NetAddr struct {IP stringPort 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 mainimport (hldlog "NTPCli/log4go""fmt""log""os""os/exec""time""github.com/beevik/ntp""gopkg.in/ini.v1"
)type NetAddr struct {IP stringPort 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)}
}
相关文章:
使用Go语言编写一个简单的NTP服务器
NTP服务介绍 NTP服务器【Network Time Protocol(NTP)】是用来使计算机时间同步化的一种协议。 应用场景说明 为了确保封闭局域网内多个服务器的时间同步,我们计划部署一个网络时间同步服务器(NTP服务器)。这一角色将…...
注意力机制篇 | YOLO11改进 | 即插即用的高效多尺度注意力模块EMA
前言:Hello大家好,我是小哥谈。与传统的注意力机制相比,多尺度注意力机制引入了多个尺度的注意力权重,让模型能够更好地理解和处理复杂数据。这种机制通过在不同尺度上捕捉输入数据的特征,让模型同时关注局部细节和全局…...
昇思大模型平台打卡体验活动:项目3基于MindSpore的GPT2文本摘要
昇思大模型平台打卡体验活动:项目3基于MindSpore的GPT2文本摘要 1. 环境设置 本项目可以沿用前两个项目的相关环境设置。首先,登陆昇思大模型平台,并进入对应的开发环境: https://xihe.mindspore.cn/my/clouddev 接着࿰…...
web——[GXYCTF2019]Ping Ping Ping1——过滤和绕过
0x00 考点 0、命令联合执行 ; 前面的执行完执行后面的 | 管道符,上一条命令的输出,作为下一条命令的参数(显示后面的执行结果) || 当前面的执行出错时(为假)执行后面的 & 将任…...
婚礼纪 9.5.57 | 解锁plus权益的全能结婚助手,一键生成结婚请柬
婚礼纪是一款结婚服务全能助手,深受9000万新人信赖的一站式结婚服务平台。解锁plus权益后,用户可以享受部分VIP会员功能。应用提供了丰富的结婚筹备工具和服务,包括一键生成结婚请柬、婚礼策划、婚纱摄影、婚宴预订等。婚礼纪旨在为新人提供全…...
M1M2 MAC安装windows11 虚拟机的全过程
M1/M2 MAC安装windows11 虚拟机的全过程 这两天折腾了一下windows11 arm架构的虚拟机,将途中遇到的坑总结一下。 1、虚拟机软件:vmware fusion 13.6 或者 parallel 19 ? 结论是:用parellel 19。 这两个软件都安装过࿰…...
监控架构-Prometheus-普罗米修斯
目录 1. Prometheus概述 2. Prometheus vs Zabbix 3. Prometheus极速上手指南 3.1 时间同步 3.2 部署Prometheus 3.3 启动Prometheus 3.4 Prometheus监控架构 3.5 补充 配置页面 简单过滤 查看数据 查看图形 http://prometheus.oldboylinux.cn:9090/metrics显示…...
Kylin Server V10 下自动安装并配置Kafka
Kafka是一个分布式的、分区的、多副本的消息发布-订阅系统,它提供了类似于JMS的特性,但在设计上完全不同,它具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性,适用于离线和在线的消息消费,如常规的消息收集、…...
windows环境下cmd窗口打开就进入到对应目录,一般人都不知道~
前言 很久以前,我还在上一家公司的时候,有一次我看到我同事打开cmd窗口的方式,瞬间把我惊呆了。原来他打开cmd窗口的方式,不是一般的在开始里面输入cmd,然后打开cmd窗口。而是另外一种方式。 我这个同事是个技术控&a…...
企微SCRM价格解析及其性价比分析
内容概要 在如今的数字化时代,企业对于客户关系管理的需求日益增长,而企微SCRM(Social Customer Relationship Management)作为一款新兴的客户管理工具,正好满足了这一需求。本文旨在为大家深入解析企微SCRM的价格体系…...
【SpringMVC】记录一次Bug——mvc:resources设置静态资源不过滤导致WEB-INF下的资源无法访问
SpringMVC 记录一次bug 其实都是小毛病,但是为了以后再出毛病,记录一下: mvc:resources设置静态资源不过滤问题 SpringMVC中配置的核心Servlet——DispatcherServlet,为了可以拦截到所有的请求(JSP页面除外…...
【React】React 生命周期完全指南
🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 React 生命周期完全指南一、生命周期概述二、生命周期的三个阶段2.1 挂载阶段&a…...
【NLP】使用 SpaCy、ollama 创建用于命名实体识别的合成数据集
命名实体识别 (NER) 是自然语言处理 (NLP) 中的一项重要任务,用于自动识别和分类文本中的实体,例如人物、位置、组织等。尽管它很重要,但手动注释大型数据集以进行 NER 既耗时又费钱。受本文 ( https://huggingface.co/blog/synthetic-data-s…...
【C++练习】二进制到十进制的转换器
题目:二进制到十进制的转换器 描述 编写一个程序,将用户输入的8位二进制数转换成对应的十进制数并输出。如果用户输入的二进制数不是8位,则程序应提示用户输入无效,并终止运行。 要求 程序应首先提示用户输入一个8位二进制数。…...
Vue功能菜单的异步加载、动态渲染
实际的Vue应用中,常常需要提供功能菜单,例如:文件下载、用户注册、数据采集、信息查询等等。每个功能菜单项,对应某个.vue组件。下面的代码,提供了一种独特的异步加载、动态渲染功能菜单的构建方法: <s…...
云技术基础学习(一)
内容预览 ≧∀≦ゞ 声明导语云技术历史 云服务概述云服务商与部署模式1. 公有云服务商2. 私有云部署3. 混合云模式 云服务分类1. 基础设施即服务(IaaS)2. 平台即服务(PaaS)3. 软件即服务(SaaS) 云架构云架构…...
【优选算法篇】微位至简,数之恢宏——解构 C++ 位运算中的理与美
文章目录 C 位运算详解:基础题解与思维分析前言第一章:位运算基础应用1.1 判断字符是否唯一(easy)解法(位图的思想)C 代码实现易错点提示时间复杂度和空间复杂度 1.2 丢失的数字(easy࿰…...
MFC工控项目实例二十九主对话框调用子对话框设定参数值
在主对话框调用子对话框设定参数值,使用theApp变量实现。 子对话框各参数变量 CString m_strTypeName; CString m_strBrand; CString m_strRemark; double m_edit_min; double m_edit_max; double m_edit_time2; double …...
Java | Leetcode Java题解之第546题移除盒子
题目: 题解: class Solution {int[][][] dp;public int removeBoxes(int[] boxes) {int length boxes.length;dp new int[length][length][length];return calculatePoints(boxes, 0, length - 1, 0);}public int calculatePoints(int[] boxes, int l…...
【前端】Svelte:响应性声明
Svelte 的响应性声明机制简化了动态更新 UI 的过程,让开发者不需要手动追踪数据变化。通过 $ 前缀与响应式声明语法,Svelte 能够自动追踪依赖关系,实现数据变化时的自动重新渲染。在本教程中,我们将详细探讨 Svelte 的响应性声明机…...
装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
