From ca6817880ee5592cf890fb5a71da41f52818d29a Mon Sep 17 00:00:00 2001 From: James Fish Date: Tue, 2 Apr 2013 00:49:07 +0100 Subject: Add Transport:sendfile/4,/5 Adds offset based sendfile to transports. Same behaviour as file:sendfile/4,/5 except socket and file arguments are reversed and either a raw file or a filename can be used. sendfile/2,/4,/5 now compulsory callbacks in ranch_transport. ranch_tcp:sendfile/2 now defaults to a chunk_size of 8191 - the default for ranch_ssl:sendfile/2. The same default is used for both ranch_tcp:sendfile/4,5 and ranch_ssl:sendfile/4,5. --- src/ranch_ssl.erl | 47 ++++++++++----------- src/ranch_tcp.erl | 51 +++++++++++++++++++---- src/ranch_transport.erl | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/ranch_ssl.erl b/src/ranch_ssl.erl index 98b2f5e..dfc5f11 100644 --- a/src/ranch_ssl.erl +++ b/src/ranch_ssl.erl @@ -34,6 +34,8 @@ -export([recv/3]). -export([send/2]). -export([sendfile/2]). +-export([sendfile/4]). +-export([sendfile/5]). -export([setopts/2]). -export([controlling_process/2]). -export([peername/1]). @@ -188,33 +190,32 @@ recv(Socket, Length, Timeout) -> send(Socket, Packet) -> ssl:send(Socket, Packet). -%% @doc Send a file on a socket. +%% @equiv sendfile(Socket, Filename, 0, 0, []) +-spec sendfile(ssl:sslsocket(), file:name_all()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, Filename) -> + sendfile(Socket, Filename, 0, 0, []). + +%% @equiv sendfile(Socket, File, Offset, Bytes, []) +-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd(), + non_neg_integer(), non_neg_integer()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, File, Offset, Bytes) -> + sendfile(Socket, File, Offset, Bytes, []). + +%% @doc Send part of a file on a socket. %% %% Unlike with TCP, no syscall can be used here, so sending files -%% through SSL will be much slower in comparison. +%% through SSL will be much slower in comparison. Note that unlike +%% file:sendfile/5 this function accepts either a file or a file name. %% -%% @see file:sendfile/2 --spec sendfile(ssl:sslsocket(), file:name()) +%% @see ranch_transport:sendfile/6 +%% @see file:sendfile/5 +-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd(), + non_neg_integer(), non_neg_integer(), ranch_transport:sendfile_opts()) -> {ok, non_neg_integer()} | {error, atom()}. -sendfile(Socket, Filepath) -> - {ok, IoDevice} = file:open(Filepath, [read, binary, raw]), - sendfile(Socket, IoDevice, 0). - --spec sendfile(ssl:sslsocket(), file:io_device(), non_neg_integer()) - -> {ok, non_neg_integer()} | {error, atom()}. -sendfile(Socket, IoDevice, Sent) -> - case file:read(IoDevice, 16#1FFF) of - eof -> - ok = file:close(IoDevice), - {ok, Sent}; - {ok, Bin} -> - case send(Socket, Bin) of - ok -> - sendfile(Socket, IoDevice, Sent + byte_size(Bin)); - {error, Reason} -> - {error, Reason} - end - end. +sendfile(Socket, File, Offset, Bytes, Opts) -> + ranch_transport:sendfile(?MODULE, Socket, File, Offset, Bytes, Opts). %% @doc Set options on the given socket. %% @see ssl:setopts/2 diff --git a/src/ranch_tcp.erl b/src/ranch_tcp.erl index 11a0843..6bdcdd0 100644 --- a/src/ranch_tcp.erl +++ b/src/ranch_tcp.erl @@ -28,6 +28,8 @@ -export([recv/3]). -export([send/2]). -export([sendfile/2]). +-export([sendfile/4]). +-export([sendfile/5]). -export([setopts/2]). -export([controlling_process/2]). -export([peername/1]). @@ -110,21 +112,52 @@ recv(Socket, Length, Timeout) -> send(Socket, Packet) -> gen_tcp:send(Socket, Packet). -%% @doc Send a file on a socket. +%% @equiv sendfile(Socket, File, Offset, Bytes, []) +-spec sendfile(inet:socket(), file:name_all()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, Filename) -> + sendfile(Socket, Filename, 0, 0, []). + +%% @equiv sendfile(Socket, File, Offset, Bytes, []) +-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, File, Offset, Bytes) -> + sendfile(Socket, File, Offset, Bytes, []). + +%% @doc Send part of a file on a socket. %% -%% This is the optimal way to send files using TCP. It uses a syscall -%% which means there is no context switch between opening the file -%% and writing its contents on the socket. +%% As with sendfile/2 this is the optimal way to send (parts) of files using +%% TCP. Note that unlike file:sendfile/5 this function accepts either a raw file +%% or a file name and the ordering of arguments is different. %% -%% @see file:sendfile/2 --spec sendfile(inet:socket(), file:name()) +%% @see file:sendfile/5 +-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer(), [{chunk_size, non_neg_integer()}]) -> {ok, non_neg_integer()} | {error, atom()}. -sendfile(Socket, Filename) -> - try file:sendfile(Filename, Socket) of +sendfile(Socket, Filename, Offset, Bytes, Opts) + when is_list(Filename) orelse is_atom(Filename) + orelse is_binary(Filename) -> + case file:open(Filename, [read, raw, binary]) of + {ok, RawFile} -> + try sendfile(Socket, RawFile, Offset, Bytes, Opts) of + Result -> Result + after + ok = file:close(RawFile) + end; + {error, _} = Error -> + Error + end; +sendfile(Socket, RawFile, Offset, Bytes, Opts) -> + Opts2 = case Opts of + [] -> [{chunk_size, 16#1FFF}]; + _ -> Opts + end, + try file:sendfile(RawFile, Socket, Offset, Bytes, Opts2) of Result -> Result catch error:{badmatch, {error, enotconn}} -> - %% file:sendfile/2 might fail by throwing a {badmatch, {error, enotconn}} + %% file:sendfile/5 might fail by throwing a {badmatch, {error, enotconn}} %% this is because its internal implementation fails with a badmatch in %% prim_file:sendfile/10 if the socket is not connected. {error, closed} diff --git a/src/ranch_transport.erl b/src/ranch_transport.erl index 38dca5a..6168b35 100644 --- a/src/ranch_transport.erl +++ b/src/ranch_transport.erl @@ -15,8 +15,12 @@ %% @private -module(ranch_transport). +-export([sendfile/6]). + -type socket() :: any(). -type opts() :: any(). +-type sendfile_opts() :: [{chunk_size, non_neg_integer()}]. +-export_type([sendfile_opts/0]). %% Name of the transport. -callback name() -> atom(). @@ -55,6 +59,19 @@ %% Send data on a socket. -callback send(socket(), iodata()) -> ok | {error, atom()}. +%% Send a file on a socket. +-callback sendfile(socket(), file:name()) + -> {ok, non_neg_integer()} | {error, atom()}. + +%% Send part of a file on a socket. +-callback sendfile(socket(), file:name() | file:fd(), non_neg_integer(), + non_neg_integer()) -> {ok, non_neg_integer()} | {error, atom()}. + +%% Send part of a file on a socket. +-callback sendfile(socket(), file:name() | file:fd(), non_neg_integer(), + non_neg_integer(), sendfile_opts()) + -> {ok, non_neg_integer()} | {error, atom()}. + %% Set options on the given socket. -callback setopts(socket(), opts()) -> ok | {error, atom()}. @@ -75,3 +92,94 @@ %% Close the given socket. -callback close(socket()) -> ok. + +%% @doc Send part of a file on a socket. +%% +%% A fallback for transports that don't have a native sendfile implementation. +%% Note that the ordering of arguments is different from file:sendfile/5 and +%% that this function accepts either a raw file or a file name. +%% +%% @see file:sendfile/5 +-spec sendfile(module(), socket(), file:filename_all() | file:fd(), + non_neg_integer(), non_neg_integer(), sendfile_opts()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Transport, Socket, Filename, Offset, Bytes, Opts) + when is_list(Filename) orelse is_atom(Filename) + orelse is_binary(Filename) -> + ChunkSize = chunk_size(Opts), + case file:open(Filename, [read, raw, binary]) of + {ok, RawFile} -> + _ = case Offset of + 0 -> + ok; + _ -> + {ok, _} = file:position(RawFile, {bof, Offset}) + end, + try + sendfile_loop(Transport, Socket, RawFile, Bytes, 0, ChunkSize) + after + ok = file:close(RawFile) + end; + {error, _Reason} = Error -> + Error + end; +sendfile(Transport, Socket, RawFile, Offset, Bytes, Opts) -> + ChunkSize = chunk_size(Opts), + Initial2 = case file:position(RawFile, {cur, 0}) of + {ok, Offset} -> + Offset; + {ok, Initial} -> + {ok, _} = file:position(RawFile, {bof, Offset}), + Initial + end, + case sendfile_loop(Transport, Socket, RawFile, Bytes, 0, ChunkSize) of + {ok, _Sent} = Result -> + {ok, _} = file:position(RawFile, {bof, Initial2}), + Result; + {error, _Reason} = Error -> + Error + end. + +-spec chunk_size(sendfile_opts()) -> pos_integer(). +chunk_size(Opts) -> + case lists:keyfind(chunk_size, 1, Opts) of + {chunk_size, ChunkSize} + when is_integer(ChunkSize) andalso ChunkSize > 0 -> + ChunkSize; + {chunk_size, 0} -> + 16#1FFF; + false -> + 16#1FFF + end. + +-spec sendfile_loop(module(), socket(), file:fd(), non_neg_integer(), + non_neg_integer(), pos_integer()) + -> {ok, non_neg_integer()} | {error, term()}. +sendfile_loop(_Transport, _Socket, _RawFile, Sent, Sent, _ChunkSize) + when Sent =/= 0 -> + %% All requested data has been read and sent, return number of bytes sent. + {ok, Sent}; +sendfile_loop(Transport, Socket, RawFile, Bytes, Sent, ChunkSize) -> + ReadSize = read_size(Bytes, Sent, ChunkSize), + case file:read(RawFile, ReadSize) of + {ok, IoData} -> + case Transport:send(Socket, IoData) of + ok -> + Sent2 = iolist_size(IoData) + Sent, + sendfile_loop(Transport, Socket, RawFile, Bytes, Sent2, + ChunkSize); + {error, _Reason} = Error -> + Error + end; + eof -> + {ok, Sent}; + {error, _Reason} = Error -> + Error + end. + +-spec read_size(non_neg_integer(), non_neg_integer(), non_neg_integer()) -> + non_neg_integer(). +read_size(0, _Sent, ChunkSize) -> + ChunkSize; +read_size(Bytes, Sent, ChunkSize) -> + min(Bytes - Sent, ChunkSize). -- cgit v1.2.3