使用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、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)}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/60114.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电信网关配置管理系统 upload_channels.php 文件上传致RCE漏洞复现

0x01 产品简介 中国电信集团有限公司(英文名称“China Telecom”、简称“中国电信”)成立于2000年9月,是中国特大型国有通信企业、上海世博会全球合作伙伴。电信网关配置管理系统是一个用于管理和配置电信网络中网关设备的软件系统。它可以帮助网络管理员实现对网关设备的远…

STM32H503开发(2)----STM32CubeProgrammer烧录

STM32H503开发----2.STM32CubeProgrammer烧录 概述硬件准备视频教学样品申请源码下载参考程序自举模式BOOT0设置UART烧录USB烧录 概述 STM32CubeProgrammer (STM32CubeProg) 是一款用于编程STM32产品的全功能多操作系统软件工具。 它提供了一个易用高效的环境&#xff0c;通过…

计算机【基础篇】

-- 选择偶然 操作系统&#xff0c;是程序员写出来的一个用于操控机器硬件的 所谓电脑就是第一台计算机&#xff0c;计算机就是能够接受用户输入的指令和资料&#xff0c;并且通过计算机的中央处理器&#xff08;CPU是计算机的大脑&#xff09;进行数学和逻辑运算后&#xff0c…

Unity Shader分段式血条

Unity Shader分段式血条 前言项目ASE连线 前言 要给单位加一个类似LOL的分段式血条&#xff0c;用ASE实现并记录一下。里面加了旋转和颜色的渐变。 项目 ASE连线

Android笔记(三十五):用责任链模式封装一个App首页Dialog管理工具

背景 项目需要在首页弹一系列弹窗&#xff0c;每个弹窗是否弹出都有自己的策略&#xff0c;以及哪个优先弹出&#xff0c;哪个在上一个关闭后再弹出&#xff0c;为了更好管理&#xff0c;于是封装了一个Dialog管理工具 效果 整体采用责任链模块设计&#xff0c;控制优先级及弹…

【SpringMVC】——Cookie和Session机制

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;实践 1&#xff1a;获取URL中的参数 &#xff08;1&#xff09;PathVariable 2&…

ROS2humble版本使用colcon构建包

colcon与与catkin相比&#xff0c;没有 devel 目录。 创建工作空间 首先&#xff0c;创建一个目录 ( ros2_example_ws ) 来包含我们的工作区: mkdir -p ~/ros2_example_ws/src cd ~/ros2_example_ws 此时&#xff0c;工作区包含一个空目录 src : . └── src1 directory, …

MySQL查询数据被截断

说明&#xff1a;本文记录一个MySQL查询&#xff0c;返回数据被截断的问题&#xff1b; 场景 假设有个用户查询列表&#xff0c;查询条件中有个用户类型&#xff08;普通用户、大会员、黄金大会员、铂金大会员、至尊大会员&#xff09;&#xff0c;是个下拉列表&#xff0c;可…

华为云计算HCIE-Cloud Computing V3.0试验考试北京考场经验分享

北京试验考场 北京考场位置 1.试验考场地址 北京市海淀区北清路156号中关村环保科技示范园区M地块Q21楼 考试场选择北京&#xff0c;就是上面这个地址&#xff0c;在预约考试的时候会显示地址&#xff0c;另外在临近考试的时候也会给你发邮件&#xff0c;邮件内会提示你考试…

GDPU Android移动应用 Broadcast Receiver

聆听广播&#xff0c;跟着节拍吧。 计时器 新建一个名为PhoneStateMonitor的工程&#xff1b; 实现一个应用运行时长的计时器&#xff0c;并在界面上刷新计数器&#xff0c;要求包括&#xff1a; &#xff08;1&#xff09;在Layout中包含两个TextView控件&#xff0c;横向分…

数据库SQL——什么是实体-联系模型(E-R模型)?

目录 什么是实体-联系模型&#xff1f; 1.实体集 2.联系集 3.映射基数 一对一&#xff08;1:1&#xff09; 一对多&#xff08;1:n&#xff09; 多对一&#xff08;n:1&#xff09; 多对多&#xff08;m:n&#xff09; 全部参与&#xff1a; 4.主码 弱实体集&#xf…

共筑开源技术新篇章 | 2024 CCF中国开源大会盛大开幕

在这个技术革新日新月异的时代&#xff0c;开源精神如同点燃创新火焰的火种&#xff0c;照亮了无数技术探索者的征途。2024年11月9日&#xff0c;备受瞩目的2024 CCF中国开源大会在深圳这座充满活力的创新之城盛大开幕。这场开源领域的顶级盛事&#xff0c;以“湾区聚力 开源启…

[极客大挑战 2019]Secret File 1

[极客大挑战 2019]Secret File 1 审题 看到题目应该是一道简单的按照要求找flag的题目 知识点 跟着题目走 解题 一&#xff0c;查看源码 找到网站进入 点开发现 【注意它说没看清吗】 二&#xff0c;使用BP抓包试试 发现新出现了/action.php 抓到后放到Repeater中响应 得…

初识Electron 进程通信

概述 Electron chromium nodejs native API&#xff0c;也就是将node环境和浏览器环境整合到了一起&#xff0c;这样就构成了桌面端&#xff08;chromium负责渲染、node负责操作系统API等&#xff09; 流程模型 预加载脚本&#xff1a;运行在浏览器环境下&#xff0c;但是…

语义分割实战——基于DeepLabv3+神经网络头发分割系统源码

第一步&#xff1a;准备数据 头发分割数据&#xff0c;总共有1050张图片&#xff0c;里面的像素值为0和1&#xff0c;所以看起来全部是黑的&#xff0c;不影响使用 第二步&#xff1a;搭建模型 DeepLabV3的网络结构如下图所示&#xff0c;主要为Encoder-Decoder结构。其中&am…

c# 开发web服务 webserver

024-11-10<<<<<<<<<<<<<<<<<<<<<<<<<< 开始插件前Cyber_CallWeb acajax_dac_database_viewer 2024-11-10<<<<<<<<<<<<<<<<<<<<…

「C/C++」C/C++ 预处理 之 常用预处理宏

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…

Javascript中如何实现函数缓存?函数缓存有哪些应用场景?

#一、是什么 函数缓存&#xff0c;就是将函数运算过的结果进行缓存 本质上就是用空间&#xff08;缓存存储&#xff09;换时间&#xff08;计算过程&#xff09; 常用于缓存数据计算结果和缓存对象 解释 const add (a,b) > ab; const calc memoize(add); // 函数缓存…

Maven的依赖管理、传递、冲突、父子工程的继承和聚合

目录 一、基于IDEA 进行Maven依赖管理 (一)依赖管理概念 (二)Maven工程核心信息配置和解读&#xff08;GAVP&#xff09; (三)Maven工程依赖管理配置 1.依赖管理和依赖添加 2.依赖版本统一提取和维护 (四)依赖范围 (五)Maven工程依赖下载失败错误解决&#xff08;重点…

iOS SmartCodable 替换 HandyJSON 适配记录

前言 HandyJSON群里说建议不要再使用HandyJSON&#xff0c;我最终选择了SmartCodable 来替换&#xff0c;原因如下&#xff1a; 首先按照 SmartCodable 官方教程替换 大概要替换的内容如图&#xff1a; 详细的替换教程请前往&#xff1a;使用SmartCodable 平替 HandyJSON …