mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-13 06:04:42 +02:00
Merge bitcoin/bitcoin#35735: Add state to HTTPRequest
9954aa7728http: don't parse any new requests from a client if m_req_busy = true (Matthew Zipkin)c7db3ae1f9test: cover HTTPRequest state machine (Matthew Zipkin)90676e24adAdd state to HTTPRequest to avoid duplicate work over I/O cycles (Matthew Zipkin)507e528e84http: reuse HTTPHeaders to parse chunked trailer (Matthew Zipkin)902d8908c9http: only read one HTTPRequest at a time per client (Matthew Zipkin) Pull request description: This PR reduces the memory consumption of the HTTP Server when reading data from connected clients, and improves performance especially when requests are large (i.e. requiring multiple TCP packets). In https://github.com/bitcoin/bitcoin/pull/35182 the server copies as much data as it can from the socket into application memory, and then tries to parse as many complete HTTP requests as possible from that data. If a request is discovered to be incomplete, the in-progress request is abandoned. The server tries again on the next I/O cycle to read the same data from the buffer, duplicating work as many times as it takes before the client finishes sending the request (or times out). This PR implements two improvements to this: 1. Only parse one request at a time from the receive buffer. The server processes requests from each client in series anyway. 2. Add state to `HTTPRequest` so it can be filled with data from the receive buffer over multiple I/O loop iterations without losing progress. If a client sends large or multiple requests, that data will sit in the kernel's socket buffer instead of the application memory. Eventually the socket buffer will fill up and TCP backpressure will kick in, dropping the TCP window to 0 and blocking the client from sending any more. A state machine for `HTTPRemoteClient` was [discussed previously](https://github.com/bitcoin/bitcoin/pull/35182#pullrequestreview-4322490068) to control resource consumption. Another nice benefit of this model (for a follow-up PR) will be to insert the RPC authentication check after reading 8kB-limited headers but before the 32MB-limited request body. ACKs for top commit: winterrdog: re-ACK9954aa7728janb84: re ACK9954aa7728frankomosh: ACK9954aa7728. fjahr: ACK9954aa7728Tree-SHA512: b7c913114283fbf1f360b40f6c65a01390a26731bf3b166f460ec260f9206f25d738b3a06887bfa839911c1c6aaf634448181da47a752a9a881aebd907e44868
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
#define BITCOIN_HTTPSERVER_H
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -105,13 +104,15 @@ public:
|
||||
*/
|
||||
void RemoveAll(std::string_view key);
|
||||
/**
|
||||
* @param[in] reader A LineReader instance initialized with the client's receive buffer.
|
||||
* @param[in] write Whether or not to write the parsed data to the object after validation.
|
||||
* @returns false if LineReader hits the end of the buffer before reading an
|
||||
* \n, meaning that we are still waiting on more data from the client.
|
||||
* true after reading an entire HTTP headers section, terminated
|
||||
* by an empty line and \n.
|
||||
* @throws on exceeded read limit and on bad headers syntax (e.g. no ":" in a line)
|
||||
*/
|
||||
bool Read(util::LineReader& reader);
|
||||
bool Read(util::LineReader& reader, bool write = true);
|
||||
std::string Stringify() const;
|
||||
|
||||
private:
|
||||
@@ -120,6 +121,9 @@ private:
|
||||
* https://httpwg.org/specs/rfc9110.html#rfc.section.5.2
|
||||
*/
|
||||
std::vector<std::pair<std::string, std::string>> m_headers;
|
||||
|
||||
//! Track total bytes consumed in Read() for limit checks
|
||||
size_t m_consumed{0};
|
||||
};
|
||||
|
||||
struct HTTPVersion {
|
||||
@@ -195,6 +199,27 @@ public:
|
||||
std::pair<bool, std::string> GetHeader(std::string_view hdr) const;
|
||||
std::string ReadBody() const { return m_body; }
|
||||
void WriteHeader(std::string&& hdr, std::string&& value);
|
||||
|
||||
enum class State {
|
||||
Init,
|
||||
NeedsHeaders,
|
||||
NeedsBody,
|
||||
Complete,
|
||||
Error
|
||||
};
|
||||
State GetState() const { return m_state; }
|
||||
void SetState(State state) { m_state = state; }
|
||||
|
||||
// If a large request is sent with "Transfer-encoding: chunked" we may
|
||||
// read the chunk size in a separate I/O loop iteration than the chunk
|
||||
// of data itself. Store the chunk size value here until the chunk is read.
|
||||
std::optional<uint64_t> m_chunk_size;
|
||||
// We may also read a large chunk over multiple loop iterations.
|
||||
// Track the progress of the chunk here.
|
||||
uint64_t m_chunk_read{0};
|
||||
|
||||
private:
|
||||
State m_state = State::Init;
|
||||
};
|
||||
|
||||
class HTTPServer
|
||||
@@ -479,10 +504,9 @@ public:
|
||||
std::string m_recv_buffer{};
|
||||
|
||||
//! Requests from a client must be processed in the order in which
|
||||
//! they were received, blocking on a per-client basis. We won't
|
||||
//! process the next request in the queue if we are currently busy
|
||||
//! handling a previous request.
|
||||
std::deque<std::unique_ptr<HTTPRequest>> m_req_queue;
|
||||
//! they were received, blocking on a per-client basis. We read
|
||||
//! one request at a time from the socket buffer then pass it to a worker.
|
||||
std::unique_ptr<HTTPRequest> m_req;
|
||||
|
||||
//! Set to true by the I/O thread when a request is popped off
|
||||
//! and passed to a worker thread, reset to false by the worker thread.
|
||||
@@ -555,12 +579,19 @@ public:
|
||||
HTTPRemoteClient(const HTTPRemoteClient&) = delete;
|
||||
HTTPRemoteClient& operator=(const HTTPRemoteClient&) = delete;
|
||||
|
||||
//! Release any in-progress request. HTTPRequest holds a shared_ptr back to its
|
||||
//! HTTPRemoteClient to keep the client alive from a worker thread. If a request
|
||||
//! hasn't been moved to a worker yet it will prevent the client from destructing
|
||||
//! and never close the socket. Therefore this must be called when disconnecting.
|
||||
void ReleaseRequest() { m_req.reset(); }
|
||||
|
||||
/**
|
||||
* Try to read an HTTP request from the receive buffer.
|
||||
* Updates HTTPRequest.m_state and drains buffer on error.
|
||||
* @param[in] req A HTTPRequest to read into
|
||||
* @returns true upon reading a complete request, otherwise false (may throw).
|
||||
* @throws std::runtime_error if request is unreadable or violates protocol
|
||||
*/
|
||||
bool ReadRequest(HTTPRequest& req);
|
||||
void ReadRequest(HTTPRequest& req);
|
||||
|
||||
/**
|
||||
* Push data (if there is any) from client's m_send_buffer to the connected socket.
|
||||
|
||||
Reference in New Issue
Block a user