实现sntp协议从网络时间服务器获取时间信息

本文参考自:C# NTP时间同步类 - 逍遥子k - 博客园

NTP报文结构参考:https://wenku.baidu.com/view/90040c2bb868a98271fe910ef12d2af90342a84b.html

原理:

1.客户端向服务器发送一个报文请求(包含本机发出时间)

2.服务器回复一个报文(包含请求报文发出时间、服务器接收到报文时间、回复报文发出时间以及服务器的其它信息);

3.客户端记录收到回复的时间,然后通过报文信息计算客户端与服务器端的时间偏差

4.计算过程:发出时间A、接收时间B、回复时间C、收到回复时间D、报文从客户端到服务器传输时长L,客户端与服务器偏差时间值X,公式1:A+X+L=B,公式2:C-X+L=D,从公式1得出L=B-A-X,那么公式2可以是C-X+B-A-X=D,推导出2X=C-A+B-D=C-A+(B-D),偏差时间值X= ((C-A)+(B-D)) / 2

    /// <summary>
    /// NTP客户端类(请先调用Connect方法获取时间数据)
    /// </summary>
    public class NTPClient
    {
        /// <summary>
        /// SNTP 数据结构长度
        /// </summary>
        private const byte SNTPDataLength = 48;

        /// <summary>
        /// SNTP 数据结构 (协议版本 RFC 2030)
        /// </summary>
        byte[] SNTPData = new byte[SNTPDataLength];

        //数据结构中时间戳的偏移常量
        /// <summary>
        /// 参考标识符偏移
        /// </summary>
        private const byte offReferenceID = 12;
        /// <summary>
        /// 最后一次被设定或更新的时间戳偏移
        /// </summary>
        private const byte offReferenceTimestamp = 16;
        /// <summary>
        /// 客户端发出时间戳偏移
        /// </summary>
        private const byte offOriginateTimestamp = 24;
        /// <summary>
        /// 服务端接收时间戳偏移
        /// </summary>
        private const byte offReceiveTimestamp = 32;
        /// <summary>
        /// 服务端发出时间戳偏移
        /// </summary>
        private const byte offTransmitTimestamp = 40;

        /// <summary>
        /// Leap Indicator Warns of an impending leap second to be inserted/deleted in the last  minute of the current day. 值为“3”时表示告警状态,时钟未被同步。为其他值时NTP本身不做处理
        /// </summary>
        public _LeapIndicator LeapIndicator
        {
            get
            {
                // 选取两个最高有效位
                byte val = (byte)(SNTPData[0] >> 6);
                switch (val)
                {
                    case 0: return _LeapIndicator.NoWarning;
                    case 1: return _LeapIndicator.LastMinute61;
                    case 2: return _LeapIndicator.LastMinute59;
                    case 3: goto default;
                    default:
                        return _LeapIndicator.Alarm;
                }
            }
        }

        /// <summary>
        /// Version Number Version number of the protocol (3 or 4) NTP的版本号
        /// </summary>
        public byte VersionNumber
        {
            get
            {
                // 选取 3 - 5位
                byte val = (byte)((SNTPData[0] & 0x38) >> 3);
                return val;
            }
        }

        /// <summary>
        /// Mode 长度为3比特,表示NTP的工作模式。不同的值所表示的含义分别是:0未定义、1表示主动对等体模式、2表示被动对等体模式、3表示客户模式、4表示服务器模式、5表示广播模式或组播模式、6表示此报文为NTP控制报文、7预留给内部使用
        /// </summary>
        public _Mode Mode
        {
            get
            {
                // 选取 6 - 8位
                byte val = (byte)(SNTPData[0] & 0x7);
                switch (val)
                {
                    case 0:
                        return _Mode.Reserved;
                    case 1:
                        return _Mode.SymmetricActive;
                    case 2:
                        return _Mode.SymmetricPassive;
                    case 3:
                        return _Mode.Client;
                    case 4:
                        return _Mode.Server;
                    case 5:
                        return _Mode.Broadcast;
                    case 6:
                        return _Mode.ReservedForNTPcontrolMessages;
                    case 7:
                        return _Mode.ReservedForPrivateUse;
                    default:
                        return _Mode.Reserved;
                }
            }
        }

        /// <summary>
        /// Stratum 系统时钟的层级等级
        /// </summary>
        public _Stratum Stratum
        {
            get
            {
                byte val = (byte)SNTPData[1];
                if (val == 0) return _Stratum.Unspecified;
                else
                    if (val == 1) return _Stratum.PrimaryReference;
                else
                        if (val <= 15) return _Stratum.SecondaryReference;
                else
                    return _Stratum.Reserved;
            }
        }
        /// <summary>
        /// 系统时钟的层级值,取值范围为1~16,它定义了时钟的准确度。层数为1的时钟准确度最高(0表示基准时钟源硬件),准确度从1到16依次递减,层数为16的时钟处于未同步状态,不能作为参考时钟
        /// </summary>
        public byte StratumValue
        {
            get
            {
                return SNTPData[1];
            }
        }
        /// <summary>
        /// Poll Interval (in seconds) Maximum interval between successive messages 轮询时间,即两个连续NTP报文之间的时间间隔
        /// </summary>
        public uint PollInterval
        {
            get
            {
                // Thanks to Jim Hollenhorst <hollenho@attbi.com>
                return (uint)(Math.Pow(2, (sbyte)SNTPData[2]));
            }
        }

        /// <summary>
        /// Precision (in seconds) Precision of the clock 系统时钟的精度
        /// </summary>
        public double Precision
        {
            get
            {
                // Thanks to Jim Hollenhorst <hollenho@attbi.com>
                return (Math.Pow(2, (sbyte)SNTPData[3]));
            }
        }

        /// <summary>
        /// Root Delay (in milliseconds) Round trip time to the primary reference source NTP服务器到主参考时钟的往返延迟
        /// </summary>
        public double RootDelay
        {
            get
            {
                int temp = 0;
                temp = 256 * (256 * (256 * SNTPData[4] + SNTPData[5]) + SNTPData[6]) + SNTPData[7];
                return 1000 * (((double)temp) / 0x10000);
            }
        }

        /// <summary>
        /// Root Dispersion (in milliseconds) Nominal error relative to the primary reference source NTP服务器相对于主参考时钟的最大误差
        /// </summary>
        public double RootDispersion
        {
            get
            {
                int temp = 0;
                temp = 256 * (256 * (256 * SNTPData[8] + SNTPData[9]) + SNTPData[10]) + SNTPData[11];
                return 1000 * (((double)temp) / 0x10000);
            }
        }

        /// <summary>
        /// 参考标识符(4个字符的字符串或IP地址)
        /// </summary>
        public string ReferenceID
        {
            get
            {
                string val = "";
                switch (Stratum)
                {
                    case _Stratum.Unspecified:
                        goto case _Stratum.PrimaryReference;
                    case _Stratum.PrimaryReference:
                        val += (char)SNTPData[offReferenceID + 0];
                        val += (char)SNTPData[offReferenceID + 1];
                        val += (char)SNTPData[offReferenceID + 2];
                        val += (char)SNTPData[offReferenceID + 3];
                        break;
                    case _Stratum.SecondaryReference:
                        switch (VersionNumber)
                        {
                            case 3:    // Version 3, Reference ID is an IPv4 address系统时间的来源IP地址(也就是上一级时钟的IP)
                                string Address = SNTPData[offReferenceID + 0].ToString() + "." +
                                                 SNTPData[offReferenceID + 1].ToString() + "." +
                                                 SNTPData[offReferenceID + 2].ToString() + "." +
                                                 SNTPData[offReferenceID + 3].ToString();
                                try
                                {
                                    IPHostEntry Host = Dns.GetHostEntry(Address);
                                    val = Host.HostName + " (" + Address + ")";
                                }
                                catch (Exception)
                                {
                                    val = "N/A" + " (" + Address + ")";
                                }
                                break;
                            case 4: // Version 4, Reference ID is the timestamp of last update
                                DateTime time = ComputeDate(GetMilliSeconds(offReferenceID));
                                // Take care of the time zone
                                TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
                                val = (time + offspan).ToString();
                                break;
                            default:
                                val = "N/A";
                                break;
                        }
                        break;
                }

                return val;
            }
        }

        /// <summary>
        /// Reference Timestamp The time at which the clock was last set or corrected NTP系统时钟最后一次被设定或更新的时间
        /// </summary>
        public DateTime ReferenceTimestamp
        {
            get
            {
                DateTime time = ComputeDate(GetMilliSeconds(offReferenceTimestamp));
                // Take care of the time zone
                TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
                return time + offspan;
            }
        }

        /// <summary>
        /// Originate Timestamp (T1)  The time at which the request departed the client for the server. 发送报文时的本机时间
        /// </summary>
        public DateTime OriginateTimestamp
        {
            get
            {
                return ComputeDate(GetMilliSeconds(offOriginateTimestamp));
            }
        }

        /// <summary>
        /// Receive Timestamp (T2) The time at which the request arrived at the server. 报文到达NTP服务器时的服务器时间
        /// </summary>
        public DateTime ReceiveTimestamp
        {
            get
            {
                DateTime time = ComputeDate(GetMilliSeconds(offReceiveTimestamp));
                // Take care of the time zone
                TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
                return time + offspan;
            }
        }

        /// <summary>
        /// Transmit Timestamp (T3) The time at which the reply departed the server for client.  报文从NTP服务器离开时的服务器时间
        /// </summary>
        public DateTime TransmitTimestamp
        {
            get
            {
                DateTime time = ComputeDate(GetMilliSeconds(offTransmitTimestamp));
                // Take care of the time zone
                TimeSpan offspan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
                return time + offspan;
            }
            set
            {
                SetDate(offTransmitTimestamp, value);
            }
        }

        /// <summary>
        /// Destination Timestamp (T4) The time at which the reply arrived at the client. 接收到来自NTP服务器返回报文时的本机时间
        /// </summary>
        public DateTime DestinationTimestamp;

        /// <summary>
        /// Round trip delay (in milliseconds) The time between the departure of request and arrival of reply 报文从本地到NTP服务器的往返时间
        /// </summary>
        public double RoundTripDelay
        {
            get
            {
                // Thanks to DNH <dnharris@csrlink.net>
                TimeSpan span = (DestinationTimestamp - OriginateTimestamp) - (ReceiveTimestamp - TransmitTimestamp);
                return span.TotalMilliseconds;
            }
        }

        /// <summary>
        /// Local clock offset (in milliseconds)  The offset of the local clock relative to the primary reference source.本机相对于NTP服务器(主时钟)的时间差
        /// </summary>
        public double LocalClockOffset
        {
            get
            {
                // 基于往返数据的网络传输时间相同,Thanks to DNH <dnharris@csrlink.net>
                TimeSpan span = (ReceiveTimestamp - OriginateTimestamp) + (TransmitTimestamp - DestinationTimestamp);
                return span.TotalMilliseconds / 2;
            }
        }

        /// <summary>
        /// 计算日期,给定1900年1月1日以来的毫秒数
        /// </summary>
        /// <param name="milliseconds">毫秒数</param>
        /// <returns></returns>
        private DateTime ComputeDate(ulong milliseconds)
        {
            TimeSpan span = TimeSpan.FromMilliseconds((double)milliseconds);
            DateTime time = new DateTime(1900, 1, 1);
            time += span;
            return time;
        }

        /// <summary>
        /// Compute the number of milliseconds, given the offset of a 8-byte array.计算指定偏移量的毫秒数
        /// </summary>
        /// <param name="offset"></param>
        /// <returns></returns>
        private ulong GetMilliSeconds(byte offset)
        {
            ulong intpart = 0, fractpart = 0;

            for (int i = 0; i <= 3; i++)
            {
                intpart = 256 * intpart + SNTPData[offset + i];
            }
            for (int i = 4; i <= 7; i++)
            {
                fractpart = 256 * fractpart + SNTPData[offset + i];
            }
            ulong milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L;
            return milliseconds;
        }

        /// <summary>
        /// Compute the 8-byte array, given the date.设置指定偏移量的时间值
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="date"></param>
        private void SetDate(byte offset, DateTime date)
        {
            ulong intpart = 0, fractpart = 0;
            DateTime StartOfCentury = new DateTime(1900, 1, 1, 0, 0, 0);    // January 1, 1900 12:00 AM

            ulong milliseconds = (ulong)(date - StartOfCentury).TotalMilliseconds;
            intpart = milliseconds / 1000;
            fractpart = ((milliseconds % 1000) * 0x100000000L) / 1000;

            ulong temp = intpart;
            for (int i = 3; i >= 0; i--)
            {
                SNTPData[offset + i] = (byte)(temp % 256);
                temp = temp / 256;
            }

            temp = fractpart;
            for (int i = 7; i >= 4; i--)
            {
                SNTPData[offset + i] = (byte)(temp % 256);
                temp = temp / 256;
            }
        }

        /// <summary>
        /// 初始化客户端请求的数据
        /// </summary>
        private void Initialize()
        {
            // Set version number to 4 and Mode to 3 (client)
            SNTPData[0] = 0x1B;
            // Initialize all other fields with 0
            for (int i = 1; i < 48; i++)
            {
                SNTPData[i] = 0;
            }
            // Initialize the transmit timestamp
            TransmitTimestamp = DateTime.Now;
        }

        /// <summary>
        /// 时间服务器的IP地址
        /// </summary>
        private IPAddress serverAddress = null;


        /// <summary>
        /// 通过主机名称构建对象
        /// </summary>
        /// <param name="host">时间服务器名称</param>
        public NTPClient(string host)
        {
            //string host = "ntp1.aliyun.com";
            //string host = "0.asia.pool.ntp.org";
            //string host = "1.asia.pool.ntp.org";
            //string host = "www.ntp.org/";

            // Resolve server address
            IPHostEntry hostadd = Dns.GetHostEntry(host);
            foreach (IPAddress address in hostadd.AddressList)
            {
                if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) //只支持IPV4协议的IP地址
                {
                    serverAddress = address;
                    break;
                }
            }

            if (serverAddress == null)
                throw new Exception("Can't get any ipaddress infomation");
        }

        /// <summary>
        /// 通过IP地址构建对象
        /// </summary>
        /// <param name="address">时间服务器IP地址</param>
        public NTPClient(IPAddress address)
        {
            if (address == null)
                throw new Exception("Can't get any ipaddress infomation");

            serverAddress = address;
        }

        /// <summary>
        /// 连接时间服务器获取时间信息(每调用一次刷新一次信息)
        /// </summary>
        /// <param name="updateSystemTime">是否更新本地时间</param>
        /// <param name="timeout">网络等待超时(毫秒)</param>
        public void Connect(bool updateSystemTime, int timeout = 3000)
        {
            IPEndPoint EPhost = new IPEndPoint(serverAddress, 123);

            //Connect the time server
            using (System.Net.Sockets.UdpClient TimeSocket = new System.Net.Sockets.UdpClient())
            {
                TimeSocket.Connect(EPhost);

                // Initialize data structure
                Initialize();
                TimeSocket.Send(SNTPData, SNTPData.Length);
                TimeSocket.Client.ReceiveTimeout = timeout;
                SNTPData = TimeSocket.Receive(ref EPhost);
                if (!IsResponseValid)
                    throw new Exception("Invalid response from " + serverAddress.ToString());
            }
            DestinationTimestamp = DateTime.Now;

            if (updateSystemTime)
                SetTime();
        }

        /// <summary>
        /// 检查服务器的响应是否有效
        /// </summary>
        /// <returns></returns>
        public bool IsResponseValid
        {
            get
            {
                return !(SNTPData.Length < SNTPDataLength || Mode != _Mode.Server);
            }
        }

        /// <summary>
        /// 显示数据信息(已重写)
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder(512);
            sb.AppendFormat("标识符: {0}", ReferenceID.ToString().Replace("\0", string.Empty));
            sb.Append("\r\n告警状态: ");
            switch (LeapIndicator)
            {
                case _LeapIndicator.NoWarning:
                    sb.Append("没有告警");
                    break;
                case _LeapIndicator.LastMinute61:
                    sb.Append("最后一分钟有61秒");
                    break;
                case _LeapIndicator.LastMinute59:
                    sb.Append("最后一分钟有59秒");
                    break;
                case _LeapIndicator.Alarm:
                    sb.Append("报警状态(时钟不同步)");
                    break;
            }
            sb.AppendFormat("\r\n版本号: {0}\r\n", VersionNumber);
            sb.Append("模式: ");
            switch (Mode)
            {
                case _Mode.Reserved:
                    sb.Append("保留");
                    break;
                case _Mode.SymmetricActive:
                    sb.Append("Symmetric Active-主动对等体模式");
                    break;
                case _Mode.SymmetricPassive:
                    sb.Append("Symmetric Pasive-被动对等体模式");
                    break;
                case _Mode.Client:
                    sb.Append("Client-客户端模式");
                    break;
                case _Mode.Server:
                    sb.Append("Server-服务器模式");
                    break;
                case _Mode.Broadcast:
                    sb.Append("Broadcast-广播模式");
                    break;
                case _Mode.ReservedForNTPcontrolMessages:
                    sb.Append("NTP控制报文");
                    break;
                case _Mode.ReservedForPrivateUse:
                    sb.Append("内部使用预留");
                    break;
            }
            sb.Append("\r\nStratum-层阶等级: ");

            switch (Stratum)
            {
                case _Stratum.Unspecified:
                    sb.Append("基准时钟硬件");
                    break;
                case _Stratum.Reserved:
                    sb.Append("保留");
                    break;
                case _Stratum.PrimaryReference:
                    sb.Append("主参考时钟");
                    break;
                case _Stratum.SecondaryReference:
                    sb.Append("一般参考时钟");
                    break;
            }
            sb.AppendFormat(",层级: {0}", StratumValue);
            sb.AppendFormat("\r\nNTP系统时钟精度: {0} s", Precision);
            sb.AppendFormat("\r\nNTP系统时钟到主参考时钟的往返延迟: {0} ms", RootDelay);
            sb.AppendFormat("\r\nNTP系统时钟相对于主参考时钟的最大误差: {0} ms", RootDispersion);
            sb.AppendFormat("\r\nNTP系统时钟最后一次更新时间: {0:yyyy-MM-dd HH:mm:ss:fff}", ReferenceTimestamp);
            sb.AppendFormat("\r\n轮询间隔: {0}", this.PollInterval.ToString());
            sb.Append("\r\n");
            sb.AppendFormat("\r\n报文开始时间: {0}", this.OriginateTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
            sb.AppendFormat("\r\n报文服务端到达时间: {0}", this.ReceiveTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
            sb.AppendFormat("\r\n报文服务端发出时间: {0}", this.TransmitTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
            sb.AppendFormat("\r\n报文完成时间: {0}", this.DestinationTimestamp.ToString("yyyy-MM-dd HH:mm:ss fff"));
            sb.AppendFormat("\r\n报文往返时间: {0} ms", RoundTripDelay);
            sb.AppendFormat("\r\n本机相对于服务端的时间差: {0} ms", LocalClockOffset);
            sb.Append("\r\n");

            return sb.ToString();
        }

        /// <summary>
        /// SYSTEMTIME structure used by SetSystemTime
        /// </summary>
        [StructLayoutAttribute(LayoutKind.Sequential)]
        private struct SYSTEMTIME
        {
            public short year;
            public short month;
            public short dayOfWeek;
            public short day;
            public short hour;
            public short minute;
            public short second;
            public short milliseconds;
        }

        [DllImport("kernel32.dll")]
        static extern bool SetLocalTime(ref SYSTEMTIME time);
        [DllImport("Kernel32.dll")]
        static extern void GetLocalTime(ref SYSTEMTIME Time);

        /// <summary>
        /// Set system time according to transmit timestamp 校准本地时间
        /// </summary>
        public void SetTime()
        {
            SYSTEMTIME st;

            DateTime trts = DateTime.Now.AddMilliseconds(LocalClockOffset);

            st.year = (short)trts.Year;
            st.month = (short)trts.Month;
            st.dayOfWeek = (short)trts.DayOfWeek;
            st.day = (short)trts.Day;
            st.hour = (short)trts.Hour;
            st.minute = (short)trts.Minute;
            st.second = (short)trts.Second;
            st.milliseconds = (short)trts.Millisecond;

            SetLocalTime(ref st);
        }
        /// <summary>
        /// 代码示例:获取NTP网络时间
        /// </summary>
        /// <returns></returns>
        public static DateTime GetDateTime()
        {
            /*
             * 中国科学院国家授时中心:ntp.ntsc.ac.cn [114.118.7.161或114.118.7.163] 层级1,网络延时40+
             * 腾讯:ntp.tencent.com [139.199.215.251] 层级2,网络延时15(推荐)
             * 阿里云:ntp.aliyun.com [203.107.6.88] 层级2,网络延时40+
             */
            NTPClient ntpClient = new NTPClient("ntp.tencent.com");
            ntpClient.Connect(false);
            return DateTime.Now.AddMilliseconds(ntpClient.LocalClockOffset);
        }
    }

    /// <summary>
    /// 告警状态
    /// </summary>
    public enum _LeapIndicator
    {
        NoWarning,        // 0 - No warning
        LastMinute61,    // 1 - Last minute has 61 seconds
        LastMinute59,    // 2 - Last minute has 59 seconds
        Alarm            // 3 - Alarm condition (clock not synchronized)
    }

    /// <summary>
    /// 模式
    /// </summary>
    public enum _Mode
    {
        Reserved,//保留。
        SymmetricActive,//主动对等体模式。
        SymmetricPassive,//被动对等体模式。
        Client,//客户端模式。
        Server,//服务器模式。
        Broadcast,//广播模式。
        ReservedForNTPcontrolMessages,//NTP控制报文。
        ReservedForPrivateUse,//内部使用预留。
    }

    /// <summary>
    ///  层级越高离核心时钟源越近,最高为0表示基准时钟源
    /// </summary>
    public enum _Stratum
    {
        Unspecified,            // 0 - 未指定或不可用,一般用硬件实现,例如原子钟(如铯、铷)、GPS时钟或其他无线电时钟。它们也被称为参考(基准)时钟
        PrimaryReference,        // 1 - 主参考时钟
        SecondaryReference,        // 2-15 - 一般参考时钟(通过NTP或SNTP)
        Reserved                // 16-255 - 保留
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SNTP协议客户端实现以及数据包格式: LI:跳跃指示器,警告在当月最后一天的最终时刻插入的迫近闺秒(闺秒)。 VN:版本号。 Mode:工作模式。该字段包括以下值:0-预留;1-对称行为;3-客户机;4-服务器;5-广播;6-NTP控制信息NTP协议具有3种工作模式,分别为主/被动对称模式、客户/服务器模式、广播模式。在主/被动对称模式中,有一对一的连接,双方均可同步对方或被对方同步,先发出申请建立连接的一方工作在主动模式下,另一方工作在被动模式下; 客户/服务器模 式与主/被动模式基本相同,惟一区别在于客户方可被服务器同步,但服务器不能被客户同步;在广播模式中,有一对多的连接,服务器不论客户工作 在何种模式下,都会主动发出时间信息,客户根据此信息调整自己的时间。 Stratum:对本地时钟级别的整体识别。 Poll:有符号整数表示连续信息间的最大间隔。 Precision:有符号整数表示本地时钟精确度。 Root Delay:表示到达主参考源的一次往复的总延迟,它是有15~16位小数部分的符号定点小 数。 Root Dispersion:表示一次到达主参考源的标准误差,它是有15~16位小数部分的无符号 定点小数。 Reference Identifier:识别特殊参考源。 Originate Timestamp:这是向服务器请求分离客户机的时间,采用64位时标格式。 Receive Timestamp:这是向服务器请求到达客户机的时间,采用64位时标格式。 Transmit Timestamp:这是向客户机答复分离服务器时间,采用64位时标格式。 Authenticator(Optional):当实现NTP认证模式时,主要标识符和信息数字域就 包括已定义的信息认证代码(MAC)信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lzl_li

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值