Loading src/http.cpp +92 −15 Original line number Diff line number Diff line Loading @@ -205,6 +205,12 @@ const std::string & libhttppp::HttpUrl::getPath() const{ return _path; } // --- TLS Session Cache (process-wide singleton) --- netplus::TlsSessionCache& libhttppp::HttpClient::tlsSessionCache() { static netplus::TlsSessionCache cache; return cache; } libhttppp::HttpClient::HttpClient(const HttpUrl& desturl) : _url(desturl){ try { Loading @@ -231,6 +237,10 @@ bool libhttppp::HttpClient::tryHttp3First(){ } void libhttppp::HttpClient::resetConnection(){ // Reset HTTP/2 connection state on any new connection _h2PrefaceSent = false; _h2NextStreamId = 1; _h2Decoder.reset(); if (_url.getProtocol() == HttpUrl::HTTP3) { _cltsock = std::make_unique<netplus::quic>(); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading @@ -247,6 +257,20 @@ void libhttppp::HttpClient::resetConnection(){ sslsock->getTls().client_alpn_protocols = std::string("\x02h2\x08http/1.1", 12); // --- TLS session resumption: attach cache and load saved session --- netplus::tls& t = sslsock->getTls(); t.session_cache = &tlsSessionCache(); // Build host:port key for the cache lookup std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort()); netplus::TlsSessionCacheEntry saved; if (tlsSessionCache().lookup_by_host(cacheKey, saved)) { t.attempt_resumption = true; t.resumption_session_id = saved.session_id; t.resumption_master_secret = saved.master_secret; t.resumption_cipher = saved.cipher_suite; } _cltsock = std::move(sslsock); _cltsock->connect(_url.getHost(), _url.getPort(), false); _cltsock->setNonBlock(); Loading @@ -254,6 +278,16 @@ void libhttppp::HttpClient::resetConnection(){ // Check negotiated ALPN after TLS handshake const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn(); _isH2 = (alpn == "h2"); // --- Save session for future resumption --- { netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls(); if (tc.handshakeDone && !tc.is_tls13 && !tc.client_session_id.empty() && !tc.masterSecret.empty()) { tlsSessionCache().store_by_host(cacheKey, tc.client_session_id, tc.masterSecret, tc.chosenSuite); } } } else { _cltsock = std::make_unique<netplus::tcp>(-1); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading @@ -264,6 +298,10 @@ void libhttppp::HttpClient::resetConnection(){ void libhttppp::HttpClient::reconnect(){ // Reset HTTP/2 connection state on reconnect _h2PrefaceSent = false; _h2NextStreamId = 1; _h2Decoder.reset(); if (_url.getProtocol() == HttpUrl::HTTP3) { _cltsock = std::make_unique<netplus::quic>(); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading @@ -280,12 +318,35 @@ void libhttppp::HttpClient::reconnect(){ sslsock->getTls().client_alpn_protocols = std::string("\x02h2\x08http/1.1", 12); // --- TLS session resumption: attach cache and load saved session --- netplus::tls& t = sslsock->getTls(); t.session_cache = &tlsSessionCache(); std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort()); netplus::TlsSessionCacheEntry saved; if (tlsSessionCache().lookup_by_host(cacheKey, saved)) { t.attempt_resumption = true; t.resumption_session_id = saved.session_id; t.resumption_master_secret = saved.master_secret; t.resumption_cipher = saved.cipher_suite; } _cltsock = std::move(sslsock); _cltsock->connect(_url.getHost(), _url.getPort(), false); _cltsock->setNonBlock(); const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn(); _isH2 = (alpn == "h2"); // --- Save session for future resumption --- { netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls(); if (tc.handshakeDone && !tc.is_tls13 && !tc.client_session_id.empty() && !tc.masterSecret.empty()) { tlsSessionCache().store_by_host(cacheKey, tc.client_session_id, tc.masterSecret, tc.chosenSuite); } } } else { _cltsock = std::make_unique<netplus::tcp>(-1); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading Loading @@ -746,10 +807,13 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( } }; // 1) Send connection preface + SETTINGS // 1) Send connection preface + SETTINGS (only once per connection) if (!_h2PrefaceSent) { std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN); preface += h2cBuildSettings(); send_all(preface); _h2PrefaceSent = true; } // 2) Build HEADERS frame for stream 1 std::string path = nreq.getRequestURL(); Loading Loading @@ -781,7 +845,8 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( std::string hpack_block = hpack::Encoder::encodeRequestHeaders( method, path, scheme, auth.str(), extra); uint32_t stream_id = 1; uint32_t stream_id = _h2NextStreamId; _h2NextStreamId += 2; // Client streams use odd IDs: 1, 3, 5, ... bool end_stream = (postBody == nullptr || postBody->empty()); uint8_t hdr_flags = H2C_FLAG_END_HEADERS; if (end_stream) hdr_flags |= H2C_FLAG_END_STREAM; Loading Loading @@ -810,7 +875,9 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( bool got_response_headers = false; bool got_end_stream = false; hpack::Decoder decoder; // Use connection-level decoder to maintain HPACK dynamic table state if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>(); hpack::Decoder &decoder = *_h2Decoder; // Read enough data into raw buffer auto ensure_bytes = [&](size_t need) { Loading Loading @@ -999,18 +1066,28 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( raw.insert(raw.end(), buf, buf + n); last_data = std::chrono::steady_clock::now(); } else { // Pump the UDP socket: read raw datagrams and process QUIC packets // without consuming stream data (recvData would consume and discard it) // Pump the UDP socket: drain ALL available datagrams and process // QUIC packets. For large responses the server sends many datagrams; // reading only one per loop iteration would be far too slow. bool pumped_any = false; for (int pump_i = 0; pump_i < 256; ++pump_i) { try { q->pumpNetwork(MSG_DONTWAIT); pumped_any = true; } catch (netplus::NetException &e) { if (e.getErrorType() != netplus::NetException::Note) { HTTPException ee; ee[HTTPException::Error] << e.what(); throw ee; } break; // EAGAIN — no more datagrams available } } // If we pumped datagrams, immediately retry recvStreamData // without waiting. Only waitRead when truly idle. if (pumped_any) continue; } size_t pos = 0; while (pos < raw.size()) { Loading src/http.h +10 −0 Original line number Diff line number Diff line Loading @@ -91,6 +91,10 @@ namespace libhttppp { const std::vector<char> Post(HttpRequest &nreq,const std::vector<char> &post); const std::vector<char> Put(HttpRequest &nreq,const std::vector<char> &put); const std::vector<char> Delete(HttpRequest &nreq); // Shared TLS session cache — enables abbreviated TLS 1.2 handshakes // across reconnects to the same host. One cache per process. static netplus::TlsSessionCache& tlsSessionCache(); private: void resetConnection(); void _ensureConnected(); Loading @@ -107,6 +111,9 @@ namespace libhttppp { // HTTP/2 client helpers bool _isH2 = false; bool _h2PrefaceSent = false; // true after connection preface sent uint32_t _h2NextStreamId = 1; // next client-initiated stream ID (odd) std::unique_ptr<hpack::Decoder> _h2Decoder; // persistent HPACK decoder for connection const std::vector<char> _h2Request(const std::string &method, HttpRequest &nreq, const std::vector<char> *postBody = nullptr); Loading Loading @@ -315,6 +322,9 @@ namespace libhttppp { struct H2PendingIncoming { std::vector<hpack::HeaderField> headers; std::string body; std::vector<uint8_t> rawHpack; // accumulates HPACK across CONTINUATION frames bool headersComplete = false; // true once END_HEADERS received bool endStreamOnHeaders = false; // END_STREAM was on HEADERS frame }; struct H2State { Loading src/httpd.cpp +122 −25 Original line number Diff line number Diff line Loading @@ -126,16 +126,42 @@ static std::string h2ConnectionWindowBoost() { return h2BuildWindowUpdate(0, boost); } // Append DATA frames chunked to H2_MAX_FRAME_SIZE // Append a single frame directly to 'out' without creating a temp string. // Avoids allocating + copying a temporary std::string per frame. static void h2AppendFrameDirect(std::string &out, uint8_t type, uint8_t flags, uint32_t stream_id, const char *payload, size_t payload_size) { size_t old_size = out.size(); out.resize(old_size + 9 + payload_size); char *f = &out[old_size]; f[0] = static_cast<char>((payload_size >> 16) & 0xff); f[1] = static_cast<char>((payload_size >> 8) & 0xff); f[2] = static_cast<char>(payload_size & 0xff); f[3] = static_cast<char>(type); f[4] = static_cast<char>(flags); f[5] = static_cast<char>((stream_id >> 24) & 0x7f); f[6] = static_cast<char>((stream_id >> 16) & 0xff); f[7] = static_cast<char>((stream_id >> 8) & 0xff); f[8] = static_cast<char>(stream_id & 0xff); if (payload_size > 0) { std::memcpy(f + 9, payload, payload_size); } } // Append DATA frames chunked to H2_MAX_FRAME_SIZE. // Uses direct memcpy to avoid temporary string copies per chunk. static void h2AppendDataFrames(std::string &out, uint32_t stream_id, const std::string &body, bool end_stream) { // Pre-reserve space for all frames: 9-byte header per chunk + body data size_t num_chunks = (body.size() + H2_MAX_FRAME_SIZE - 1) / H2_MAX_FRAME_SIZE; out.reserve(out.size() + body.size() + num_chunks * H2_FRAME_HEADER_LEN); size_t offset = 0; while (offset < body.size()) { size_t chunk = std::min(body.size() - offset, H2_MAX_FRAME_SIZE); bool last_chunk = (offset + chunk >= body.size()); uint8_t flags = (last_chunk && end_stream) ? H2_FLAG_END_STREAM : 0; out += h2BuildFrame(H2_FRAME_DATA, flags, stream_id, body.substr(offset, chunk)); h2AppendFrameDirect(out, H2_FRAME_DATA, flags, stream_id, body.data() + offset, chunk); offset += chunk; } } Loading Loading @@ -270,6 +296,11 @@ libhttppp::HttpEvent::HttpEvent(std::vector<netplus::socket*> serversocket, int // Set ALPN selection and framing callbacks on SSL server sockets. // These are propagated to child sockets via ssl::accept(). if (auto* sslsock = dynamic_cast<netplus::ssl*>(sock)) { // Attach the TLS session cache to the server socket. // Child sockets created by accept() inherit the cache pointer // so that abbreviated TLS 1.2 handshakes can be used. sslsock->getTls().session_cache = &_tlsSessionCache; // Prefer h2, fall back to http/1.1 sslsock->setAlpnSelectCallback([](const std::vector<std::string>& protos) -> std::string { for (auto& p : protos) { Loading Loading @@ -468,6 +499,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, cureq._httpProtocol = 1; // Mark connection as HTTP/2 std::string out; out.reserve(4096); // Pre-allocate to avoid repeated reallocations size_t off = 0; // Consume HTTP/2 client connection preface if present Loading Loading @@ -518,16 +550,52 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, hpack_len -= 5; } // Decode HPACK headers using the connection's decoder // (maintains dynamic table state across frames) if (fflags & H2_FLAG_END_HEADERS) { // Complete header block in this single frame auto decoded = cureq.h2state().hpackDecoder.decode(hpack_data, hpack_len); if (fflags & H2_FLAG_END_STREAM) { // No body expected (GET, HEAD, etc.) — dispatch immediately _dispatchH2Stream(cureq, out, sid, decoded, "", tid, args); } else { // Body expected via DATA frames (POST, PUT, etc.) cureq.h2state().pendingIncoming[sid] = {std::move(decoded), ""}; auto &pending = cureq.h2state().pendingIncoming[sid]; pending.headers = std::move(decoded); pending.headersComplete = true; } } else { // END_HEADERS not set: HPACK block continues in CONTINUATION // frames. Accumulate raw bytes and defer decoding. auto &pending = cureq.h2state().pendingIncoming[sid]; pending.rawHpack.assign(hpack_data, hpack_data + hpack_len); pending.headersComplete = false; pending.endStreamOnHeaders = (fflags & H2_FLAG_END_STREAM) != 0; } break; } case H2_FRAME_CONTINUATION: { // RFC 7540 §6.10: CONTINUATION carries more HPACK data for // a HEADERS block whose END_HEADERS was not yet set. auto it = cureq.h2state().pendingIncoming.find(sid); if (it != cureq.h2state().pendingIncoming.end() && !it->second.headersComplete) { const uint8_t *cont_data = reinterpret_cast<const uint8_t*>(data + off); it->second.rawHpack.insert(it->second.rawHpack.end(), cont_data, cont_data + flen); if (fflags & H2_FLAG_END_HEADERS) { // Full HPACK block received — decode now it->second.headers = cureq.h2state().hpackDecoder.decode( it->second.rawHpack.data(), it->second.rawHpack.size()); it->second.rawHpack.clear(); it->second.rawHpack.shrink_to_fit(); it->second.headersComplete = true; if (it->second.endStreamOnHeaders) { // No body expected — dispatch immediately _dispatchH2Stream(cureq, out, sid, it->second.headers, "", tid, args); cureq.h2state().pendingIncoming.erase(it); } } } break; } Loading @@ -545,7 +613,9 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, } if (fflags & H2_FLAG_END_STREAM) { // All body data received — dispatch the request // Only dispatch once headers are fully received // (CONTINUATION may still be pending) if (it->second.headersComplete) { _dispatchH2Stream(cureq, out, sid, it->second.headers, it->second.body, Loading @@ -553,6 +623,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, cureq.h2state().pendingIncoming.erase(it); } } } break; } Loading Loading @@ -580,9 +651,27 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, case H2_FRAME_GOAWAY: goto done; case H2_FRAME_RST_STREAM: { // Clean up pending state for the reset stream to prevent // memory leaks and stalled stream bookkeeping. auto it = cureq.h2state().pendingIncoming.find(sid); if (it != cureq.h2state().pendingIncoming.end()) { cureq.h2state().pendingIncoming.erase(it); } // Also remove any pending outbound responses for this stream auto &pr = cureq.h2state().pendingResponses; for (auto pit = pr.begin(); pit != pr.end(); ) { if (pit->streamId == sid) { pit = pr.erase(pit); } else { ++pit; } } break; } case H2_FRAME_WINDOW_UPDATE: case H2_FRAME_PRIORITY: case H2_FRAME_RST_STREAM: // Acknowledge by ignoring — these don't need explicit responses break; Loading Loading @@ -888,16 +977,24 @@ void libhttppp::HttpEvent::ResponseEvent(netplus::con &curcon,const int tid,ULON if (cureq._httpProtocol == 1) { if (cureq._h2 && !cureq._h2->pendingResponses.empty()) { auto &pending = cureq._h2->pendingResponses.front(); // Batch up to 8 DATA chunks per event loop iteration // for significantly higher throughput on large responses. std::string batch; batch.reserve(std::min(pending.body.size() - pending.offset, 8 * H2_MAX_FRAME_SIZE) + 8 * H2_FRAME_HEADER_LEN); for (int i = 0; i < 8 && pending.offset < pending.body.size(); ++i) { size_t remaining = pending.body.size() - pending.offset; if (remaining > 0) { size_t chunk = std::min(remaining, H2_MAX_FRAME_SIZE); bool last = (pending.offset + chunk >= pending.body.size()); uint8_t flags = last ? H2_FLAG_END_STREAM : 0; std::string frame = h2BuildFrame( H2_FRAME_DATA, flags, pending.streamId, pending.body.substr(pending.offset, chunk)); cureq.SendData.append(frame.data(), frame.size()); h2AppendFrameDirect(batch, H2_FRAME_DATA, flags, pending.streamId, pending.body.data() + pending.offset, chunk); pending.offset += chunk; if (last) break; } if (!batch.empty()) { cureq.SendData.append(batch.data(), batch.size()); } if (pending.offset >= pending.body.size()) { cureq._h2->pendingResponses.pop_front(); Loading src/httpd.h +4 −0 Original line number Diff line number Diff line Loading @@ -66,6 +66,10 @@ namespace libhttppp { protected: void CreateConnection(std::shared_ptr<netplus::con> &res); std::string _altSvcH3; // Alt-Svc value for HTTP/3 advertisement // TLS session cache — shared across all accepted SSL connections // to enable abbreviated TLS 1.2 handshakes on client reconnection. netplus::TlsSessionCache _tlsSessionCache; private: // Per-stream buffer for HTTP/3: accumulates data until fin struct H3StreamBuffer { Loading Loading
src/http.cpp +92 −15 Original line number Diff line number Diff line Loading @@ -205,6 +205,12 @@ const std::string & libhttppp::HttpUrl::getPath() const{ return _path; } // --- TLS Session Cache (process-wide singleton) --- netplus::TlsSessionCache& libhttppp::HttpClient::tlsSessionCache() { static netplus::TlsSessionCache cache; return cache; } libhttppp::HttpClient::HttpClient(const HttpUrl& desturl) : _url(desturl){ try { Loading @@ -231,6 +237,10 @@ bool libhttppp::HttpClient::tryHttp3First(){ } void libhttppp::HttpClient::resetConnection(){ // Reset HTTP/2 connection state on any new connection _h2PrefaceSent = false; _h2NextStreamId = 1; _h2Decoder.reset(); if (_url.getProtocol() == HttpUrl::HTTP3) { _cltsock = std::make_unique<netplus::quic>(); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading @@ -247,6 +257,20 @@ void libhttppp::HttpClient::resetConnection(){ sslsock->getTls().client_alpn_protocols = std::string("\x02h2\x08http/1.1", 12); // --- TLS session resumption: attach cache and load saved session --- netplus::tls& t = sslsock->getTls(); t.session_cache = &tlsSessionCache(); // Build host:port key for the cache lookup std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort()); netplus::TlsSessionCacheEntry saved; if (tlsSessionCache().lookup_by_host(cacheKey, saved)) { t.attempt_resumption = true; t.resumption_session_id = saved.session_id; t.resumption_master_secret = saved.master_secret; t.resumption_cipher = saved.cipher_suite; } _cltsock = std::move(sslsock); _cltsock->connect(_url.getHost(), _url.getPort(), false); _cltsock->setNonBlock(); Loading @@ -254,6 +278,16 @@ void libhttppp::HttpClient::resetConnection(){ // Check negotiated ALPN after TLS handshake const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn(); _isH2 = (alpn == "h2"); // --- Save session for future resumption --- { netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls(); if (tc.handshakeDone && !tc.is_tls13 && !tc.client_session_id.empty() && !tc.masterSecret.empty()) { tlsSessionCache().store_by_host(cacheKey, tc.client_session_id, tc.masterSecret, tc.chosenSuite); } } } else { _cltsock = std::make_unique<netplus::tcp>(-1); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading @@ -264,6 +298,10 @@ void libhttppp::HttpClient::resetConnection(){ void libhttppp::HttpClient::reconnect(){ // Reset HTTP/2 connection state on reconnect _h2PrefaceSent = false; _h2NextStreamId = 1; _h2Decoder.reset(); if (_url.getProtocol() == HttpUrl::HTTP3) { _cltsock = std::make_unique<netplus::quic>(); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading @@ -280,12 +318,35 @@ void libhttppp::HttpClient::reconnect(){ sslsock->getTls().client_alpn_protocols = std::string("\x02h2\x08http/1.1", 12); // --- TLS session resumption: attach cache and load saved session --- netplus::tls& t = sslsock->getTls(); t.session_cache = &tlsSessionCache(); std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort()); netplus::TlsSessionCacheEntry saved; if (tlsSessionCache().lookup_by_host(cacheKey, saved)) { t.attempt_resumption = true; t.resumption_session_id = saved.session_id; t.resumption_master_secret = saved.master_secret; t.resumption_cipher = saved.cipher_suite; } _cltsock = std::move(sslsock); _cltsock->connect(_url.getHost(), _url.getPort(), false); _cltsock->setNonBlock(); const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn(); _isH2 = (alpn == "h2"); // --- Save session for future resumption --- { netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls(); if (tc.handshakeDone && !tc.is_tls13 && !tc.client_session_id.empty() && !tc.masterSecret.empty()) { tlsSessionCache().store_by_host(cacheKey, tc.client_session_id, tc.masterSecret, tc.chosenSuite); } } } else { _cltsock = std::make_unique<netplus::tcp>(-1); _cltsock->connect(_url.getHost(), _url.getPort(), false); Loading Loading @@ -746,10 +807,13 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( } }; // 1) Send connection preface + SETTINGS // 1) Send connection preface + SETTINGS (only once per connection) if (!_h2PrefaceSent) { std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN); preface += h2cBuildSettings(); send_all(preface); _h2PrefaceSent = true; } // 2) Build HEADERS frame for stream 1 std::string path = nreq.getRequestURL(); Loading Loading @@ -781,7 +845,8 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( std::string hpack_block = hpack::Encoder::encodeRequestHeaders( method, path, scheme, auth.str(), extra); uint32_t stream_id = 1; uint32_t stream_id = _h2NextStreamId; _h2NextStreamId += 2; // Client streams use odd IDs: 1, 3, 5, ... bool end_stream = (postBody == nullptr || postBody->empty()); uint8_t hdr_flags = H2C_FLAG_END_HEADERS; if (end_stream) hdr_flags |= H2C_FLAG_END_STREAM; Loading Loading @@ -810,7 +875,9 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( bool got_response_headers = false; bool got_end_stream = false; hpack::Decoder decoder; // Use connection-level decoder to maintain HPACK dynamic table state if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>(); hpack::Decoder &decoder = *_h2Decoder; // Read enough data into raw buffer auto ensure_bytes = [&](size_t need) { Loading Loading @@ -999,18 +1066,28 @@ const std::vector<char> libhttppp::HttpClient::_h2Request( raw.insert(raw.end(), buf, buf + n); last_data = std::chrono::steady_clock::now(); } else { // Pump the UDP socket: read raw datagrams and process QUIC packets // without consuming stream data (recvData would consume and discard it) // Pump the UDP socket: drain ALL available datagrams and process // QUIC packets. For large responses the server sends many datagrams; // reading only one per loop iteration would be far too slow. bool pumped_any = false; for (int pump_i = 0; pump_i < 256; ++pump_i) { try { q->pumpNetwork(MSG_DONTWAIT); pumped_any = true; } catch (netplus::NetException &e) { if (e.getErrorType() != netplus::NetException::Note) { HTTPException ee; ee[HTTPException::Error] << e.what(); throw ee; } break; // EAGAIN — no more datagrams available } } // If we pumped datagrams, immediately retry recvStreamData // without waiting. Only waitRead when truly idle. if (pumped_any) continue; } size_t pos = 0; while (pos < raw.size()) { Loading
src/http.h +10 −0 Original line number Diff line number Diff line Loading @@ -91,6 +91,10 @@ namespace libhttppp { const std::vector<char> Post(HttpRequest &nreq,const std::vector<char> &post); const std::vector<char> Put(HttpRequest &nreq,const std::vector<char> &put); const std::vector<char> Delete(HttpRequest &nreq); // Shared TLS session cache — enables abbreviated TLS 1.2 handshakes // across reconnects to the same host. One cache per process. static netplus::TlsSessionCache& tlsSessionCache(); private: void resetConnection(); void _ensureConnected(); Loading @@ -107,6 +111,9 @@ namespace libhttppp { // HTTP/2 client helpers bool _isH2 = false; bool _h2PrefaceSent = false; // true after connection preface sent uint32_t _h2NextStreamId = 1; // next client-initiated stream ID (odd) std::unique_ptr<hpack::Decoder> _h2Decoder; // persistent HPACK decoder for connection const std::vector<char> _h2Request(const std::string &method, HttpRequest &nreq, const std::vector<char> *postBody = nullptr); Loading Loading @@ -315,6 +322,9 @@ namespace libhttppp { struct H2PendingIncoming { std::vector<hpack::HeaderField> headers; std::string body; std::vector<uint8_t> rawHpack; // accumulates HPACK across CONTINUATION frames bool headersComplete = false; // true once END_HEADERS received bool endStreamOnHeaders = false; // END_STREAM was on HEADERS frame }; struct H2State { Loading
src/httpd.cpp +122 −25 Original line number Diff line number Diff line Loading @@ -126,16 +126,42 @@ static std::string h2ConnectionWindowBoost() { return h2BuildWindowUpdate(0, boost); } // Append DATA frames chunked to H2_MAX_FRAME_SIZE // Append a single frame directly to 'out' without creating a temp string. // Avoids allocating + copying a temporary std::string per frame. static void h2AppendFrameDirect(std::string &out, uint8_t type, uint8_t flags, uint32_t stream_id, const char *payload, size_t payload_size) { size_t old_size = out.size(); out.resize(old_size + 9 + payload_size); char *f = &out[old_size]; f[0] = static_cast<char>((payload_size >> 16) & 0xff); f[1] = static_cast<char>((payload_size >> 8) & 0xff); f[2] = static_cast<char>(payload_size & 0xff); f[3] = static_cast<char>(type); f[4] = static_cast<char>(flags); f[5] = static_cast<char>((stream_id >> 24) & 0x7f); f[6] = static_cast<char>((stream_id >> 16) & 0xff); f[7] = static_cast<char>((stream_id >> 8) & 0xff); f[8] = static_cast<char>(stream_id & 0xff); if (payload_size > 0) { std::memcpy(f + 9, payload, payload_size); } } // Append DATA frames chunked to H2_MAX_FRAME_SIZE. // Uses direct memcpy to avoid temporary string copies per chunk. static void h2AppendDataFrames(std::string &out, uint32_t stream_id, const std::string &body, bool end_stream) { // Pre-reserve space for all frames: 9-byte header per chunk + body data size_t num_chunks = (body.size() + H2_MAX_FRAME_SIZE - 1) / H2_MAX_FRAME_SIZE; out.reserve(out.size() + body.size() + num_chunks * H2_FRAME_HEADER_LEN); size_t offset = 0; while (offset < body.size()) { size_t chunk = std::min(body.size() - offset, H2_MAX_FRAME_SIZE); bool last_chunk = (offset + chunk >= body.size()); uint8_t flags = (last_chunk && end_stream) ? H2_FLAG_END_STREAM : 0; out += h2BuildFrame(H2_FRAME_DATA, flags, stream_id, body.substr(offset, chunk)); h2AppendFrameDirect(out, H2_FRAME_DATA, flags, stream_id, body.data() + offset, chunk); offset += chunk; } } Loading Loading @@ -270,6 +296,11 @@ libhttppp::HttpEvent::HttpEvent(std::vector<netplus::socket*> serversocket, int // Set ALPN selection and framing callbacks on SSL server sockets. // These are propagated to child sockets via ssl::accept(). if (auto* sslsock = dynamic_cast<netplus::ssl*>(sock)) { // Attach the TLS session cache to the server socket. // Child sockets created by accept() inherit the cache pointer // so that abbreviated TLS 1.2 handshakes can be used. sslsock->getTls().session_cache = &_tlsSessionCache; // Prefer h2, fall back to http/1.1 sslsock->setAlpnSelectCallback([](const std::vector<std::string>& protos) -> std::string { for (auto& p : protos) { Loading Loading @@ -468,6 +499,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, cureq._httpProtocol = 1; // Mark connection as HTTP/2 std::string out; out.reserve(4096); // Pre-allocate to avoid repeated reallocations size_t off = 0; // Consume HTTP/2 client connection preface if present Loading Loading @@ -518,16 +550,52 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, hpack_len -= 5; } // Decode HPACK headers using the connection's decoder // (maintains dynamic table state across frames) if (fflags & H2_FLAG_END_HEADERS) { // Complete header block in this single frame auto decoded = cureq.h2state().hpackDecoder.decode(hpack_data, hpack_len); if (fflags & H2_FLAG_END_STREAM) { // No body expected (GET, HEAD, etc.) — dispatch immediately _dispatchH2Stream(cureq, out, sid, decoded, "", tid, args); } else { // Body expected via DATA frames (POST, PUT, etc.) cureq.h2state().pendingIncoming[sid] = {std::move(decoded), ""}; auto &pending = cureq.h2state().pendingIncoming[sid]; pending.headers = std::move(decoded); pending.headersComplete = true; } } else { // END_HEADERS not set: HPACK block continues in CONTINUATION // frames. Accumulate raw bytes and defer decoding. auto &pending = cureq.h2state().pendingIncoming[sid]; pending.rawHpack.assign(hpack_data, hpack_data + hpack_len); pending.headersComplete = false; pending.endStreamOnHeaders = (fflags & H2_FLAG_END_STREAM) != 0; } break; } case H2_FRAME_CONTINUATION: { // RFC 7540 §6.10: CONTINUATION carries more HPACK data for // a HEADERS block whose END_HEADERS was not yet set. auto it = cureq.h2state().pendingIncoming.find(sid); if (it != cureq.h2state().pendingIncoming.end() && !it->second.headersComplete) { const uint8_t *cont_data = reinterpret_cast<const uint8_t*>(data + off); it->second.rawHpack.insert(it->second.rawHpack.end(), cont_data, cont_data + flen); if (fflags & H2_FLAG_END_HEADERS) { // Full HPACK block received — decode now it->second.headers = cureq.h2state().hpackDecoder.decode( it->second.rawHpack.data(), it->second.rawHpack.size()); it->second.rawHpack.clear(); it->second.rawHpack.shrink_to_fit(); it->second.headersComplete = true; if (it->second.endStreamOnHeaders) { // No body expected — dispatch immediately _dispatchH2Stream(cureq, out, sid, it->second.headers, "", tid, args); cureq.h2state().pendingIncoming.erase(it); } } } break; } Loading @@ -545,7 +613,9 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, } if (fflags & H2_FLAG_END_STREAM) { // All body data received — dispatch the request // Only dispatch once headers are fully received // (CONTINUATION may still be pending) if (it->second.headersComplete) { _dispatchH2Stream(cureq, out, sid, it->second.headers, it->second.body, Loading @@ -553,6 +623,7 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, cureq.h2state().pendingIncoming.erase(it); } } } break; } Loading Loading @@ -580,9 +651,27 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, case H2_FRAME_GOAWAY: goto done; case H2_FRAME_RST_STREAM: { // Clean up pending state for the reset stream to prevent // memory leaks and stalled stream bookkeeping. auto it = cureq.h2state().pendingIncoming.find(sid); if (it != cureq.h2state().pendingIncoming.end()) { cureq.h2state().pendingIncoming.erase(it); } // Also remove any pending outbound responses for this stream auto &pr = cureq.h2state().pendingResponses; for (auto pit = pr.begin(); pit != pr.end(); ) { if (pit->streamId == sid) { pit = pr.erase(pit); } else { ++pit; } } break; } case H2_FRAME_WINDOW_UPDATE: case H2_FRAME_PRIORITY: case H2_FRAME_RST_STREAM: // Acknowledge by ignoring — these don't need explicit responses break; Loading Loading @@ -888,16 +977,24 @@ void libhttppp::HttpEvent::ResponseEvent(netplus::con &curcon,const int tid,ULON if (cureq._httpProtocol == 1) { if (cureq._h2 && !cureq._h2->pendingResponses.empty()) { auto &pending = cureq._h2->pendingResponses.front(); // Batch up to 8 DATA chunks per event loop iteration // for significantly higher throughput on large responses. std::string batch; batch.reserve(std::min(pending.body.size() - pending.offset, 8 * H2_MAX_FRAME_SIZE) + 8 * H2_FRAME_HEADER_LEN); for (int i = 0; i < 8 && pending.offset < pending.body.size(); ++i) { size_t remaining = pending.body.size() - pending.offset; if (remaining > 0) { size_t chunk = std::min(remaining, H2_MAX_FRAME_SIZE); bool last = (pending.offset + chunk >= pending.body.size()); uint8_t flags = last ? H2_FLAG_END_STREAM : 0; std::string frame = h2BuildFrame( H2_FRAME_DATA, flags, pending.streamId, pending.body.substr(pending.offset, chunk)); cureq.SendData.append(frame.data(), frame.size()); h2AppendFrameDirect(batch, H2_FRAME_DATA, flags, pending.streamId, pending.body.data() + pending.offset, chunk); pending.offset += chunk; if (last) break; } if (!batch.empty()) { cureq.SendData.append(batch.data(), batch.size()); } if (pending.offset >= pending.body.size()) { cureq._h2->pendingResponses.pop_front(); Loading
src/httpd.h +4 −0 Original line number Diff line number Diff line Loading @@ -66,6 +66,10 @@ namespace libhttppp { protected: void CreateConnection(std::shared_ptr<netplus::con> &res); std::string _altSvcH3; // Alt-Svc value for HTTP/3 advertisement // TLS session cache — shared across all accepted SSL connections // to enable abbreviated TLS 1.2 handshakes on client reconnection. netplus::TlsSessionCache _tlsSessionCache; private: // Per-stream buffer for HTTP/3: accumulates data until fin struct H3StreamBuffer { Loading