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("时间设置命令执行失败!");
}
}
}