headers = new LinkedHashMap<>();
+
+ Builder() { }
+
+ public Builder secure(boolean secure) { this.secure = secure; return this; }
+ public Builder user(String user) { this.user = user; return this; }
+ public Builder password(String password) { this.password = password; return this; }
+ public Builder host(String host) { this.host = host; return this; }
+ public Builder port(int port) { this.port = port; return this; }
+ public Builder param(String name, String v) { params.put(name, v == null ? "" : v); return this; }
+ public Builder header(String name, String v) { headers.put(name, v == null ? "" : v); return this; }
+
+ public SipUri build() {
+ return new SipUri(
+ secure,
+ Optional.ofNullable(user),
+ Optional.ofNullable(password),
+ host,
+ port,
+ params,
+ headers);
+ }
+ }
+}
diff --git a/sip-message/src/main/java/com/sip/message/uri/Uri.java b/sip-message/src/main/java/com/sip/message/uri/Uri.java
new file mode 100644
index 0000000..528e41c
--- /dev/null
+++ b/sip-message/src/main/java/com/sip/message/uri/Uri.java
@@ -0,0 +1,16 @@
+package com.sip.message.uri;
+
+/**
+ * Abstract base for URIs that may appear in a SIP Request-Line or header
+ * (RFC 3261 §19.1).
+ *
+ * SIP messages can carry {@code sip:}, {@code sips:}, {@code tel:}, and
+ * absolute URIs in general. This sealed hierarchy keeps message types total
+ * while leaving room for additional schemes to be added without touching
+ * call sites that already pattern-match on the known cases.
+ */
+public sealed interface Uri permits SipUri, OpaqueUri {
+
+ /** URI scheme, lowercase (e.g. {@code sip}, {@code sips}, {@code tel}). */
+ String scheme();
+}
diff --git a/sip-message/src/main/java/module-info.java b/sip-message/src/main/java/module-info.java
new file mode 100644
index 0000000..bc93f69
--- /dev/null
+++ b/sip-message/src/main/java/module-info.java
@@ -0,0 +1,11 @@
+/**
+ * Immutable SIP message model: methods, URIs, headers, requests, responses.
+ *
+ * This module is the foundation of the stack and must remain dependency-free
+ * (apart from {@code java.base}). No logging, no networking, no framework imports.
+ */
+module com.sip.message {
+ exports com.sip.message;
+ exports com.sip.message.header;
+ exports com.sip.message.uri;
+}
diff --git a/sip-message/src/test/java/com/sip/message/SipMessagePatternMatchTest.java b/sip-message/src/test/java/com/sip/message/SipMessagePatternMatchTest.java
new file mode 100644
index 0000000..952e495
--- /dev/null
+++ b/sip-message/src/test/java/com/sip/message/SipMessagePatternMatchTest.java
@@ -0,0 +1,35 @@
+package com.sip.message;
+
+import com.sip.message.header.Headers;
+import com.sip.message.uri.SipUri;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Demonstrates that {@link SipMessage} is a sealed hierarchy and pattern
+ * matching is exhaustive without a default branch. If a new permitted subtype
+ * is ever added, this test will fail to compile — a desired property.
+ */
+class SipMessagePatternMatchTest {
+
+ @Test
+ void switchOnSipMessageIsExhaustive() {
+ SipMessage req = SipRequest.builder()
+ .method(SipMethod.OPTIONS)
+ .requestUri(SipUri.builder().host("example.com").build())
+ .headers(Headers.empty())
+ .build();
+ SipMessage rsp = SipResponse.builder().status(200).reason("OK").build();
+
+ assertThat(summarize(req)).startsWith("REQ ");
+ assertThat(summarize(rsp)).isEqualTo("RSP 200");
+ }
+
+ private static String summarize(SipMessage m) {
+ return switch (m) {
+ case SipRequest r -> "REQ " + r.method();
+ case SipResponse r -> "RSP " + r.status();
+ };
+ }
+}
diff --git a/sip-message/src/test/java/com/sip/message/SipMethodTest.java b/sip-message/src/test/java/com/sip/message/SipMethodTest.java
new file mode 100644
index 0000000..7b5cdac
--- /dev/null
+++ b/sip-message/src/test/java/com/sip/message/SipMethodTest.java
@@ -0,0 +1,44 @@
+package com.sip.message;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SipMethodTest {
+
+ @Test
+ void wellKnownMethodsAreInterned() {
+ assertThat(SipMethod.of("INVITE")).isSameAs(SipMethod.INVITE);
+ assertThat(SipMethod.of("REGISTER")).isSameAs(SipMethod.REGISTER);
+ assertThat(SipMethod.of("ACK")).isSameAs(SipMethod.ACK);
+ }
+
+ @Test
+ void methodTokensAreCaseSensitive() {
+ // RFC 3261 §7.1 — method names are case-sensitive
+ SipMethod custom = SipMethod.of("invite");
+ assertThat(custom).isNotSameAs(SipMethod.INVITE);
+ assertThat(custom.name()).isEqualTo("invite");
+ }
+
+ @Test
+ void customExtensionMethodIsAccepted() {
+ SipMethod custom = SipMethod.of("PING");
+ assertThat(custom.name()).isEqualTo("PING");
+ }
+
+ @Test
+ void rejectsEmptyToken() {
+ assertThatThrownBy(() -> SipMethod.of(""))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void rejectsIllegalCharacters() {
+ assertThatThrownBy(() -> SipMethod.of("BAD METHOD"))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> SipMethod.of("BAD/METHOD"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+}
diff --git a/sip-message/src/test/java/com/sip/message/header/HeaderNameTest.java b/sip-message/src/test/java/com/sip/message/header/HeaderNameTest.java
new file mode 100644
index 0000000..ac7f354
--- /dev/null
+++ b/sip-message/src/test/java/com/sip/message/header/HeaderNameTest.java
@@ -0,0 +1,44 @@
+package com.sip.message.header;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class HeaderNameTest {
+
+ @Test
+ void equalityIsCaseInsensitive() {
+ assertThat(HeaderName.of("call-id")).isEqualTo(HeaderName.of("CALL-ID"));
+ assertThat(HeaderName.of("Content-Type"))
+ .isEqualTo(HeaderName.of("CONTENT-TYPE"));
+ }
+
+ @Test
+ void compactFormsExpand() {
+ assertThat(HeaderName.of("v")).isEqualTo(HeaderName.VIA);
+ assertThat(HeaderName.of("f")).isEqualTo(HeaderName.FROM);
+ assertThat(HeaderName.of("t")).isEqualTo(HeaderName.TO);
+ assertThat(HeaderName.of("i")).isEqualTo(HeaderName.CALL_ID);
+ assertThat(HeaderName.of("m")).isEqualTo(HeaderName.CONTACT);
+ assertThat(HeaderName.of("l")).isEqualTo(HeaderName.CONTENT_LENGTH);
+ assertThat(HeaderName.of("c")).isEqualTo(HeaderName.CONTENT_TYPE);
+ }
+
+ @Test
+ void canonicalFormPreservesOriginalCasing() {
+ // The first registration wins on display casing — but equality is
+ // case-insensitive, so the stack will always match correctly.
+ HeaderName h = HeaderName.of("X-My-Header");
+ assertThat(h.canonical()).isEqualTo("X-My-Header");
+ assertThat(h.lowercase()).isEqualTo("x-my-header");
+ }
+
+ @Test
+ void rejectsIllegalCharacters() {
+ assertThatThrownBy(() -> HeaderName.of("Bad Header"))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> HeaderName.of(""))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+}
diff --git a/sip-transaction/pom.xml b/sip-transaction/pom.xml
new file mode 100644
index 0000000..596dcec
--- /dev/null
+++ b/sip-transaction/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+
+ com.sip
+ sip-stack-parent
+ 0.1.0-SNAPSHOT
+
+
+ sip-transaction
+ SIP :: Transaction Layer
+
+ RFC 3261 §17 transaction layer: INVITE / Non-INVITE client and server
+ state machines, retransmission and timer management.
+
+
+
+
+ com.sip
+ sip-message
+
+
+ com.sip
+ sip-transport-api
+
+
+ org.slf4j
+ slf4j-api
+
+
+
diff --git a/sip-transaction/src/main/java/com/sip/transaction/TransactionLayer.java b/sip-transaction/src/main/java/com/sip/transaction/TransactionLayer.java
new file mode 100644
index 0000000..b884acf
--- /dev/null
+++ b/sip-transaction/src/main/java/com/sip/transaction/TransactionLayer.java
@@ -0,0 +1,16 @@
+package com.sip.transaction;
+
+/**
+ * Scaffold marker for the transaction-layer module.
+ *
+ *
This type exists so the {@code com.sip.transaction} JPMS package is
+ * non-empty before the concrete FSM lands. It is intentionally not part
+ * of any stable contract and may be removed once the layer is real.
+ */
+public final class TransactionLayer {
+
+ /** Indicates whether the layer is wired up. Always {@code false} for now. */
+ public static final boolean IMPLEMENTED = false;
+
+ private TransactionLayer() { }
+}
diff --git a/sip-transaction/src/main/java/com/sip/transaction/package-info.java b/sip-transaction/src/main/java/com/sip/transaction/package-info.java
new file mode 100644
index 0000000..a52e815
--- /dev/null
+++ b/sip-transaction/src/main/java/com/sip/transaction/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Transaction layer scaffold. Concrete state machines, timer wheel and
+ * transaction table land in the next iteration. The package is published
+ * now so higher modules can depend on a stable module name.
+ */
+package com.sip.transaction;
diff --git a/sip-transaction/src/main/java/module-info.java b/sip-transaction/src/main/java/module-info.java
new file mode 100644
index 0000000..971ab21
--- /dev/null
+++ b/sip-transaction/src/main/java/module-info.java
@@ -0,0 +1,13 @@
+/**
+ * Transaction layer (RFC 3261 §17).
+ *
+ * Owns the four FSMs (INVITE-client, INVITE-server, Non-INVITE-client,
+ * Non-INVITE-server), the eleven timers (A..K), and the transaction table.
+ */
+module com.sip.transaction {
+ requires transitive com.sip.message;
+ requires transitive com.sip.transport.api;
+ requires org.slf4j;
+
+ exports com.sip.transaction;
+}
diff --git a/sip-transport-api/pom.xml b/sip-transport-api/pom.xml
new file mode 100644
index 0000000..08207a9
--- /dev/null
+++ b/sip-transport-api/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+
+ com.sip
+ sip-stack-parent
+ 0.1.0-SNAPSHOT
+
+
+ sip-transport-api
+ SIP :: Transport SPI
+
+ Pure interfaces that decouple the protocol core from any specific
+ networking implementation (NIO, Netty, Vert.x, in-memory test fakes).
+
+
+
+
+ com.sip
+ sip-message
+
+
+
diff --git a/sip-transport-api/src/main/java/com/sip/transport/Endpoint.java b/sip-transport-api/src/main/java/com/sip/transport/Endpoint.java
new file mode 100644
index 0000000..07aea15
--- /dev/null
+++ b/sip-transport-api/src/main/java/com/sip/transport/Endpoint.java
@@ -0,0 +1,27 @@
+package com.sip.transport;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * A network endpoint: transport + host + port.
+ *
+ * Carries enough information to drive RFC 3261 §18 send rules and the
+ * {@code Via} {@code sent-by} computation without exposing the underlying
+ * I/O implementation.
+ */
+public record Endpoint(TransportType transport, InetSocketAddress address) {
+
+ public Endpoint {
+ Objects.requireNonNull(transport, "transport");
+ Objects.requireNonNull(address, "address");
+ }
+
+ public String host() {
+ return address.getHostString();
+ }
+
+ public int port() {
+ return address.getPort();
+ }
+}
diff --git a/sip-transport-api/src/main/java/com/sip/transport/InboundMessage.java b/sip-transport-api/src/main/java/com/sip/transport/InboundMessage.java
new file mode 100644
index 0000000..39aba20
--- /dev/null
+++ b/sip-transport-api/src/main/java/com/sip/transport/InboundMessage.java
@@ -0,0 +1,33 @@
+package com.sip.transport;
+
+import com.sip.message.SipMessage;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * A SIP message received from the network.
+ *
+ * Captures the raw {@link SipMessage} alongside the peer endpoint, the
+ * local endpoint that received it, and the receive timestamp. These three
+ * pieces of metadata are required by higher layers:
+ *
+ *
+ * - Transaction layer: select source / response routing (RFC 3261 §18).
+ * - Dialog layer: derive {@code received} / {@code rport} (RFC 3581).
+ * - Timers and metrics: use {@code receivedAt} as the reference time.
+ *
+ */
+public record InboundMessage(
+ SipMessage message,
+ Endpoint peer,
+ Endpoint local,
+ Instant receivedAt) {
+
+ public InboundMessage {
+ Objects.requireNonNull(message, "message");
+ Objects.requireNonNull(peer, "peer");
+ Objects.requireNonNull(local, "local");
+ Objects.requireNonNull(receivedAt, "receivedAt");
+ }
+}
diff --git a/sip-transport-api/src/main/java/com/sip/transport/MessageListener.java b/sip-transport-api/src/main/java/com/sip/transport/MessageListener.java
new file mode 100644
index 0000000..77acd8b
--- /dev/null
+++ b/sip-transport-api/src/main/java/com/sip/transport/MessageListener.java
@@ -0,0 +1,20 @@
+package com.sip.transport;
+
+/**
+ * Receives messages from a {@link Transport}.
+ *
+ * The contract is one-shot per call and the listener is expected to
+ * complete quickly. Long-running work should be dispatched onto a
+ * virtual-thread executor managed by the caller.
+ */
+@FunctionalInterface
+public interface MessageListener {
+
+ void onMessage(InboundMessage inbound);
+
+ /**
+ * Invoked when a low-level I/O error is observed for a peer connection.
+ * Default implementation is a no-op so that listeners can opt in.
+ */
+ default void onError(Endpoint peer, Throwable error) { }
+}
diff --git a/sip-transport-api/src/main/java/com/sip/transport/Transport.java b/sip-transport-api/src/main/java/com/sip/transport/Transport.java
new file mode 100644
index 0000000..a312e13
--- /dev/null
+++ b/sip-transport-api/src/main/java/com/sip/transport/Transport.java
@@ -0,0 +1,53 @@
+package com.sip.transport;
+
+import com.sip.message.SipMessage;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Pluggable transport. One instance binds a single local endpoint
+ * ({@code transport + host:port}) and exposes send/receive APIs.
+ *
+ * Implementations:
+ *
+ * - {@code sip-transport-nio} — JDK 21 NIO + virtual threads (default).
+ * - {@code sip-transport-netty} — optional, for extreme throughput.
+ * - Tests may provide in-memory implementations that loop messages
+ * between paired endpoints.
+ *
+ *
+ * {@link AutoCloseable} so transports can be used in try-with-resources
+ * and integration tests can deterministically tear them down.
+ */
+public interface Transport extends AutoCloseable {
+
+ /** The transport type this implementation serves. */
+ TransportType type();
+
+ /** The bound local endpoint. Valid only after {@link #start()} completes. */
+ Endpoint local();
+
+ /**
+ * Binds the socket(s) and begins accepting traffic.
+ *
+ * The returned future completes when the transport is ready to send
+ * and receive. Failures (e.g. address in use) complete the future
+ * exceptionally.
+ */
+ CompletableFuture start();
+
+ /**
+ * Sends {@code message} to {@code peer}. For connection-oriented
+ * transports the implementation transparently opens or reuses a
+ * connection per RFC 5923. Completion of the future indicates the bytes
+ * are handed to the kernel; it is not a delivery acknowledgement.
+ */
+ CompletableFuture send(SipMessage message, Endpoint peer);
+
+ /** Registers a listener; replaces any previous registration. */
+ void listener(MessageListener listener);
+
+ @Override
+ void close() throws IOException;
+}
diff --git a/sip-transport-api/src/main/java/com/sip/transport/TransportType.java b/sip-transport-api/src/main/java/com/sip/transport/TransportType.java
new file mode 100644
index 0000000..f6e9914
--- /dev/null
+++ b/sip-transport-api/src/main/java/com/sip/transport/TransportType.java
@@ -0,0 +1,26 @@
+package com.sip.transport;
+
+/**
+ * SIP transport protocols recognized by the stack (RFC 3261 §18, RFC 7118).
+ *
+ * {@link #SCTP} is reserved for future use and not enabled by the default
+ * NIO transport implementation.
+ */
+public enum TransportType {
+ UDP,
+ TCP,
+ TLS,
+ WS,
+ WSS,
+ SCTP;
+
+ /** {@code true} when this transport is reliable (RFC 3261 §17.1.1.2). */
+ public boolean isReliable() {
+ return this != UDP;
+ }
+
+ /** {@code true} when this transport carries TLS (RFC 3261 §26, RFC 7118). */
+ public boolean isSecure() {
+ return this == TLS || this == WSS;
+ }
+}
diff --git a/sip-transport-api/src/main/java/com/sip/transport/package-info.java b/sip-transport-api/src/main/java/com/sip/transport/package-info.java
new file mode 100644
index 0000000..02806cb
--- /dev/null
+++ b/sip-transport-api/src/main/java/com/sip/transport/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Transport SPI.
+ *
+ * Higher layers (transaction, dialog, UA) depend only on this package,
+ * never on a specific I/O implementation. The default NIO implementation
+ * ships in {@code sip-transport-nio} and is wired in via
+ * {@link java.util.ServiceLoader} or explicit composition.
+ */
+package com.sip.transport;
diff --git a/sip-transport-api/src/main/java/module-info.java b/sip-transport-api/src/main/java/module-info.java
new file mode 100644
index 0000000..a45c7c3
--- /dev/null
+++ b/sip-transport-api/src/main/java/module-info.java
@@ -0,0 +1,9 @@
+/**
+ * Pure transport SPI. Implementations (NIO, Netty, in-memory fakes) live in
+ * sibling modules and bind to this contract.
+ */
+module com.sip.transport.api {
+ requires transitive com.sip.message;
+
+ exports com.sip.transport;
+}
diff --git a/sip-transport-nio/pom.xml b/sip-transport-nio/pom.xml
new file mode 100644
index 0000000..10bb100
--- /dev/null
+++ b/sip-transport-nio/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+
+ com.sip
+ sip-stack-parent
+ 0.1.0-SNAPSHOT
+
+
+ sip-transport-nio
+ SIP :: Transport (NIO + Virtual Threads)
+
+ Default transport implementation. UDP via NIO DatagramChannel,
+ TCP/TLS via java.net.Socket + virtual threads (JDK 21+).
+ No third-party runtime dependencies.
+
+
+
+
+ com.sip
+ sip-message
+
+
+ com.sip
+ sip-codec
+
+
+ com.sip
+ sip-transport-api
+
+
+ org.slf4j
+ slf4j-api
+
+
+
diff --git a/sip-transport-nio/src/main/java/com/sip/transport/nio/NioTcpTransport.java b/sip-transport-nio/src/main/java/com/sip/transport/nio/NioTcpTransport.java
new file mode 100644
index 0000000..9def796
--- /dev/null
+++ b/sip-transport-nio/src/main/java/com/sip/transport/nio/NioTcpTransport.java
@@ -0,0 +1,71 @@
+package com.sip.transport.nio;
+
+import com.sip.message.SipMessage;
+import com.sip.transport.Endpoint;
+import com.sip.transport.MessageListener;
+import com.sip.transport.Transport;
+import com.sip.transport.TransportType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * TCP transport built on {@link java.net.ServerSocket} + virtual threads.
+ *
+ * One virtual thread per accepted connection, one virtual thread per
+ * outbound dial. The JVM multiplexes them onto a small pool of carrier
+ * threads; we never have to write event-loop code.
+ *
+ * Framing follows RFC 3261 §18.3 — the parser must consume
+ * {@code Content-Length} bytes after the header CRLFCRLF.
+ */
+public final class NioTcpTransport implements Transport {
+
+ private static final Logger log = LoggerFactory.getLogger(NioTcpTransport.class);
+
+ private final InetSocketAddress bindAddress;
+ private volatile MessageListener listener;
+ private volatile Endpoint local;
+
+ public NioTcpTransport(InetSocketAddress bindAddress) {
+ this.bindAddress = bindAddress;
+ }
+
+ @Override
+ public TransportType type() {
+ return TransportType.TCP;
+ }
+
+ @Override
+ public Endpoint local() {
+ return local;
+ }
+
+ @Override
+ public CompletableFuture start() {
+ log.debug("TCP transport scaffold for {}", bindAddress);
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "NioTcpTransport is a scaffold; binding lands in the next iteration."));
+ }
+
+ @Override
+ public CompletableFuture send(SipMessage message, Endpoint peer) {
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "NioTcpTransport.send not yet implemented."));
+ }
+
+ @Override
+ public void listener(MessageListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void close() throws IOException {
+ log.debug("TCP transport close (no-op while scaffold)");
+ }
+}
diff --git a/sip-transport-nio/src/main/java/com/sip/transport/nio/NioUdpTransport.java b/sip-transport-nio/src/main/java/com/sip/transport/nio/NioUdpTransport.java
new file mode 100644
index 0000000..38f8d96
--- /dev/null
+++ b/sip-transport-nio/src/main/java/com/sip/transport/nio/NioUdpTransport.java
@@ -0,0 +1,78 @@
+package com.sip.transport.nio;
+
+import com.sip.message.SipMessage;
+import com.sip.transport.Endpoint;
+import com.sip.transport.MessageListener;
+import com.sip.transport.Transport;
+import com.sip.transport.TransportType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * UDP transport built directly on NIO {@link java.nio.channels.DatagramChannel}.
+ *
+ * The implementation lands in the next iteration. The shape of the class
+ * is locked in now so higher layers can compile against it.
+ *
+ * Design notes
+ *
+ * - One {@code DatagramChannel} per bound endpoint.
+ * - A single dedicated platform thread runs the read loop. Each received
+ * datagram is handed off to a virtual thread for parsing and dispatch.
+ * - Outbound sends are non-blocking on the kernel side; we never block
+ * the carrier thread.
+ * - UDP MTU guard (RFC 3261 §18.1.1): messages near the MTU are flagged
+ * and may trigger a downgrade to TCP at a higher layer.
+ *
+ */
+public final class NioUdpTransport implements Transport {
+
+ private static final Logger log = LoggerFactory.getLogger(NioUdpTransport.class);
+
+ private final InetSocketAddress bindAddress;
+ private volatile MessageListener listener;
+ private volatile Endpoint local;
+
+ public NioUdpTransport(InetSocketAddress bindAddress) {
+ this.bindAddress = bindAddress;
+ }
+
+ @Override
+ public TransportType type() {
+ return TransportType.UDP;
+ }
+
+ @Override
+ public Endpoint local() {
+ return local;
+ }
+
+ @Override
+ public CompletableFuture start() {
+ log.debug("UDP transport scaffold for {}", bindAddress);
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "NioUdpTransport is a scaffold; binding lands in the next iteration."));
+ }
+
+ @Override
+ public CompletableFuture send(SipMessage message, Endpoint peer) {
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException(
+ "NioUdpTransport.send not yet implemented."));
+ }
+
+ @Override
+ public void listener(MessageListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void close() throws IOException {
+ log.debug("UDP transport close (no-op while scaffold)");
+ }
+}
diff --git a/sip-transport-nio/src/main/java/module-info.java b/sip-transport-nio/src/main/java/module-info.java
new file mode 100644
index 0000000..6b10bb4
--- /dev/null
+++ b/sip-transport-nio/src/main/java/module-info.java
@@ -0,0 +1,17 @@
+/**
+ * Default transport implementation built on JDK 21 NIO and virtual threads.
+ *
+ *
+ * - UDP: a single NIO {@code DatagramChannel} per bound endpoint with a
+ * dedicated read-loop thread.
+ * - TCP / TLS: classic blocking sockets, one virtual thread per
+ * connection — the JVM multiplexes them onto carrier threads.
+ *
+ */
+module com.sip.transport.nio {
+ requires transitive com.sip.transport.api;
+ requires com.sip.codec;
+ requires org.slf4j;
+
+ exports com.sip.transport.nio;
+}
diff --git a/sip-ua/pom.xml b/sip-ua/pom.xml
new file mode 100644
index 0000000..1972765
--- /dev/null
+++ b/sip-ua/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+
+ com.sip
+ sip-stack-parent
+ 0.1.0-SNAPSHOT
+
+
+ sip-ua
+ SIP :: User Agent
+
+ High-level UA APIs used by applications (UAC/UAS, registrar client,
+ eventing). The first concrete target is a GB28181-friendly UAC/UAS.
+
+
+
+
+ com.sip
+ sip-message
+
+
+ com.sip
+ sip-codec
+
+
+ com.sip
+ sip-transport-api
+
+
+ com.sip
+ sip-transaction
+
+
+ com.sip
+ sip-dialog
+
+
+ org.slf4j
+ slf4j-api
+
+
+
diff --git a/sip-ua/src/main/java/com/sip/ua/SipStackInfo.java b/sip-ua/src/main/java/com/sip/ua/SipStackInfo.java
new file mode 100644
index 0000000..5584bd3
--- /dev/null
+++ b/sip-ua/src/main/java/com/sip/ua/SipStackInfo.java
@@ -0,0 +1,18 @@
+package com.sip.ua;
+
+/**
+ * Build-time metadata for the SIP stack.
+ *
+ * Acts as both the human-readable version anchor and the JPMS-mandated
+ * non-empty type for the {@code com.sip.ua} package.
+ */
+public final class SipStackInfo {
+
+ /** Semantic version of the published artifacts. */
+ public static final String VERSION = "0.1.0-SNAPSHOT";
+
+ /** Project codename. */
+ public static final String NAME = "sip-stack";
+
+ private SipStackInfo() { }
+}
diff --git a/sip-ua/src/main/java/com/sip/ua/package-info.java b/sip-ua/src/main/java/com/sip/ua/package-info.java
new file mode 100644
index 0000000..5af57af
--- /dev/null
+++ b/sip-ua/src/main/java/com/sip/ua/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * UA scaffold. Once message + codec + transaction + dialog are real, this
+ * module will host {@code SipStack}, {@code UserAgent}, and the high-level
+ * facade users see.
+ */
+package com.sip.ua;
diff --git a/sip-ua/src/main/java/module-info.java b/sip-ua/src/main/java/module-info.java
new file mode 100644
index 0000000..6faf980
--- /dev/null
+++ b/sip-ua/src/main/java/module-info.java
@@ -0,0 +1,17 @@
+/**
+ * High-level UA APIs used by applications.
+ *
+ * The first concrete target on the roadmap is a GB28181-friendly
+ * UAC/UAS able to register against a domain server and exchange the
+ * national-standard control messages.
+ */
+module com.sip.ua {
+ requires transitive com.sip.message;
+ requires transitive com.sip.transport.api;
+ requires transitive com.sip.transaction;
+ requires transitive com.sip.dialog;
+ requires com.sip.codec;
+ requires org.slf4j;
+
+ exports com.sip.ua;
+}
diff --git a/src/main/java/com/sip/annotation/EnableSipServer.java b/src/main/java/com/sip/annotation/EnableSipServer.java
deleted file mode 100644
index 7bf3aef..0000000
--- a/src/main/java/com/sip/annotation/EnableSipServer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index a86133b..0000000
--- a/src/main/java/com/sip/annotation/SipHandler.java
+++ /dev/null
@@ -1,37 +0,0 @@
-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
deleted file mode 100644
index 393d57b..0000000
--- a/src/main/java/com/sip/annotation/SipMethod.java
+++ /dev/null
@@ -1,39 +0,0 @@
-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
deleted file mode 100644
index 99d9c97..0000000
--- a/src/main/java/com/sip/autoconfigure/SipAutoConfiguration.java
+++ /dev/null
@@ -1,104 +0,0 @@
-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
deleted file mode 100644
index f0bd92a..0000000
--- a/src/main/java/com/sip/autoconfigure/SipServerRunner.java
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index 5393876..0000000
--- a/src/main/java/com/sip/client/SipClient.java
+++ /dev/null
@@ -1,478 +0,0 @@
-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
deleted file mode 100644
index 9a9abd7..0000000
--- a/src/main/java/com/sip/codec/SipDatagramDecoder.java
+++ /dev/null
@@ -1,66 +0,0 @@
-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