Skip to content

Commit

Permalink
Support Unix Domain Socket connections (#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpgrayson committed Nov 28, 2023
1 parent 55f6921 commit a370d9e
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 8 deletions.
7 changes: 6 additions & 1 deletion .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# run this with docker-compose -f docker/docker-compose.yml run test
# run this with: docker-compose -f docker-compose.yml run test
version: "3.3"

services:
app:
image: swift:5.9
volumes:
- ..:/workspace
- mosquitto-socket:/workspace/mosquitto/socket
depends_on:
- mosquitto
environment:
Expand All @@ -18,8 +19,12 @@ services:
volumes:
- ../mosquitto/config:/mosquitto/config
- ../mosquitto/certs:/mosquitto/certs
- mosquitto-socket:/mosquitto/socket
ports:
- "1883:1883"
- "8883:8883"
- "8080:8080"
- "8081:8081"

volumes:
mosquitto-socket:
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
volumes:
- ${{ github.workspace }}/mosquitto/config:/mosquitto/config
- ${{ github.workspace }}/mosquitto/certs:/mosquitto/certs
- ${{ github.workspace }}/mosquitto/socket:/mosquitto/socket

steps:
- name: Checkout
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ MQTTNIO is a Swift NIO based implementation of a MQTT client. It supports
- WebSocket connections
- Posix sockets
- Apple's Network framework via [NIOTransportServices](https://github.com/apple/swift-nio-transport-services) (required for iOS).
- Unix domain sockets

You can find documentation for MQTTNIO
[here](https://swift-server-community.github.io/mqtt-nio/documentation/mqttnio/). There is also a sample demonstrating the use MQTTNIO in an iOS app found [here](https://github.com/adam-fowler/EmCuTeeTee)
24 changes: 24 additions & 0 deletions Sources/MQTTNIO/MQTTClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,30 @@ public final class MQTTClient {
self.inflight = .init()
}

/// Create MQTT client
/// - Parameters:
/// - unixSocketPath: Path to unix socket of MQTT broker
/// - identifier: Client identifier. This must be unique
/// - eventLoopGroupProvider: EventLoopGroup to run on
/// - logger: Logger client should use
/// - configuration: Configuration of client
public convenience init(
unixSocketPath: String,
identifier: String,
eventLoopGroupProvider: NIOEventLoopGroupProvider,
logger: Logger? = nil,
configuration: Configuration = Configuration()
) {
self.init(
host: unixSocketPath,
port: 0,
identifier: identifier,
eventLoopGroupProvider: eventLoopGroupProvider,
logger: logger,
configuration: configuration
)
}

deinit {
guard isShutdown.load(ordering: .relaxed) else {
preconditionFailure("Client not shut down before the deinit. Please call client.syncShutdownGracefully() when no longer needed.")
Expand Down
12 changes: 10 additions & 2 deletions Sources/MQTTNIO/MQTTConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ final class MQTTConnection {
do {
// get bootstrap based off what eventloop we are running on
let bootstrap = try getBootstrap(client: client)
bootstrap
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.connectTimeout(client.configuration.connectTimeout)
Expand Down Expand Up @@ -75,7 +74,16 @@ final class MQTTConnection {
return channel.pipeline.addHandlers(handlers)
}
}
.connect(host: client.host, port: client.port)

let channelFuture: EventLoopFuture<Channel>

if client.port == 0 {
channelFuture = bootstrap.connect(unixDomainSocketPath: client.host)
} else {
channelFuture = bootstrap.connect(host: client.host, port: client.port)
}

channelFuture
.map { channel in
if !client.configuration.useWebSockets {
channelPromise.succeed(channel)
Expand Down
22 changes: 21 additions & 1 deletion Sources/MQTTNIO/MQTTNIO.docc/mqttnio-connections.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Connections

Support for TLS and WebSockets.
Support for TLS, WebSockets, and Unix Domain Sockets.

## TLS

Expand Down Expand Up @@ -31,3 +31,23 @@ MQTT also supports Web Socket connections. Provide a `WebSocketConfiguration` wh
## NIO Transport Services

On macOS and iOS you can use the NIO Transport Services library (NIOTS) and Apple's `Network.framework` for communication with the MQTT broker. If you don't provide an `eventLoopGroup` or a `TLSConfigurationType` then this is the default for both platforms. If you do provide either of these then the library will base it's decision on whether to use NIOTS or NIOSSL on what you provide. Provide a `MultiThreadedEventLoopGroup` or `NIOSSL.TLSConfiguration` and the client will use NIOSSL. Provide a `NIOTSEventLoopGroup` or `TSTLSConfiguration` and the client will use NIOTS. If you provide a `MultiThreadedEventLoopGroup` and a `TSTLSConfiguration` then the client will throw an error. If you are running on iOS you should always choose NIOTS.

## Unix Domain Sockets

MQTT NIO can connect to a local MQTT broker via a Unix Domain Socket.

```swift
let client = MQTTClient(
unixSocketPath: "/path/to/broker.socket",
identifier: "UDSClient",
eventLoopGroupProvider: .createNew
)
```

Under the hood, `MQTTClient.port` will be 0 and `MQTTClient.host` will be the specified unix socket path when connecting to a unix socket.

Note that mosquitto supports listening on a unix domain socket. This can be enabled by adding a `listener` option to the mosquitto config.

```
listener 0 /path/to/broker.socket
```
14 changes: 14 additions & 0 deletions Tests/MQTTNIOTests/MQTTNIOTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ final class MQTTNIOTests: XCTestCase {
}
#endif

func testUnixDomainConnect() throws {
let client = MQTTClient(
unixSocketPath: MQTTNIOTests.rootPath + "/mosquitto/socket/mosquitto.sock",
identifier: "testUnixDomainConnect",
eventLoopGroupProvider: .createNew,
logger: self.logger,
configuration: .init()
)
defer { XCTAssertNoThrow(try client.syncShutdownGracefully()) }
_ = try client.connect().wait()
try client.ping().wait()
try client.disconnect().wait()
}

func testMQTTPublishQoS0() throws {
let client = self.createClient(identifier: "testMQTTPublishQoS0")
defer { XCTAssertNoThrow(try client.syncShutdownGracefully()) }
Expand Down
9 changes: 7 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# run this with docker-compose -f docker/docker-compose.yml run test
# run this with: docker-compose -f docker-compose.yml run test
version: "3.3"

services:
Expand All @@ -7,20 +7,25 @@ services:
working_dir: /mqtt-nio
volumes:
- .:/mqtt-nio
- mosquitto-socket:/mqtt-nio/mosquitto/socket
depends_on:
- mosquitto
environment:
- MOSQUITTO_SERVER=mosquitto
- CI=true
command: /bin/bash -xcl "swift test --enable-test-discovery"
command: /bin/bash -xcl "swift test"

mosquitto:
image: eclipse-mosquitto
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/certs:/mosquitto/certs
- mosquitto-socket:/mosquitto/socket
ports:
- "1883:1883"
- "8883:8883"
- "8080:8080"
- "8081:8081"

volumes:
mosquitto-socket:
5 changes: 5 additions & 0 deletions mosquitto/config/mosquitto.conf
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ allow_anonymous true
cafile ./mosquitto/certs/ca.pem
certfile ./mosquitto/certs/server.pem
keyfile ./mosquitto/certs/server.key

# Unix Domain Socket
listener 0 ./mosquitto/socket/mosquitto.sock
protocol mqtt
allow_anonymous true
7 changes: 7 additions & 0 deletions mosquitto/socket/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
A local mosquitto server using `mosquitto/config/mosquitto.conf` will create a `mosquitto.sock` socket in this directory.

If using the `docker-compose.yml` container environment, a shared container volume will be mounted here.

This allows tests that connect to mosquitto via unix domain socket to assume the socket(s) will be found in this directory and work from multiple environments.

Do not remove this directory.
11 changes: 9 additions & 2 deletions scripts/mosquitto.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ run-containerized-mosquitto()
-p 8081:8081 \
-v "$(pwd)"/mosquitto/config:/mosquitto/config \
-v "$(pwd)"/mosquitto/certs:/mosquitto/certs \
-v "$(pwd)"/mosquitto/socket:/mosquitto/socket \
eclipse-mosquitto
}

Expand All @@ -49,13 +50,19 @@ fi
cd "$(dirname "$(dirname "$0")")"

if [[ $USE_CONTAINER -eq 1 ]]; then
if [ "$(uname)" != "Linux" ]; then
echo "warning: unix domain socket connections will not work with a mosquitto container on $(uname)"
fi
run-containerized-mosquitto
elif command -v mosquitto >/dev/null; then
run-installed-mosquitto
elif [ "$(uname)" = "Linux" ]; then
echo "notice: mosquitto not installed; running eclipse-mosquitto container instead..."
run-containerized-mosquitto
else
echo "error: mosquitto must be installed"
if [ "$(uname)" = "Darwin" ]; then
echo "mosquitto can be installed on MacOS with: brew install mosquitto"
fi
echo "notice: mosquitto not installed; running eclipse-mosquitto container instead..."
run-containerized-mosquitto
exit 1
fi

0 comments on commit a370d9e

Please sign in to comment.