gobwas / ws

Tiny WebSocket library for Go.
MIT License
6.1k stars 373 forks source link

Intermittent protocol failure and malformed payload #30

Closed Maldris closed 6 years ago

Maldris commented 6 years ago

I'm willing to bet this is an oversight on my part, but here goes.

I wrote a light weight websocket connection between two servers for work, with both client and server utilising gobwas/ws.

Intermittently (and much more frequently on windows) I will get a read error "frames from client to server must be masked" on the server, and terminate the connection. However, all writes from my client code use wsutil.WriteClientMessage, so all messages should be masked (assuming my understanding of the library is correct). I note that this, and some other protocol errors happened early on when I was accidentally reading from the connection in two goroutines simultaneously, though I believe this has been resolved.

I also see the occasional issue where a message payload contains additional bytes (usually ~4) on the front that look like part of the websocket frame data (payload is always JSON in current usages).

I suspect I've overlooked something in one of the systems designs, but at this point I've run out of ideas as to the cause, so I'm appealing for assistance.

Summary:

the client library I wrote can be seen here: https://github.com/Maldris/evtWebsocketClient and a gist of the server connection/communication logic can be seen here: https://gist.github.com/Maldris/4f62a8b7456fd82c3de6551ef4d8d99c

gobwas commented 6 years ago

Hi @Maldris! At this weekend Im planning to work on this. Thank you!

Maldris commented 6 years ago

Thanks for the assistance. I feel like I've been beating my head against a brick wall trying to figure it out at this point. But I still feel like I've missed something bleedingly obvious and made a stupid mistake somewhere.

gobwas commented 6 years ago

Hi @Maldris! Sorry for the delay. After digging into your code I eventually find this:

var buf bytes.Buffer
msg, err := wsutil.ReadClientData(struct { 
    io.Reader
    io.Writer
}{conn, &buf})
if err != nil {
    // handle error
}
if buf.Len() > 0 {
    // write or enqueue to be written buf.Bytes()
}
// process msg.

We can think about a special notice in comment to the ReadData* functions, to prevent misunderstanding on what they do and why there is an io.ReadWriter and not just io.Reader. Also there is a variant to simply remove them? :)

gobwas commented 6 years ago

@Maldris 🏓

Maldris commented 6 years ago

Hi @gobwas, Sorry for the delay, I got crazy busy as we got some purchase inquiries in another part of the business that we weren't expecting interest in, then I got rather ill, and simply forgot to respond here.

After implementing the suggested solution with a buffer (and a direct write method rather than the existing wrapped one) I haven't noticed the errors occurring again. So it does appear that it was simultaneous writes to the connection from different goroutines.

I may not have time before new years but Ill fork the project and try make some candidates for updated comments on those functions soon, investment has just dumped a lot of additional work in my lap.

gobwas commented 6 years ago

Nice to hear it @Maldris! Have a nice holidays and see you in 2018 🎆

Maldris commented 6 years ago

I wanted to run the general format of the comments past you as there are limited warnings in the documentation, and wanted to check some formatting options to match the existing documentation style.

The following examples will be as though for the method ReadClientData (for convenience of discussion)

the closest equivalent style to other notes:

// ReadClientData reads next data message from rw, considering that caller
// represents server side. It is a shortcut for ReadData(rw, ws.StateServerSide).
//
// Note this may handle and write control frames to the io.ReadWriter representing
// the connection.

the closest to things like the warning on DebugDialer:

// ReadClientData reads next data message from rw, considering that caller
// represents server side. It is a shortcut for ReadData(rw, ws.StateServerSide).
//
// Note that the connection should not be written to while this function is being
// executed as doing so risks corruption to message frames.

a much more verbose option that explains the consequences/requirements (seems vastly overkill)

// ReadClientData reads next data message from rw, considering that caller
// represents server side. It is a shortcut for ReadData(rw, ws.StateServerSide).
//
// Note this accepts a io.ReadWriter, and will internally handle
// control frames, writing to the connection, ensure that no writes occur to
// the connection while ReadClientData is executing, or frames with overlap
// and become corrupt.
gobwas commented 6 years ago

Hi @Maldris! Thanks for such deep thoughts on giving a warning. Will be this (the first option with little changes) ok?

// ReadClientData reads next data message from rw, considering that caller
// represents server side. It is a shortcut for ReadData(rw, ws.StateServerSide).
//
// Note this may handle and write control frames into the writer part of a given io.ReadWriter.