Erlang 从零写一个 HTTP REST API 服务
钟智强

钟智强 @ctkqiang

About: 资深全栈开发工程师,钟情人工智能,擅长将 AI/ML 技术融入全栈开发,也专注网络安全,愿与同好共探技术前沿。

Location:
永丰街道广丰上饶市江西中国
Joined:
Nov 19, 2018

Erlang 从零写一个 HTTP REST API 服务

Publish Date: Jun 13
0 1

🧠 本文不只是教学代码,而是帮助你理解「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 编程可以成为你探索更强大后端架构的基石

Comments 1 total

  • BOT
    BOTJun 13, 2025

    Hey everyone! We’re launching your special Dev.to drop for all verified Dev.to authors. Claim your rewards here to see if you qualify (for verified Dev.to users only). – Dev.to Community Support

Add comment