Hi Cory and everyone,
I'm trying to implement a UDP based protocol and everything has been working fine except when I get a datagram with no data. When that happens, my server crashes with an assertion failure.
For the protocol I'm implementing, an empty datagram has a specific meaning. Is there a way to make it work?
This is the error I get:
NIOPosix/SocketChannel.swift:628: Assertion failed
This is what tcpdump
shows immediate before the error occurs:
14:13:00.931507 IP 206.189.113.124.52962 > 159.65.88.158.9999: UDP, length 0
Here's the failing assertion in swift-nio
:
Here's my application code:
import NIOCore
import NIOPosix
@main
public struct SwiftProtohackers {
public static func main() throws {
let unusualDatabaseMessageHandler = UnusualDatabaseMessageHandler()
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = DatagramBootstrap(group: group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.channelInitializer { channel in
channel.pipeline.addHandler(UnusualDatabaseMessageDecoder()).flatMap { v in
channel.pipeline.addHandler(unusualDatabaseMessageHandler)
}
}
defer {
try! group.syncShutdownGracefully()
}
let channel = try bootstrap.bind(host: "0.0.0.0", port: 9999).wait()
print("Server started and listening on \(channel.localAddress!)")
try channel.closeFuture.wait()
print("Server closed")
}
}
actor UnusualDatabase {
enum Operation {
case insert(key: String, value: String)
case retrieve(key: String)
init(rawValue s: String) {
if let eqIdx = s.firstIndex(of: "=") {
self = .insert(key: String(s[..<eqIdx]), value: String(s[eqIdx...].dropFirst()))
} else {
self = .retrieve(key: s)
}
}
}
private var data: [String: String] = ["version": "SwiftProtohackers 1.0"]
func perform(_ op: Operation) -> String? {
switch op {
case let .insert(key, value):
if key != "version" {
data[key] = value
}
return nil
case let .retrieve(key):
return "\(key)=\(data[key] ?? "")"
}
}
}
final class UnusualDatabaseMessageDecoder: ChannelInboundHandler {
typealias InboundIn = AddressedEnvelope<ByteBuffer>
typealias InboundOut = AddressedEnvelope<UnusualDatabase.Operation>
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let envelope = unwrapInboundIn(data)
var buffer = envelope.data
guard let message = buffer.readString(length: buffer.readableBytes)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
print("Error: invalid string received")
return
}
let operation = UnusualDatabase.Operation(rawValue: message)
context.fireChannelRead(
wrapInboundOut(AddressedEnvelope(remoteAddress: envelope.remoteAddress, data: operation))
)
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("error:", error)
context.close(promise: nil)
}
}
final class UnusualDatabaseMessageHandler: ChannelInboundHandler {
typealias InboundIn = AddressedEnvelope<UnusualDatabase.Operation>
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
let database = UnusualDatabase()
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let request = unwrapInboundIn(data)
let channel = context.channel
Task {
if let response = await database.perform(request.data), channel.isWritable {
let buffer = channel.allocator.buffer(string: response)
let envelope = AddressedEnvelope(
remoteAddress: request.remoteAddress,
data: buffer
)
channel.writeAndFlush(envelope, promise: nil)
}
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("error:", error)
context.close(promise: nil)
}
}
As an aside for the curious, I'm using Protohackers exercises to learn Swift NIO. This one is an implementation of problem 4. My solutions are on GitHub. I'm doing one commit per problem and force pushing when I change things, so the code might look different on the repo.