java实现NTP服务以及服务调用端(Client)功能

1、服务端

1、服务端代码

java 复制代码
package com.test.demo.utils.ntp;

import java.net.*;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @description: 本地NTP服务
 * @create: 2025-10-21 15:47
 **/
public class LocalNtpServer {
    // 本地NTP服务端口(默认123,需root权限;非特权端口可改用10123等)
    private static final int NTP_PORT = 123;
    // NTP与Java时间戳偏移(1900-01-01到1970-01-01的毫秒数)
    private static final long NTP_EPOCH_OFFSET = 2208988800000L;
    // 线程池(处理并发请求,本地服务建议固定大小)
    private static final ExecutorService executor = Executors.newFixedThreadPool(5);

    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket(NTP_PORT)) {
            System.out.println("本地NTP授时服务启动,监听端口:" + NTP_PORT);
            System.out.println("服务将使用本地系统时间为其他服务授时");

            byte[] buffer = new byte[48]; // NTP最小报文长度
            while (true) {
                DatagramPacket request = new DatagramPacket(buffer, buffer.length);
                socket.receive(request); // 阻塞等待请求
                // 提交任务到线程池处理,避免单线程阻塞
                executor.submit(() -> handleRequest(socket, request));
            }
        } catch (Exception e) {
            System.err.println("NTP服务异常:" + e.getMessage());
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }

    /**
     * 处理客户端请求(线程池执行)
     */
    private static void handleRequest(DatagramSocket socket, DatagramPacket request) {
        try {
            // 1. 解析请求(提取客户端发送时间戳)
            byte[] requestData = request.getData();
            long clientSendTime = getOriginateTimestamp(requestData);

            // 2. 生成响应报文(使用当前系统时间作为授时源)
            long serverReceiveTime = System.currentTimeMillis(); // 服务端接收时间
            long serverSendTime = System.currentTimeMillis();    // 服务端发送时间(简化为同一时间)
            byte[] response = buildResponse(clientSendTime, serverReceiveTime, serverSendTime);

            // 3. 发送响应
            DatagramPacket reply = new DatagramPacket(
                    response, response.length,
                    request.getAddress(), request.getPort()
            );
            socket.send(reply);

            System.out.printf("已为 %s:%d 授时(服务端发送时间:%d)%n",
                    request.getAddress(), request.getPort(), serverSendTime);
        } catch (Exception e) {
            System.err.println("处理请求异常:" + e.getMessage());
        }
    }

    /**
     * 构建NTP响应报文
     * @param clientSendTime 客户端发送时间戳(Originate Timestamp)
     * @param serverReceiveTime 服务端接收时间戳(Receive Timestamp)
     * @param serverSendTime 服务端发送时间戳(Transmit Timestamp)
     */
    private static byte[] buildResponse(long clientSendTime, long serverReceiveTime, long serverSendTime) {
        ByteBuffer buffer = ByteBuffer.allocate(48);

        // 头部:LI=0(无闰秒),VN=4(版本4),Mode=4(服务器模式)→ 0x24
        buffer.put((byte) 0x24);
        // Stratum=2(次级服务器,同步到本地系统时间)
        buffer.put((byte) 0x02);
        // Poll=6(轮询间隔64秒,本地服务可缩短)
        buffer.put((byte) 0x06);
        // Precision=-20(约1微秒精度)
        buffer.put((byte) 0xec);

        // Root Delay、Root Dispersion、Reference ID(本地服务简化为0)
        buffer.putInt(0); // Root Delay(根延迟)
        buffer.putInt(0); // Root Dispersion(根离散)
        buffer.putInt(0x4C4F4341); // Reference ID(标识为"LOCAL",本地时间源)

        // Reference Timestamp(参考时间戳,简化为服务端接收时间)
        buffer.putLong(convertToNtpTimestamp(serverReceiveTime));
        // Originate Timestamp(客户端发送时间,从请求中获取)
        buffer.putLong(convertToNtpTimestamp(clientSendTime));
        // Receive Timestamp(服务端接收时间)
        buffer.putLong(convertToNtpTimestamp(serverReceiveTime));
        // Transmit Timestamp(服务端发送时间,核心授时字段)
        buffer.putLong(convertToNtpTimestamp(serverSendTime));

        return buffer.array();
    }

    /**
     * 转换Java时间戳(1970年起,毫秒)为NTP时间戳(1900年起,64位:32位秒+32位小数秒)
     */
    private static long convertToNtpTimestamp(long javaMillis) {
        if (javaMillis < 0) return 0; // 无效时间处理
        long ntpMillis = javaMillis + NTP_EPOCH_OFFSET;
        long seconds = ntpMillis / 1000;
        long fractional = (ntpMillis % 1000) * 0x100000000L / 1000; // 小数秒精度转换
        return (seconds << 32) | (fractional & 0xFFFFFFFFL);
    }

    /**
     * 从请求中提取客户端发送时间戳(Originate Timestamp,偏移24字节)
     */
    private static long getOriginateTimestamp(byte[] requestData) {
        if (requestData.length < 40) return System.currentTimeMillis(); // 无效请求用当前时间兜底
        ByteBuffer buffer = ByteBuffer.wrap(requestData);
        buffer.position(24);
        long ntpTimestamp = buffer.getLong();
        // 转换NTP时间戳为Java时间戳(毫秒)
        long seconds = (ntpTimestamp >> 32) & 0xFFFFFFFFL;
        long fractional = ntpTimestamp & 0xFFFFFFFFL;
        long javaMillis = (seconds * 1000) - NTP_EPOCH_OFFSET + (fractional * 1000) / 0x100000000L;
        return javaMillis < 0 ? System.currentTimeMillis() : javaMillis;
    }
}

2、服务端日志

2、服务调用端

1、服务调用端/客户端代码

java 复制代码
package com.test.demo.utils.ntp;


import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.TimeInfo;
import java.net.InetAddress;


/**
 * @description: 调用本地NTP服务的其他服务
 * @create: 2025-10-21 15:49
 **/
public class LocalNtpClient {
    public static void main(String[] args) throws Exception {
        NTPUDPClient client = new NTPUDPClient();
        client.setDefaultTimeout(1000); // 本地服务超时设短
        InetAddress serverAddr = InetAddress.getByName("127.0.0.1"); // 本地NTP服务地址
        TimeInfo info = client.getTime(serverAddr, 123); // 端口与服务一致
        long ntpTime = info.getMessage().getTransmitTimeStamp().getTime();
        System.out.println("从本地NTP服务获取的时间:" + new java.util.Date(ntpTime));
    }
}

2、服务调用端/客户端日志

3、java代码根据NTP时间修改Linux服务器时间

java 复制代码
package com.byy.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.TimeInfo;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

import static java.lang.Thread.sleep;

/**
 * 根据ip同步NTP时间
 *
 */
@Slf4j
@Service
public class NtpTimeFetcherUtil {

    private static final int TIMEOUT_MS = 3000;

    /**
     * 根据目标IP同步NTP时间
     *
     * @param netServerIp IP地址
     */
    public static void obtainTime(String netServerIp){
        try {
            //1、判断网络是否可达
            InetAddress byName = InetAddress.getByName(netServerIp);
            if (!byName.isReachable(TIMEOUT_MS)) {
                // 抛异常或者错误日志
                throw new RuntimeException("修改服务器时间异常,网络不可达", e);
            }
            //2、获取NTP时间
            LocalDateTime ntpTime = getNtpTime(netServerIp);
            // 获取当前系统时间
            LocalDateTime systemTime = LocalDateTime.now();
            // 计算时间偏移
            long offset = java.time.Duration.between(systemTime, ntpTime).toMillis();

            // 格式化时间输出
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
            log.info("local time: " + formatter.format(systemTime) + ",NTP time: " + formatter.format(ntpTime) + ", offset: " + offset + " ms");

            //3、设置Linux系统时间
            setSystemTime(ntpTime);

        } catch (Exception e) {
            log.error("修改服务器时间异常", e);
            throw new RuntimeException("修改服务器时间异常", e);
        }
    }

    /**
     * 从NTP服务器获取时间
     *
     * @return NTP服务器当前时间
     * @throws Exception 获取时间异常
     */
    private static LocalDateTime getNtpTime(String netServerIp) throws Exception {

        NTPUDPClient client = new NTPUDPClient();
        client.setDefaultTimeout(TIMEOUT_MS);
        try {
            client.open();
            InetAddress hostAddr = InetAddress.getByName(netServerIp);
            TimeInfo timeInfo = client.getTime(hostAddr);
            timeInfo.computeDetails();

            // 获取NTP时间戳
            long ntpTimeMillis = timeInfo.getMessage().getTransmitTimeStamp().getTime();

            // 转换为LocalDateTime
            return Instant.ofEpochMilli(ntpTimeMillis)
                    .atZone(ZoneId.systemDefault())
                    .toLocalDateTime();
        } finally {
            client.close();
        }
    }

    /**
     * 设置Linux系统时间
     *
     * @param newTime 当前时间
     * @throws Exception 设置异常
     */
    public static void setSystemTime(LocalDateTime newTime) throws Exception {
        // 格式化时间为Linux date命令接受的格式
        String timeString = newTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        // 构建命令
        String[] commands = {
                "/bin/bash",
                "-c",
                "echo '时间同步操作' && " +
                        "sudo date -s '" + timeString + "' && " +
                        "hwclock --systohc"
        };

        // 执行命令
        Process process = Runtime.getRuntime().exec(commands);
        // 读取命令输出
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                log.info("CMD命令: " + line);
            }
        }

        // 检查命令执行结果
        int exitCode = process.waitFor();
        sleep(5000);
        if (exitCode != 0) {
            log.error("时间设置命令执行失败!");
        }
    }
}

4、国家授时中心地址

http://pool.ntp.org

相关推荐
攻城狮CSU8 小时前
类型转换汇总 之C#
java·算法·c#
敲敲了个代码8 小时前
UniApp 多页面编译优化:编译时间从10分钟到1分钟
开发语言·前端·javascript·学习·uni-app
zl9798998 小时前
SpringBoot-Web开发之请求参数处理
java·spring boot
新建文件夹-8 小时前
深入浅出Langchain4j——构建Java大语言模型应用的新范式
java·开发语言·语言模型
絔宝9 小时前
Eclipse控制台乱码解决方式
java·ide·eclipse
.小小陈.9 小时前
数据结构3:复杂度
c语言·开发语言·数据结构·笔记·学习·算法·visual studio
小白银子9 小时前
零基础从头教学Linux(Day 55)
java·linux·服务器·python
包饭厅咸鱼9 小时前
QT----使用onnxRuntime运行图像分类模型
开发语言·qt·分类
Matlab程序猿小助手9 小时前
【MATLAB源码-第303期】基于matlab的蒲公英优化算法(DO)机器人栅格路径规划,输出做短路径图和适应度曲线.
开发语言·算法·matlab·机器人·kmeans