mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Reduce number of Transport:send/2 calls for HTTP/2
When sending a complete response it is far more efficient
to send the headers and the body in one Transport:send/2
call instead of two or more, at least for small responses.
This is the HTTP/2 counterpart to what was done for HTTP/1.1
many years ago in bfab8d4b22
.
In HTTP/2's case however the implementation is a little
more difficult due to flow control. On the other hand the
optimization will apply not only for headers/body but also
for the body of multiple separate responses, which may need
to be sent all at the same time when we receive a WINDOW_UPDATE
frame.
When a body is sent using sendfile however a separate call
is still made.
This commit is contained in:
parent
3a7232b019
commit
592029070d
1 changed files with 73 additions and 35 deletions
|
@ -345,7 +345,7 @@ frame(State=#state{http2_machine=HTTP2Machine0}, Frame) ->
|
||||||
%% We may need to send an alarm for each of the streams sending data.
|
%% We may need to send an alarm for each of the streams sending data.
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun({StreamID, _, _}, S) -> maybe_send_data_alarm(S, HTTP2Machine0, StreamID) end,
|
fun({StreamID, _, _}, S) -> maybe_send_data_alarm(S, HTTP2Machine0, StreamID) end,
|
||||||
send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData),
|
send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData, []),
|
||||||
SendData);
|
SendData);
|
||||||
{error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} ->
|
{error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} ->
|
||||||
reset_stream(State#state{http2_machine=HTTP2Machine},
|
reset_stream(State#state{http2_machine=HTTP2Machine},
|
||||||
|
@ -623,11 +623,11 @@ commands(State0, StreamID, [{headers, StatusCode, Headers}|Tail]) ->
|
||||||
commands(State, StreamID, Tail);
|
commands(State, StreamID, Tail);
|
||||||
%% Send a response body chunk.
|
%% Send a response body chunk.
|
||||||
commands(State0, StreamID, [{data, IsFin, Data}|Tail]) ->
|
commands(State0, StreamID, [{data, IsFin, Data}|Tail]) ->
|
||||||
State = maybe_send_data(State0, StreamID, IsFin, Data),
|
State = maybe_send_data(State0, StreamID, IsFin, Data, []),
|
||||||
commands(State, StreamID, Tail);
|
commands(State, StreamID, Tail);
|
||||||
%% Send trailers.
|
%% Send trailers.
|
||||||
commands(State0, StreamID, [{trailers, Trailers}|Tail]) ->
|
commands(State0, StreamID, [{trailers, Trailers}|Tail]) ->
|
||||||
State = maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}),
|
State = maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}, []),
|
||||||
commands(State, StreamID, Tail);
|
commands(State, StreamID, Tail);
|
||||||
%% Send a push promise.
|
%% Send a push promise.
|
||||||
%%
|
%%
|
||||||
|
@ -728,7 +728,7 @@ update_window(State=#state{socket=Socket, transport=Transport,
|
||||||
|
|
||||||
%% Send the response, trailers or data.
|
%% Send the response, trailers or data.
|
||||||
|
|
||||||
send_response(State0, StreamID, StatusCode, Headers, Body) ->
|
send_response(State0=#state{http2_machine=HTTP2Machine0}, StreamID, StatusCode, Headers, Body) ->
|
||||||
Size = case Body of
|
Size = case Body of
|
||||||
{sendfile, _, Bytes, _} -> Bytes;
|
{sendfile, _, Bytes, _} -> Bytes;
|
||||||
_ -> iolist_size(Body)
|
_ -> iolist_size(Body)
|
||||||
|
@ -738,8 +738,14 @@ send_response(State0, StreamID, StatusCode, Headers, Body) ->
|
||||||
State = send_headers(State0, StreamID, fin, StatusCode, Headers),
|
State = send_headers(State0, StreamID, fin, StatusCode, Headers),
|
||||||
maybe_terminate_stream(State, StreamID, fin);
|
maybe_terminate_stream(State, StreamID, fin);
|
||||||
_ ->
|
_ ->
|
||||||
State = send_headers(State0, StreamID, nofin, StatusCode, Headers),
|
%% @todo Add a test for HEAD to make sure we don't send the body when
|
||||||
maybe_send_data(State, StreamID, fin, Body)
|
%% returning {response...} from a stream handler (or {headers...} then {data...}).
|
||||||
|
{ok, _IsFin, HeaderBlock, HTTP2Machine}
|
||||||
|
= cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, nofin,
|
||||||
|
#{status => cow_http:status_to_integer(StatusCode)},
|
||||||
|
headers_to_list(Headers)),
|
||||||
|
maybe_send_data(State0#state{http2_machine=HTTP2Machine}, StreamID, fin, Body,
|
||||||
|
[cow_http2:headers(StreamID, nofin, HeaderBlock)])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
send_headers(State=#state{socket=Socket, transport=Transport,
|
send_headers(State=#state{socket=Socket, transport=Transport,
|
||||||
|
@ -758,17 +764,24 @@ headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) ->
|
||||||
headers_to_list(Headers) ->
|
headers_to_list(Headers) ->
|
||||||
maps:to_list(Headers).
|
maps:to_list(Headers).
|
||||||
|
|
||||||
maybe_send_data(State0=#state{http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0) ->
|
maybe_send_data(State0=#state{socket=Socket, transport=Transport,
|
||||||
|
http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0, Prefix) ->
|
||||||
Data = case is_tuple(Data0) of
|
Data = case is_tuple(Data0) of
|
||||||
false -> {data, Data0};
|
false -> {data, Data0};
|
||||||
true -> Data0
|
true -> Data0
|
||||||
end,
|
end,
|
||||||
case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of
|
case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of
|
||||||
{ok, HTTP2Machine} ->
|
{ok, HTTP2Machine} ->
|
||||||
|
%% If we have prefix data (like a HEADERS frame) we need to send it
|
||||||
|
%% even if we do not send any DATA frames.
|
||||||
|
case Prefix of
|
||||||
|
[] -> ok;
|
||||||
|
_ -> Transport:send(Socket, Prefix)
|
||||||
|
end,
|
||||||
maybe_send_data_alarm(State0#state{http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID);
|
maybe_send_data_alarm(State0#state{http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID);
|
||||||
{send, SendData, HTTP2Machine} ->
|
{send, SendData, HTTP2Machine} ->
|
||||||
State = #state{http2_status=Status, streams=Streams}
|
State = #state{http2_status=Status, streams=Streams}
|
||||||
= send_data(State0#state{http2_machine=HTTP2Machine}, SendData),
|
= send_data(State0#state{http2_machine=HTTP2Machine}, SendData, Prefix),
|
||||||
%% Terminate the connection if we are closing and all streams have completed.
|
%% Terminate the connection if we are closing and all streams have completed.
|
||||||
if
|
if
|
||||||
Status =:= closing, Streams =:= #{} ->
|
Status =:= closing, Streams =:= #{} ->
|
||||||
|
@ -778,39 +791,64 @@ maybe_send_data(State0=#state{http2_machine=HTTP2Machine0}, StreamID, IsFin, Dat
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
send_data(State, []) ->
|
send_data(State0=#state{socket=Socket, transport=Transport, opts=Opts}, SendData, Prefix) ->
|
||||||
State;
|
{Acc, State} = prepare_data(State0, SendData, [], Prefix),
|
||||||
send_data(State0, [{StreamID, IsFin, SendData}|Tail]) ->
|
_ = [case Data of
|
||||||
State = send_data(State0, StreamID, IsFin, SendData),
|
{sendfile, Offset, Bytes, Path} ->
|
||||||
send_data(State, Tail).
|
%% When sendfile is disabled we explicitly use the fallback.
|
||||||
|
_ = case maps:get(sendfile, Opts, true) of
|
||||||
|
true -> Transport:sendfile(Socket, Path, Offset, Bytes);
|
||||||
|
false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
Transport:send(Socket, Data)
|
||||||
|
end || Data <- Acc],
|
||||||
|
State.
|
||||||
|
|
||||||
send_data(State0, StreamID, IsFin, [Data]) ->
|
prepare_data(State, [], Acc, []) ->
|
||||||
State = send_data_frame(State0, StreamID, IsFin, Data),
|
{lists:reverse(Acc), State};
|
||||||
maybe_terminate_stream(State, StreamID, IsFin);
|
prepare_data(State, [], Acc, Buffer) ->
|
||||||
send_data(State0, StreamID, IsFin, [Data|Tail]) ->
|
{lists:reverse([lists:reverse(Buffer)|Acc]), State};
|
||||||
State = send_data_frame(State0, StreamID, nofin, Data),
|
prepare_data(State0, [{StreamID, IsFin, SendData}|Tail], Acc0, Buffer0) ->
|
||||||
send_data(State, StreamID, IsFin, Tail).
|
{Acc, Buffer, State} = prepare_data(State0, StreamID, IsFin, SendData, Acc0, Buffer0),
|
||||||
|
prepare_data(State, Tail, Acc, Buffer).
|
||||||
|
|
||||||
send_data_frame(State=#state{socket=Socket, transport=Transport},
|
prepare_data(State0, StreamID, IsFin, [], Acc, Buffer) ->
|
||||||
StreamID, IsFin, {data, Data}) ->
|
State = maybe_terminate_stream(State0, StreamID, IsFin),
|
||||||
Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)),
|
{Acc, Buffer, State};
|
||||||
State;
|
prepare_data(State0, StreamID, IsFin, [FrameData|Tail], Acc, Buffer) ->
|
||||||
send_data_frame(State=#state{socket=Socket, transport=Transport, opts=Opts},
|
FrameIsFin = case Tail of
|
||||||
StreamID, IsFin, {sendfile, Offset, Bytes, Path}) ->
|
[] -> IsFin;
|
||||||
Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)),
|
_ -> nofin
|
||||||
%% When sendfile is disabled we explicitly use the fallback.
|
|
||||||
_ = case maps:get(sendfile, Opts, true) of
|
|
||||||
true -> Transport:sendfile(Socket, Path, Offset, Bytes);
|
|
||||||
false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
|
|
||||||
end,
|
end,
|
||||||
State;
|
case prepare_data_frame(State0, StreamID, FrameIsFin, FrameData) of
|
||||||
|
{{MoreData, Sendfile}, State} when is_tuple(Sendfile) ->
|
||||||
|
case Buffer of
|
||||||
|
[] ->
|
||||||
|
prepare_data(State, StreamID, IsFin, Tail,
|
||||||
|
[Sendfile, MoreData|Acc], []);
|
||||||
|
_ ->
|
||||||
|
prepare_data(State, StreamID, IsFin, Tail,
|
||||||
|
[Sendfile, lists:reverse([MoreData|Buffer])|Acc], [])
|
||||||
|
end;
|
||||||
|
{MoreData, State} ->
|
||||||
|
prepare_data(State, StreamID, IsFin, Tail,
|
||||||
|
Acc, [MoreData|Buffer])
|
||||||
|
end.
|
||||||
|
|
||||||
|
prepare_data_frame(State, StreamID, IsFin, {data, Data}) ->
|
||||||
|
{cow_http2:data(StreamID, IsFin, Data),
|
||||||
|
State};
|
||||||
|
prepare_data_frame(State, StreamID, IsFin, Sendfile={sendfile, _, Bytes, _}) ->
|
||||||
|
{{cow_http2:data_header(StreamID, IsFin, Bytes), Sendfile},
|
||||||
|
State};
|
||||||
%% The stream is terminated in cow_http2_machine:prepare_trailers.
|
%% The stream is terminated in cow_http2_machine:prepare_trailers.
|
||||||
send_data_frame(State=#state{socket=Socket, transport=Transport,
|
prepare_data_frame(State=#state{http2_machine=HTTP2Machine0},
|
||||||
http2_machine=HTTP2Machine0}, StreamID, nofin, {trailers, Trailers}) ->
|
StreamID, nofin, {trailers, Trailers}) ->
|
||||||
{ok, HeaderBlock, HTTP2Machine}
|
{ok, HeaderBlock, HTTP2Machine}
|
||||||
= cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers),
|
= cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers),
|
||||||
Transport:send(Socket, cow_http2:headers(StreamID, fin, HeaderBlock)),
|
{cow_http2:headers(StreamID, fin, HeaderBlock),
|
||||||
State#state{http2_machine=HTTP2Machine}.
|
State#state{http2_machine=HTTP2Machine}}.
|
||||||
|
|
||||||
%% After we have sent or queued data we may need to set or clear an alarm.
|
%% After we have sent or queued data we may need to set or clear an alarm.
|
||||||
%% We do this by comparing the HTTP2Machine buffer state before/after for
|
%% We do this by comparing the HTTP2Machine buffer state before/after for
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue