# UDP в Lua: LuaSocket на практике

> Как работать с UDP в Lua через LuaSocket: неблокирующий сервер на LuaJIT, клиент для MoonLoader с wait(0), sendto и receivefrom, борьба с потерей и порядком пакетов. Примеры кода.

- URL: https://pakulichev.com/wiki/luasocket-udp
- Author: Pakulichev (https://pakulichev.com/)
- Language: ru
- Updated: 2026-10-16
- Whole site for LLMs: https://pakulichev.com/llms.txt (index), https://pakulichev.com/llms-full.txt (full text)

# UDP в Lua: LuaSocket на практике

[SL:MP](https://pakulichev.com/projects/sl-mp) начался в августе 2020 года с простого вопроса: получится ли синхронизировать игроков в GTA: San Andreas одним LuaSocket поверх SA:MP? Получилось. Дальше протокол прошёл путь от сжатого JSON через сериализованные Lua-таблицы к бинарному SL:NET поверх UDP. Сетевую часть я затем вынес в отдельную библиотеку [SNET](https://pakulichev.com/projects/snet). Ниже – тот самый фундамент: голый LuaSocket и UDP, без надстроек.

## Почему UDP

TCP гарантирует доставку и порядок. Для игры это скорее минус: если один сегмент потерялся, всё, что пришло за ним, ждёт повторной отправки. Позиция игрока, которая опоздала на полсекунды, уже никому не нужна, а из-за неё задерживаются и свежие данные.

UDP отправляет датаграммы как есть. Каждая приходит целиком или не приходит вовсе. Цена такая:

- пакеты теряются, и никто об этом не сообщит;
- приходят не в том порядке, в каком ушли;
- иногда дублируются.

Для потока позиций это терпимо: следующий пакет придёт через 100 мс и перекроет потерянный. Для сообщения в чат или выбора пункта в диалоге – уже нет.

## Сервер на LuaJIT

Основные функции LuaSocket для UDP: `socket.udp()` создаёт сокет, `setsockname` привязывает его к адресу и порту, `receivefrom` читает датаграмму вместе с адресом отправителя, `sendto` отправляет на конкретный адрес. Сервер ниже принимает пакеты от клиентов и рассылает каждый всем остальным:

```lua
local socket = require("socket")

local udp = assert(socket.udp())
assert(udp:setsockname("*", 7777))
udp:settimeout(0)

local clients = {}

while true do
  local data, ip, port = udp:receivefrom()
  if data then
    local key = ip .. ":" .. port
    clients[key] = { ip = ip, port = port, seen = socket.gettime() }
    for k, c in pairs(clients) do
      if k ~= key then
        udp:sendto(key .. " " .. data, c.ip, c.port)
      end
    end
  else
    -- очередь пуста: чистим молчащих клиентов и отдаём процессор
    local now = socket.gettime()
    for k, c in pairs(clients) do
      if now - c.seen > 10 then
        clients[k] = nil
      end
    end
    socket.sleep(0.01)
  end
end
```

`"*"` в `setsockname` означает все локальные интерфейсы. «Подключения» в UDP нет, поэтому клиента идентифицирует пара IP и порт, а отключение приходится определять по тишине – здесь это 10 секунд без пакетов.

С таймаутом 0 `receivefrom` при пустой очереди возвращает `nil, "timeout"`. Пока пакеты есть, цикл вычитывает их без пауз, а `socket.sleep` срабатывает только когда читать нечего.

## Клиент в MoonLoader

Клиенту нужен один сервер, поэтому удобнее `setpeername`: после него `send` и `receive` работают без адреса. В отличие от `sendto`, `setpeername` принимает и имя хоста.

```lua
local socket = require("socket")

local udp = assert(socket.udp())
udp:settimeout(0)
assert(udp:setpeername("127.0.0.1", 7777))

local function send_position()
  local x, y, z = getCharCoordinates(PLAYER_PED)
  udp:send(string.format("%.2f %.2f %.2f", x, y, z))
end

local function read_packets()
  while true do
    local data = udp:receive()
    if not data then break end
    local id, x, y, z = data:match("^(%S+) (%S+) (%S+) (%S+)$")
    if id then
      print(id, tonumber(x), tonumber(y), tonumber(z))
    end
  end
end

function main()
  local last_send = 0
  while true do
    wait(0)
    read_packets()
    local now = socket.gettime()
    if now - last_send >= 0.1 then
      send_position()
      last_send = now
    end
  end
end

function onScriptTerminate(script, quit_game)
  if script == thisScript() then
    udp:close()
  end
end
```

`wait(0)` отдаёт управление игре до следующего кадра, так что скрипт опрашивает сокет раз в кадр и не подвешивает GTA. Отправка ограничена десятью пакетами в секунду: слать позицию каждый кадр при 60 FPS – лишний трафик без заметной пользы. Вместо `print` в настоящем мультиплеере здесь двигался бы персонаж другого игрока.

## Порядок и потери

Самая простая защита от перемешанных пакетов – порядковый номер. Отправитель увеличивает счётчик и кладёт его в пакет, получатель отбрасывает всё, что старее последнего принятого:

```lua
local last_seq = {}

local function accept(id, seq)
  if last_seq[id] and seq <= last_seq[id] then
    return false
  end
  last_seq[id] = seq
  return true
end
```

Для позиций этого достаточно: опоздавший пакет просто выбрасывается. С важными событиями сложнее. Их нужно подтверждать и переотправлять, пока подтверждение не придёт, и при этом не задерживать поток позиций. Плюс текстовый формат из примера неэкономен: числа в строке занимают в несколько раз больше байт, чем в бинарном виде. В SL:MP мы в итоге пришли к бинарному SL:NET с собственным битстримом.

Ещё одно ограничение касается размера. Типичный MTU в Ethernet – 1500 байт, и датаграмма крупнее этого дробится на IP-фрагменты. Если потеряется любой фрагмент, пропадёт вся датаграмма, так что большой пакет теряется заметно чаще маленького. Игровые пакеты лучше держать в пределах нескольких сотен байт, а большие данные (список игроков при входе, содержимое диалога) резать на части или отправлять через механизм с подтверждением.

## Что добавляет SNET

[SNET](https://pakulichev.com/projects/snet) решает как раз эти задачи поверх того же LuaSocket и UDP. В нём есть подтверждение доставки для важных пакетов и приоритеты: что-то уходит «как получится», что-то обязано дойти. Ограничения на число клиентов в самой библиотеке нет. Работает SNET на LuaJIT под Windows и Linux и в MoonLoader, протокол одинаковый на обеих сторонах. Сообщество портировало его на Node.js (node-snet), был и экспериментальный порт на Go.

Если нужен собственный сетевой мод на Lua, начать с голого LuaSocket полезно хотя бы для того, чтобы понять, какие проблемы решает библиотека. Код в статье оформлен по [правилам из моего гайда](https://pakulichev.com/wiki/lua-code-style), а посмотреть, как всё это выглядит в готовом мультиплеере, можно, [запустив сервер SL:MP](https://pakulichev.com/wiki/slmp-server).

## FAQ

### Почему sendto выдаёт ошибку, если передать имя хоста?

В LuaSocket sendto принимает только IP-адрес: имена хостов там не разрешаются ради скорости. Либо заранее получите IP через socket.dns.toip(), либо используйте setpeername, который имена принимает.

### Зачем settimeout(0), если можно читать сокет в отдельном потоке?

В Lua нет потоков в привычном смысле, а MoonLoader выполняет скрипты в корутинах внутри игрового цикла. Блокирующий receive остановил бы весь скрипт до прихода пакета. С таймаутом 0 вызов сразу возвращает nil и "timeout", если читать нечего.

### Чем UDP-сокет с setpeername отличается от обычного?

После setpeername сокет становится «подключённым»: send и receive работают без адреса, а датаграммы от других адресов отбрасываются. Для клиента с одним сервером это удобно, для сервера с многими клиентами нужен неподключённый сокет и sendto/receivefrom.
