目录
- Skynet.socket 函数族使用详解
- 核心功能分类
- 一、TCP 连接管理
- 1. 监听端口
- 2. 建立连接
- 3. 关闭连接
- 二、数据读写操作
- 1. 阻塞式读取
- 2. 写入数据
- 2.1 `socket.write(fd, data)` 的返回值
- 2.2 示例代码
- 2.3 关键注意事项
- 2.4 与其他函数的区别
- 2.5 底层原理
- 2.6 总结
- 三、UDP 处理
- 1. 创建 UDP 句柄
- 2. 发送 UDP 数据
- 四、高级控制与监控
- 1. 缓冲区过载警告
- 2. 域名解析
- 五、SocketChannel 封装
- 1. 创建 Channel 对象
- 2. 发送请求
- 六、最佳实践与注意事项
- 总结
Skynet.socket 函数族使用详解
Skynet 的 skynet.socket
模块提供了 TCP/UDP 网络通信的核心 API,结合协程机制实现了阻塞式调用模型,简化了异步网络编程。本文详细解析其核心函数、使用场景及最佳实践。
核心功能分类
- TCP 连接管理(监听、连接、关闭)
- 数据读写(阻塞式读写、分包处理)
- UDP 支持(数据包收发、地址管理)
- 高级控制(缓冲区警告、域名解析、过载处理)
一、TCP 连接管理
1. 监听端口
local socket = require "skynet.socket"-- 启动 TCP 服务器
skynet.start(function()local listen_fd = socket.listen("0.0.0.0", 8888) -- 监听 8888 端口socket.start(listen_fd, function(client_fd, addr)-- 新连接回调,处理客户端请求socket.start(client_fd)-- ... 处理数据逻辑end)
end)
socket.listen(host, port [, backlog])
返回监听套接字的文件描述符listen_fd
。
backlog
:等待连接队列的最大长度(可选,默认 SOMAXCONN)。
2. 建立连接
local client_fd = socket.open("127.0.0.1", 6379) -- 连接 Redis
if client_fd thensocket.start(client_fd)socket.write(client_fd, "PING\r\n")
end
socket.open(host, port)
同步阻塞连接目标地址,返回客户端套接字client_fd
。
3. 关闭连接
socket.close(client_fd) -- 安全关闭,等待未完成读写
socket.close_fd(client_fd) -- 强制立即关闭(慎用)
socket.shutdown(client_fd) -- 强制关闭(适用于 __gc 元方法)
- 区别:
close
:等待其他协程完成读写后关闭。close_fd
/shutdown
:直接关闭,可能导致未处理数据丢失。
二、数据读写操作
1. 阻塞式读取
-- 读取固定字节
local data, partial = socket.read(client_fd, 1024) -- 读 1024 字节
if data thenprint("完整数据:", data)
elseprint("部分数据:", partial) -- 连接已关闭
end-- 读取一行(默认以 \n 分割)
local line = socket.readline(client_fd, "\r\n") -- 自定义分隔符
socket.read(fd, sz)
sz
为nil
时读取尽可能多的数据(至少 1 字节)。- 返回完整数据或
false + 已读部分数据
(连接关闭时)。
2. 写入数据
socket.write(client_fd, "Hello Skynet!\r\n") -- 高优先级写入
socket.lwrite(client_fd, "Low priority data\r\n") -- 低优先级写入
- 优先级区别:
write
:数据进入高优先级队列,优先发送。lwrite
:数据进入低优先级队列,高优先级队列为空时发送。
在 Skynet 框架中,socket.write
方法的返回值取决于数据是否成功写入内核的发送缓冲区。以下是具体说明:
2.1 socket.write(fd, data)
的返回值
-
成功时:
- 返回
true
,表示数据已成功加入内核的发送队列,不保证对端已接收。 - 注意:返回值仅表示数据成功提交到操作系统的网络协议栈,实际网络传输是异步的。
- 返回
-
失败时:
- 返回
nil
+ 错误信息(如"closed"
表示连接已关闭)。 - 常见错误:
"closed"
: 连接已关闭。"timeout"
: 发送超时(需结合socketdriver.settimeout
设置)。"error"
: 其他底层错误。
- 返回
2.2 示例代码
local skynet = require "skynet"
local socket = require "skynet.socket"local fd = ... -- 假设 fd 是已建立的客户端连接-- 尝试发送数据
local ok, err = socket.write(fd, "Hello World")
if not ok thenskynet.error("Send failed:", err)socket.close(fd) -- 关闭失效连接
end
2.3 关键注意事项
- 异步发送:
socket.write
是非阻塞的,数据可能仍在发送队列中未实际传输。 - 流量控制:若发送速度超过网络带宽或对端接收速度,可能导致缓冲区积压,最终触发错误。
- 错误处理:务必检查返回值,及时关闭失效的
fd
,避免资源泄漏。 - 大包分片:单次写入数据过大可能被系统拆分,需结合业务逻辑处理完整性(如添加长度头)。
2.4 与其他函数的区别
socket.send
:与socket.write
行为一致,两者是别名关系。socket.lwrite
:专用于发送 Lua 字符串(内部优化),行为相同。
2.5 底层原理
Skynet 的 socket.write
最终调用操作系统的 send
系统调用,但通过非阻塞模式封装。若内核发送缓冲区已满,数据会排队等待,此时返回 true
;若连接已异常(如对端关闭),则直接返回错误。
2.6 总结
- 返回值意义:
true
表示数据提交成功,nil + err
表示失败。 - 必须处理错误:尤其要捕获
"closed"
错误,及时清理连接状态。 - 性能影响:高频发送时建议结合
socketdriver.setqueue_max
控制缓冲区大小,避免内存暴涨。
三、UDP 处理
1. 创建 UDP 句柄
local udp_fd = socket.udp(function(data, from)print("收到 UDP 数据:", data, "来源:", socket.udp_address(from))
end, "0.0.0.0", 9999) -- 绑定 9999 端口
socket.udp(callback [, host, port])
创建 UDP 句柄并绑定回调,收到数据时触发callback(data, from)
。
2. 发送 UDP 数据
socket.sendto(udp_fd, from_address, "ACK") -- 发送到指定地址
socket.write(udp_fd, "Ping") -- 若已设置默认地址,直接写入
socket.sendto(fd, from, data)
from
为接收到的来源地址字符串,不可手动构造。
四、高级控制与监控
1. 缓冲区过载警告
socket.warning(client_fd, function(fd, size)if size > 0 thenprint("警告:待发数据超过", size, "KB")elseprint("缓冲区已清空")end
end)
socket.warning(fd, callback)
监控待发数据量,超过 1MB 触发回调(默认每超 64KB 打印错误日志)。
2. 域名解析
local dns = require "skynet.dns"
dns.server("8.8.8.8") -- 设置 DNS 服务器
local ip, all_ips = dns.resolve("www.example.com") -- 解析域名
dns.resolve(name [, ipv6])
返回解析到的 IP 地址及所有 IP 列表,避免阻塞 socket 线程。
五、SocketChannel 封装
1. 创建 Channel 对象
local sc = require "skynet.socketchannel"
local channel = sc.channel {host = "127.0.0.1",port = 6379,response = function(sock)return true, sock:readline("\r\n") -- 解析 Redis 响应end,
}
- 模式选择:
- 提供
response
函数则进入 Session 模式(如 MongoDB)。 - 否则为 请求-回应模式(如 Redis)。
- 提供
2. 发送请求
local resp = channel:request("PING\r\n") -- 请求并等待响应
local resp2 = channel:request("GET key\r\n", function(sock)return true, sock:read(5) -- 自定义响应解析
end)
channel:request(req [, response | session])
发送请求并自动匹配响应,支持自定义解析逻辑。
六、最佳实践与注意事项
-
连接生命周期管理
- 使用
socket.close
确保安全关闭。 - 避免在
__gc
中使用阻塞操作,优先用shutdown
。
- 使用
-
协程调度优化
- 高频读写时,合理使用
socket.lwrite
避免阻塞关键数据。 - 结合
skynet.fork
处理并发请求。
- 高频读写时,合理使用
-
错误处理
- 所有读写操作需包裹在
pcall
中捕获异常。 - UDP 需处理乱序和丢包,不可依赖时序。
- 所有读写操作需包裹在
-
性能监控
- 使用
socket.warning
监控缓冲区,防止内存溢出。 - 避免频繁 DNS 查询,通过缓存或独立服务处理。
- 使用
总结
skynet.socket
通过协程化阻塞 API 简化了网络编程复杂度,结合 socketchannel
可高效处理复杂协议。开发者需注意:
- 连接安全性:合理关闭连接,避免资源泄漏。
- 协议适配:根据场景选择基础 API 或高级封装。
- 性能调优:监控缓冲区,平衡吞吐量与内存消耗。
通过阅读 lualib/socket.lua
和参考 service/gate.lua
,可深入理解底层实现机制。