Об IRC. Доступно и со вкусом.

UDP Сокеты. Сбор статистики с серверов Quake3 и Counter-Strike

В этой статье попытаюсь объяснить на примерах работу с UDP сокетами.
За примеры возьмем сбор статистики с игровых серверов Quake3 и Counter-Strike.
Немного теории (Подробное описание содержится в официальной спецификации UDP (RFC 768)).

UDP (User Datagram Protocol) - протокол без установки соединения, ориентированный на передачу датаграмм (чтобы сильно не углубляться в теорию: датаграмма - пакет данных, который мы будем отсылать/принимать серверу/от сервера).
Отослать такую датаграмму средствами mIRC можно командой sockudp (за один раз отсылается только одна датаграмма)

Т.к. соединение не устанавливается, то для работы с UDP сокетами не понадобятся обработчики on SOCKLISTEN on SOCKOPEN и on SOCKCLOSE
После отправки данных командой sockudp в буфер считываются принятые данные, которые можно обработать через on UDPREAD
Во время отправки срабатывает обработчик on SOCKWRITE, через который можно отловить ошибки возникшие при выполнении sockudp

Ну, а теперь практика.

Рассмотрим пример получения и вывода статистики игрового сервера Quake3

Общий принцип получения тактой информации очень прост:
1. Посылаем команду, на которую сервер должен выдать свои настройки, текущее/максимальное кол-во игроков, название карты и тд. и тп.
2. Посылаем следующую команду для получения информации о самих игроках (фраги, пинг, ник и тп.)

Нужную команду и формат данных можно отследить снифером.

Алиасы потребующиеся для работы:

alias q3s return 83.102.231.34
;ip адрес сервера
alias q3sp return 27960
;порт q3 сервера

alias q3getinfo sockudp -kn q3getinfo_sock $q3s $q3sp яяяяgetinfo
;алиас отсылающий серверу команду для получения информации о нем
;флаги sockudp
; -k оставляет сокет открытым, чтобы мы могли получить все данные
; -n добавляет символы конца строки $crlf (если их нет) к передаваемой строке
; (некоторые серверы считывают строку от начала до этих символов)
;q3getinfo_sock - имя сокета для обработки принятых от него данных

alias q3getstatus {
if ($sock(q3getstatus_sock)) sockclose q3getstatus_sock
;если сокет открыт закрываем его
sockudp -kn q3getstatus_sock $q3s $q3sp яяяяgetstatus
}
;алиас отсылающий серверу команду для получения информации о игроках

alias q3getoptionval {
if ($regex(q3,$2-,$+(\\,[ [ $1 ] ],\\,$chr(40),.*?,$chr(41),\\))) return $regml(q3,1)
}
;алиас возвращающий значение опции по названию \название_опции\значение\

alias q3colors {
if ($1) {
;если первый параметр не равен null, то продолжаем
var %i, %buff = $remove($1-,")
;объявляем переменные, в %buff заносим весь текст переданный алиасу и удаляем из него символы "
if ($regex(q3,%buff,/(\^x[a-f\d]+|\^[a-z])/ig)) {
;проверяем нашло ли текст в переменной %buff, который соответствует регулярному выражению
;текст может быть выглядеть так: ^XAAbFAC или ^h
;флаг i означает, что ищем текст независимо от регистра
;флаг g - поиск по всей строке
%i = 1
while ($regml(q3,%i)) {
;цикл с изменяющимся, за счет увеличения %i, условием
;$regml(q3,%i) - возвращает %i-й текст найденный через $regex
%buff = $remove(%buff,$ifmatch)
;$ifmatch - возвращает значение условия находящегося в while или if
inc %i
}
}
%buff = $replace(%buff,^1,$chr(3) $+ 04,^2,$chr(3) $+ 09,^3,$chr(3) $+ 08,^4,$chr(3) $+ 12,^5,$chr(3) $+ 11,^6,$chr(3) $+ 13,^7,$chr(3) $+ 00,^8,$chr(3) $+ 01,^9,$chr(3) $+ 04,^0,$chr(3) $+ 01)
;заменяем цвета q3 на цвета мирка
return %buff $+ $chr(3)
;возвращаем обработанный текст в переменной %buff и заканчиваем его символом цвета в мирке (чтобы, текст далее идущий не раскрасило цветами)
}
}
;алиас заменяющий q3 цвета на цвета мирка

;После того как выполнится алиас q3getinfo в буфер приема должны поступить данные, которые можно обработать следующим образом

on *:udpread:q3getinfo_sock:{
sockread -f %q3
;считываем данные в переменную, флаг -f заставляет mIRC заполнить переменную любым текстом, какой находится в буфере

if ((Response isin %q3) && ($chr(92) !isin %q3)) q3getinfo

;после первого выполнения алиаса q3getinfo должны получить пакет следующего содержания: яяяяstatusResponse
;в нем не должно быть символа перечисления настроек (\dmflags\0\fraglimit\30 .....)
;если так оно и есть, то выполняем еще раз все тотже алиас q3getinfo для получения следующей порции инфы

elseif ($q3getoptionval(clients, %q3) >= 1) {

;после второго выполнения алиаса q3getinfo должны получить примерно следующее:
;\game\osp\punkbuster\0\pure\0\gametype\0\sv_maxclients\17\clients\11\mapname\ospdm9\hostname\name_server\protocol\68
;и тут мы проверяем если игроков больше 1, то будем запрашивать инфу о них, а затем выводить ее в нужном формате

set %q3p $q3getoptionval(clients, %q3)
;заносим в переменную %q3p количество игроков \clients\11\

q3getstatus

echo -ag Имя сервера: $q3colors($q3getoptionval(hostname, %q3)) Игроков: $+($q3getoptionval(clients, %q3),/,$q3getoptionval(sv_maxclients, %q3)) Карта: $q3getoptionval(mapname, %q3)
;выводим нужную информацию

set %q3temp 1
;эта переменная будет хранить порядковый номемер игрока при выводе

sockclose $sockname
;закрываем сокет.. дальнейшая обработка будет выполнятся над данными полученными с помощью алиаса q3getstatus
}
else {
;если игроков нет то просто выводим информацию о сервере
echo -ag Имя сервера: $q3colors($q3getoptionval(hostname, %q3)) Игроков: $+($q3getoptionval(clients, %q3),/,$q3getoptionval(sv_maxclients, %q3)) Карта: $q3getoptionval(mapname, %q3)
sockclose $sockname
unset %q3*
}
}


;Здесь обрабатываем инфу о игроках добытую с помощью алиаса q3getstatus

on *:udpread:q3getstatus_sock:{
sockread -f %q3
if (Response !isin %q3) && ($chr(92) !isin %q3) {
tokenize 32 %q3
;разбиваем переменную по пробелам, в которой содержится следующее: 4 31 "Hrensgory" , на токены $1 $2 $3 ...
;формат данных в ней: фраги_число пинг_число "ник игрока"

echo -ag Игрок №: %q3temp Фраги: $1 Ник: $q3colors($3-) Пинг: $2

if (%q3temp < %q3p) inc %q3temp
else {
echo -ag Конец вывода
sockclose $sockname
unset %q3*
}
}
}

Копируем все это дело в Remote (Alt+R) и набираем /q3getinfo
Получаем следующее:

Имя сервера: www.q3dm6.ru Corbina Q3A FFA Server!!! Игроков: 11/17 Карта: pro-q3tourney4
Игрок №: 1 Фраги: 0 Ник: MrAlex Пинг: 37
Игрок №: 2 Фраги: 6 Ник: CMD Пинг: 8
Игрок №: 3 Фраги: 4 Ник: 19 Пинг: 31
Игрок №: 4 Фраги: 4 Ник: KiWi Пинг: 41
Игрок №: 5 Фраги: 6 Ник: kenny Пинг: 4
Игрок №: 6 Фраги: 6 Ник: vasya Пинг: 24
Игрок №: 7 Фраги: 5 Ник: K1M0 Пинг: 37
Игрок №: 8 Фраги: 8 Ник: yup Пинг: 31
Игрок №: 9 Фраги: 14 Ник: Alex Пинг: 14
Конец вывода


Теперь рассмотрим такой же пример с сервером Counter-Strike
Практически тоже самое, но при считывании информации из буфера будем использовать не простую, а бинарную переменную (&var) т.к. данные, которые будет нам выдавать сервер CS, содержат "невидимые" символы (с кодом <= 32) (при занесении таких символов в обычную переменную мирк может их просто обрезать при выводе/работе). Код такого символа может обозначать кол-во фрагов игрока.

Алиасы потребующиеся для работы:

alias css return 62.192.233.6
;ip адрес сервера
alias cssp return 27015
;порт сервера

alias csgetinfo {
var %i = 1, %result
;объявляем переменные

var %buf = $regsubex(cs,$2-,^255 255 255 255 109,), %buf = $regsubex(cs,%buf,^255 255 255 255 68 \d+,)
;присваиваем переменной %buf входящий текст начиная со второго слова (первое будет параметром, по которому будем определять нужное поле)
;и удаляем из него текст попадающий под регулярные выражения

var %re = $iif($gettok($2-, 5, 32) == 109, /(\d+.*?)\s0[\s]?/g, /(\d+\s.*?\s[0]\s\d+\s\d+\s\d+\s\d+\s\d+\s\d+\s\d+\s\d+)/g)
;определяем регулярное выражение, по которому будем искать нужные данные.. если $gettok($2-, 5, 32) пятый параметр, в передаваемой строке начиная со второго слова, равен 109,
;то значит нам нужно искать поле в данных о сервере (т.к. они начинаются с заголовка 255 255 255 255 149), если же нет то значит ищем данные о игроке
;конструкция $iif : $iif(условие, A, B) вернет A если условие верно, B наоборот
;^ - начало строки \d - одна цифра \d+ - последовательность рядом стоящих цифр \s - пробел . - любой символ .* - любая последовательность символов ? - одно вхождение [] - перечисление доступных символов
;выражение пишется в / /, после второго слеша можно поставить какой-нибудь флаг (или сразу несколько), например, g - поиск по всему тексту или i - поиск без учета регистра
;подробнее о регулярных выражениях можно почитать на ru.php.net

if ($regex(cs,%buf,%re)) {
;если что-нибудь найдено по выражению находящемуся в переменной %re, то идем дальше
while ($regml(cs,%i)) {
;цикл с изменяющимся, за счет увеличения %i, условием
;$regml(q3,%i) - возвращает %i-й текст найденный через $regex
bset &tmp 1 $ifmatch
;заполняем бинарную переменную &tmp содержимым $ifmatch
;$ifmatch - возвращает значение условия находящегося в while или if
%result = $iif($gettok($2-, 5, 32) == 109, $bvar(&tmp, 1, $bvar(&tmp,0)).text, $bvar(&tmp, 1, $bvar(&tmp,0)))
;в зависимости от условия заполняем переменную %result
;$bvar(&tmp, 1, $bvar(&tmp,0)).text - вернет содержимое переменной текстом
;$bvar(&tmp, 1, $bvar(&tmp,0)) - вернет содержимое переменной ascii кодами символов текста, находящегося в &tmp
bunset &tmp
if (%i == $1) break
;если счетчик равен первому переданному параметру, то значит мы нашли то, что нужно
inc %i
}
}
if ($prop) {
;$prop - значение параметра передаваемого через точку, $csgetinfo().значение
;если он есть то выводим не весь %result, а только только ascii код символа порядковый номер которого равен $prop
%result = $asc($mid(%result, $prop, 1))
}
return %result
}
;алиас возвращающий значение поля/данные о игроке по номеру

alias csgetinfoplayers sockudp -kn csgetinfoplayers_sock $css $cssp яяяяUяяяя
;алиас отсылающий серверу команду для получения информации о игроках
;для версии cs 1.5 команда может быть яяяяplayers


alias csgetinfoserv sockudp -kn csgetinfo_sock $css $cssp яяяяT
;алиас отсылающий серверу команду для получения информации о нем
;для версии cs 1.5 команда может быть яяяяdetails


;После выполнения алиаса csgetinfoserv в буфер должны поступить данные о сервере
;информация в полученных данных распределена следующим образом:
;заголовок + cимвол "m" + адрес сервера / имя сервера / имя карты / директория / описание / текущее кол-во игроков (1символ) + максимальное кол-во игроков (1символ) + версия протокола (1символ) + тип сервера (1символ) + ос сервера (l - linux w - windows) (1символ) .. / ...
;все поля разделены нулевым символом
;заголовок состоит из 4х символов с кодом 255
;
;для упрощения изъятия нужного поля был написан алиас csgetinfo, синтаксис алиаса:
;$csgetinfo(3, %var) - вернет третье поле т.е. название карты
;$csgetinfo(6, %var).1 - вернет код первого символа 6-го поля т.е. текущее кол-во игроков
;$csgetinfo(6, %var).5 - вернет код пятого символа 6-го поля т.е. если сделать $chr($csgetinfo(6, %var).5) можно опр еделить ос сервера l или w
;
;итак, обрабатываем нифу полученную через csgetinfoserv

on *:udpread:csgetinfo_sock:{
sockread -fn &tmp
;считываем данные из буфера в бинарную переменную
;после этого она будет содержать ascii код каждого символа считанного из буфера
var %cstmp = $bvar(&tmp,1,$bvar(&tmp,0))
;присваиваем обычной переменной содержание бинарной переменной &tmp
;теперь переменная %cstmp содержит ascii код символа начиная с первого по последний. $bvar(&tmp,0) - возвращает длину переменной т.е. кол-во кодов
set %csnum $csgetinfo(6, %cstmp).1
;эта переменная будет хранить количиство игроков на сервере
echo -a Имя сервера: $csgetinfo(2, %cstmp) Карта: $csgetinfo(3, %cstmp) Игроков: $+(%csnum,/,$csgetinfo(6, %cstmp).2)
if (%csnum > 0) csgetinfoplayers
;если игроков > 0 отправляем команду алиасом csgetinfoplayers для получения данных о игроках
else unset %cs*

sockclose $sockname
}
;приступаем к обработке данных о игроках полученных с помощью алиаса csgetinfoplayers
on *:udpread:csgetinfoplayers_sock:{
sockread -fn &tmp
var %cstmp = $bvar(&tmp,1,$bvar(&tmp,0)), %i = 1, %cstmptext
while (%i <= %csnum) {

;алиас csgetinfo написан не только для изъятия полей данных сервера
;им еще можно "фильтровать" данные игроков (эти данных аслиа возвратит не как текст при извлечении инфы о сервере, а код каждого символа текста)
;после считывания данных командой sockread переменная &tmp будет содержать:
;255 255 255 255 68 4 1 82 111 109 97 0 23 0 0 0 128 241 143 69 2 67 66 85 72 84 89 67 0 4 0 0 0 235 31 135 66 ...
;здес формат следующий:
;заголовок + символ D : 255 255 255 255 68
;кол-во игроков : 4
;данные первого игрока 1 82 111 109 97 0 23 0 0 0 128 241 143 69
;данные второго игрока 2 67 66 85 72 84 89 67 0 4 0 0 0 235 31 135 66
; 2 - порядковый номер
; 67 66 85 72 84 89 67 - коды символов ника
; 0 - нулевой символ разделитель
; 4 0 0 0 - фраги
; 235 31 135 66 - время

bset &cstmpinfo 1 $csgetinfo(%i, %cstmp)
;заполняем бинарную переменную нашими кодами %i -го игрока

%cstmptext = $bvar(&cstmpinfo,1,$bvar(&cstmpinfo,0)).text
;.text возвращает текст из бинарной переменной
%cstmpascii = $bvar(&cstmpinfo,1,$bvar(&cstmpinfo,0))
echo -a Игрок №: $chr($csgetinfo(%i, %cstmp).1) Ник: $mid(%cstmptext, 2) Фраги: $gettok(%cstmpascii, $calc($findtok(%cstmpascii, 0, 1, 32) + 1), 32)
bunset &cstmpinfo
;очищаем память &cstmpinfo, если этого не сделать, то получится следующее:
;в переменной находится текст abcdefg мы заполнияем ее текстом меньшей длины 123 => получаем 123defg
;этого конечно можно избежать если запоминать длину вносимого текста и выводить по длине

inc %i
}
sockclose $sockname
unset %cs*
}

Прописываем все в Remote и выполняем /csgetinfoserv
Получаем:

Имя сервера: ATKNet Counter-Strike 1.6 Public Server Карта: de_nuke Игроков: 3/26
Игрок №: 1 Ник: Roma Фраги: 1
Игрок №: 2 Ник: ][AOC Фраги: 0
Игрок №: 3 Ник: ShiftCtrl Фраги: 1


На этом пока все, старался расписать все подробно, если что не понятно - можно спросить на форуме