DEV Community

钟智强
钟智强

Posted on

Erlang 从零写一个 HTTP REST API 服务

🧠 本文不只是教学代码,而是帮助你理解「Erlang 写 Web 服务」的底层原理,用最野性但真实的方式。

你可能已经习惯了 Java 的 Spring Boot、Node.js 的 Express、Python 的 Flask —— 但这些都有框架。而这次,我们直接用 Erlang 写 HTTP 服务,不依赖 cowboy、mochiweb 或 yaws,完全裸写 socket 层,硬核程度拉满。

👩‍💻 目标:

  • 写一个能处理 GET 和 POST 请求的 Web 服务器
  • 路由分发基本 API 请求路径
  • 接收 JSON(模拟即可)
  • 返回标准 HTTP 响应

在这里插入图片描述

建立最原始的 TCP 服务

Erlang 的 gen_tcp 模块就是我们所有操作的起点:

start() ->
    {ok, ListenSocket} = gen_tcp:listen(8080, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]),
    loop_accept(ListenSocket).

loop_accept(ListenSocket) ->
    {ok, Socket} = gen_tcp:accept(ListenSocket),
    spawn(fun() -> handle(Socket) end),
    loop_accept(ListenSocket).
Enter fullscreen mode Exit fullscreen mode
  • 监听本地 8080 端口
  • 接收到连接后,spawn 一个进程专门处理这个 Socket(这就是 Erlang 并发威力!)

解析 HTTP 请求(简单模拟)

真正的 HTTP 协议超复杂,这里我们做「最小可运行子集」:

handle(Socket) ->
    {ok, Data} = gen_tcp:recv(Socket, 0),
    Request = binary_to_list(Data),
    io:format("Request: ~s~n", [Request]),
    Response = route(Request),
    gen_tcp:send(Socket, Response),
    gen_tcp:close(Socket).
Enter fullscreen mode Exit fullscreen mode

现在的重点是 route/1 —— 我们来构造最基础的路由逻辑。


基于请求构建路由

route(Request) ->
    %% 基于最简单的方式分析请求头第一行
    case string:tokens(Request, " \r\n") of
        ["GET", "/", _] ->
            ok("Hello from Erlang REST API!");
        ["GET", "/ping", _] ->
            ok("pong");
        ["POST", "/echo", _ | _] ->
            ok("echo placeholder");
        _ ->
            not_found()
    end.
Enter fullscreen mode Exit fullscreen mode

构造 HTTP 响应(手撸)

ok(Body) ->
    list_to_binary(
        "HTTP/1.1 200 OK\r\n" ++
        "Content-Type: text/plain\r\n" ++
        "Content-Length: " ++ integer_to_list(length(Body)) ++ "\r\n\r\n" ++
        Body
    ).

not_found() ->
    list_to_binary(
        "HTTP/1.1 404 Not Found\r\n" ++
        "Content-Length: 0\r\n\r\n"
    ).
Enter fullscreen mode Exit fullscreen mode

完整代码

%%%-------------------------------------------------------------------
%%% @doc 超轻量级 HTTP 服务器,无框架,无依赖,纯 Erlang socket 实现
%%%-------------------------------------------------------------------
-module(simple_http_server).
-export([start/0]).

start() ->
    {ok, ListenSocket} = gen_tcp:listen(8080, [
        binary,
        {packet, 0},
        {active, false},
        {reuseaddr, true}
    ]),
    io:format("🚀 Server started at http://localhost:8080~n"),
    loop(ListenSocket).

loop(ListenSocket) ->
    {ok, Socket} = gen_tcp:accept(ListenSocket),
    spawn(fun() -> handle(Socket) end),
    loop(ListenSocket).

handle(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            Request = binary_to_list(Data),
            io:format("📥 Incoming Request:\n~s~n", [Request]),
            Response = dispatch(Request),
            gen_tcp:send(Socket, Response),
            gen_tcp:close(Socket);
        {error, closed} ->
            gen_tcp:close(Socket);
        {error, Reason} ->
            io:format("❌ Error: ~p~n", [Reason]),
            gen_tcp:close(Socket)
    end.

dispatch(Request) ->
    case string:tokens(Request, " \r\n") of
        ["GET", "/", _] ->
            ok("🌈 Welcome to Erlang REST API");
        ["GET", "/ping", _] ->
            ok("pong 🏓");
        ["POST", "/echo", _ | _] ->
            ok("🔁 Echo endpoint reached (body parsing TBD)");
        ["GET", Path, _] ->
            case is_static(Path) of
                true -> ok("[static] no content");
                false -> not_found()
            end;
        _ ->
            not_found()
    end.

is_static(Path) ->
    lists:any(fun(Ext) -> string:ends_with(Path, Ext) end, [".css", ".js", ".less", ".png", ".jpg"]).

ok(Body) ->
    list_to_binary(
        "HTTP/1.1 200 OK\r\n" ++
        "Content-Type: text/plain; charset=utf-8\r\n" ++
        "Content-Length: " ++ integer_to_list(length(Body)) ++ "\r\n\r\n" ++
        Body
    ).

not_found() ->
    list_to_binary(
        "HTTP/1.1 404 Not Found\r\n" ++
        "Content-Type: text/plain\r\n" ++
        "Content-Length: 13\r\n\r\n404 Not Found"
    ).

Enter fullscreen mode Exit fullscreen mode

测试一下

erl
Enter fullscreen mode Exit fullscreen mode

然后

Erlang/OTP 27 [erts-15.2.6] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]

Eshell V15.2.6 (press Ctrl+G to abort, type help(). for help)

1> c(simple_http_server).
{ok,simple_http_server}
2> simple_http_server:start().
Enter fullscreen mode Exit fullscreen mode

运行一下

curl http://localhost:8080/                     # Hello from Erlang REST API
curl http://localhost:8080/ping                 # pong
curl -X POST http://localhost:8080/echo
Enter fullscreen mode Exit fullscreen mode

你到底做了什么?

✅ 你构建了一个最小可运行的 HTTP Server(支持 GET/POST)
✅ 没有用任何框架,全靠你自己解析请求、构造响应
✅ 所有逻辑都是原生进程,不靠 gen_server,也没 OTP 套路

其实很多人觉得 Erlang 写 Web 太麻烦、过时了,但只要你理解其并发模型和分布式能力,这种原始的 socket 编程可以成为你探索更强大后端架构的基石

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.