Channel is the external peer of one Stream. The server or the client owns the connection, and the connection owns the stream. A channel does not own either. It stores a pointer to the stream, and the stream stores a pointer back. One stream has one channel. A second open() throws std::logic_error.
Binding is how an upgrade is accepted. On the server, Service::onUpgrade() receives the stream the server already owns, with the request and the reply of the finished 101. A derived channel constructs itself with that stream, or calls open() from its own accept method. After onUpgrade() returns, a stream with no channel is declined and the server closes it. On the client, Client::upgrade() returns the stream the client already owns, and the derived channel opens that stream after the 101 reply.
This base does not transfer bytes. The derived type uses the stream it holds: Stream::buffer(), beginInput(), beginOutput(), and the ready signals of that stream. A framed protocol formats into the stream buffer and connects its own slots to the stream. Application code does not derive this base for WebSocket. The client uses WebSocket, and the server uses WebSocketSession.
open() is protected because the derived type owns the handshake. shutdown() on that type writes the shutdown frame. close() is public. It releases the stream and does not write a frame. A close that starts here calls Stream::close(). A close that starts at the stream calls onDetachStream(). That call runs onClose() and drops the stream pointer. It does not close the stream. Neither path emits a protocol signal.
The destructor calls close(). Destroying the stream calls onDetachStream(). The channel object stays, and stream() is null. isOpen() is false after either side ends.