Is there a length restriction on channelRead?

I created a server and a client using SwiftNIO. They communicate via TCP. To ensure a correct communication, I put every message from server to client and vice versa in a xml-string like this:
<packet ...>...</packet>.

As far as I could see, this works perfect and everytime channelRead was called, I had a complete packet. My question is now: Can there be a chance, that one packet will be split up in two calls of channelRead although the other side just sent on message?

By the way, my func looks like this:

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    var byteBuffer = self.unwrapInboundIn(data)

    if let string = byteBuffer.readString(length: byteBuffer.readableBytes) {
        self.messageHandler?(context.channel, string)
    }
}

Yes. TCP is stream-oriented: it does not expose message boundaries, and treats the data as a stream of bytes. Your code does need to guard against the possibility that messages will be split up across packets.

A good thing to do is to write a ByteToMessageDecoder that can process this framing. As an example (it's not super efficient but it's workable):

struct XMLFraming: ByteToMessageDecoder {
    typealias InboundOut = ByteBuffer

    mutating func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
        var view = buffer.readableBytesView

        while view.count > 0, let index = view.firstIndex(of: UInt8(ascii: "<")) {
            view = view[index...]
            if view.starts(with: "</packet>".utf8) {
                // Got a match.
                view = view.dropFirst("</packet>".utf8.count)
                let packet = buffer.readSlice(length: buffer.readableBytes - view.count)!
                context.fireChannelRead(self.wrapInboundOut(packet))
                return .continue
            }
        }

        return .needMoreData
    }
}

This will chunk up the messages, sending one ByteBuffer that contains the full framing. If you wanted you could also arrange for this object to strip off the leading <packet ...> and trailing </packet>. You can then create a ByteToMessageHandler from this ByteToMessageDecoder, and insert it into the pipeline before your ChannelHandler.

This will guarantee that every channelRead contains one and only one packet that matches your framing.

Thank you very much for your answer and even for your example!

I checked my code again, because I was wondering, why it was working all the time and then I was a little bit surprised, because I realized, that I answered this question to myself a year ago, as I wrote that code. Actually I solved this problem in a different way:

let packetBegin = "<packet"
let packetEnd = "</packet>"
var dataRead = ""

func handleMessageString(messageString: String) {
    dataRead = dataRead + messageString

    if let startRange = dataRead.range(of: packetBegin), let endRange = dataRead.range(of: packetEnd) {
        let xmlString = String(dataRead[startRange.lowerBound..<endRange.upperBound])
        dataRead = String(dataRead[endRange.upperBound...])   // leave everything, that is behind </packet>
        ...
    }
}

This function is called every time from channelRead (see the self.messageHandler in my first post).

But I will have a closer look at your solution as well.

You can also use ByteToMessageDecoderVerifier from NIOTestUtils to test your ChannelHandler's ability to deal with incomplete messages. It tests different sizes of input for you to verify your handling of incomplete messages.

For example:

func testPingThenDisconnectDecoding() {
    // Given
    let channel = EmbeddedChannel()
    var disconnectInput = channel.allocator.buffer(capacity: 4)
    disconnectInput.writeBytes([0b11000000, 0b00000000, 0b11100000, 0b00000000])
    let expectedInOuts = [(disconnectInput, [Packet.ping(.init()), Packet.disconnect(.init())])]
    
    // When, Then
    XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: expectedInOuts,
                                                                    decoderFactory: { PacketDecoder() }))
}
1 Like
Terms of Service

Privacy Policy

Cookie Policy