/
githubmirror
/
libmicrohttpd
Обзор
Документация
Войти
/
githubmirror
/
libmicrohttpd
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
doc/chapters/asyncresponses.inc
501 строка
19 KB
Christian Grothoff
add tutorial for #5657
28 июл 2026, 23:47
Не верифицирован
28 июл 2026, 23:47
8dbbdd7
Код
Авторство
О чём код?
The previous chapter left one question open. A content reader that has no data ready may return @code{0}, but doing so turns the daemon into a busy loop, and the honest answer---suspend the connection---was only sketched there. This chapter builds the thing itself: a server whose slow work happens in threads of its own, whose connections are parked while that work is under way, and which is driven by an external @code{select()} loop so that the application stays in charge of its own event handling. The combination is a common one. Most requests are cheap and can be answered on the spot; a few of them start a report, a backup or a query that runs for seconds. Handling the slow ones inline would block every other client for as long as they take. Giving every connection its own thread---@code{MHD_USE_THREAD_PER_CONNECTION}---is no answer either, both because it scales badly and because suspending is not available in that mode at all. What is wanted is a single event loop that never blocks, plus a worker per slow job. @heading Who may call what Almost every difficulty in this design comes from one rule, so it is worth stating before any code: @table @asis @item @code{MHD_queue_response()} Must be called from the thread that runs the daemon, from inside the access handler. A worker thread must never queue a response. @item @code{MHD_suspend_connection()} May only be called from the access handler or from a content reader callback---that is, from @emph{MHD}'s own thread, while @emph{MHD} is calling into the application. @item @code{MHD_resume_connection()} May be called from any thread at any time, and this is the exception that makes the whole design work. It is the only @emph{MHD} function a worker thread ever needs. @end table @noindent So the split is: the worker computes and, when it has something, resumes. Everything that touches the connection or the response happens on the daemon's thread, where @emph{MHD} calls the application back. The worker and the daemon share exactly one object, a per-request context, and the only thing the worker does with the connection handle it holds is pass it to @code{MHD_resume_connection()}. The daemon has to be started with @code{MHD_ALLOW_SUSPEND_RESUME}; both suspend and resume call @code{MHD_PANIC()} and abort the process otherwise. Note also that suspending does not work with a thread pool: @code{MHD_resume_connection()} asserts that the daemon has none. @heading The event loop Leaving out @code{MHD_USE_INTERNAL_POLLING_THREAD} puts @emph{MHD} into external polling mode: it starts no thread of its own and does nothing until the application asks it to. The loop is the usual one, and the example adds its own descriptor to the very same sets---there is nothing special about @emph{MHD}'s: @verbatim FD_ZERO (&rs); FD_ZERO (&ws); FD_ZERO (&es); max = 0; if (MHD_YES != MHD_get_fdset (daemon, &rs, &ws, &es, &max)) break; /* fatal internal error */ FD_SET (wake_pipe[0], &rs); /* our own wakeup pipe */ if (max < wake_pipe[0]) max = wake_pipe[0]; if (MHD_YES == MHD_get_timeout64 (daemon, &mhd_timeout)) { ...fill tv...; tvp = &tv; } else tvp = NULL; /* nothing to wait for */ select ((int) max + 1, &rs, &ws, &es, tvp); MHD_run_from_select (daemon, &rs, &ws, &es); @end verbatim @noindent The interesting branch is the one that sets @code{tvp} to @code{NULL}. @code{MHD_get_timeout64()} returns @code{MHD_NO} when @emph{MHD} has nothing whatsoever to wait for, and with every connection suspended that is precisely the situation: suspended connections do not time out, so there is no deadline to wake up for. The loop then blocks in @code{select()} indefinitely. Whether the server works at all depends on something waking it up. @heading Waking the loop up That something is @emph{MHD}'s inter-thread communication channel. @code{MHD_ALLOW_SUSPEND_RESUME} is defined as @code{8192 | MHD_USE_ITC}, so requesting suspend support requests the ITC as well; @code{MHD_get_fdset()} puts the reading end of that channel into the read set, ahead of everything else; and @code{MHD_resume_connection()} writes a byte to it. A worker thread that resumes a connection therefore breaks the main thread out of @code{select()}, even though the two share nothing but the daemon handle. This deserves emphasis because the header file is misleading about it: the comment on @code{MHD_USE_ITC} says the flag is ignored with external polling. That is true for the flag in isolation, but not for the channel: @code{MHD_ALLOW_SUSPEND_RESUME} creates it regardless of the polling mode, and in external mode it is the application's own @code{select()} that watches it. Without that, a resumed connection would sit untouched until some unrelated event happened to wake the loop. There is no race to guard against here. If the worker resumes in the window between @code{MHD_get_fdset()} and @code{select()}, the byte is already in the channel when @code{select()} is entered, and @code{select()} returns immediately---the channel is level triggered, so a wakeup cannot be missed. The same property makes it safe for a worker to resume a connection that has not finished suspending yet: @emph{MHD} remembers the request and cancels the pending suspension instead of parking the connection. Neither side needs to know what the other is doing. @heading Answering in one piece The simplest of the two patterns is for an answer that is only useful as a whole---a generated report, the result of a query. Nothing can be sent before it is finished, so the connection is suspended in the access handler and the response is queued when @emph{MHD} comes back: @verbatim static enum MHD_Result handle_slow (struct MHD_Connection *connection, void **req_cls) { struct Job *job = *req_cls; if (NULL == job) { /* First call for this request: start the work and ask MHD to come back to us. */ job = job_create (connection, 0); if (NULL == job) return MHD_NO; if (MHD_NO == job_start (job)) { job_unref (job); job_unref (job); return MHD_NO; } *req_cls = job; return MHD_YES; } pthread_mutex_lock (&job->lock); if (0 == job->finished) { job->suspended = 1; MHD_suspend_connection (connection); pthread_mutex_unlock (&job->lock); return MHD_YES; } ...copy the result out from under the lock... pthread_mutex_unlock (&job->lock); ...MHD_create_response_from_buffer_copy() and MHD_queue_response()... } @end verbatim @noindent The control flow is worth following, because it is not obvious that it terminates. @emph{MHD} calls the access handler for a request more than once; the first call is the one where @code{*req_cls} is still @code{NULL}, and returning @code{MHD_YES} without queueing anything tells @emph{MHD} to carry on. On the second call the worker is running but not finished, so the connection is suspended and the handler returns again. When the worker resumes the connection, @emph{MHD} calls the handler a third time---on its own thread, with the same @code{*req_cls}---and this time @code{finished} is set and the response is queued. That third call is the answer to the question this pattern exists for. The worker never queues anything; it merely makes the daemon call the application back at a moment when the answer is ready. @heading Answering as it is produced The second pattern is the more useful one when the answer is long, and it is what makes a browser show results as they appear rather than after a long blank pause. Here the response is queued @emph{immediately}, with a content reader and an unknown size, so the headers go out at once and the body follows as the worker fills it in: @verbatim job = job_create (connection, 1); response = MHD_create_response_from_callback (MHD_SIZE_UNKNOWN, IO_BLOCK_SIZE, &stream_reader, job, &stream_done); ... MHD_add_response_header (response, MHD_HTTP_HEADER_CONTENT_TYPE, "text/event-stream"); ret = MHD_queue_response (connection, MHD_HTTP_OK, response); MHD_destroy_response (response); @end verbatim @noindent Because the size is unknown, @emph{MHD} uses chunked transfer encoding, and every piece the content reader hands back is framed as its own chunk and goes out on the wire straight away. The content reader is where suspending happens: @verbatim static ssize_t stream_reader (void *cls, uint64_t pos, char *buf, size_t max) { struct Job *job = cls; size_t ready; pthread_mutex_lock (&job->lock); if (job->off == job->fill) { job->off = 0; job->fill = 0; if (0 != job->finished) { pthread_mutex_unlock (&job->lock); return MHD_CONTENT_READER_END_OF_STREAM; } /* Nothing to send yet. Take the connection out of the event loop; the worker will put it back in. */ job->suspended = 1; MHD_suspend_connection (job->connection); pthread_mutex_unlock (&job->lock); return 0; } ready = job->fill - job->off; if (ready > max) ready = max; memcpy (buf, &job->payload[job->off], ready); job->off += ready; pthread_mutex_unlock (&job->lock); return (ssize_t) ready; } @end verbatim @noindent The return value is still @code{0}, exactly as in the busy-waiting version the previous chapter warned about. The difference is the line above it. Suspending removes the connection from the event loop, so @emph{MHD} has no reason to come back until the worker says so, and the process goes to sleep. The example ships with a test that measures this: streaming a document that takes one second to produce costs the server no measurable CPU time at all while the connection is suspended, and very nearly a full second---one core, saturated---when the @code{MHD_suspend_connection()} call is removed and only the @code{return 0} is left. The bytes that arrive are identical in both cases, which is what makes this such an easy mistake to keep. @heading The shared state Everything the two threads share sits in one structure per request: @verbatim struct Job { pthread_mutex_t lock; pthread_cond_t cond; struct MHD_Connection *connection; char payload[MAX_PAYLOAD]; size_t fill; size_t off; unsigned int rc; int finished; int suspended; int abandoned; ... }; @end verbatim @noindent There is no registry of pending requests involved in answering them, and no lookup: the pointer travels in @code{*req_cls} for the first pattern and in the content reader's @code{crc_cls} for the second, and both the daemon and the worker are handed it directly. A shared list of all jobs is still useful, but only for shutting down in an orderly fashion, and it is protected by a lock of its own that is always taken before any job's lock, never after. The structure is reference counted, with one reference held by @emph{MHD} and one by the worker. The @emph{MHD} side lets go in the free callback of the response for a streamed answer, and in the @code{MHD_OPTION_NOTIFY_COMPLETED} callback for the other pattern; the worker lets go when it returns. Dropping the last reference does not free the job, though, because the worker still has to be joined and only the main thread may do that. The main loop therefore ends with a sweep that joins and frees whatever has no references left. @heading When the client goes away This is the part that is easy to get wrong, because it does not happen during development and does happen constantly in production. A client that closes the connection halfway through a download leaves a worker running with a pointer to a connection @emph{MHD} is about to free. The two callbacks that tell the application a request is over---the response's free callback and @code{MHD_OPTION_NOTIFY_COMPLETED}---are therefore where the worker is cut loose: @verbatim static void job_detach (struct Job *job) { pthread_mutex_lock (&job->lock); job->abandoned = 1; job->connection = NULL; pthread_cond_signal (&job->cond); pthread_mutex_unlock (&job->lock); job_unref (job); } @end verbatim @noindent and the worker checks that flag before every resume: @verbatim static void job_resume_locked (struct Job *job) { if ( (0 == job->suspended) || (0 != job->abandoned) ) return; job->suspended = 0; MHD_resume_connection (job->connection); } @end verbatim @noindent Note that this function is called with the job's lock @emph{held}, and calls into @emph{MHD} without releasing it. That is deliberate and it is what makes the pattern safe. @emph{MHD} destroys the connection only after the completion callback has returned, and that callback cannot run while the worker holds the lock; so for as long as the worker is inside @code{MHD_resume_connection()}, the connection it passes is guaranteed to still exist. Dropping the lock first and resuming afterwards would open exactly the window this is meant to close. The reverse order never occurs: @emph{MHD} invokes the free and completion callbacks without holding the internal locks that @code{MHD_resume_connection()} acquires, so there is no way for the two to deadlock. The condition variable in the same function is a convenience rather than a necessity. Signalling it wakes a worker that is asleep between steps, so that the work stops within microseconds of the client disappearing instead of running to completion for nobody. @heading Shutting down Stopping a daemon that still has suspended connections is an API violation, and a suspended connection is one that only a worker can release. The order is therefore fixed: @enumerate @item Stop handing out new jobs. @item Tell every worker to give up, and @emph{join} them. After this point no connection can be suspended any more: the workers resume before they exit, and the content reader only ever suspends while a worker is running. @item Keep running the event loop until the connections have drained, so that the last bytes actually reach the clients. @item @code{MHD_stop_daemon()}. @end enumerate @noindent Skipping the join in step two is the tempting shortcut, and it is the one that leaves a connection parked forever with nothing left alive to wake it. @heading Example code The complete program is available as @code{asyncresponse.c}. It runs one external @code{select()} loop and serves four URLs: @table @code @item / A small page whose JavaScript opens the stream below and appends a row for every event as it arrives. The rows visibly trickle in, one per step, which is the most direct demonstration of what the chapter is about. @item /events The stream: a response is queued at once, and the content reader suspends between steps. The body is a sequence of server-sent events, which needs no library on the browser side. @item /slow The other pattern: the connection is suspended in the access handler and a complete answer is queued once the worker is done. @item /fast Answered on the spot. The page has a button that requests it and reports the round trip time; it stays in the low milliseconds while jobs are running, which is the point of suspending rather than blocking. @end table @noindent The program takes the port, the duration of a step in milliseconds and the number of steps on the command line. Passing @code{0} as the port makes the operating system choose a free one, which the program then reports via @code{MHD_DAEMON_INFO_BIND_PORT}: @verbatim $ ./asyncresponse 0 250 20 Listening on port 44321 @end verbatim @noindent The accompanying @code{test_asyncresponse.c} starts exactly that binary and checks over HTTP that @code{/slow} waits for its worker, that the body of @code{/events} really arrives in pieces spread over the lifetime of the request rather than in one burst at the end, that @code{/fast} is still served promptly while jobs are parked, that a client which hangs up in the middle takes neither the server nor the worker with it, and that the process exits cleanly when it is signalled while a connection is suspended. It also compares the server's CPU time against the wall clock time of a transfer, which is the only one of those checks that notices the difference between a suspended connection and a busy-waiting one. @heading Remarks @emph{MHD} does not detect that a client has disconnected while a connection is suspended, and the connection timeout does not run either. A suspended connection stays suspended until somebody resumes it, without exception. Any timeout on the work itself is therefore the application's responsibility. Suspended connections still count against the connection limits, both the global one and the per-IP one. A server that parks connections for minutes needs its limits sized for the number of jobs it expects to have in flight, not for the number it expects to be transmitting. If the client vanishes in the middle of a chunked response, the next write fails and @emph{MHD} logs it---with @code{MHD_USE_ERROR_LOG} enabled the example prints @code{Failed to send the chunked response body ... The socket is no longer available for sending}. That is the normal course of events for an aborted download and not a sign of a problem. Finally, none of this is specific to @code{select()}. The same argument holds for @code{poll()} and for @code{epoll}, where the daemon's descriptor can be obtained with @code{MHD_DAEMON_INFO_EPOLL_FD} and added to an epoll set of the application's own; @code{suspend_resume_epoll.c} among the distribution's examples shows that variant. @heading Exercises @itemize @bullet @item Give the jobs a deadline. Since a suspended connection never times out on its own, add a check to the main loop that resumes---and answers with @code{503}---any job that has been running for too long, and confirm with a step duration long enough to trigger it. @item Remove the @code{MHD_suspend_connection()} call from the content reader, leaving only the @code{return 0}, and watch the server process with @code{top} while a single client downloads the stream. Then put it back and watch again. Both servers send the same bytes. @item Take the resume out of the worker's final block, so that a finished job leaves its connection parked, and then stop the server while a job is in flight. Watch what the shutdown does---and what @code{valgrind} has to say about it---and work out which of the four steps above was violated. @item Replace the per-request thread with a fixed pool of worker threads and a queue of pending jobs. The reference counting and the @code{abandoned} flag should not need to change at all; if they do, the ownership rules were not as clean as they looked. @item Serve the stream to two browser windows at once and confirm from the timestamps that the two jobs run concurrently and are interleaved by a single-threaded event loop. @end itemize