@@ -63,17 +119,44 @@
UTF-8
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
org.apache.maven.plugins
maven-surefire-plugin
3.1.2
- org.codehaus.mojo
- exec-maven-plugin
- 3.1.0
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+
+ attach-javadocs
+
+ jar
+
+
+
- com.sip.example.SIPServerExample
+ UTF-8
+ UTF-8
+ UTF-8
diff --git a/src/main/java/com/sip/annotation/EnableSipServer.java b/src/main/java/com/sip/annotation/EnableSipServer.java
new file mode 100644
index 0000000..7bf3aef
--- /dev/null
+++ b/src/main/java/com/sip/annotation/EnableSipServer.java
@@ -0,0 +1,32 @@
+package com.sip.annotation;
+
+import com.sip.autoconfigure.SipAutoConfiguration;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+/**
+ * 启用 SIP 服务器
+ *
+ * 在 Spring Boot 主类上使用此注解以启用 SIP 服务器功能
+ *
+ *
+ *
+ * 使用示例:
+ * {@code
+ * @SpringBootApplication
+ * @EnableSipServer
+ * public class MyApplication {
+ * public static void main(String[] args) {
+ * SpringApplication.run(MyApplication.class, args);
+ * }
+ * }
+ * }
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(SipAutoConfiguration.class)
+public @interface EnableSipServer {
+}
diff --git a/src/main/java/com/sip/annotation/SipHandler.java b/src/main/java/com/sip/annotation/SipHandler.java
new file mode 100644
index 0000000..a86133b
--- /dev/null
+++ b/src/main/java/com/sip/annotation/SipHandler.java
@@ -0,0 +1,37 @@
+package com.sip.annotation;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+/**
+ * SIP 处理器注解
+ *
+ * 标记类为 SIP 处理器组件
+ *
+ *
+ *
+ * 使用示例:
+ * {@code
+ * @SipHandler
+ * public class MySipHandler {
+ *
+ * @SipMethod("INVITE")
+ * public void handleInvite(SIPRequest request, InetSocketAddress remoteAddress) {
+ * // 处理 INVITE 请求
+ * }
+ * }
+ * }
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface SipHandler {
+
+ /**
+ * Bean 名称
+ */
+ String value() default "";
+}
diff --git a/src/main/java/com/sip/annotation/SipMethod.java b/src/main/java/com/sip/annotation/SipMethod.java
new file mode 100644
index 0000000..393d57b
--- /dev/null
+++ b/src/main/java/com/sip/annotation/SipMethod.java
@@ -0,0 +1,39 @@
+package com.sip.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * SIP 方法处理器注解
+ *
+ * 标记方法以处理特定的 SIP 请求方法
+ *
+ *
+ *
+ * 使用示例:
+ * {@code
+ * @Component
+ * public class MySipHandler {
+ *
+ * @SipMethod("INVITE")
+ * public void handleInvite(SIPRequest request, InetSocketAddress remoteAddress) {
+ * // 处理 INVITE 请求
+ * }
+ *
+ * @SipMethod("REGISTER")
+ * public void handleRegister(SIPRequest request, InetSocketAddress remoteAddress) {
+ * // 处理 REGISTER 请求
+ * }
+ * }
+ * }
+ *
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SipMethod {
+
+ /**
+ * SIP 方法名称(如 INVITE, REGISTER, BYE 等)
+ */
+ String value();
+}
diff --git a/src/main/java/com/sip/autoconfigure/SipAutoConfiguration.java b/src/main/java/com/sip/autoconfigure/SipAutoConfiguration.java
new file mode 100644
index 0000000..99d9c97
--- /dev/null
+++ b/src/main/java/com/sip/autoconfigure/SipAutoConfiguration.java
@@ -0,0 +1,104 @@
+package com.sip.autoconfigure;
+
+import com.sip.client.SipClient;
+import com.sip.config.SipProperties;
+import com.sip.server.DefaultSipMessageProcessor;
+import com.sip.server.SipMessageProcessor;
+import com.sip.server.SipServer;
+import com.sip.transaction.SIPTransactionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * SIP 自动配置
+ *
+ * Spring Boot 自动配置类,用于自动装配 SIP 相关组件
+ *
+ *
+ *
+ * 配置示例 (application.yml):
+ * sip:
+ * server:
+ * enabled: true
+ * host: 0.0.0.0
+ * udp-port: 5060
+ * tcp-port: 5060
+ * transport:
+ * udp-enabled: true
+ * tcp-enabled: true
+ * ws-enabled: false
+ *
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(SipProperties.class)
+@ConditionalOnProperty(prefix = "sip.server", name = "enabled", havingValue = "true", matchIfMissing = true)
+public class SipAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(SipAutoConfiguration.class);
+
+ /**
+ * SIP 事务管理器
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public SIPTransactionManager sipTransactionManager() {
+ logger.info("Creating SIP transaction manager");
+ return new SIPTransactionManager();
+ }
+
+ /**
+ * 默认 SIP 消息处理器
+ *
+ * 如果用户没有自定义处理器,则使用默认处理器(发布事件)
+ *
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public SipMessageProcessor sipMessageProcessor(ApplicationEventPublisher eventPublisher) {
+ logger.info("Creating default SIP message processor");
+ return new DefaultSipMessageProcessor(eventPublisher);
+ }
+
+ /**
+ * SIP 服务器
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = "sip.server", name = "enabled", havingValue = "true", matchIfMissing = true)
+ public SipServer sipServer(SipProperties properties,
+ ApplicationEventPublisher eventPublisher,
+ SipMessageProcessor messageProcessor) {
+ logger.info("Creating SIP server");
+ return new SipServer(properties, eventPublisher, messageProcessor);
+ }
+
+ /**
+ * SIP 客户端
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public SipClient sipClient(SipProperties properties, ApplicationEventPublisher eventPublisher) {
+ logger.info("Creating SIP client");
+ return new SipClient(properties, eventPublisher);
+ }
+
+ /**
+ * SIP 服务器生命周期管理
+ */
+ @Configuration
+ @ConditionalOnProperty(prefix = "sip.server", name = "enabled", havingValue = "true", matchIfMissing = true)
+ static class SipServerLifecycle {
+
+ @Bean
+ public SipServerRunner sipServerRunner(SipServer sipServer) {
+ return new SipServerRunner(sipServer);
+ }
+ }
+}
diff --git a/src/main/java/com/sip/autoconfigure/SipServerRunner.java b/src/main/java/com/sip/autoconfigure/SipServerRunner.java
new file mode 100644
index 0000000..f0bd92a
--- /dev/null
+++ b/src/main/java/com/sip/autoconfigure/SipServerRunner.java
@@ -0,0 +1,35 @@
+package com.sip.autoconfigure;
+
+import com.sip.server.SipServer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.annotation.Order;
+
+/**
+ * SIP 服务器启动器
+ *
+ * 在 Spring Boot 应用启动后自动启动 SIP 服务器
+ *
+ */
+@Order(100)
+public class SipServerRunner implements CommandLineRunner {
+
+ private static final Logger logger = LoggerFactory.getLogger(SipServerRunner.class);
+
+ private final SipServer sipServer;
+
+ public SipServerRunner(SipServer sipServer) {
+ this.sipServer = sipServer;
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ logger.info("Starting SIP server...");
+
+ sipServer.start().exceptionally(e -> {
+ logger.error("Failed to start SIP server", e);
+ return null;
+ });
+ }
+}
diff --git a/src/main/java/com/sip/client/SipClient.java b/src/main/java/com/sip/client/SipClient.java
new file mode 100644
index 0000000..5393876
--- /dev/null
+++ b/src/main/java/com/sip/client/SipClient.java
@@ -0,0 +1,478 @@
+package com.sip.client;
+
+import com.sip.codec.SipDatagramDecoder;
+import com.sip.codec.SipDatagramEncoder;
+import com.sip.codec.SipMessageDecoder;
+import com.sip.codec.SipMessageEncoder;
+import com.sip.config.SipProperties;
+import com.sip.core.SIPMessage;
+import com.sip.core.SIPRequest;
+import com.sip.core.SIPResponse;
+import com.sip.event.SipRequestEvent;
+import com.sip.event.SipResponseEvent;
+import com.sip.handler.SipUdpHandler;
+import com.sip.server.SipMessageProcessor;
+import com.sip.transaction.SIPTransaction;
+import com.sip.transaction.SIPTransactionManager;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
+
+import jakarta.annotation.PreDestroy;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * SIP 客户端
+ *
+ * 用于发起 SIP 请求的客户端组件
+ *
+ *
+ *
+ * 使用示例:
+ * {@code
+ * @Autowired
+ * private SipClient sipClient;
+ *
+ * public void makeCall() {
+ * CompletableFuture future = sipClient.sendInvite(
+ * "sip:callee@example.com",
+ * "sip:caller@example.com",
+ * null // SDP body
+ * );
+ *
+ * future.thenAccept(response -> {
+ * if (response.isSuccess()) {
+ * System.out.println("Call connected!");
+ * }
+ * });
+ * }
+ * }
+ *
+ */
+public class SipClient {
+
+ private static final Logger logger = LoggerFactory.getLogger(SipClient.class);
+
+ private final SipProperties properties;
+ private final ApplicationEventPublisher eventPublisher;
+ private final SIPTransactionManager transactionManager;
+
+ private EventLoopGroup workerGroup;
+ private Channel udpChannel;
+ private final Map tcpConnections = new ConcurrentHashMap<>();
+
+ private String localHost = "127.0.0.1";
+ private int localPort = 5060;
+ private volatile boolean running = false;
+
+ public SipClient(SipProperties properties, ApplicationEventPublisher eventPublisher) {
+ this.properties = properties;
+ this.eventPublisher = eventPublisher;
+ this.transactionManager = new SIPTransactionManager();
+ }
+
+ /**
+ * 初始化客户端
+ */
+ public CompletableFuture start() {
+ return start(localHost, 0); // 使用随机端口
+ }
+
+ /**
+ * 初始化客户端
+ *
+ * @param host 本地地址
+ * @param port 本地端口(0 表示随机端口)
+ */
+ public CompletableFuture start(String host, int port) {
+ if (running) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ this.localHost = host;
+ this.localPort = port;
+
+ CompletableFuture future = new CompletableFuture<>();
+
+ workerGroup = new NioEventLoopGroup();
+
+ // 初始化 UDP 通道
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(workerGroup)
+ .channel(NioDatagramChannel.class)
+ .option(ChannelOption.SO_BROADCAST, true)
+ .handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(DatagramChannel ch) {
+ ch.pipeline()
+ .addLast("decoder", new SipDatagramDecoder())
+ .addLast("encoder", new SipDatagramEncoder())
+ .addLast("handler", new ClientUdpHandler());
+ }
+ });
+
+ bootstrap.bind(host, port).addListener((ChannelFuture f) -> {
+ if (f.isSuccess()) {
+ udpChannel = f.channel();
+ InetSocketAddress addr = (InetSocketAddress) udpChannel.localAddress();
+ this.localPort = addr.getPort();
+ this.localHost = host.equals("0.0.0.0") ? getLocalIp() : host;
+ running = true;
+ logger.info("SIP client started on {}:{}", this.localHost, this.localPort);
+ future.complete(null);
+ } else {
+ future.completeExceptionally(f.cause());
+ }
+ });
+
+ return future;
+ }
+
+ /**
+ * 停止客户端
+ */
+ @PreDestroy
+ public void stop() {
+ running = false;
+
+ if (udpChannel != null) {
+ udpChannel.close();
+ }
+
+ tcpConnections.values().forEach(Channel::close);
+ tcpConnections.clear();
+
+ if (workerGroup != null) {
+ workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+ }
+
+ transactionManager.shutdown();
+
+ logger.info("SIP client stopped");
+ }
+
+ /**
+ * 发送 REGISTER 请求
+ */
+ public CompletableFuture sendRegister(String registrarUri, String aor, int expires) {
+ SIPRequest request = createRequest("REGISTER", registrarUri);
+ request.setHeader("To", "<" + aor + ">");
+ request.setHeader("From", "<" + aor + ">;tag=" + generateTag());
+ request.setHeader("Expires", String.valueOf(expires));
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送 INVITE 请求
+ */
+ public CompletableFuture sendInvite(String toUri, String fromUri, byte[] sdpBody) {
+ SIPRequest request = createRequest("INVITE", toUri);
+ request.setHeader("To", "<" + toUri + ">");
+ request.setHeader("From", "<" + fromUri + ">;tag=" + generateTag());
+
+ if (sdpBody != null && sdpBody.length > 0) {
+ request.setBody(sdpBody);
+ request.setHeader("Content-Type", "application/sdp");
+ request.setHeader("Content-Length", String.valueOf(sdpBody.length));
+ }
+
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送 BYE 请求
+ */
+ public CompletableFuture sendBye(String toUri, String fromUri, String callId, String toTag, String fromTag, int cseq) {
+ SIPRequest request = createRequest("BYE", toUri);
+ request.setHeader("To", "<" + toUri + ">;tag=" + toTag);
+ request.setHeader("From", "<" + fromUri + ">;tag=" + fromTag);
+ request.setHeader("Call-ID", callId);
+ request.setHeader("CSeq", cseq + " BYE");
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送 ACK 请求
+ */
+ public void sendAck(String toUri, String fromUri, String callId, String toTag, String fromTag, int cseq) {
+ SIPRequest request = createRequest("ACK", toUri);
+ request.setHeader("To", "<" + toUri + ">;tag=" + toTag);
+ request.setHeader("From", "<" + fromUri + ">;tag=" + fromTag);
+ request.setHeader("Call-ID", callId);
+ request.setHeader("CSeq", cseq + " ACK");
+
+ InetSocketAddress address = parseAddress(toUri);
+ sendUdp(request, address);
+ }
+
+ /**
+ * 发送 CANCEL 请求
+ */
+ public CompletableFuture sendCancel(String toUri, String fromUri, String callId, String toTag, String fromTag, int cseq) {
+ SIPRequest request = createRequest("CANCEL", toUri);
+ request.setHeader("To", "<" + toUri + ">" + (toTag != null ? ";tag=" + toTag : ""));
+ request.setHeader("From", "<" + fromUri + ">;tag=" + fromTag);
+ request.setHeader("Call-ID", callId);
+ request.setHeader("CSeq", cseq + " CANCEL");
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送 OPTIONS 请求
+ */
+ public CompletableFuture sendOptions(String uri) {
+ SIPRequest request = createRequest("OPTIONS", uri);
+ request.setHeader("To", "<" + uri + ">");
+ request.setHeader("From", ";tag=" + generateTag());
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送 MESSAGE 请求
+ */
+ public CompletableFuture sendMessage(String toUri, String fromUri, String contentType, byte[] body) {
+ SIPRequest request = createRequest("MESSAGE", toUri);
+ request.setHeader("To", "<" + toUri + ">");
+ request.setHeader("From", "<" + fromUri + ">;tag=" + generateTag());
+
+ if (body != null && body.length > 0) {
+ request.setBody(body);
+ request.setHeader("Content-Type", contentType);
+ request.setHeader("Content-Length", String.valueOf(body.length));
+ }
+
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送 SUBSCRIBE 请求
+ */
+ public CompletableFuture sendSubscribe(String toUri, String fromUri, String event, int expires) {
+ SIPRequest request = createRequest("SUBSCRIBE", toUri);
+ request.setHeader("To", "<" + toUri + ">");
+ request.setHeader("From", "<" + fromUri + ">;tag=" + generateTag());
+ request.setHeader("Event", event);
+ request.setHeader("Expires", String.valueOf(expires));
+ return sendRequest(request);
+ }
+
+ /**
+ * 发送自定义请求
+ */
+ public CompletableFuture sendRequest(SIPRequest request) {
+ CompletableFuture future = new CompletableFuture<>();
+
+ // 解析目标地址
+ InetSocketAddress address = parseAddress(request.getRequestUri());
+
+ // 创建事务
+ SIPTransaction transaction = transactionManager.createClientTransaction(request, address, "UDP");
+
+ // 发送请求
+ sendUdp(request, address);
+
+ // 设置超时
+ long timeout = properties.getTransaction().getTimeout().toMillis();
+ transaction.getResponseFuture()
+ .orTimeout(timeout, TimeUnit.MILLISECONDS)
+ .thenAccept(response -> {
+ future.complete(response);
+ transactionManager.removeTransaction(transaction);
+ })
+ .exceptionally(e -> {
+ future.completeExceptionally(e);
+ transactionManager.removeTransaction(transaction);
+ return null;
+ });
+
+ return future;
+ }
+
+ /**
+ * 创建 SIP 请求
+ */
+ private SIPRequest createRequest(String method, String requestUri) {
+ SIPRequest request = new SIPRequest(method, requestUri);
+
+ // Call-ID
+ request.setHeader("Call-ID", generateCallId());
+
+ // CSeq
+ request.setHeader("CSeq", new Random().nextInt(1000) + 1 + " " + method);
+
+ // Via
+ String branch = "z9hG4bK" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
+ request.setHeader("Via", "SIP/2.0/UDP " + localHost + ":" + localPort + ";branch=" + branch + ";rport");
+
+ // Contact
+ request.setHeader("Contact", "");
+
+ // Max-Forwards
+ request.setHeader("Max-Forwards", String.valueOf(properties.getUserAgent().getMaxForwards()));
+
+ // User-Agent
+ request.setHeader("User-Agent", properties.getUserAgent().getName());
+
+ // Content-Length
+ request.setHeader("Content-Length", "0");
+
+ return request;
+ }
+
+ /**
+ * 通过 UDP 发送消息
+ */
+ private void sendUdp(SIPMessage message, InetSocketAddress address) {
+ if (udpChannel == null || !udpChannel.isActive()) {
+ logger.error("UDP channel is not active");
+ return;
+ }
+
+ SipDatagramEncoder.SipOutgoingDatagram datagram =
+ new SipDatagramEncoder.SipOutgoingDatagram(message, address);
+ udpChannel.writeAndFlush(datagram);
+ }
+
+ /**
+ * 生成 Call-ID
+ */
+ private String generateCallId() {
+ return UUID.randomUUID().toString().replace("-", "") + "@" + localHost;
+ }
+
+ /**
+ * 生成 tag
+ */
+ private String generateTag() {
+ return UUID.randomUUID().toString().replace("-", "").substring(0, 8);
+ }
+
+ /**
+ * 解析地址
+ */
+ private InetSocketAddress parseAddress(String uri) {
+ try {
+ String addr = uri;
+ if (addr.contains("<")) {
+ addr = addr.substring(addr.indexOf("<") + 1);
+ }
+ if (addr.contains(">")) {
+ addr = addr.substring(0, addr.indexOf(">"));
+ }
+ if (addr.startsWith("sip:")) {
+ addr = addr.substring(4);
+ }
+ if (addr.startsWith("sips:")) {
+ addr = addr.substring(5);
+ }
+
+ // 移除用户部分
+ if (addr.contains("@")) {
+ addr = addr.substring(addr.indexOf("@") + 1);
+ }
+
+ // 移除参数
+ if (addr.contains(";")) {
+ addr = addr.substring(0, addr.indexOf(";"));
+ }
+
+ String host = addr;
+ int port = 5060;
+
+ if (addr.contains(":")) {
+ String[] parts = addr.split(":");
+ host = parts[0];
+ port = Integer.parseInt(parts[1]);
+ }
+
+ return new InetSocketAddress(host, port);
+ } catch (Exception e) {
+ logger.error("Failed to parse address: {}", uri, e);
+ return new InetSocketAddress("127.0.0.1", 5060);
+ }
+ }
+
+ /**
+ * 获取本地 IP
+ */
+ private String getLocalIp() {
+ try {
+ return java.net.InetAddress.getLocalHost().getHostAddress();
+ } catch (Exception e) {
+ return "127.0.0.1";
+ }
+ }
+
+ /**
+ * 检查客户端是否运行中
+ */
+ public boolean isRunning() {
+ return running;
+ }
+
+ /**
+ * 获取本地地址
+ */
+ public String getLocalHost() {
+ return localHost;
+ }
+
+ /**
+ * 获取本地端口
+ */
+ public int getLocalPort() {
+ return localPort;
+ }
+
+ /**
+ * 客户端 UDP 处理器
+ */
+ private class ClientUdpHandler extends SimpleChannelInboundHandler {
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, SipDatagramDecoder.SipDatagramMessage datagram) {
+ InetSocketAddress remoteAddress = datagram.getSender();
+
+ if (datagram.getMessage() instanceof SIPResponse) {
+ SIPResponse response = (SIPResponse) datagram.getMessage();
+
+ // 查找事务
+ SIPTransaction transaction = transactionManager.findTransaction(response);
+ if (transaction != null) {
+ transaction.setResponse(response);
+ }
+
+ // 发布事件
+ if (eventPublisher != null) {
+ eventPublisher.publishEvent(new SipResponseEvent(this, response, remoteAddress, "UDP"));
+ }
+ } else if (datagram.getMessage() instanceof SIPRequest) {
+ SIPRequest request = (SIPRequest) datagram.getMessage();
+
+ // 发布事件
+ if (eventPublisher != null) {
+ eventPublisher.publishEvent(new SipRequestEvent(this, request, remoteAddress, "UDP"));
+ }
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ logger.error("Client UDP handler exception: {}", cause.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/sip/codec/SipDatagramDecoder.java b/src/main/java/com/sip/codec/SipDatagramDecoder.java
new file mode 100644
index 0000000..9a9abd7
--- /dev/null
+++ b/src/main/java/com/sip/codec/SipDatagramDecoder.java
@@ -0,0 +1,66 @@
+package com.sip.codec;
+
+import com.sip.core.SIPMessage;
+import com.sip.parser.SIPParser;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.socket.DatagramPacket;
+import io.netty.handler.codec.MessageToMessageDecoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * SIP UDP 数据报解码器
+ *
+ * 将 UDP 数据报解码为 SIPMessage 对象
+ *
+ */
+public class SipDatagramDecoder extends MessageToMessageDecoder {
+
+ private static final Logger logger = LoggerFactory.getLogger(SipDatagramDecoder.class);
+
+ @Override
+ protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List