Files
ppsspp/Common/Net/WebsocketServer.h
T
Henrik RydgårdandClaude Opus 5 abd91da5eb WebSocket: teach the input sink about EOF, and stop losing why a read failed
Follow-up to the previous commit, from Nemoumbra's questions - which found a worse
spin than the one that fix addressed.

InputSink couldn't tell "nothing right now" from "peer is gone": Fill() treats
recv() == 0 as no data and only sets hasError_ on a real error. Block() then waits
with WaitUntilReady(), which reports a closed socket as ready immediately and
forever, so TakeExact() looped on it without ever returning. A client that
disconnects with half a frame buffered - easy to do while blasting messages - put
the server in an infinite loop inside TakeExact, never even returning to Process().
Measured 7.95 CPU-seconds over 8 seconds; 0.08 after.

So: track EOF explicitly (sticky atEnd_, exposed as AtEnd()), and have Block() give
up when nothing more can arrive.

That information was being thrown away in three more places:

* Process() only tried to fill when the sink was already empty, so a disconnect went
  unnoticed for as long as there were leftovers - and if those leftovers were a
  partial frame, the read above never completed. Always fill, and close once the
  peer is gone and we've consumed what it sent.
* ReadPending() uses TakeAtMost(), which returns 0 both for "nothing right now" and
  "nothing ever again", and then reported success having consumed nothing. Ask the
  sink which it was.
* Both TakeExact() call sites answered a failed read with POLICY_VIOLATION, blaming
  the client for a protocol error when it had simply disconnected. Check the sink
  and report ABNORMAL when that's what happened.

Also stop queueing data once our own close frame is queued. RFC 6455 5.5.1 forbids
data frames after a close, and beyond the protocol, anything appended afterwards
keeps the buffers non-empty and starves the "everything is flushed" check that ends
the connection. Observed the server pumping 167MB of log broadcasts after being
asked to close.

The repeated close-and-discard is now one helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-31 00:57:17 +02:00

118 lines
3.5 KiB
C++

#pragma once
#include <functional>
#include <string>
#include "Common/Net/HTTPServer.h"
#include "Common/Net/Sinks.h"
namespace net {
enum class WebSocketClose : uint16_t {
NORMAL = 1000,
GOING_AWAY = 1001,
PROTOCOL_ERROR = 1002,
UNSUPPORTED_DATA = 1003,
INVALID_DATA = 1007,
POLICY_VIOLATION = 1008,
MESSAGE_TOO_LONG = 1009,
MISSING_EXTENSION = 1010,
INTERNAL_ERROR = 1011,
SERVICE_RESTART = 1012,
TRY_AGAIN_LATER = 1013,
BAD_GATEWAY = 1014,
NO_STATUS = 1005,
ABNORMAL = 1006,
};
// RFC 6455
class WebSocketServer {
public:
static WebSocketServer *CreateAsUpgrade(const http::ServerRequest &request, const std::string &protocol = "");
void Send(const std::string &str);
void Send(const std::vector<uint8_t> &payload);
// Call with finish = false to start and continue, then finally with finish = true to complete.
// Note: Fragmented data cannot be interleaved, per protocol.
void AddFragment(bool finish, const std::string &str);
void AddFragment(bool finish, const std::vector<uint8_t> &payload);
void Ping(const std::vector<uint8_t> &payload = {});
void Pong(const std::vector<uint8_t> &payload = {});
void Close(WebSocketClose reason = WebSocketClose::GOING_AWAY);
// Note: may interrupt early. Call in a loop.
bool Process(float timeout = -1.0f);
void SetTextHandler(std::function<void(const std::string &)> func) {
text_ = func;
}
void SetBinaryHandler(std::function<void(const std::vector<uint8_t> &)> func) {
binary_ = func;
}
// Doesn't need to send a Pong.
void SetPingHandler(std::function<void(const std::vector<uint8_t> &)> func) {
ping_ = func;
}
void SetPongHandler(std::function<void(const std::vector<uint8_t> &)> func) {
pong_ = func;
}
bool IsOpen() {
return open_;
}
WebSocketClose CloseReason() {
return closeReason_;
}
protected:
WebSocketServer(size_t fd, InputSink *in, OutputSink *out) : fd_(fd), in_(in), out_(out) {
}
void SendHeader(bool fin, int opcode, size_t sz);
void SendBytes(const void *p, size_t sz);
// True once we've queued our own close frame. RFC 6455 5.5.1: no data frames may follow it.
// Just as importantly, anything queued after that point keeps outBuf_/out_ non-empty, which
// starves the "everything is flushed" check in Process() so the connection never finishes
// closing - and since the socket stays writable, select() stops blocking and we spin.
bool SendingIsOver() const { return sentClose_; }
void CloseAbnormally();
void CloseForReadFailure();
void SendFlush();
void CompactOutBuf();
size_t OutBufPending() const { return outBuf_.size() - outBufOffset_; }
bool ReadFrames();
bool ReadFrame();
bool ReadPending();
bool ReadControlFrame(int opcode, size_t sz);
bool open_ = true;
bool sentClose_ = false;
int fragmentOpcode_ = -1;
size_t fd_ = 0;
InputSink *in_ = nullptr;
OutputSink *out_ = nullptr;
WebSocketClose closeReason_ = WebSocketClose::NO_STATUS;
std::vector<uint8_t> outBuf_;
// How much of outBuf_ has already been handed to out_. See CompactOutBuf().
size_t outBufOffset_ = 0;
size_t lastPressure_ = 0;
std::vector<uint8_t> pendingBuf_;
uint8_t pendingMask_[4]{};
// Bytes left to read in the frame (in case of a partial frame read.)
uint64_t pendingLeft_ = 0;
int pendingOpcode_ = -1;
// Waiting for a frame with FIN.
bool pendingFin_ = false;
std::function<void(const std::string &)> text_;
std::function<void(const std::vector<uint8_t> &)> binary_;
std::function<void(const std::vector<uint8_t> &)> ping_;
std::function<void(const std::vector<uint8_t> &)> pong_;
};
};