Chapter Orientation
Volume 9 gave you every HTTP part (parsing, framing, fields, decoding); Volume 10's xhttp assembles them into a high-level client — this chapter starts at the thinnest layer: the easy convenience layer. xrtHttpClientGetSync(客户端, URL, NULL) (client, URL, NULL) completes request-response in one line; the key is that behind this line there is no hidden Engine, Client, or second request implementation — it merely creates a temporary request and hands it to xrtHttpClientDo() to freeze; timeouts, cancellation, redirects, cookies, caching, proxies, TLS, decompression, diagnostics, and the error chain are all identical to the low-level entrance. After this chapter you hold "the shortest usable path" plus a judgment: when to stay at the easy layer and when to sink to Chapter 99's request builder.
98.1引入
Writing a "fetch a URL and print the status" tool is one command in the curl era; in the C-library era it is usually thirty lines of assembly (Engine, Client, request, callback, wait, cleanup). The easy layer compresses this to three core lines — but the design question lies beyond "compression": most libraries' convenience layers are a second code path, quietly diverging from the low-level entrance (easy doesn't support redirects, error codes differ, diagnostics missing). xhttp's stance is that the convenience layer is only syntactic sugar: http_client_easy creates a temporary xhttprequest for the duration of the call, hands it to Do to freeze, and destroys it before returning — you can always mechanically rewrite an easy call into builder form with unchanged behavior.
This layer's existence makes xhttp's learning curve a three-step staircase: this chapter (one-line calls) → Chapter 99 (builders and the runtime panorama) → Chapter 104 (the server).
98.2概念
Three convenience entrances and three completion shapes
| Entrance | Semantics |
|---|---|
xrtHttpClientGet(客户端, URL, 选项, 回调, 上下文) (client, URL, options, callback, context) | body-less GET (never fabricates an empty body) |
xrtHttpClientPost(客户端, URL, 字节, 类型, 选项, 回调, 上下文) (client, URL, bytes, type, options, callback, context) | copies a fixed byte body |
xrtHttpClientSendBytes(...) | PUT/PATCH/any method carrying a fixed byte body |
The callback shape is the baseline; enabling http_client_easy_future adds Async (returning an xfuture) and Sync (blocking for an xhttpresult*) suffixes to the same entrances.Sync entrances must never block on a network Worker — they await the final state on the calling thread; calling them on a Worker deadlocks — the same discipline as Chapter 82's synchronous Future waiting.
The semantic divide: GET without a body vs POST with an empty body
The convenience layer deliberately distinguishes two "nothing heres": a GET has no body (no Content-Length/Content-Type generated); POST/SendBytes given an empty byte view means an explicit zero-length body (generating Content-Length: 0). The distinction comes from protocol semantics — a GET's body is undefined, while a POST empty body is a legal and explicit statement. Likewise, an empty ContentType generates no Content-Type — no lying.
The convenience layer's boundary: what it deliberately does not accept
Headers, streaming bodies, forms, auth, upload — the convenience entrances deliberately refuse these. When you need them, sink: xrtHttpRequestCreate() + xrtHttpRequestSetBody() + xrtHttpClientDo*() — the low-level entrances stay public alongside the convenience layer (Chapter 99). This "deliberately" is design: the convenience layer's parameter table stays one-line sized; complex needs are expressed with the explicit builder — there is no "hidden config struct of the convenience layer" middle state.
The result object: a read-only snapshot
xhttpresult is the read-only snapshot handed to the caller on completion: xrtHttpResultResponse(结果) (result) lends out an xhttpresponse (version/status/reason/Headers/trailers/the final effective URL — the landing point after redirects, Chapter 100), xrtHttpResponseBody(响应) (response) a contiguous body view, xrtHttpResultDestroy frees. The body grows on demand without a reserved fixed buffer; streaming execution (Chapter 99) allocates no body buffer and records only byte counts.The result must be Destroyed when used up — it is this chapter's only owning object you manage.
The minimal assembly
Three fixed steps before any easy call: xrtNetEngineConfigInit/Create/Start (Engine) → xrtHttpClientConfigInit + xrtHttpClientCreate(引擎, 配置) (engine, config) (Client) → the convenience call. Client creation validates the static configuration of Dial/Stream/Exchange/private Resolver on the spot — an invalid strategy fails immediately, not deferred to the first request (the same philosophy as Chapter 83's identity "validate at startup"). Cleanup order matches Chapter 67: Client first, then Engine; the Engine's Destroy may need to wait for Workers to drain (spin-retry is the standard posture — this chapter's sample's teardown is the template).
easy's place in xhttp's layering
98.3示例
First complete program: a one-line GET
The program below is from examples/http/client_easy — the complete tool of three core lines:
#include <stdio.h> #include <string.h> #include <xhttp.h> /* 用一行 GET 便利入口执行请求并输出响应状态与正文。 */ int main(int argc, char** argv) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xnetengine* pEngine; xhttpclient* pClient; xhttpresult* pResult; const xhttpresponse* pResponse; int iExit = 1; if ( argc != 2 ) { fprintf(stderr, "usage: client_easy <http-url>\n"); return 0; } xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { goto cleanup_engine; } xrtHttpClientConfigInit(&ClientConfig); pClient = xrtHttpClientCreate(pEngine, &ClientConfig); if ( pClient == NULL ) { goto cleanup_engine; } pResult = xrtHttpClientGetSync( pClient, (xstrview){ argv[1], strlen(argv[1]) }, NULL ); if ( pResult == NULL ) { goto cleanup_client; } pResponse = xrtHttpResultResponse(pResult); if ( pResponse != NULL ) { xbytesview Body = xrtHttpResponseBody(pResponse); printf("status: %u\n", (unsigned)xrtHttpResponseStatus(pResponse)); if ( Body.Size != 0 ) { (void)fwrite(Body.Data, 1, Body.Size, stdout); (void)fputc('\n', stdout); } iExit = 0; } xrtHttpResultDestroy(pResult); cleanup_client: xrtHttpClientDestroy(pClient); cleanup_engine: if ( pEngine != NULL ) { while ( !xrtNetEngineDestroy(pEngine) ) { xrtClearError(); xrtThreadYield(); } } return iExit; }
What just happened. (1) After the three-step assembly, xrtHttpClientGetSync(pClient, URL视图, NULL) (URL view, NULL) submits and waits in one line — the third parameter NULL takes the default call options (timeouts/redirects/caching etc. inherit the Client configuration). (2) The success path's trio: HttpResultResponse lends the response → HttpResponseStatus status code + HttpResponseBody body view (fwrite directly, zero copy) → HttpResultDestroy frees. (3) On failure GetSync returns NULL with details in the thread error slot (Chapter 4's model running through to here). (4) The teardown's spin while (!xrtNetEngineDestroy(...)) deserves note — Engine destruction may fail over in-flight draining; clearing the error and retrying until success is the standard xhttp spelling of "destroy the Engine last".
Second complete program: the Future shape
The second program is from examples/http/client_future — the asynchronous face of the same calls:
#include <stdio.h> #include <string.h> #include <xhttp.h> /* 以通用 Future 执行一次 GET,并在宿主线程读取拥有型 HTTP 结果。 */ int main(int argc, char** argv) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xnetengine* pEngine; xhttpclient* pClient; xhttprequest* pRequest; xfuture* pFuture; xfuture* pClosed; xhttpresult* pResult; const xhttpresponse* pResponse; int iExit = 1; if ( argc != 2 ) { fprintf(stderr, "usage: client_future <http-url>\n"); return 0; } xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { goto cleanup_engine; } xrtHttpClientConfigInit(&ClientConfig); pClient = xrtHttpClientCreate(pEngine, &ClientConfig); if ( pClient == NULL ) { goto cleanup_engine; } pRequest = xrtHttpRequestCreate( XRT_STR_LITERAL("GET"), (xstrview){ argv[1], strlen(argv[1]) } ); if ( pRequest == NULL ) { goto cleanup_client; } pFuture = xrtHttpClientDoAsync( pClient, pRequest, NULL ); xrtHttpRequestDestroy(pRequest); if ( (pFuture == NULL) || (xrtFutureWait(pFuture) != XWAIT_OK) || (xrtFutureState(pFuture) != XFUTURE_RESOLVED) ) { xrtFutureDestroy(pFuture); goto cleanup_client; } pResult = (xhttpresult*)xrtFutureValue(pFuture); pResponse = xrtHttpResultResponse(pResult); if ( pResponse != NULL ) { xbytesview Body = xrtHttpResponseBody(pResponse); printf("status: %u\n", (unsigned)xrtHttpResponseStatus(pResponse)); if ( Body.Size != 0 ) { (void)fwrite(Body.Data, 1, Body.Size, stdout); (void)fputc('\n', stdout); } iExit = 0; } xrtFutureDestroy(pFuture); cleanup_client: pClosed = xrtHttpClientWaitAsync(pClient); if ( iExit == 0 ) { (void)xrtHttpClientDrain(pClient); } else { (void)xrtHttpClientAbort(pClient); } xrtHttpClientDestroy(pClient); if ( pClosed != NULL ) { (void)xrtFutureWait(pClosed); xrtFutureDestroy(pClosed); } cleanup_engine: if ( pEngine != NULL ) { while ( !xrtNetEngineDestroy(pEngine) ) { xrtClearError(); xrtThreadYield(); } } return iExit; }
What just happened. (1) This one submits via the builder path (HttpRequestCreate + HttpClientDo), because the demonstration target includes the request object's explicit lifecycle — against easy: the Get family is exactly this three-line sequence folded. (2) After the Future completes, xrtFutureValue takes the result and HttpResultResponse walks the same read-only-snapshot path — Sync and Future share one result object; the convenience layer differs only in "who waits" (Chapter 82's dual shapes, xhttp edition). (3) Read the two samples side by side: same assembly, same teardown, same error model — the difference is only the two lines of submit and wait. That is the verifiable shape of "no second implementation".
98.4契约
- No second implementation: easy only creates a temporary request handed to
Doto freeze; every behavior (timeouts/cancellation/redirects/cookies/caching/proxy/TLS/decompression/diagnostics/error chain) matches the low level; mechanically rewritable into builder form. - Three entrances, three shapes: Get/Post/SendBytes × callback/Async/Sync; Sync must never block on a Worker.
- Body semantics: GET has no body (no fabricated empty body); an empty byte view = explicit zero length; an empty ContentType = the field is not generated.
- Deliberate refusals: Header/streaming/forms/auth — sink to the builder (Chapter 99); no middle state.
- Result object: read-only snapshot;
ResultResponselends the response,ResponseBodylends the body; the final effective URL includes the redirect landing point;ResultDestroywhen used up. - Startup validation: Client creation validates the static configuration; invalid strategies are not deferred to the first request.
- Trimming:
XHTTP_MODULE_HTTP_CLIENT_EASY(callback) and_EASY_FUTURE(Future/sync) independent. - Assembly discipline: Engine started first; cleanup Client before Engine; Engine Destroy spins and retries.
Common Mistakes
Symptom: deadlock — the completion callback runs on a Worker, and Sync waits for completion on the same thread.
Cause: Sync's implementation is "the calling thread awaits the final state"; a Worker waiting on work it must itself complete is a mutual wait forever. Inside a callback, either forward to another thread or just use the callback shape itself.
static void on_done(xhttpcall* pCall, ..., ptr pCtx) { /* on a Worker: */ pResult = xrtHttpClientGetSync(pClient, OtherUrl, NULL); /* deadlock */ }
static void on_done(xhttpcall* pCall, ..., ptr pCtx) { /* in a callback, submit the next request with the callback shape */ xrtHttpClientGet(pClient, OtherUrl, NULL, on_next, pCtx); } /* truly need serial blocking waits: use Sync/Future on a non-Worker thread */
Symptom: needing one Header, you abandon easy — hand-assembling Engine+DNS+TCP+TLS to rewrite the request path; or conversely, jamming a global config struct into easy's parameters.
Cause: missing that the boundary is deliberate: there is no hidden config above easy; what you need is not "a smarter easy" but sinking one layer — the builder's one HttpRequestSetHeader is enough, and the whole assembly is reused.
/* dropping the whole xhttp client for one Header */ hand_roll_engine_dns_tls_http(); /* 300 lines */
xhttprequest* pReq = xrtHttpRequestCreate(); xrtHttpRequestSetMethod(pReq, ...); xrtHttpRequestSetUrl(pReq, ...); xrtHttpRequestSetHeader(pReq, XRT_STR_LITERAL("Authorization"), Token); xrtHttpRequestSetBytes(pReq, Body, iSize, Type); pCall = xrtHttpClientDo(pClient, pReq, NULL, on_done, pCtx); xrtHttpRequestDestroy(pReq); /* after Do freezes the snapshot, the original request can be destroyed at once */
Symptom: one result object leaked per request; or the body view used halfway when the response is freed — crash.
Cause: xhttpresult is the owning snapshot (response/body belong to it); ResultResponse/ResponseBody are only borrows. The lifetime rule: the views live in the result; the result lives until your Destroy.
const xhttpresponse* pR = xrtHttpResultResponse(pResult); xrtHttpResultDestroy(pResult); /* destroyed the home first */ printf("%.*s\n", Body from pR); /* the view dangles */
const xhttpresponse* pR = xrtHttpResultResponse(pResult); xbytesview Body = xrtHttpResponseBody(pR); consume(Body); /* consume first */ xrtHttpResultDestroy(pResult); /* release after */
Programming Exercises
Against the same test service (a local Chapter 104 server sample or any httpbin-like service), fetch results with Get/Post/SendBytes respectively, printing status code and body length. Acceptance criteria: the assembly code of the three entrances is identical; the GET response generates no Content-Type request header.
Complete the same URL once with GetSync and once with Get+Future; use xhttpcallresult.Info's FirstByte/RequestWireBytes to compare the diagnostics of the two calls. Acceptance criteria: the diagnostic fields are semantically identical across paths; you can explain the difference between WireBytes and BodyBytes (on-the-wire bytes vs delivered body — different after decompression).
Rewrite a Post (with type) call line by line into HttpRequestCreate/SetMethodUrl/SetBytes + Do form; compare outputs and diagnostics of both versions. Then rewrite Get. Acceptance criteria: behavior item-by-item identical (status/body/final URL/error category); the rewrite needs no "easy-specific" documentation — because none exists.
Summary
| Topic | Quick reference |
|---|---|
| Design stance | no second implementation: a temporary request handed to Do to freeze; behavior fully consistent with the low level |
| Three entrances | Get (no body) / Post (copies bytes) / SendBytes (any method + bytes) |
| Three shapes | callback baseline; _EASY_FUTURE adds Async (xfuture) / Sync (blocking) |
| Body semantics | GET no body; empty view = explicit zero length; empty type = field not generated |
| Boundary | deliberately refuses Header/streaming/forms/auth - sink to the builder |
| Result object | read-only snapshot; Response/Body borrowed; final URL includes the redirect landing; Destroy mandatory |
| Sync discipline | never block on a Worker - use the callback shape inside callbacks |
| Startup validation | Client creation validates the static configuration |
| Assembly | Engine start → Client create → call; cleanup in reverse, Engine spins to destroy |
| Trimming | EASY and EASY_FUTURE independent macros |
📖 API references in this chapter:xhttp-http_client (101 entries)xhttp-http_client_easy (9 entries)net (247 entries)
Click for full signatures, parameter constraints, return values and error notes