当前位置: 首页 > news >正文

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

相关文章:

使用Go语言编写一个简单的NTP服务器

NTP服务介绍 NTP服务器【Network Time Protocol&#xff08;NTP&#xff09;】是用来使计算机时间同步化的一种协议。 应用场景说明 为了确保封闭局域网内多个服务器的时间同步&#xff0c;我们计划部署一个网络时间同步服务器&#xff08;NTP服务器&#xff09;。这一角色将…...

注意力机制篇 | YOLO11改进 | 即插即用的高效多尺度注意力模块EMA

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。与传统的注意力机制相比&#xff0c;多尺度注意力机制引入了多个尺度的注意力权重&#xff0c;让模型能够更好地理解和处理复杂数据。这种机制通过在不同尺度上捕捉输入数据的特征&#xff0c;让模型同时关注局部细节和全局…...

昇思大模型平台打卡体验活动:项目3基于MindSpore的GPT2文本摘要

昇思大模型平台打卡体验活动&#xff1a;项目3基于MindSpore的GPT2文本摘要 1. 环境设置 本项目可以沿用前两个项目的相关环境设置。首先&#xff0c;登陆昇思大模型平台&#xff0c;并进入对应的开发环境&#xff1a; https://xihe.mindspore.cn/my/clouddev 接着&#xff0…...

web——[GXYCTF2019]Ping Ping Ping1——过滤和绕过

0x00 考点 0、命令联合执行 ; 前面的执行完执行后面的 | 管道符&#xff0c;上一条命令的输出&#xff0c;作为下一条命令的参数&#xff08;显示后面的执行结果&#xff09; || 当前面的执行出错时&#xff08;为假&#xff09;执行后面的 & 将任…...

婚礼纪 9.5.57 | 解锁plus权益的全能结婚助手,一键生成结婚请柬

婚礼纪是一款结婚服务全能助手&#xff0c;深受9000万新人信赖的一站式结婚服务平台。解锁plus权益后&#xff0c;用户可以享受部分VIP会员功能。应用提供了丰富的结婚筹备工具和服务&#xff0c;包括一键生成结婚请柬、婚礼策划、婚纱摄影、婚宴预订等。婚礼纪旨在为新人提供全…...

M1M2 MAC安装windows11 虚拟机的全过程

M1/M2 MAC安装windows11 虚拟机的全过程 这两天折腾了一下windows11 arm架构的虚拟机&#xff0c;将途中遇到的坑总结一下。 1、虚拟机软件&#xff1a;vmware fusion 13.6 或者 parallel 19 &#xff1f; 结论是&#xff1a;用parellel 19。 这两个软件都安装过&#xff0…...

监控架构-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是一个分布式的、分区的、多副本的消息发布-订阅系统&#xff0c;它提供了类似于JMS的特性&#xff0c;但在设计上完全不同&#xff0c;它具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性&#xff0c;适用于离线和在线的消息消费&#xff0c;如常规的消息收集、…...

windows环境下cmd窗口打开就进入到对应目录,一般人都不知道~

前言 很久以前&#xff0c;我还在上一家公司的时候&#xff0c;有一次我看到我同事打开cmd窗口的方式&#xff0c;瞬间把我惊呆了。原来他打开cmd窗口的方式&#xff0c;不是一般的在开始里面输入cmd&#xff0c;然后打开cmd窗口。而是另外一种方式。 我这个同事是个技术控&a…...

企微SCRM价格解析及其性价比分析

内容概要 在如今的数字化时代&#xff0c;企业对于客户关系管理的需求日益增长&#xff0c;而企微SCRM&#xff08;Social Customer Relationship Management&#xff09;作为一款新兴的客户管理工具&#xff0c;正好满足了这一需求。本文旨在为大家深入解析企微SCRM的价格体系…...

【SpringMVC】记录一次Bug——mvc:resources设置静态资源不过滤导致WEB-INF下的资源无法访问

SpringMVC 记录一次bug 其实都是小毛病&#xff0c;但是为了以后再出毛病&#xff0c;记录一下&#xff1a; mvc:resources设置静态资源不过滤问题 SpringMVC中配置的核心Servlet——DispatcherServlet&#xff0c;为了可以拦截到所有的请求&#xff08;JSP页面除外&#xf…...

【React】React 生命周期完全指南

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 React 生命周期完全指南一、生命周期概述二、生命周期的三个阶段2.1 挂载阶段&a…...

【NLP】使用 SpaCy、ollama 创建用于命名实体识别的合成数据集

命名实体识别 (NER) 是自然语言处理 (NLP) 中的一项重要任务&#xff0c;用于自动识别和分类文本中的实体&#xff0c;例如人物、位置、组织等。尽管它很重要&#xff0c;但手动注释大型数据集以进行 NER 既耗时又费钱。受本文 ( https://huggingface.co/blog/synthetic-data-s…...

【C++练习】二进制到十进制的转换器

题目&#xff1a;二进制到十进制的转换器 描述 编写一个程序&#xff0c;将用户输入的8位二进制数转换成对应的十进制数并输出。如果用户输入的二进制数不是8位&#xff0c;则程序应提示用户输入无效&#xff0c;并终止运行。 要求 程序应首先提示用户输入一个8位二进制数。…...

Vue功能菜单的异步加载、动态渲染

实际的Vue应用中&#xff0c;常常需要提供功能菜单&#xff0c;例如&#xff1a;文件下载、用户注册、数据采集、信息查询等等。每个功能菜单项&#xff0c;对应某个.vue组件。下面的代码&#xff0c;提供了一种独特的异步加载、动态渲染功能菜单的构建方法&#xff1a; <s…...

云技术基础学习(一)

内容预览 ≧∀≦ゞ 声明导语云技术历史 云服务概述云服务商与部署模式1. 公有云服务商2. 私有云部署3. 混合云模式 云服务分类1. 基础设施即服务&#xff08;IaaS&#xff09;2. 平台即服务&#xff08;PaaS&#xff09;3. 软件即服务&#xff08;SaaS&#xff09; 云架构云架构…...

【优选算法篇】微位至简,数之恢宏——解构 C++ 位运算中的理与美

文章目录 C 位运算详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;位运算基础应用1.1 判断字符是否唯一&#xff08;easy&#xff09;解法&#xff08;位图的思想&#xff09;C 代码实现易错点提示时间复杂度和空间复杂度 1.2 丢失的数字&#xff08;easy&#xff0…...

MFC工控项目实例二十九主对话框调用子对话框设定参数值

在主对话框调用子对话框设定参数值&#xff0c;使用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题移除盒子

题目&#xff1a; 题解&#xff1a; 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 的过程&#xff0c;让开发者不需要手动追踪数据变化。通过 $ 前缀与响应式声明语法&#xff0c;Svelte 能够自动追踪依赖关系&#xff0c;实现数据变化时的自动重新渲染。在本教程中&#xff0c;我们将详细探讨 Svelte 的响应性声明机…...

网络编程(Modbus进阶)

思维导图 Modbus RTU&#xff08;先学一点理论&#xff09; 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议&#xff0c;由 Modicon 公司&#xff08;现施耐德电气&#xff09;于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

电脑插入多块移动硬盘后经常出现卡顿和蓝屏

当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时&#xff0c;可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案&#xff1a; 1. 检查电源供电问题 问题原因&#xff1a;多块移动硬盘同时运行可能导致USB接口供电不足&#x…...

Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器

第一章 引言&#xff1a;语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域&#xff0c;文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量&#xff0c;支撑着搜索引擎、推荐系统、…...

WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)

一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解&#xff0c;适合用作学习或写简历项目背景说明。 &#x1f9e0; 一、概念简介&#xff1a;Solidity 合约开发 Solidity 是一种专门为 以太坊&#xff08;Ethereum&#xff09;平台编写智能合约的高级编…...