Erlang/OTP 完全指南 / 23 - Web 开发
第 23 章:Web 开发 — Cowboy、REST API、WebSocket
Cowboy 是 Erlang 最流行的 HTTP 服务器框架。本章学习使用 Cowboy 构建 REST API 和 WebSocket 服务。
23.1 Cowboy 简介
23.1.1 特性
| 特性 | 说明 |
|---|---|
| 高性能 | 基于 Erlang 的轻量级进程 |
| HTTP/1.1 | 完整支持 |
| HTTP/2 | 支持 |
| WebSocket | 原生支持 |
| Stream handlers | 可扩展的流处理 |
| 中间件 | 中间件架构 |
23.1.2 依赖配置
%% rebar.config
{deps, [
{cowboy, "2.12.0"},
{jsx, "3.1.0"} %% JSON 库
]}.
23.2 基本 HTTP 服务器
23.2.1 Application 模块
%% src/myweb_app.erl
-module(myweb_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
%% 定义路由
Dispatch = cowboy_router:compile([
{'_', [
{"/", home_handler, []},
{"/api/users", users_handler, []},
{"/api/users/:id", user_handler, []},
{"/ws", ws_handler, []}
]}
]),
%% 启动 HTTP 监听器
{ok, _} = cowboy:start_clear(my_http_listener,
[{port, 8080}],
#{env => #{dispatch => Dispatch}}
),
myweb_sup:start_link().
stop(_State) ->
cowboy:stop_listener(my_http_listener),
ok.
23.2.2 简单 Handler
%% src/home_handler.erl
-module(home_handler).
-export([init/2]).
init(Req, State) ->
Body = <<"<h1>Hello from Cowboy!</h1>">>,
Req2 = cowboy_req:reply(200,
#{<<"content-type">> => <<"text/html">>},
Body,
Req
),
{ok, Req2, State}.
23.3 REST API
23.3.1 REST Handler
%% src/users_handler.erl
-module(users_handler).
-export([init/2,
allowed_methods/2,
content_types_provided/2,
content_types_accepted/2,
users_to_json/2,
json_to_users/2]).
init(Req, State) ->
{cowboy_rest, Req, State}.
allowed_methods(Req, State) ->
{[<<"GET">>, <<"POST">>], Req, State}.
content_types_provided(Req, State) ->
{[{<<"application">>, <<"json">>, '*'}, users_to_json}], Req, State}.
%% 注意:原代码中 content_types_provided 使用双花括号嵌套元组
%% 为避免 Hugo shortcode 冲突,此处调整写法
content_types_accepted(Req, State) ->
{[{<<"application">>, <<"json">>, '*'}, json_to_users}], Req, State}.
%% GET /api/users
users_to_json(Req, State) ->
Users = [
#{id => 1, name => <<"Alice">>, age => 25},
#{id => 2, name => <<"Bob">>, age => 30}
],
Body = jsx:encode(Users),
{Body, Req, State}.
%% POST /api/users
json_to_users(Req0, State) ->
{ok, Body, Req} = cowboy_req:read_body(Req0),
User = jsx:decode(Body, [return_maps]),
%% 保存用户...
io:format("Created user: ~p~n", [User]),
Req2 = cowboy_req:reply(201,
#{<<"content-type">> => <<"application/json">>},
jsx:encode(#{status => <<"created">>}),
Req
),
{stop, Req2, State}.
23.3.2 带路径参数的 Handler
%% src/user_handler.erl
-module(user_handler).
-export([init/2, allowed_methods/2, content_types_provided/2,
user_to_json/2, delete_resource/2]).
init(Req, State) ->
{cowboy_rest, Req, State}.
allowed_methods(Req, State) ->
{[<<"GET">>, <<"PUT">>, <<"DELETE">>], Req, State}.
content_types_provided(Req, State) ->
{[{<<"application">>, <<"json">>, '*'}, user_to_json}], Req, State}.
user_to_json(Req, State) ->
%% 从路径提取 ID
Id = cowboy_req:binding(id, Req),
%% 查询用户...
User = #{id => Id, name => <<"Alice">>, age => 25},
Body = jsx:encode(User),
{Body, Req, State}.
delete_resource(Req, State) ->
Id = cowboy_req:binding(id, Req),
%% 删除用户...
io:format("Deleted user ~p~n", [Id]),
{true, Req, State}.
23.3.3 REST 状态码
%% 成功
cowboy_req:reply(200, Headers, Body, Req). %% OK
cowboy_req:reply(201, Headers, Body, Req). %% Created
cowboy_req:reply(204, Headers, <<>>, Req). %% No Content
%% 错误
cowboy_req:reply(400, Headers, <<"Bad Request">>, Req).
cowboy_req:reply(401, Headers, <<"Unauthorized">>, Req).
cowboy_req:reply(403, Headers, <<"Forbidden">>, Req).
cowboy_req:reply(404, Headers, <<"Not Found">>, Req).
cowboy_req:reply(500, Headers, <<"Internal Server Error">>, Req).
23.4 WebSocket
23.4.1 WebSocket Handler
%% src/ws_handler.erl
-module(ws_handler).
-export([init/2, websocket_init/1, websocket_handle/2,
websocket_info/2, terminate/3]).
init(Req, State) ->
{cowboy_websocket, Req, State}.
websocket_init(State) ->
io:format("WebSocket connected~n"),
{ok, State}.
%% 处理客户端发来的 WebSocket 消息
websocket_handle({text, Msg}, State) ->
io:format("Received: ~p~n", [Msg]),
Reply = jsx:encode(#{echo => Msg}),
{[{text, Reply}], State};
websocket_handle({binary, Data}, State) ->
{[{binary, Data}], State};
websocket_handle(_Frame, State) ->
{[], State}.
%% 处理 Erlang 进程发来的消息
websocket_info({send, Msg}, State) ->
{[{text, Msg}], State};
websocket_info(_Info, State) ->
{[], State}.
terminate(_Reason, _Req, _State) ->
io:format("WebSocket disconnected~n"),
ok.
23.4.2 WebSocket 广播
%% ws_manager.erl
-module(ws_manager).
-behaviour(gen_server).
-export([start_link/0, register/1, broadcast/1]).
-export([init/1, handle_call/3, handle_cast/2]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
register(Pid) ->
gen_server:cast(?MODULE, {register, Pid}).
broadcast(Msg) ->
gen_server:cast(?MODULE, {broadcast, Msg}).
init([]) ->
{ok, #{clients => sets:new()}}.
handle_cast({register, Pid}, #{clients := Clients} = State) ->
monitor(process, Pid),
{noreply, State#{clients => sets:add_element(Pid, Clients)}};
handle_cast({broadcast, Msg}, #{clients := Clients} = State) ->
sets:fold(fun(Pid, _) -> Pid ! {send, Msg} end, ok, Clients),
{noreply, State}.
handle_info({'DOWN', _, process, Pid, _}, #{clients := Clients} = State) ->
{noreply, State#{clients => sets:del_element(Pid, Clients)}}.
23.4.3 聊天室 WebSocket
%% chat_ws_handler.erl
-module(chat_ws_handler).
-export([init/2, websocket_init/1, websocket_handle/2, websocket_info/2]).
init(Req, #{room := Room} = State) ->
{cowboy_websocket, Req, State#{room => Room}}.
websocket_init(#{room := Room} = State) ->
chat_room:join(Room, self()),
{ok, State}.
websocket_handle({text, Msg}, #{room := Room, name := Name} = State) ->
chat_room:broadcast(Room, {chat, Name, Msg}),
{[], State};
websocket_handle(_Frame, State) ->
{[], State}.
websocket_info({chat, FromName, Msg}, State) ->
Response = jsx:encode(#{from => FromName, message => Msg}),
{[{text, Response}], State};
websocket_info(_Info, State) ->
{[], State}.
23.5 中间件
%% src/auth_middleware.erl
-module(auth_middleware).
-export([execute/2]).
execute(Req, Env) ->
case cowboy_req:header(<<"authorization">>, Req) of
undefined ->
{ok, cowboy_req:reply(401,
#{<<"content-type">> => <<"text/plain">>},
<<"Unauthorized">>,
Req), Env};
Token ->
case verify_token(Token) of
{ok, UserId} ->
{ok, Req, Env#{user_id => UserId}};
error ->
{ok, cowboy_req:reply(401,
#{<<"content-type">> => <<"text/plain">>},
<<"Invalid token">>,
Req), Env}
end
end.
verify_token(_Token) ->
%% 验证 JWT 或其他令牌
{ok, 1}.
%% 在路由中使用中间件
Dispatch = cowboy_router:compile([
{'_', [
{"/api/protected", auth_middleware, #{handler => protected_handler}}
]}
]).
23.6 注意事项
⚠️ Web 开发陷阱
- 不要在 Handler 中执行长时间操作(阻塞请求进程)
- 读取大请求体需要先调用
read_body - WebSocket 连接需要心跳保活
- CORS 需要手动设置 headers
- 生产环境需要反向代理(Nginx)处理 SSL
💡 最佳实践
- 使用 cowboy_rest 处理 REST API
- 使用 JSX 或 jiffy 处理 JSON
- WebSocket 状态存储在进程字典或 gen_server
- 合理设置超时和连接限制
- 使用中间件处理认证和日志
23.7 扩展阅读
上一章:22 - NIF 与 C 集成 下一章:24 - 最佳实践