Skip to content

网络工具例程 4:手写 TCP 服务器(支持多个客户端) ​

这一页只讲这一个例程。同组其余例程、以及这个库的函数说明都在 05-05-net。

适用:需要自定义协议的 TCP 服务端 | 依赖模块:net timer | 难度:★★★

场景:tcp_server 模块够用时优先用它;这里演示用 net 从零搭一个能同时带多个客户端的回显服务器。

完整代码(可直接下载运行):

lua
local PORT = 8000

local srv, e = net.socket("TCP")
if not srv then
    print("建不出来:", net.errstr(e))
    return
end
srv:setoption("reuseaddr")                       -- 重启后端口能立刻再绑
if srv:bind("0.0.0.0", PORT) ~= 0 then            -- 服务器必须给端口
    print("端口绑定失败", PORT)
    return
end
srv:listen(4)
print("TCP 服务器已启动:", PORT)

local clients = {}                               -- 在线客户端集合

timer.start(200, function()
    -- 1) 收新连接:给 10ms 超时,没有新连接就立刻返回
    local c = srv:accept(10)
    if c then
        clients[c] = true
        local ip = c:peer()
        print("新客户端:", net.unpackip(ip, "*s"))
    end

    -- 2) 收数据:只对"有话要说"的客户端 recv,不会被沉默的连接堵住
    local list = {}
    for s in pairs(clients) do list[#list + 1] = s end
    if #list == 0 then return end

    for _, s in ipairs(net.select(list, 0)) do
        local data, rerr = s:recv(256)
        if rerr == 0 and #data > 0 then
            s:send("echo:" .. data)
        elseif rerr == 2 then                     -- 2 = 对端已断开
            s:close()
            clients[s] = nil                     -- 不剔除就一直占着那 16 个名额
        end
    end
end)

要点:

  • 顺序是 socket → setoption("reuseaddr") → bind(端口必填)→ listen → 周期 accept。
  • accept 只在真的等到连接时返回句柄,超时返回 nil + 1(超时),所以放在定时器里反复调用就行,一个服务器能接多个客户端。
  • 多客户端必须配 net.select:直接对每个客户端调 recv 会被第一个没数据的连接卡到超时。
  • recv 返回 2(连接已关闭)说明对端断开,要 close 并从集合里剔除;忘了 close 也会在句柄被回收时关掉,但主动关更可控。

验证方法:

  1. 电脑上用 nc 192.168.1.x 8000(或串口工具的 TCP 客户端)连上,发一段文字,会收到 echo: 前缀的回包。
  2. 同时开两个客户端,两边都能收到各自的回包(验证 select 没被其中一个堵死)。
  3. 关掉一个客户端,服务器日志不再报错,另一个照常工作。

注意事项:

  • 忘记 listen 直接 accept 会报「socket 还没 listen」。
  • accept() 不传超时表示一直等,放在定时器里会把整个 Lua 卡死;要轮询就传一个小超时(如 10)。
  • 忘了 reuseaddr,重启脚本后 bind 会失败(端口还被上次的连接占着)。

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研