Sync coding agent context

This commit is contained in:
vczh
2026-07-20 06:01:38 -07:00
parent b6101f7329
commit 9bd72bee09
5 changed files with 143 additions and 72 deletions
+3 -2
View File
@@ -140,10 +140,11 @@ Production-usable asynchronous text-protocol and typed named-channel abstraction
Testing-only layered loopback TCP and HTTP/1.1 APIs for asynchronous binary streams, parsed request/response connections and prefix-dispatched Mini HTTP services. Do not use these APIs in production code.
- Use `vl::inter_process::async_tcp_socket::IAsyncSocketServer`, `vl::inter_process::async_tcp_socket::IAsyncSocketClient`, `vl::inter_process::async_tcp_socket::IAsyncSocketConnection`, `vl::inter_process::async_tcp_socket::IAsyncSocketCallback` and `vl::inter_process::async_tcp_socket::AsyncSocketBuffer` for asynchronous loopback byte streams.
- Use `vl::inter_process::async_tcp_socket::IAsyncSocketServer`, `vl::inter_process::async_tcp_socket::IAsyncSocketClient`, `vl::inter_process::async_tcp_socket::IAsyncSocketConnection`, `vl::inter_process::async_tcp_socket::IAsyncSocketCallback` and `vl::inter_process::async_tcp_socket::AsyncSocketBuffer` for asynchronous loopback byte streams. Each server and client exposes its immutable construction port through `GetPort()`, and a client creates a fresh transport-preserving lane through `CreateSameEndpointClient()`.
- Use the platform-neutral `vl::inter_process::async_tcp_socket::CreateDefaultAsyncSocketServer` and `vl::inter_process::async_tcp_socket::CreateDefaultAsyncSocketClient` factories at the composition boundary.
- Use `vl::inter_process::async_tcp_socket::windows_socket::AsyncSocketServer` / `vl::inter_process::async_tcp_socket::windows_socket::AsyncSocketClient`, `vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketServer` / `vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketClient`, or `vl::inter_process::async_tcp_socket::macos_socket::AsyncSocketServer` / `vl::inter_process::async_tcp_socket::macos_socket::AsyncSocketClient` for the current platform.
- Use `vl::inter_process::async_tcp_socket::HttpRequest`, `vl::inter_process::async_tcp_socket::HttpResponse`, `vl::inter_process::async_tcp_socket::HttpRequestServer`, `vl::inter_process::async_tcp_socket::HttpRequestClient`, `vl::inter_process::async_tcp_socket::IHttpRequestConnection` and `vl::inter_process::async_tcp_socket::IHttpRequestCallback` for binary-safe sequential HTTP/1.1 exchanges.
- Use `vl::inter_process::async_tcp_socket::SocketHttpServerApi`, `vl::inter_process::async_tcp_socket::SocketHttpRequestContext` and `vl::inter_process::async_tcp_socket::SocketHttpClientApi` for prefix-dispatched Mini HTTP request/response work.
- Use `vl::inter_process::async_tcp_socket::SocketHttpServerApi`, `vl::inter_process::async_tcp_socket::SocketHttpRequestContext` and `vl::inter_process::async_tcp_socket::SocketHttpClientApi` for prefix-dispatched Mini HTTP request/response work; inject socket dependencies explicitly, derive the port from the injected socket, reuse the same server pointer when multiple prefixes must share one listener, and treat client-side 404 as a terminal `ResponseNotFound` error.
[API Explanation](./KB_VlppOS_InterProcessAsyncSocketBasedMiniHttpApi.md)
@@ -11,8 +11,8 @@ The types in `vl::inter_process::async_tcp_socket` form a portable, loopback-onl
| Layer | Main types | Responsibility |
| --- | --- | --- |
| Socket HTTP protocol and channels | `SocketHttpServer`, `SocketHttpClient`, `NetworkProtocolChannelServer`, `NetworkProtocolChannelClient` | Optional `WString` protocol over Mini HTTP and typed named channels over that protocol |
| Mini HTTP API | `SocketHttpServerApi`, `SocketHttpRequestContext`, `SocketHttpClientApi` | URL-prefix dispatch and convenient asynchronous queries |
| HTTP/1.1 messages | `HttpRequestServer`, `HttpRequestClient`, `HttpRequestConnection`, `IHttpRequestConnection` | Parse, serialize and sequence complete HTTP messages |
| Mini HTTP API | `SocketHttpServerApi`, `SocketHttpRequestContext`, `SocketHttpClientApi` | URL-prefix dispatch, parsed request conveniences, normalized response builders and asynchronous queries |
| HTTP/1.1 messages | `HttpRequestServer`, `HttpRequestClient`, `HttpRequestConnection`, `IHttpRequestConnection` | Parse, serialize and sequence complete HTTP messages; provide canonical framing, field, body and strict UTF-8 primitives |
| Asynchronous bytes | `IAsyncSocketServer`, `IAsyncSocketClient`, `IAsyncSocketConnection`, `AsyncSocketBuffer` | Ordered full-duplex byte streams |
| Native backend | Platform `AsyncSocketServer` and `AsyncSocketClient` | Winsock/IOCP, `io_uring`, or Network.framework |
@@ -37,26 +37,18 @@ All native implementations bind and connect to numeric IPv4 loopback. The public
The repository build scripts already supply these dependencies. A custom build must preserve the same link and compile options.
Source-tree code can select a native pair at the composition boundary:
Platform-neutral code selects the compiled native backend through the common factories declared in `AsyncSocket.h`:
```C++
#if defined VCZH_MSVC
using NativeServer = vl::inter_process::async_tcp_socket::windows_socket::AsyncSocketServer;
using NativeClient = vl::inter_process::async_tcp_socket::windows_socket::AsyncSocketClient;
#elif defined VCZH_GCC && defined VCZH_APPLE
using NativeServer = vl::inter_process::async_tcp_socket::macos_socket::AsyncSocketServer;
using NativeClient = vl::inter_process::async_tcp_socket::macos_socket::AsyncSocketClient;
#elif defined VCZH_GCC
using NativeServer = vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketServer;
using NativeClient = vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketClient;
#endif
auto socketServer = CreateDefaultAsyncSocketServer(port);
auto socketClient = CreateDefaultAsyncSocketClient(port);
```
The released platform umbrella is `VlppOS.Windows.h` on Windows and `VlppOS.Linux.h` on Linux and macOS. The common Mini HTTP server APIs select `NativeServer` internally. `SocketHttpClientApi` and the HTTP request wrappers intentionally require an explicit native client or server, while the higher `SocketHttpClient` selects its native client internally.
The released platform umbrella is `VlppOS.Windows.h` on Windows and `VlppOS.Linux.h` on Linux and macOS. Each compiled platform translation unit defines the same two factories. Higher HTTP layers never select a backend internally: inject the returned server or client at the composition boundary. `SocketHttpClient` takes only one client pointer, uses that exact object for its first physical lane, and obtains the additional independent lanes required by the logical protocol through `IAsyncSocketClient::CreateSameEndpointClient()`.
## Async Socket API
The async socket interfaces are defined in `Source/InterProcess/AsyncSocket/AsyncSocket.h`.
The async socket interfaces are defined in `Source/InterProcess/AsyncSocket/AsyncSocket.h`. `IAsyncSocketServer::GetPort()` and `IAsyncSocketClient::GetPort()` return the immutable loopback port selected during construction, so higher adapters do not accept a duplicate port argument. `IAsyncSocketClient::CreateSameEndpointClient()` returns a distinct fresh `Ready` client with the same transport configuration and port; it is the transport-preserving way for a multi-lane adapter to acquire another physical connection without accepting a separate factory.
### Connections and Callbacks
@@ -75,10 +67,10 @@ The async socket interfaces are defined in `Source/InterProcess/AsyncSocket/Asyn
An `IAsyncSocketServerCallback` accepts or rejects each physical connection. For an accepted connection, install its `IAsyncSocketCallback`, start reading, and then return `WaitForClientResult::Accept`.
```C++
auto server = Ptr<IAsyncSocketServer>(new NativeServer(port));
auto server = CreateDefaultAsyncSocketServer(port);
server->Start(&serverCallback);
auto client = Ptr<IAsyncSocketClient>(new NativeClient(port));
auto client = CreateDefaultAsyncSocketClient(port);
auto connection = client->GetConnection();
connection->InstallCallback(&clientCallback);
client->WaitForServer();
@@ -114,6 +106,34 @@ The binary-safe message values are defined in `Source/InterProcess/AsyncSocket/H
- `HttpBody` stores binary chunks and ordered trailers. Chunk boundaries and trailers are preserved for chunked messages.
- `ParseHttpRequestBodyToChunks` is a framing helper used by the parser; application code normally consumes the completed body instead.
### Canonical Analysis and Conversion Helpers
`HttpRequest.h` also exposes the protocol-neutral helpers used by the parser, serializer and higher layers. `AnalyzeHttpFraming(fields, framing)` is the single canonical analysis of `Content-Length`, `Transfer-Encoding` and `Connection: close` fields. It resets `framing` on entry; the output is authoritative only when the result is `HttpFramingAnalysisResult::Succeeded`.
`HttpFraming` reports:
- `kind` as `None`, `ContentLength` or `Chunked`;
- the agreed numeric `contentLength`;
- `contentLengthFieldCount` separately from the number of comma-list values in `contentLengthValueCount`;
- whether every physical length value is one unadorned digit sequence through `contentLengthValuesPlainDecimal`; and
- whether a `Connection` field contains `close`.
The analyzer expects already validated, lowercase-normalized field names and compares them exactly. Equal duplicate or comma-list `Content-Length` values can be valid ordinary HTTP framing, while conflicts, malformed values and `Content-Length` combined with `Transfer-Encoding` are invalid. Anything other than exactly one parameter-free `chunked` transfer coding is reported as `UnsupportedTransferCoding`. Successful framing analysis therefore describes the wire framing; a higher layer can still impose stricter field-count, plain-decimal or transfer-coding policy.
The remaining public helpers avoid reimplementing byte parsing in consumers:
| Helpers | Contract |
| --- | --- |
| `FindHttpField`, `CountHttpFields` | Find the first or count all exact matches for a caller-supplied lowercase normalized name; no case folding is performed. |
| `CreateAsciiHttpField` | Validate an ASCII token name, lowercase it and validate the ASCII field value. Invalid input raises `CHECK_ERROR`. |
| `DecodeAsciiHttpFieldValue`, `HttpFieldValueEqualsAscii` | Decode or compare explicit ASCII bytes without treating a non-ASCII value as text. |
| `TryGetHttpBodySize`, `FlattenHttpBody` | Count or flatten chunk data up to `HttpBodySizeLimit`. Trailers are not included, and a failed output operation leaves its output unchanged. |
| `SetHttpBodyBytes` | Replace chunks and trailers with an empty body or one flat chunk. It checks the body limit but does not reconcile enclosing framing fields. |
| `EncodeStrictUtf8`, `DecodeStrictUtf8` | Convert explicit-length Unicode/UTF-8 strictly, rejecting malformed Unicode and malformed UTF-8 while permitting empty text and embedded NUL. A failed conversion leaves its output unchanged. |
| `ValidateHttpRequestLine` | Validate the ASCII token method, printable-ASCII target and configured request-line size, returning `Succeeded`, `InvalidMethod`, `InvalidRequestTarget` or `TooLong`. |
These helpers deliberately do not decide routes, media types, logical-message validity or whether embedded NUL is acceptable. Those are policies for their consuming layer. Likewise, body-container helpers do not claim that a message is serializable when its headers and body disagree; serialization performs that complete validation.
The parser and serializer support HTTP/1.1 fixed-length and chunked framing and reject ambiguous or unsupported framing. The configured limits are:
| Item | Limit |
@@ -136,11 +156,11 @@ When directly constructing lower-layer `HttpRequest` or `HttpResponse` values, h
Derive from `HttpRequestServer`, inject `Ptr<IAsyncSocketServer>`, and override `OnClientConnected(IHttpRequestConnection*)`. The override retains and installs one thread-safe `IHttpRequestCallback`, calls `BeginReadingLoopUnsafe`, and accepts or rejects the connection. A most-derived destructor must call `HttpRequestServer::Stop` before destroying callback-visible state.
```C++
auto nativeServer = Ptr<IAsyncSocketServer>(new NativeServer(port));
auto nativeServer = CreateDefaultAsyncSocketServer(port);
RequestServer server(nativeServer); // Derives from HttpRequestServer.
server.Start();
auto nativeClient = Ptr<IAsyncSocketClient>(new NativeClient(port));
auto nativeClient = CreateDefaultAsyncSocketClient(port);
HttpRequestClient client(nativeClient);
auto connection = client.GetConnection();
connection->InstallCallback(&clientCallback);
@@ -166,20 +186,21 @@ The lower request layer does not synthesize `Host`, route a target, decode a que
## Mini HTTP Server API
`SocketHttpServerApi`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpServerApi.h`, owns a URL-prefix registration. Construction parses and stores the prefix. `Start` registers it; the first active API on a port creates and starts the native listener, while later prefixes join that shared listener.
`SocketHttpServerApi`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpServerApi.h`, owns a URL-prefix registration over a caller-injected `IAsyncSocketServer`. Construction parses and stores the prefix and reads the port from the server. `Start` registers it; the first active API receiving a particular server pointer starts that listener, while later APIs receiving the same pointer join it. The API never creates or replaces the socket server.
### Prefixes and Dispatch
Construct a derived server with `SocketHttpServerApi(urlPrefix, respondToOptions)`.
Construct a derived server with `SocketHttpServerApi(socketServer, urlPrefix, respondToOptions)`. `respondToOptions` defaults to `true`.
- The prefix must use plain `http://`, `localhost` or `127.0.0.1`, and an explicit port from 1 through 65535.
- An optional path may be percent-encoded UTF-8. Raw non-ASCII characters and backslashes are rejected; non-ASCII text must use percent-encoded UTF-8. Query and fragment components, NUL, invalid UTF-8, encoded separators and malformed escapes are also rejected.
- Trailing slashes are removed. `GetUrlPrefix` returns the normalized prefix.
- Multiple API objects with different prefixes can share one native listener on a port.
- The injected server supplies a port from 1 through 65535. The prefix is an empty origin path or begins with `/`; it does not contain a scheme, host or port.
- The path may contain percent-encoded UTF-8. Raw non-ASCII characters and backslashes are rejected; non-ASCII text must use percent-encoded UTF-8. Query and fragment components, NUL, invalid UTF-8, encoded separators and malformed escapes are also rejected.
- Trailing slashes are removed. `GetUrlPrefix` returns the absolute normalized value `http://localhost:PORT{urlPrefix}`.
- A prefix matches its exact path and slash-delimited descendants. For example, `/ABC/def` matches `/ABC/def` and `/ABC/def/item`, but not `/ABC/defghi`.
- Multiple API objects with different prefixes share one listener only when they receive the exact same `Ptr<IAsyncSocketServer>`. Different server objects that report the same port do not join each other.
- Every request is dispatched to the longest matching active prefix, including later requests on a persistent connection.
- A duplicate normalized prefix on the same port is rejected.
- A duplicate normalized prefix sharing the same socket server is rejected.
Before routing, the dispatcher requires HTTP/1.1 and exactly one valid `Host` whose authority matches an active prefix. `SocketHttpClientApi` supplies this field automatically; a manually constructed lower-layer request must supply it. The dispatcher accepts `GET`, `HEAD`, `POST` and `OPTIONS`. Unsupported methods and malformed requests receive automatic responses. When `respondToOptions` is true, supported browser preflight requests are answered before application dispatch.
Before routing, the dispatcher requires HTTP/1.1 and exactly one valid `Host` whose name is `localhost` or `127.0.0.1` and whose port matches the injected listener. `SocketHttpClientApi` supplies this field automatically; a manually constructed lower-layer request must supply it. The dispatcher accepts `GET`, `HEAD`, `POST` and `OPTIONS`. Unsupported methods and malformed requests receive automatic responses. When `respondToOptions` is true, supported browser preflight requests are answered before application dispatch.
### Handling a Request
@@ -188,10 +209,14 @@ Override `OnHttpRequestReceived(Ptr<SocketHttpRequestContext>)`:
- `GetRequest` returns the exact parsed `async_tcp_socket::HttpRequest`.
- `GetRelativePath` returns the decoded path relative to the selected prefix. An exact prefix match is `/`.
- `GetQuery` returns the raw query without the leading `?`.
- `TryGetBodyUtf8` flattens the complete body and strictly decodes UTF-8. It returns `false` for an oversized or malformed body without changing its output. Empty text and embedded NUL are valid at this layer; the caller owns any application-message policy.
- `Respond` wins at most once. Its optional completion callback receives `true` only after the physical response write completes.
- `RespondStatus` builds an empty response, `RespondBytes` builds a binary response, and `RespondUtf8` strictly encodes a text response. All three delegate to `Respond`, so they retain the same normalization, completion and context-race behavior.
- `Cancel` wins at most once, abandons the response, and closes that physical connection.
- A context can be retained and completed from another thread.
The response conveniences accept status codes from 200 through 599, a printable-ASCII reason and an optional ASCII content type. An empty reason uses the normal default reason during normalization, and an empty content type omits that field. Byte and UTF-8 bodies are limited by `HttpBodySizeLimit`; `RespondUtf8` rejects invalid Unicode. Arguments are validated before the context lifecycle is claimed: invalid input raises `CHECK_ERROR` even for an already consumed context, while a valid call on a consumed context returns `false`. Keep `Respond(Ptr<HttpResponse>)` for custom headers, chunk containers or other binary-oriented construction.
```C++
class StatusApi : public SocketHttpServerApi
{
@@ -200,22 +225,21 @@ protected:
{
if (context->GetRelativePath() != L"/status")
{
auto response = Ptr(new HttpResponse);
response->statusCode = 404;
response->reason = L"Not Found";
context->Respond(response);
context->RespondStatus(404, L"Not Found");
return;
}
auto response = Ptr(new HttpResponse);
response->statusCode = 200;
response->reason = L"OK";
context->Respond(response);
context->RespondUtf8(
200,
L"OK",
L"application/json; charset=utf-8",
L"{\"status\":\"ok\"}"
);
}
public:
StatusApi()
: SocketHttpServerApi(L"http://localhost:8888/api", true)
StatusApi(Ptr<IAsyncSocketServer> socketServer)
: SocketHttpServerApi(socketServer, L"/api")
{
}
@@ -225,7 +249,8 @@ public:
}
};
StatusApi api;
auto socketServer = CreateDefaultAsyncSocketServer(8888);
StatusApi api(socketServer);
api.Start();
shutdownRequested.Wait(); // Application/test coordination.
api.Stop();
@@ -233,17 +258,17 @@ api.Stop();
`Respond` normalizes the response. It validates the status, reason, headers and body; rejects response transfer coding and trailers; supplies missing `Date`, `Cache-Control: no-store` and `Access-Control-Allow-Origin: *` policy fields; and validates then rebuilds `Content-Length` framing. It also enforces `HEAD`, 204 and 304 body rules.
Always call `Stop` in the most-derived destructor before destroying fields used by `OnHttpRequestReceived`, response completions or `OnHttpServerStopping`. From outside callbacks, `Stop` unregisters the prefix, cancels its pending contexts and drains callbacks. A callback-reentrant call cannot unwind its current frame, so that frame's visible state must remain alive until it returns. The final API on a port also stops the shared listener.
Always call `Stop` in the most-derived destructor before destroying fields used by `OnHttpRequestReceived`, response completions or `OnHttpServerStopping`. From outside callbacks, `Stop` unregisters the prefix, cancels its pending contexts and drains callbacks. A callback-reentrant call cannot unwind its current frame, so that frame's visible state must remain alive until it returns. The final active API sharing an injected server also stops that listener.
The portable file-serving example is `Test/UnitTest/MiniHttpServer/Main.cpp`. It demonstrates multiple prefixes on different ports, binary response bodies, content types and explicit start/stop ordering.
## Mini HTTP Client API
`SocketHttpClientApi`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpClientApi.h`, owns one `HttpRequestClient` and one physical persistent connection. Inject a platform `IAsyncSocketClient` and supply the HTTP authority explicitly because the socket interface exposes only the port.
`SocketHttpClientApi`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpClientApi.h`, owns one `HttpRequestClient` and one physical persistent connection. Inject an `IAsyncSocketClient` and the loopback server name; the client supplies its locked-in port. This keeps socket selection and endpoint ownership outside the HTTP API.
```C++
auto nativeClient = Ptr<IAsyncSocketClient>(new NativeClient(8888));
auto client = Ptr(new SocketHttpClientApi(nativeClient, L"localhost:8888"));
auto nativeClient = CreateDefaultAsyncSocketClient(8888);
auto client = Ptr(new SocketHttpClientApi(nativeClient, L"localhost"));
client->WaitForServer();
vl::EventObject queryCompleted;
@@ -269,7 +294,13 @@ if (client->GetStatus() == ClientStatus::Connected)
}
auto&& response = result.Get<windows_http::HttpResponse>();
auto text = response.GetBodyUtf8();
WString text;
if (!response.TryGetBodyUtf8(text))
{
// The flat response body is not strict UTF-8.
queryCompleted.Signal();
return;
}
// Inspect response.statusCode, contentType, cookie and body.
queryCompleted.Signal();
}
@@ -290,13 +321,14 @@ client->Stop();
These convenient values differ from the lower binary-oriented message types:
- `windows_http::HttpRequest` has method, target query, flat body bytes, content type, accept values, cookie, extra headers and timeout fields.
- `SocketHttpClientApi` adds `Host` and `Accept-Encoding: identity`, then maps the flat request to `async_tcp_socket::HttpRequest`.
- TLS, credentials and `keepAliveOnStop` are rejected. A caller-provided `Host` must match the constructor authority, and response compression other than identity is unsupported.
- `SocketHttpClientApi` combines the constructor server and `client->GetPort()` into the `Host` authority, adds `Accept-Encoding: identity`, then maps the flat request to `async_tcp_socket::HttpRequest`.
- TLS, credentials and `keepAliveOnStop` are rejected. A caller-provided `Host` must match the authority formed from the constructor server and injected-client port, and response compression other than identity is unsupported.
- The injected socket owns connection and send timing. Only `receiveTimeout` controls the response deadline for an accepted HTTP exchange.
- `windows_http::HttpResponse` contains status, a flattened body, the first content type and the first returned cookie.
- HTTP status codes such as 404 are successful `HttpResponse` values. `HttpError` represents client validation or physical HTTP failure.
- `windows_http::HttpResponse::TryGetBodyUtf8` strictly decodes that flat body and distinguishes malformed UTF-8 from a valid empty body without changing its output on failure. `GetBodyUtf8` remains available for compatibility but does not provide this failure signal.
- Status codes other than 404 remain successful `HttpResponse` values. `HttpRequestClient` classifies 404 as the structured fatal failure `HttpResponseFailure::NotFound`, stops its injected socket connection, and `SocketHttpClientApi` reports `HttpError` with `SocketHttpClientErrorCode::ResponseNotFound`.
A transport, framing, unsupported-coding or response-timeout failure makes that `SocketHttpClientApi` terminal and completes its accepted queue with errors. Create a new API with a fresh native client when a higher layer requires physical reconnection. From outside callbacks, `Stop` cancels current and queued work and drains callbacks. A callback-reentrant `Stop` is supported, but the current callback must return before its captured state can be destroyed.
A 404, transport, framing, unsupported-coding or response-timeout failure makes that `SocketHttpClientApi` terminal and completes its accepted queue with errors. The API never creates or replaces the injected client; create a new API with a fresh native client when a higher layer requires physical reconnection. From outside callbacks, `Stop` cancels current and queued work and drains callbacks. A callback-reentrant `Stop` is supported, but the current callback must return before its captured state can be destroyed.
## Lifecycle Rules Across the Stack
@@ -317,4 +349,5 @@ A transport, framing, unsupported-coding or response-timeout failure makes that
- HTTP request client wrapper: [AsyncSocket_HttpRequestClient.h](../../Source/InterProcess/AsyncSocket/AsyncSocket_HttpRequestClient.h)
- Mini HTTP server API: [AsyncSocket_HttpServerApi.h](../../Source/InterProcess/AsyncSocket/AsyncSocket_HttpServerApi.h)
- Mini HTTP client API: [AsyncSocket_HttpClientApi.h](../../Source/InterProcess/AsyncSocket/AsyncSocket_HttpClientApi.h)
- Portable HTTP compatibility values: [NetworkProtocolHttp.h](../../Source/InterProcess/NetworkProtocolHttp.h)
- Portable Mini HTTP example server: [MiniHttpServer Main.cpp](../../Test/UnitTest/MiniHttpServer/Main.cpp)
@@ -44,8 +44,13 @@ The most-derived server destructor must call `Stop` before destroying fields tha
using namespace vl;
using namespace vl::inter_process;
auto socketClient = async_tcp_socket::CreateDefaultAsyncSocketClient(8888);
auto client = Ptr<INetworkProtocolClient>(
new async_tcp_socket::SocketHttpClient(L"/example", 8888)
new async_tcp_socket::SocketHttpClient(
socketClient,
L"localhost",
L"/example"
)
);
auto connection = client->GetConnection();
@@ -99,10 +104,10 @@ public:
ChannelServer(
const SerializationContext& serializationContext,
const WString& baseUrl,
vint port
Ptr<async_tcp_socket::IAsyncSocketServer> socketServer,
const WString& urlPrefix
)
: Base(serializationContext, baseUrl, port)
: Base(serializationContext, socketServer, urlPrefix)
{
}
@@ -153,8 +158,13 @@ public:
}
};
auto socketClient = async_tcp_socket::CreateDefaultAsyncSocketClient(8888);
auto protocolClient = Ptr<INetworkProtocolClient>(
new async_tcp_socket::SocketHttpClient(L"/example", 8888)
new async_tcp_socket::SocketHttpClient(
socketClient,
L"localhost",
L"/example"
)
);
auto channelClient = Ptr(
new ChannelClient<TPackage, TSerialization>(
@@ -180,7 +190,7 @@ The derived client can also handle `OnConnected`, `OnDisconnected`, `OnReadError
`async_tcp_socket::SocketHttpServer`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpServer.h`, derives from `async_tcp_socket::SocketHttpServerApi` and implements `INetworkProtocolServer`. `async_tcp_socket::SocketHttpClient`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpClient.h`, implements both `INetworkProtocolClient` and its single logical `INetworkProtocolConnection`.
The transport keeps a logical connection above short-lived or replaceable physical HTTP connections. These routes are relative to the configured `baseUrl`:
The transport keeps a logical connection above short-lived or replaceable physical HTTP connections. These routes are relative to the configured `urlPrefix`:
- `GET /VlppInterProcess/Connect` creates one logical connection token.
- `POST /VlppInterProcess/Request/{token}` is the client-maintained long poll for server-to-client messages.
@@ -188,6 +198,18 @@ The transport keeps a logical connection above short-lived or replaceable physic
Each normal body is the direct UTF-8 encoding of one `WString`. The media type is `application/json; charset=utf8`, but the body is not JSON syntax.
### Shared Wire-Contract Helpers
`Source/InterProcess/NetworkProtocolHttp.h` exposes the common, platform-neutral pieces of this wire contract in `vl::inter_process`:
- `HttpNetworkProtocolContentType` is the canonical media-type value used by all three routes.
- `CreateHttpNetworkProtocolConnectBody` and `ParseHttpNetworkProtocolConnectBody` construct and split the `requestPath;responsePath` Connect payload. Construction rejects empty paths and semicolons; parsing requires exactly one semicolon with a nonempty value on each side. Parsing does not validate either endpoint path.
- `ValidateHttpNetworkProtocolBaseUrl` and `ValidateHttpNetworkProtocolEndpointPath` apply the protocol's origin-path grammar. Base URLs may be empty and reject a trailing slash; endpoint paths must be nonempty. Use the endpoint validator separately after parsing a Connect payload.
- `IsValidHttpNetworkProtocolMessage` checks only the logical-message requirements that the value is nonempty and contains no NUL. It does not validate Unicode or encoded size; the protocol adapters additionally use strict UTF-8 conversion and enforce `async_tcp_socket::HttpBodySizeLimit` before accepting an outbound message.
- `CreateHttpNetworkProtocolConnectRequest`, `CreateHttpNetworkProtocolReceiveRequest` and `CreateHttpNetworkProtocolSendRequest` build the shared method, target, Accept, content-type, explicit empty-poll length and body shape in the portable `windows_http::HttpRequest` value. They do not validate the supplied target or select operation-specific timeout and stop-lifecycle options. The protocol client validates its targets and applies those options, including the infinite receive-poll timeout, after construction.
These helpers let another implementation reproduce the established wire facts without depending on either Socket HTTP state machine. They do not define retry, polling, FIFO, callback or shutdown policy.
The client uses two physical `async_tcp_socket::SocketHttpClientApi` lanes for one logical token. One lane keeps the receive poll alive; the other serializes connection control and client sends. Replacing a failed physical lane does not replace the logical `INetworkProtocolConnection` unless the transport reports final disconnection.
The server maps the token to its logical connection, queues server messages when no receive request is available, and dispatches client messages through `INetworkProtocolCallback::OnReadString`. The channel bridge above this layer is unaware of the HTTP routes and physical lanes.
@@ -198,16 +220,22 @@ High-level Socket HTTP construction and startup are identical on all supported p
```C++
typename TSerialization::ContextType serializationContext{};
auto socketServer = async_tcp_socket::CreateDefaultAsyncSocketServer(8888);
ChannelServer<TPackage, TSerialization> server(
serializationContext,
L"/example",
8888
socketServer,
L"/example"
);
server.Start();
{
auto socketClient = async_tcp_socket::CreateDefaultAsyncSocketClient(8888);
auto protocolClient = Ptr<INetworkProtocolClient>(
new async_tcp_socket::SocketHttpClient(L"/example", 8888)
new async_tcp_socket::SocketHttpClient(
socketClient,
L"localhost",
L"/example"
)
);
auto channelClient = Ptr(
new ChannelClient<TPackage, TSerialization>(
@@ -226,7 +254,7 @@ server.Start();
server.Stop();
```
The common implementation selects the native loopback backend at build time:
The common `CreateDefaultAsyncSocketServer` and `CreateDefaultAsyncSocketClient` factories select the compiled native loopback backend:
| Platform guard | Native server | Native client |
| --- | --- | --- |
@@ -236,10 +264,12 @@ The common implementation selects the native loopback backend at build time:
Use `VlppOS.Windows.h` on Windows; Winsock initialization and `Ws2_32.lib` linkage are internal. Use `VlppOS.Linux.h` on Linux and link `liburing`. Use the same `VlppOS.Linux.h` umbrella on macOS; custom builds must enable Clang Blocks and link CoreFoundation and Network.framework. The repository projects and build scripts already supply these platform settings.
`SocketHttpServer::Start` creates the platform listener internally. The default `SocketHttpClient` constructor creates platform clients internally. `WaitForServer` blocks while establishing the logical connection and returns after connection or after the client reaches a terminal stopped state, so call it on a thread that may block. Both sides use IPv4 loopback; they do not expose a remote-host option.
`SocketHttpServer` reads its port from and starts only the injected listener; it never selects or creates another server. `SocketHttpClient` takes only the injected client, reads its port, uses that exact object for the first physical lane, and obtains additional lanes through `client->CreateSameEndpointClient()`. `WaitForServer` blocks while establishing the logical connection and returns after connection or after the client reaches a terminal stopped state, so call it on a thread that may block. Both sides use IPv4 loopback; the explicit client server name is `localhost` (case-insensitive) or `127.0.0.1`.
Use the `SocketHttpClient(const WString&, vint, NativeClientFactory)` overload only when the composition root must select the native client explicitly. The factory receives the port and must return a fresh non-null `IAsyncSocketClient` for every initial or replacement physical connection.
`IAsyncSocketClient::CreateSameEndpointClient()` must return a distinct fresh non-null client in `ClientStatus::Ready` with the same immutable endpoint and `GetPort()` value, even while the source client is active or stopped. This capability keeps transport creation behind the injected abstraction while preserving the simultaneous receive long-poll lane, control/send lane, and terminal-lane recovery.
`baseUrl` is empty for the origin root or an ASCII origin-form prefix such as `/example`. A nonempty `baseUrl` must start with `/`, must not end with `/`, and must not contain a query, fragment, backslash, NUL, malformed escape, or encoded path separator. Server and client must use the same `baseUrl` and port.
`urlPrefix` is empty for the origin root or an ASCII origin-form prefix such as `/example`. Both adapters remove trailing slashes, so `/` becomes the origin root. A nonempty normalized prefix must start with `/` and must not contain a query, fragment, backslash, NUL, malformed escape, or encoded path separator. Server and client must use the same normalized prefix, and their injected sockets must report the same port.
The portable `HttpRequestClient` treats a 404 response as `HttpResponseFailure::NotFound`, reports a fatal error, and stops its socket. `SocketHttpClientApi` exposes this as `SocketHttpClientErrorCode::ResponseNotFound`; the logical `SocketHttpClient` reports one fatal local error immediately instead of retrying `/Connect`, `/Request`, or `/Response`.
For the lower socket, HTTP request and Mini HTTP API startup rules on each platform, see [Inter-Process Async-Socket-Based Mini HTTP API](./KB_VlppOS_InterProcessAsyncSocketBasedMiniHttpApi.md).
+3 -1
View File
@@ -10,9 +10,9 @@
- Proactively remove code made redundant by refactoring [8]
- Make `Stop()` drain asynchronous work before returning [6]
- Validate expectations against implementation and existing tests [5]
- Fix behavior at the owning state instead of patching symptoms [5]
- Use `WString::IndexOf` with `wchar_t` (not `const wchar_t*`) [4]
- Use `collections::BinarySearchLambda` on contiguous buffers (guard empty) [4]
- Fix behavior at the owning state instead of patching symptoms [4]
- Verify and localize portability on every target OS [3]
- Use `vl::Exception` for expected semantic failures and `CHECK_ERROR` for invariants [3]
- Extract abstractions only for real shared behavior [3]
@@ -294,6 +294,8 @@ When a lifecycle guarantee applies to every subclass, publish completion and fin
When shared code and tests pass on other platforms but fail on one target, fix the failing platform-specific implementation instead of weakening shared behavior or changing already-correct tests.
When a layered transport needs a stricter response policy than its general parser, enforce that policy in the object that owns the physical connection. Keep the lower parser reusable, and let the connection owner classify the response, report a structured failure, and stop or retry the transport as appropriate.
## Verify and localize portability on every target OS
Run the relevant tests on every target operating system whose behavior is being claimed, and report only the platforms actually exercised. Use contrasts between passing and failing platforms to narrow investigation toward the failing platform's implementation before changing shared code, while retaining cross-platform regression verification.
@@ -214,19 +214,24 @@ Use **async_tcp_socket::NetworkProtocolServer\<TAsyncSocketServer\>** and **asyn
Use **async_tcp_socket::SocketHttpServer** and **async_tcp_socket::SocketHttpClient** when the transport must use the legacy VlppOS HTTP wire protocol or interoperate with the Windows HTTP implementation.
The public construction surface is:
The public construction surface keeps native socket composition explicit:
```C++
SocketHttpServer(const WString& baseUrl, vint port);
SocketHttpServer(
Ptr<IAsyncSocketServer> socketServer,
const WString& urlPrefix
);
using NativeClientFactory = Func<Ptr<IAsyncSocketClient>(vint)>;
SocketHttpClient(const WString& baseUrl, vint port);
SocketHttpClient(const WString& baseUrl, vint port, NativeClientFactory clientFactory);
SocketHttpClient(
Ptr<IAsyncSocketClient> socketClient,
const WString& server,
const WString& urlPrefix
);
```
The default client factory selects the current platform's native async-socket client. The factory overload is per logical client and must return a fresh non-null native client for every initial or replacement physical connection.
Create the injected dependency at the application boundary with **CreateDefaultAsyncSocketServer(port)** or **CreateDefaultAsyncSocketClient(port)**. Each socket exposes that immutable construction port through **GetPort()**, so no HTTP constructor accepts a duplicate port. The server adapter never creates another listener. The client constructor takes only one socket client, uses that exact object for its first lane, and obtains the additional physical lanes required for full duplex and recovery through **IAsyncSocketClient::CreateSameEndpointClient()**. That method must return a distinct fresh **Ready** client with the same transport configuration and port.
The base URL is empty for the origin root or an ASCII origin-form prefix beginning with **/** and having no trailing slash, query, fragment, backslash, NUL, malformed escape, or encoded separator. The server listens at **http://localhost:PORT{baseUrl}** and the client sends the exact **localhost:PORT** HTTP authority.
The URL prefix is empty for the origin root or an ASCII origin-form prefix beginning with **/**. Both adapters remove trailing slashes, so **/** becomes the origin root. Prefixes cannot contain a query, fragment, backslash, NUL, malformed escape, or encoded separator. The server listens at **http://localhost:PORT{urlPrefix}** and the client combines the explicit loopback server with its injected socket port for the HTTP authority.
The HTTP adapter has one logical token and two physical client lanes:
@@ -238,7 +243,7 @@ logical connection {token}
**/Connect** creates one server logical connection and returns the two token-bearing paths. The receive lane submits a replacement poll before delivering a nonempty response to **OnReadString**. The send lane accepts one nonempty NUL-free **WString** per request, encodes it as direct UTF-8 bytes, and keeps one active FIFO head so retries cannot be overtaken. A server message generated synchronously while handling the same **/Response** can be piggybacked in that HTTP response; otherwise server messages complete the pending long poll in FIFO order.
HTTP status, exact content type, body size, UTF-8, NUL, and returned-path validation belong to the adapter. A non-200 or malformed response retries on the healthy physical API. A terminal transport failure replaces only the affected physical lane through **NativeClientFactory**, retaining the logical token. **/Connect** and **/Response** stop after three failed attempts with two nonfatal local errors followed by one fatal local error; **/Request** retries silently while running.
HTTP status, exact content type, body size, UTF-8, NUL, and returned-path validation belong to the adapter. A non-200 response other than 404 or a malformed response retries on the healthy physical API. A terminal transport failure replaces only the affected physical lane through **CreateSameEndpointClient()**, retaining the logical token. **HttpRequestClient** classifies 404 as **HttpResponseFailure::NotFound** and stops the physical socket; **SocketHttpClient** reports it immediately as one fatal local error without retrying. Other **/Connect** and **/Response** failures stop after three failed attempts with two nonfatal local errors followed by one fatal local error; **/Request** retries silently while running.
**Stop** rejects new sends, gives accepted sends a bounded drain opportunity, cancels the infinite poll, drains replacement workers and lower callbacks, and reports **OnDisconnected** once. It can be called repeatedly or from an adapter callback; a reentrant call waits for other callback frames but not the current frame.
@@ -246,7 +251,7 @@ The protocol has no acknowledgement, deduplication, heartbeat, or disconnect rou
### Portable HTTP Request Helpers
**async_tcp_socket::SocketHttpServerApi** and **async_tcp_socket::SocketHttpClientApi** are lower request/response helpers, not raw protocol transports. The server API owns prefix dispatch, response framing, CORS and callback draining. **SocketHttpRequestContext::Respond** is one-shot and reports whether its physical response completed; **Cancel** abandons a pending context. One client API serializes HTTP exchanges on one physical connection and becomes terminal after transport/framing/timeout failure. **HttpRequestServer**, **HttpRequestClient**, and **HttpRequestConnection** are the still-lower HTTP/1.1 layer.
**async_tcp_socket::SocketHttpServerApi** and **async_tcp_socket::SocketHttpClientApi** are lower request/response helpers, not raw protocol transports. Construct a server API with **(socketServer, urlPrefix)**; APIs share a listener only when given the same server pointer, and a prefix matches only itself or slash-delimited descendants. Construct a client API with **(socketClient, server)**. Both derive the port from the injected socket, and neither chooses a platform socket internally. The server API owns prefix dispatch, response framing, CORS and callback draining. **SocketHttpRequestContext::Respond** is one-shot and reports whether its physical response completed; **Cancel** abandons a pending context. One client API serializes HTTP exchanges on one physical connection and becomes terminal after 404, transport, framing or timeout failure; 404 is reported as **SocketHttpClientErrorCode::ResponseNotFound**. **HttpRequestServer**, **HttpRequestClient**, and **HttpRequestConnection** are the still-lower HTTP/1.1 layer.
## Windows Implementations