Websocket 处理程序
Websocket处理程序提供了一个升级到Websocket的HTTP/1.1连接的接口,并在Websocket连接上发送或接收帧。
由于Websocket连接是通过HTTP/1.1升级机制建立的,所以Websocket处理器需要能够首先接收升级的HTTP请求,然后切换到Websocket并接管连接。然后,它们可以接收或发送Websocket帧,处理传入的Erlang消息或关闭连接。
升级
当接收到请求时,调用init/2回调函数。要建立Websocket连接,你必须切换到cowboy_websocket模块:
init(Req, State) ->
{cowboy_websocket, Req, State}.
Cowboy将立即执行Websocket握手。注意,如果客户端没有请求升级到Websocket,握手将失败。
此函数返回后,Req对象将不可用。正确执行Websocket处理程序所需的任何信息都必须保存在状态中。
子协议
客户端可以在sec-websocket-protocol报头中提供它支持的Websocket子协议列表。服务器必须选择其中一个并将其发送回客户机,否则握手将失败。
例如,客户端可以通过Websocket理解STOMP和MQTT,并提供报头:
sec-websocket-protocol: v12.stomp, mqtt
如果服务器只理解MQTT,它可以返回:
sec-websocket-protocol: mqtt
这个选择必须在init/2中完成。用法示例如下:
init(Req0, State) ->
case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of
undefined ->
{cowboy_websocket, Req0, State};
Subprotocols ->
case lists:keymember(<<"mqtt">>, 1, Subprotocols) of
true ->
Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>,
<<"mqtt">>, Req0),
{cowboy_websocket, Req, State};
false ->
Req = cowboy_req:reply(400, Req0),
{ok, Req, State}
end
end.
Post-upgrade初始化
Cowboy有单独的进程来处理连接和请求。因为Websocket接管了连接,所以Websocket协议处理和请求处理发生在不同的进程中。
这反映在Websocket处理程序的不同回调中。init/2回调函数从临时请求进程调用,websocket_回调函数从连接进程调用。
这意味着一些初始化不能从init/2中完成。任何需要当前pid或绑定到当前pid的操作都将无法正常工作。可以使用可选的websocket_init/1代替:
websocket_init(State) ->
erlang:start_timer(1000, self(), <<"Hello!">>),
{ok, State}.
所有的Websocket回调函数都有相同的返回值。这意味着我们可以在升级后立即向客户端发送帧:
websocket_init(State) ->
{[{text, <<"Hello!">>}], State}.
接收帧
每当一个文本、二进制、ping或pong帧从客户端到达时,Cowboy将调用websocket_handle/2。
处理程序可以处理或忽略帧。它也可以发送帧回客户端或停止连接。
以下代码片段将回显接收到的任何文本帧,并忽略其他所有文本帧:
websocket_handle(Frame = {text, _}, State) ->
{[Frame], State};
websocket_handle(_Frame, State) ->
{ok, State}.
注意,ping和pong帧不需要管理员的操作,因为Cowboy会自动回复ping帧。它们仅供提供信息之用。
接收Erlang的信息
每当Erlang消息到达时,Cowboy将调用websocket_info/2。
处理程序可以处理或忽略消息。它也可以发送帧到客户端或停止连接。
下面的代码片段将日志消息转发给客户端,并忽略其他所有消息:
websocket_info({log, Text}, State) ->
{[{text, Text}], State};
websocket_info(_Info, State) ->
{ok, State}.
发送帧
所有websocket_回调函数共享返回值。它们可以向客户端发送0、1或多个帧。
不发送任何东西,只返回一个ok元组:
websocket_info(_Info, State) ->
{ok, State}.
要发送一个帧,返回要发送的帧:
websocket_info(_Info, State) ->
{[{text, <<"Hello!">>}], State}.
您可以发送任何类型的帧:text、binary、ping、pong或close frames。
你可以在同一时间发送许多帧:
websocket_info(_Info, State) ->
{[
{text, "Hello"},
{text, <<"world!">>},
{binary, <<0:8000>>}
], State}.
它们是按给定的顺序发送的。
保持连接状态
Cowboy将自动响应客户端发送的ping帧。出于提供信息的目的,它们仍然被转发给处理程序,但不需要进一步的操作。
Cowboy自己不会发送ping帧。如果需要,处理程序可以这样做。在大多数情况下,更好的解决方案是让客户端处理ping。在处理程序中执行这一操作意味着为每个连接添加额外的计时器,对于需要处理大量连接的服务器来说,这可能会带来相当大的成本。
Cowboy可以配置为自动关闭空闲连接。强烈建议在这里配置一个超时,以避免进程停留的时间超过需要。
init/2回调函数可以设置连接的超时时间。例如,这将使Cowboy关闭空闲连接超过30秒:
init(Req, State) ->
{cowboy_websocket, Req, State, #{
idle_timeout => 30000}}.
该值一旦设置,就不能更改。默认值为60000。
限制帧大小
默认情况下Cowboy接受任何大小的帧。您应该根据处理程序可能处理的内容来限制大小。你可以通过init/2回调来做到这一点:
init(Req, State) ->
{cowboy_websocket, Req, State, #{
max_frame_size => 8000000}}.
缺乏限制是历史遗留问题。未来版本的Cowboy将会有一个更合理的违约。
节省内存
Websocket连接进程可以在回调函数返回后设置为hibernate。
只需将hibernate字段添加到返回的元组:
websocket_init(State) ->
{[], State, hibernate}.
websocket_handle(_Frame, State) ->
{[], State, hibernate}.
websocket_info(_Info, State) ->
{[{text, <<"Hello!">>}], State, hibernate}.
强烈建议在编写处理程序时启用hibernate,因为这样可以大大减少内存使用。但是请注意,可以观察到CPU使用量或延迟的增加,特别是对于更繁忙的连接。
关闭连接
连接可以在任何时候关闭,要么告诉Cowboy停止它,要么发送一个close帧。
告诉Cowboy关闭连接,使用stop元组:
websocket_info(_Info, State) ->
{stop, State}.
发送一个close帧将立即启动关闭Websocket连接。注意,当发送一个包含close帧的帧列表时,在close帧之后找到的任何帧都不会被发送。
下面的例子发送一个带有reason消息的close帧:
websocket_info(_Info, State) ->
{[{close, 1000, <<"some-reason">>}], State}.