Defold 网络通信:HTTP、WebSocket 与 REST API

系统讲解 Defold 网络通信体系:http.request 异步 GET/POST、JSON 解析与序列化、WebSocket 连接建立与消息收发、REST API 封装、认证 Token 管理、断线重连策略,以及跨平台网络差异处理。

引言

现代游戏离不开网络:排行榜同步、云存档、多人联机、实时推送。Defold 提供了两套基础网络工具——HTTP(请求-响应) 和 WebSocket(全双工实时通信),覆盖了绝大多数联网需求。本文系统讲解 Defold 网络通信:http.request 异步 GET/POST 用法、JSON 解析与序列化、WebSocket 连接管理与消息收发、REST API 封装模式、认证 Token 管理方案、断线重连策略,以及 iOS/Android/HTML5/Desktop 各平台的网络差异处理。

前置:/defold-script-system-lua/(Lua 异步回调与消息系统)。多人联机同步见 /defold-multiplayer-sync/。


目录


1. 网络架构选型:HTTP vs WebSocket

Defold 提供两套内置网络 API:

特性HTTPWebSocket
通信模式请求-响应全双工持续连接
适用场景REST API、排行榜、存档实时对战、聊天、推送
连接开销每次请求新建 TCP一次握手,长期持有
服务器推送轮询/long-polling原生支持
数据格式任意(JSON/二进制)文本/二进制帧
断线感知请求超时心跳/close 事件
选型建议:
  → 低频数据(排行榜、配置)→ HTTP GET/POST
  → 高频实时(位置同步、聊天)→ WebSocket
  → 混合方案:HTTP 做登录/配置,WebSocket 做实时数据

记忆:HTTP 像发邮件(单向一问一答),WebSocket 像打电话(双向实时通话)——按需选择,也可以组合使用。


2. http.request:异步 GET/POST

2.1 基本 GET 请求

-- 异步 GET:获取排行榜数据
http.request(
    "https://api.example.com/leaderboard?limit=10",
    "GET",
    function(self, id, response)
        -- response 字段:status, response, headers, bytes_received
        if response.status == 200 then
            print("排行榜数据:", response.response)
            local data = json.decode(response.response)
            self.leaderboard = data.players
        else
            print("请求失败,状态码:", response.status)
        end
    end
)

http.request 签名:

http.request(url, method, callback, [headers], [post_data], [options])
参数类型说明
urlstring请求地址
methodstring“GET”, “POST”, “PUT”, “DELETE” 等
callbackfunction回调函数 (self, id, response)
headerstable可选请求头 {["Content-Type"] = "application/json"}
post_datastringPOST 请求体
optionstable{timeout = 10, ignore_cache = true}

2.2 POST 请求(JSON 数据)

-- 异步 POST:上传分数
function submit_score(self, player_id, score)
    local payload = json.encode({
        player_id = player_id,
        score = score,
        timestamp = os.time()
    })

    http.request(
        "https://api.example.com/scores",
        "POST",
        function(self, id, response)
            if response.status == 200 or response.status == 201 then
                print("分数上传成功!")
            else
                print("上传失败:", response.status, response.response)
            end
        end,
        {
            ["Content-Type"] = "application/json",
            ["Authorization"] = "Bearer " .. self.auth_token
        },
        payload,
        { timeout = 10 }
    )
end

2.3 超时与错误处理

function safe_request(self, url, method, headers, body, on_success, on_error)
    local timer_id = nil

    http.request(url, method,
        function(self, id, response)
            -- 取消超时 timer
            if timer_id then
                timer.cancel(timer_id)
                timer_id = nil
            end

            if response.status >= 200 and response.status < 300 then
                if on_success then on_success(response) end
            else
                if on_error then
                    on_error("HTTP " .. response.status .. ": " .. (response.response or ""))
                end
            end
        end,
        headers,
        body,
        { timeout = 15 }
    )

    -- 额外的超时防护(部分平台 timeout 选项行为不同)
    timer_id = timer.delay(20, false, function()
        if on_error then
            on_error("Request timeout")
        end
    end)
end

3. JSON 解析与序列化

Defold 内置 json 模块,无需额外库。

3.1 基本用法

local json = require("json")

-- 解码(JSON string → Lua table)
local data = json.decode('{"name":"player1","score":5000}')
print(data.name, data.score)   -- player1  5000

-- 编码(Lua table → JSON string)
local str = json.encode({ level = 5, coins = 123 })
print(str)   -- {"level":5,"coins":123}

3.2 处理 JSON 数组

-- 排行榜响应
local response_str = [[
{
    "players": [
        {"name": "Alice", "score": 9999},
        {"name": "Bob", "score": 8888},
        {"name": "Charlie", "score": 7777}
    ]
}
]]

local data = json.decode(response_str)
for i, player in ipairs(data.players) do
    print(string.format("%d. %s: %d", i, player.name, player.score))
end

3.3 处理嵌套数据

-- 复杂响应结构
local response = json.decode(response_str)

-- 安全访问(防 nil)
local first_score = response.players
    and response.players[1]
    and response.players[1].score
    or 0

-- 或使用辅助函数
function safe_get(t, ...)
    local result = t
    for _, key in ipairs({...}) do
        if type(result) ~= "table" then return nil end
        result = result[key]
    end
    return result
end

local score = safe_get(response, "players", 1, "score")

4. WebSocket:连接、发送与接收

4.1 WebSocket 连接

local socket = require("websocket")

function connect_websocket(self, url)
    self.ws = nil
    self.ws_connected = false

    -- HTML5 平台需特殊处理(见第 8 节)
    local conn, err = websocket.connect(url,
        { timeout = 5 },
        function(self, conn, data)
            -- 连接成功回调
            self.ws = conn
            self.ws_connected = true
            print("[WS] 连接成功:", url)

            -- 发送登录消息
            websocket.send(conn, json.encode({
                type = "login",
                player_id = self.player_id
            }))
        end,
        function(self, conn, data)
            -- 收到消息回调
            handle_ws_message(self, data)
        end,
        function(self, conn, data)
            -- 断开回调
            self.ws_connected = false
            self.ws = nil
            print("[WS] 连接断开,准备重连...")
            schedule_reconnect(self)
        end
    )

    if not conn then
        print("[WS] 连接失败:", err)
        schedule_reconnect(self)
    end
end

Defold 的 websocket 模块需要引擎内置或扩展支持。如果编辑器版本不含 websocket,可通过 Native Extension 引入第三方库。

4.2 发送消息

function send_ws_message(self, msg_type, payload)
    if not self.ws_connected or not self.ws then
        print("[WS] 未连接,消息入队")
        table.insert(self.msg_queue, { type = msg_type, payload = payload })
        return false
    end

    local msg = json.encode({
        type = msg_type,
        data = payload,
        seq = self.send_seq
    })

    self.send_seq = self.send_seq + 1
    websocket.send(self.ws, msg)
    return true
end

4.3 接收与分发消息

function handle_ws_message(self, raw_data)
    local ok, msg = pcall(json.decode, raw_data)
    if not ok then
        print("[WS] JSON 解析失败:", raw_data)
        return
    end

    if msg.type == "player_joined" then
        spawn_remote_player(self, msg.player_id, msg.position)
    elseif msg.type == "player_moved" then
        update_remote_player(self, msg.player_id, msg.position)
    elseif msg.type == "chat" then
        show_chat_message(self, msg.from, msg.text)
    elseif msg.type == "ping" then
        -- 回复 pong
        websocket.send(self.ws, json.encode({ type = "pong", time = msg.time }))
    else
        print("[WS] 未知消息类型:", msg.type)
    end
end

5. REST API 封装与解耦设计

5.1 网络层封装

-- net.lua —— 网络请求模块
local M = {}
local BASE_URL = "https://api.mygame.com/v1"

local function make_url(endpoint)
    return BASE_URL .. endpoint
end

function M.get(path, callback)
    http.request(make_url(path), "GET",
        function(self, id, response)
            local ok, data = pcall(json.decode, response.response)
            callback(ok and response.status == 200, data, response.status)
        end
    )
end

function M.post(path, body_table, callback)
    local body = json.encode(body_table)
    http.request(make_url(path), "POST",
        function(self, id, response)
            local ok, data = pcall(json.decode, response.response)
            callback(ok and (response.status == 200 or response.status == 201),
                     data, response.status)
        end,
        { ["Content-Type"] = "application/json" },
        body,
        { timeout = 15 }
    )
end

return M

5.2 业务层调用

-- leaderboard.lua —— 排行榜业务
local net = require("net")

local M = {}

function M.fetch_top10(callback)
    net.get("/leaderboard?limit=10", function(success, data, status)
        if success then
            callback(data.players or {})
        else
            print("获取排行榜失败:", status)
            callback({})
        end
    end)
end

function M.submit_score(player_id, score, callback)
    net.post("/scores", {
        player_id = player_id,
        score = score
    }, function(success, data, status)
        if callback then callback(success, data) end
    end)
end

return M

5.3 使用

-- game.script
local leaderboard = require("leaderboard")

function init(self)
    leaderboard.fetch_top10(function(players)
        for _, p in ipairs(players) do
            print(p.name, p.score)
        end
    end)
end

记忆:分层设计 = net.lua(原始 HTTP)→ leaderboard.lua(业务 API)→ game.script(游戏逻辑)——每层只依赖下一层,可独立测试、替换后端。


6. 认证 Token 管理

6.1 Token 生命周期

1. 登录(HTTP POST /login)→ 返回 access_token + refresh_token
2. 后续请求 Header 带 Authorization: Bearer <access_token>
3. access_token 过期(401)→ 用 refresh_token 换取新 token
4. refresh_token 也过期 → 强制重新登录

6.2 Token 管理实现

-- auth.lua
local M = {
    access_token = nil,
    refresh_token = nil,
    expires_at = 0,
}

function M.set_tokens(access, refresh, expires_in)
    M.access_token = access
    M.refresh_token = refresh
    M.expires_at = os.time() + (expires_in or 3600)
    sys.save(sys.get_save_file("mygame", "auth"), {
        access = access,
        refresh = refresh,
        expires = M.expires_at
    })
end

function M.load_saved_tokens()
    local data = sys.load(sys.get_save_file("mygame", "auth"))
    if data and data.expires and data.expires > os.time() then
        M.access_token = data.access
        M.refresh_token = data.refresh
        M.expires_at = data.expires
        return true
    end
    return false
end

function M.get_auth_header()
    if M.access_token then
        return { ["Authorization"] = "Bearer " .. M.access_token }
    end
    return {}
end

function M.is_expired()
    return os.time() >= M.expires_at - 60   -- 提前 60 秒刷新
end

return M

6.3 自动刷新 Token

function auto_refresh_token(self, on_done)
    if not auth.is_expired() then
        if on_done then on_done(true) end
        return
    end

    http.request(BASE_URL .. "/refresh", "POST",
        function(self, id, response)
            if response.status == 200 then
                local data = json.decode(response.response)
                auth.set_tokens(data.access_token, data.refresh_token, data.expires_in)
                if on_done then on_done(true) end
            else
                print("Token 刷新失败,需要重新登录")
                auth.set_tokens(nil, nil, 0)
                if on_done then on_done(false) end
            end
        end,
        { ["Content-Type"] = "application/json" },
        json.encode({ refresh_token = auth.refresh_token }),
        { timeout = 10 }
    )
end

7. 断线重连与网络状态监控

7.1 WebSocket 断线重连

function schedule_reconnect(self)
    if self.reconnect_timer then
        timer.cancel(self.reconnect_timer)
    end

    self.reconnect_attempts = (self.reconnect_attempts or 0) + 1
    local delay = math.min(self.reconnect_attempts * 2, 30)  -- 指数退避,最大 30 秒

    print("[WS] 将在", delay, "秒后重连(第", self.reconnect_attempts, "次)")

    self.reconnect_timer = timer.delay(delay, false, function()
        connect_websocket(self, self.ws_url)
    end)
end

function on_ws_connected(self)
    self.reconnect_attempts = 0
    if self.reconnect_timer then
        timer.cancel(self.reconnect_timer)
        self.reconnect_timer = nil
    end

    -- 重连后发送积压消息
    while #self.msg_queue > 0 do
        local msg = table.remove(self.msg_queue, 1)
        send_ws_message(self, msg.type, msg.payload)
    end
end

7.2 心跳保活

function start_heartbeat(self)
    if self.heartbeat_timer then
        timer.cancel(self.heartbeat_timer)
    end

    self.heartbeat_timer = timer.delay(5, true, function()
        if self.ws_connected then
            websocket.send(self.ws, json.encode({
                type = "ping",
                time = socket.gettime()  -- 需 time 模块
            }))
        end
    end)
end

function handle_ws_message(self, raw_data)
    local msg = json.decode(raw_data)
    if msg.type == "pong" then
        local rtt = socket.gettime() - msg.time
        self.ping_ms = math.floor(rtt * 1000)
        print("[WS] RTT:", self.ping_ms, "ms")
    end
end

注意:Defold 内置无 socket.gettime(),可以用 os.time()(秒级精度)或通过 sys 适配。更精确的时间可用 timer API 差值计算。

7.3 网络状态检测

function check_network(self)
    -- 简单探测:请求一个小接口
    http.request("https://api.example.com/health", "GET",
        function(self, id, response)
            self.is_online = (response.status == 200)
            if self.is_online then
                print("[net] 网络正常")
            else
                print("[net] 网络异常,状态:", response.status)
            end
        end,
        nil, nil,
        { timeout = 5 }
    )
end

8. 跨平台差异与注意事项

8.1 各平台网络行为

平台特性注意
Desktop完整 TCP/HTTP/WebSocket无限制
iOSATS(App Transport Security)默认只允许 HTTPS;HTTP 需在 Info.plist 声明
Android权限 INTERNETAndroidManifest.xml 需声明;API >= 28 限制明文 HTTP
HTML5Web API(fetch/WebSocket)CORS 需服务器配合;无原始 socket
Steam有独立的 relay API可结合 Steamworks NE

8.2 iOS ATS 配置

<!-- Info.plist(放在 Native Extension 的 res/ios/ 目录) -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>   <!-- 开发时允许 HTTP,上架前取消 -->
</dict>

8.3 HTML5 CORS

服务器响应头必须包含:
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET, POST, OPTIONS
  Access-Control-Allow-Headers: Content-Type, Authorization

8.4 请求线程问题

Defold 网络请求全部是异步的(回调机制),不会阻塞主线程。
→ 无需手动线程管理
→ 回调在 Defold 主线程执行(线程安全)
→ 但避免在回调中做大量计算,防止掉帧

9. 速查表

需求API/方法说明
异步 GEThttp.request(url, "GET", callback)回调接收 response 对象
异步 POSThttp.request(url, "POST", cb, headers, body, opts)Content-Type 设 application/json
JSON 解码json.decode(string)string → table
JSON 编码json.encode(table)table → string
WebSocket 连接websocket.connect(url, opts, connected_cb, msg_cb, disc_cb)四个回调
WebSocket 发送websocket.send(conn, text)发文本或二进制
Token 存储sys.save(path, data)存本地持久化
断线重连timer.delay + 指数退避避免频繁重试
心跳timer.delay 定时发 ping检测连接存活
HTTP 超时options = { timeout = N }单位秒
iOS HTTPInfo.plist NSAllowsArbitraryLoads开发放行,上架改 HTTPS
HTML5 CORS服务端 Access-Control-Allow-Origin必须

一句话记忆:Defold 网络 = HTTP(低频:排行榜、存档:http.request + json 编解码)+ WebSocket(高频:实时对战:connect/send + 消息分发);网络层封装解耦(net.lua → business.lua → game),Token 靠 sys.save 本地持久 + 自动刷新,断线重连用指数退避 + 积压队列,跨平台注意 iOS ATS + HTML5 CORS——网络不稳定是常态,做好重试和降级。


相关阅读

  • /defold-multiplayer-sync/ — 多人联机同步与权威服务器
  • /defold-script-system-lua/ — Lua 异步消息与回调机制
  • /defold-save-serialization/ — 数据序列化与本地存储

延伸阅读

  • /defold-cross-platform-publish/ — 多平台发布与权限配置
  • /defold-native-extensions/ — 自定义网络库扩展
  • /defold-profiling-debugging/ — 网络性能分析
  • 网络编程专题 — TCP/HTTP/WebSocket 协议原理
  • Node.js 专题 — 用 Node.js 写游戏后端
  • 游戏开发专题 — 多人游戏网络架构
-- ======================================
-- 完整可运行示例:HTTP + WebSocket + REST 封装
-- 放入 network_manager.script
-- ======================================

local json = require("json")
local BASE_URL = "https://api.example.com/v1"

function init(self)
    self.ws = nil
    self.ws_connected = false
    self.ws_url = "wss://game.example.com/ws"
    self.msg_queue = {}
    self.reconnect_attempts = 0
    self.reconnect_timer = nil
    self.heartbeat_timer = nil
    self.send_seq = 0
    self.auth_token = nil

    print("[init] Network Manager 初始化完成")
end

-- ===== HTTP 请求 =====

function http_get(self, path, callback)
    local headers = {}
    if self.auth_token then
        headers["Authorization"] = "Bearer " .. self.auth_token
    end

    http.request(BASE_URL .. path, "GET",
        function(self, id, response)
            local ok, data = pcall(json.decode, response.response)
            callback(ok and response.status == 200, data, response.status)
        end,
        headers,
        nil,
        { timeout = 15 }
    )
end

function http_post(self, path, body_table, callback)
    local headers = {
        ["Content-Type"] = "application/json"
    }
    if self.auth_token then
        headers["Authorization"] = "Bearer " .. self.auth_token
    end

    http.request(BASE_URL .. path, "POST",
        function(self, id, response)
            local ok, data = pcall(json.decode, response.response)
            local success = ok and (response.status == 200 or response.status == 201)
            callback(success, data, response.status)
        end,
        headers,
        json.encode(body_table),
        { timeout = 15 }
    )
end

-- ===== WebSocket =====

function ws_connect(self)
    if self.ws_connected then return end

    local conn, err = websocket.connect(self.ws_url,
        { timeout = 5 },
        function(self, conn, data)
            self.ws = conn
            self.ws_connected = true
            self.reconnect_attempts = 0
            print("[WS] 连接成功")
            start_heartbeat(self)
            flush_msg_queue(self)
        end,
        function(self, conn, data)
            handle_ws_data(self, data)
        end,
        function(self, conn, data)
            self.ws_connected = false
            self.ws = nil
            stop_heartbeat(self)
            print("[WS] 断开连接")
            schedule_reconnect(self)
        end
    )

    if not conn then
        print("[WS] 连接失败:", err)
        schedule_reconnect(self)
    end
end

function ws_send(self, msg_type, payload)
    local msg = json.encode({
        type = msg_type,
        data = payload,
        seq = self.send_seq
    })
    self.send_seq = self.send_seq + 1

    if self.ws_connected and self.ws then
        websocket.send(self.ws, msg)
        return true
    else
        table.insert(self.msg_queue, { type = msg_type, payload = payload })
        return false
    end
end

function handle_ws_data(self, raw)
    local ok, msg = pcall(json.decode, raw)
    if not ok then
        print("[WS] 解析失败:", raw)
        return
    end

    if msg.type == "ping" then
        ws_send(self, "pong", { time = msg.time })
    elseif msg.type == "state_update" then
        msg.post("/game#script", "remote_state", msg.data)
    else
        msg.post("/game#script", "ws_message", msg)
    end
end

-- ===== 重连与心跳 =====

function schedule_reconnect(self)
    if self.reconnect_timer then timer.cancel(self.reconnect_timer) end
    self.reconnect_attempts = self.reconnect_attempts + 1
    local delay = math.min(self.reconnect_attempts * 2, 30)
    print("[WS] 重连倒计时:", delay, "秒")
    self.reconnect_timer = timer.delay(delay, false, function()
        ws_connect(self)
    end)
end

function start_heartbeat(self)
    if self.heartbeat_timer then timer.cancel(self.heartbeat_timer) end
    self.heartbeat_timer = timer.delay(5, true, function()
        if self.ws_connected then
            ws_send(self, "ping", { time = os.time() })
        end
    end)
end

function stop_heartbeat(self)
    if self.heartbeat_timer then
        timer.cancel(self.heartbeat_timer)
        self.heartbeat_timer = nil
    end
end

function flush_msg_queue(self)
    while #self.msg_queue > 0 do
        local m = table.remove(self.msg_queue, 1)
        ws_send(self, m.type, m.payload)
    end
end

-- ===== 消息入口 =====

function on_message(self, message_id, message, sender)
    if message_id == hash("connect_ws") then
        self.ws_url = message.url or self.ws_url
        ws_connect(self)
    elseif message_id == hash("disconnect_ws") then
        if self.ws then
            websocket.disconnect(self.ws)
        end
    elseif message_id == hash("ws_send") then
        ws_send(self, message.msg_type, message.payload)
    elseif message_id == hash("http_get") then
        http_get(self, message.path, message.callback or function() end)
    elseif message_id == hash("http_post") then
        http_post(self, message.path, message.body, message.callback or function() end)
    elseif message_id == hash("set_auth_token") then
        self.auth_token = message.token
    end
end

function final(self)
    if self.ws then websocket.disconnect(self.ws) end
    if self.heartbeat_timer then timer.cancel(self.heartbeat_timer) end
    if self.reconnect_timer then timer.cancel(self.reconnect_timer) end
    print("[final] Network Manager 退出")
end

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章

  1. Defold Native Extensions:C/C++ 扩展引擎能力
  2. Defold Render Scripts 与自定义渲染管线
  3. Defold Tilemap 碰撞与关卡设计