在 Erlang 应用中处理 WebSocket 连接





3.00/5 (1投票)
如何在 Erlang 应用中处理 WebSocket 连接
基本上,当需要 Erlang/OTP 应用程序通过 HTTP 或 WebSockets 与外部世界通信时,使用 Cowboy Web 服务器是一个非常好的主意。以下是使用它的一些优点:
- 模块化,小巧但功能强大。 Cowboy 包括用于 http、rest、spdy 和 websockets 的不同基本处理程序。 你所需要做的就是更新你的应用程序依赖项,配置 Cowboy,创建你自己的“继承”自基本处理程序的处理程序;
- 非常快。 Cowboy 将自定义处理程序作为单独的 Erlang 进程启动。 因此,如果你有很多 http 或 websocket 连接,它们将在 Erlang VM 中并发处理,这非常棒! 在我们的一个项目中,Cowboy 在具有 4 个 CPU 内核的单个 Amazon 实例上处理了 5 万个并发 websocket 连接;
- 可以轻松地嵌入到另一个 Erlang 应用程序中。 并且在具有选择性调度和 fastCGI 支持的同时,可以将一些请求重定向到 PHP、Ruby、Python 应用程序。
更新应用程序依赖项
在 Oxagile,我们使用 rebar 作为构建工具。 基本上,它就像 PHP 应用程序的 composer
,但更加高级。 因此,要将 cowboy
作为依赖项添加,我们应该通过将以下行添加到 deps
部分来更新我们的 rebar.config
文件
{deps, [
{
cowboy,
".*",
{git, "https://github.com/extend/cowboy.git", {tag, "1.0.3"}}
},
% other dependencies
{
mochijson2,
".*",
{git, "https://github.com/tel/mochijson2.git", {tag, "2.3.2"}}
},
{
amqp_client,
".*",
{git, "git://github.com/jbrisbin/amqp_client.git", {tag, "3.5.2"}}
}
]}
然后只需转到控制台并运行:$ rebar get deps
。
然后,请通过将 cowboy
条目添加到 applications
部分来更新你的 *_app.src
文件。 因此,当你将应用程序打包到 release 中时,Cowboy 将在发布启动时自动启动。
{application, demo,
[
{description, "ws demo app"},
{vsn, "0.0.1"},
{registered, []},
{applications, [
kernel,
stdlib,
inets,
cowboy,
mnesia
]},
{mod, { demo_app, []}},
{env, [ ]}
]}.
在你的应用程序中配置 Cowboy
假设我们的应用程序名称是 demo
。 因此,你应该在你的 src
文件夹中拥有 demo_app.erl
文件。 让我们通过为 Cowboy 添加一些配置来修改它。 首先,我们将配置路由和 Cowboy 将侦听的端口。
-module(demo_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{'_', [
{"/ws", demo_cowboy_handler_ws, []},
{"/auth/:token/:user", demo_cowboy_handler_rest, [auth]},
]}
]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8889}], [{env, [{dispatch, Dispatch}]}]),
demo_sup:start_link().
stop(_State) ->
ok.
在上面的示例中,我们添加了两个具有不同处理程序的路由,这些处理程序应该被实现,并告诉 Cowboy 监听 8889 端口。 此外,还可以监听多个端口。 你所需要做的就是多次调用 cowboy:start_http
,但请注意,该函数的第一个参数(监听器名称)每次都应该不同。
start(_StartType, _StartArgs) ->
DispatchWs = cowboy_router:compile([
{'_', [
{"/ws", demo_cowboy_handler_ws, []}
]}
]),
DispatchRest = cowboy_router:compile([
{'_', [
{"/auth/:token/:user", demo_cowboy_handler_rest, [auth]}
]}
]),
{ok, _} = cowboy:start_http(ws, 100, [{port, 8889}], [{env, [{dispatch, DispatchWs}]}]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8888}], [{env, [{dispatch, DispatchRest}]}]),
demo_sup:start_link().
创建你的自定义 Cowboy 处理程序
剩下的最后一件事就是为 websocket 连接创建一个处理程序。 它是一个常规的 Erlang 模块,但应该“继承”自 cowboy_websocket_handler
。 Erlang 行为很棒,因为它们具有明确定义的接口:一组具有自定义实现但具有已定义输入和输出的回调函数。 因此,对于 cowboy_websocket_handler
,这些函数是
init
- 初始化 websocket 连接(见下文)websocket_init
- 初始化 ws 连接的状态。
可以通过 cowboy_router:compile
传递一些初始化参数,如下所示
DispatchWs = cowboy_router:compile([
{'_', [
{"/ws", demo_cowboy_handler_ws, [SomeConfigParams]}
]}
]),
并且 websocket_init
回调中的第 3 个参数 Opts
将包含这些参数
websocket_info
- 处理 Erlang 消息。 可以像任何其他 erlang 进程一样向处理程序的进程发送消息,例如WsPid ! {data, SomeData}
;websocket_handle
- 处理从客户端接收的帧;websocket_terminate
- 处理 ws 连接的终止。 此函数在 websocket 终止之前被调用;
详细的回调规范可以在 这里 找到。 这是我们的演示处理程序的代码
-module(demo_cowboy_handler_ws).
-behaviour(cowboy_websocket_handler).
-export([init/3]).
-export([
websocket_init/3, websocket_handle/3,
websocket_info/3, websocket_terminate/3
]).
init({tcp, http}, _Req, _Opts) ->
{upgrade, protocol, cowboy_websocket}.
websocket_init(_TransportName, Req, Opts) ->
{ok, Req, undefined_state}.
websocket_handle({text, Msg}, Req, State) ->
{reply, {text, << "responding to ", Msg/binary >>}, Req, State, hibernate }.
websocket_info({data, SomeData}, Req, State) ->
{reply, {text, SomeData}, Req, State};
websocket_info(_Info, Req, State) ->
{ok, Req, State, hibernate}.
websocket_terminate(_Reason, _Req, _State) ->
ok.
基本上,这就是 websockets 的全部内容。 Cowboy 使事情变得更加容易,并允许我们关注应用程序逻辑。 为了更好地理解 cowboy_websocket_handler
行为,请参考文档,你可以在其中找到回调函数的输入和输出的详细说明。
我们认为,Cowboy websocket 处理程序中只存在一个小问题:无法在 websocket_init
回调中终止(带或不带消息)WS 连接,但是可以通过向 WS 进程发送 Erlang 消息并使用 websocket_info
回调处理它来轻松解决此问题。
总结
假设我们有一个软件解决方案,其中包括使用不同技术实现的多个模块,如下例所示:
因此,我们有 WebSocket 客户端和 PHP App 客户端(很可能这是 JavaScript 应用程序的 2 个部分)。 此应用程序通过 8080 端口发出 HTTP 请求,并使用 8089 端口建立 WS 连接。
为了处理这些连接,我们创建了 2 个独立的 Cowboy 处理程序。 所有其他连接都被防火墙禁止。 但是,在我们的集群(局域网)内部,我们允许所有连接。 因此,我们的内部应用程序可以向 Cowboy 和它的 rest 处理程序发出请求,该处理程序对外部世界是隐藏的。