引言
现代游戏离不开网络:排行榜同步、云存档、多人联机、实时推送。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
- 2. http.request:异步 GET/POST
- 3. JSON 解析与序列化
- 4. WebSocket:连接、发送与接收
- 5. REST API 封装与解耦设计
- 6. 认证 Token 管理
- 7. 断线重连与网络状态监控
- 8. 跨平台差异与注意事项
- 9. 速查表
- 相关阅读
- 延伸阅读
1. 网络架构选型:HTTP vs WebSocket
Defold 提供两套内置网络 API:
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信模式 | 请求-响应 | 全双工持续连接 |
| 适用场景 | 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])
| 参数 | 类型 | 说明 |
|---|---|---|
| url | string | 请求地址 |
| method | string | “GET”, “POST”, “PUT”, “DELETE” 等 |
| callback | function | 回调函数 (self, id, response) |
| headers | table | 可选请求头 {["Content-Type"] = "application/json"} |
| post_data | string | POST 请求体 |
| options | table | {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 | 无限制 |
| iOS | ATS(App Transport Security) | 默认只允许 HTTPS;HTTP 需在 Info.plist 声明 |
| Android | 权限 INTERNET | AndroidManifest.xml 需声明;API >= 28 限制明文 HTTP |
| HTML5 | Web 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/方法 | 说明 |
|---|---|---|
| 异步 GET | http.request(url, "GET", callback) | 回调接收 response 对象 |
| 异步 POST | http.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 HTTP | Info.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
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。