Extending WebSocket Transports

 

Author: Eike Stepper

Net4j exposes a provider boundary for WebSocket implementations that need different client or server libraries. This article describes the supported factory and message-connection contracts and the obligations an implementation must satisfy. It deliberately does not present a fabricated third-party transport implementation.

Table of Contents

1 Configuring and Registering Client Providers
2 The Message-Connection Factory Contract
3 Implementing a Different WebSocket Integration
4 Threading, Buffering, and Backpressure
5 Lifecycle and Testing

1  Configuring and Registering Client Providers

The built-in JDK provider is a public utility for configuring a registered factory. Give each distinct client policy its own provider type and register the factory in the container that owns the connectors. The connector description's provider: token selects that factory. This example uses the existing JDK provider with a custom opening timeout; the application still owns the HTTP client. Java 11 has no HttpClient.close(). On Java 21 and later, the application may close its client only after every connector using that client has stopped; the provider factory does not own or close it.

RegisterJDKProvider.java      
IManagedContainer container = ContainerUtil.createInitializedContainer();
HttpClient client = null;
IWSConnector connector = null;

try
{
  client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(7)).build();
  IFactory provider = JDKWSUtil.createMessageConnectionFactory("partner-client", client, Duration.ofSeconds(9));
  container.registerFactory(provider);

  connector = WSSUtil.getConnector(container, URI.create("wss://partner.example.net/net4j"), "partner", "provider:partner-client");
  connector.connect(15_000);
  // Use this connector with the partner's Net4j protocol.
}
finally
{
  try
  {
    if (connector != null)
    {
      connector.close();
    }
  }
  finally
  {
    LifecycleUtil.deactivate(container);
  }
}

2  The Message-Connection Factory Contract

A custom provider extends the SPI MessageConnectionFactory and implements create(String description). It returns a fresh, not-yet-started IMessageConnection for each connector. The factory may retain shared immutable configuration or a thread-safe client, but each returned connection must have independent runtime state and be owned by its requesting connector.

Register providers under MessageConnectionFactory.PRODUCT_GROUP with a stable type string. In OSGi, the bundle can contribute a factory through the standard Net4j factories extension point; programmatic registration is useful when the provider depends on application-created configuration. The selected type must be present in the connector's managed container before the provider connection is created.

3  Implementing a Different WebSocket Integration

The supported provider boundary is the message-connection SPI, not Net4j's internal JDK or Jetty implementation classes. A client provider must perform its own handshake, emit complete Net4j frames, accept outgoing frames, and report readiness, received data, writability, and closure to the message connection listener. A server provider must perform upgrade routing and pass each accepted transport to IWSAcceptor.accept(IMessageConnection).

On the server, Jetty's adapter demonstrates one integration policy: it scopes a path mapping to an explicit set of active acceptors and requires the path route and Net4jAcceptor header to agree. That policy is specific to the Jetty adapter; do not treat its internal classes as extension APIs.

The public IWSConnector, IWSAcceptor, and IMessageConnection contracts are appropriate integration points. Packages named internal are implementation details, even when visible in a developer workspace or test bundle.

4  Threading, Buffering, and Backpressure

Providers must treat send buffers as borrowed storage. When send accepts a buffer, it returns true and invokes exactly one terminal callback only after it stops accessing that storage. If it returns false or throws before accepting the buffer, it must not retain the buffer or invoke the callback. It must complete accepted sends during shutdown as well as normal operation.

Receive demand counts complete messages. Each delivery consumes one requested message, deliveries remain ordered, and each provided storage lease remains valid until its release action runs. The connector may request more data while an earlier frame is still leased, so a provider must retain each frame's storage independently. Abort must not wait indefinitely for callbacks.

Keep provider callbacks non-blocking where the underlying WebSocket API permits asynchronous completion. Do not add another unbounded queue that defeats Net4j's demand and buffer ownership contracts.

5  Lifecycle and Testing

A provider factory belongs to its managed container; each connection belongs to one Net4j connector. Connector activation starts its provider connection and connector deactivation closes or aborts it according to the transport lifecycle. A factory must not close a shared HttpClient that belongs to the application.

Test factory registration and selection, independent state across two connections, failed handshakes, send acceptance and rejection, exactly-once completion, ordered receive demand, buffer release timing, graceful close, abort, and reactivation. The repository's JDK provider configuration and Jetty server adapter tests are useful references for scenario setup and lifecycle assertions. Run a test through the appropriate Net4j suite rather than relying only on direct construction when validating integration behavior.