原文:Nine Nines: Build an FTP Server with Ranch in 30 Minutes
本文的目的是展示如何使用 Ranch 编写网络协议实现,Ranch 如何让您编写重要的代码,以及编写服务器时使用的常用技术。
让我们从创建一个空项目开始。 创建一个新目录,然后在该目录中打开一个终端。 第一步是将 Ranch 添加为依赖项。 创建 rebar.config
文件并添加以下 3 行。
{deps, [
{ranch, ".*", {git, "git://github.com/extend/ranch.git", "master"}}
]}.
这使您的应用程序依赖于 master 分支上可用的最后一个 Ranch 版本。 这对于开发来说很好,但是当您开始将应用程序推送到生产环境时,您将需要重新访问此文件以硬编码您正在使用的确切版本,以确保您在生产环境中运行相同版本的依赖项。
您现在可以获取依赖项。
$ rebar get-deps
==> ranch_ftp (get-deps)
Pulling ranch from {git,"git://github.com/extend/ranch.git","master"}
Cloning into 'ranch'...
==> ranch (get-deps)
这将创建一个 deps/ 包含 Ranch
我们实际上不需要任何其他东西来编写协议代码。 我们可以为它创建一个应用程序,但这不是本文的目的,所以让我们继续编写协议本身。 创建文件 ranch_ftp_protocol.erl 并在您喜欢的编辑器中打开它。
$ vim ranch_ftp_protocol.erl
让我们从一个空白的协议模块开始。
-module(ranch_ftp_protocol).
-export([start_link/4, init/3]).
start_link(ListenerPid, Socket, Transport, Opts) ->
Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport]),
{ok, Pid}.
init(ListenerPid, Socket, Transport) ->
io:format("Got a connection!~n"),
ok.
当 Ranch 收到一个连接时,它会调用 start_link/4
函数与侦听器的 pid、套接字、要使用的传输模块以及我们在启动侦听器时定义的选项。 出于本文的目的,我们不需要选项,因此我们不会将它们传递给我们正在创建的流程。
让我们打开一个 shell 并启动一个 Ranch 侦听器以开始接受连接。 我们只需要调用一个函数。 您可能应该在另一个终端中打开它并为方便起见保持打开状态。 如果您退出 shell,您将不得不重复这些命令才能继续。
另请注意,您需要输入 c(ranch_ftp_protocol).
重新编译并重新加载协议的代码。 但是,您不需要重新启动任何进程。
$ erl -pa ebin deps/*/ebin
Erlang R15B02 (erts-5.9.2) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
1> application:start(ranch).
ok
2> ranch:start_listener(my_ftp, 10,
ranch_tcp, [{port, 2121}],
ranch_ftp_protocol, []).
{ok,<0.40.0>}
这将启动一个名为 my_ftp
运行你自己的 ranch_ftp_protocol
通过 TCP,监听端口 2121
. 最后一个参数是我们之前忽略的协议的选项。
要尝试您的代码,您可以使用以下命令。 应该可以连接,服务器会在控制台打印一条消息,然后客户端会打印一个错误。
$ ftp localhost 2121
让我们继续实际编写协议。
一旦您创建了新进程并返回了 pid,Ranch 将把套接字的所有权交给您。 不过,这需要一个同步步骤。
init(ListenerPid, Socket, Transport) ->
ok = ranch:accept_ack(ListenerPid),
ok.
现在您已确认新连接,您可以安全地使用它。
当 FTP 服务器接受连接时,它首先发送一条欢迎消息,该消息可以是一行或多行以代码开头 200
. 然后服务器将等待客户端对用户进行身份验证,如果身份验证成功(本文将始终这样做),它将回复 230
代码。
init(ListenerPid, Socket, Transport) ->
ok = ranch:accept_ack(ListenerPid),
Transport:send(Socket, <<"200 My cool FTP server welcomes you!\r\n">>),
{ok, Data} = Transport:recv(Socket, 0, 30000),
auth(Socket, Transport, Data).
auth(Socket, Transport, <<"USER ", Rest/bits>>) ->
io:format("User authenticated! ~p~n", [Rest]),
Transport:send(Socket, <<"230 Auth OK\r\n">>),
ok.
如您所见,我们不需要复杂的解析代码。 我们可以简单地匹配参数中的二进制!
接下来,如果我们希望我们的服务器变得有用,我们需要循环接收数据命令并选择性地执行它们。
我们将更换 ok.
符合对以下函数的调用。 新函数是递归的,每次调用都从套接字接收数据并发送响应。 现在,我们将为客户端发送的所有命令发送错误响应。
loop(Socket, Transport) ->
case Transport:recv(Socket, 0, 30000) of
{ok, Data} ->
handle(Socket, Transport, Data),
loop(Socket, Transport);
{error, _} ->
io:format("The client disconnected~n")
end.
handle(Socket, Transport, Data) ->
io:format("Command received ~p~n", [Data]),
Transport:send(Socket, <<"500 Bad command\r\n">>).
有了这个,我们几乎准备好开始执行命令了。 但是对于这样的代码,如果客户端没有为每个数据包仅发送一个命令,或者数据包到达速度太快,或者命令被拆分为多个数据包,我们可能会遇到错误。
为了解决这个问题,我们需要使用缓冲区。 每次我们接收到数据时,我们都会追加到缓冲区中,然后在运行之前检查我们是否已经完全接收到命令。 代码可能类似于以下内容。
loop(Socket, Transport, Buffer) ->
case Transport:recv(Socket, 0, 30000) of
{ok, Data} ->
Buffer2 = << Buffer/binary, Data/binary >>,
{Commands, Rest} = split(Buffer2),
[handle(Socket, Transport, C) || C <- Commands],
loop(Socket, Transport, Rest);
{error, _} ->
io:format("The client disconnected~n")
end.
实施 split/1
留给读者作为练习。 您可能还想处理 QUIT
命令,它必须停止任何处理并关闭连接。
细心的读者还会注意到,在基于文本的协议中,命令由换行符分隔,您可以使用 Transport:setopts/2
并由 Erlang 自己免费为您完成所有缓冲。
正如您现在肯定注意到的那样,Ranch 允许我们通过完全超越初始设置来构建网络应用程序。 它允许您使用二进制模式匹配的强大功能,只需几行代码即可编写文本和二进制协议实现。