Итак, поскольку необходимых нам функций, как я уже говорил, в Delphi
нет и не предвидится, работу придется начать с заголовочного файла. Нам
понадобятся следующие функции: AllocateAndGetUdpExTableFromStack, AllocateAndGetTcpExTableFromStack, CreateToolhelp32Snapshot, Process32First и Process32Next. Первые две из них реализованы в библиотеке iphlpapi.dll
и необходимы для получения из стека таблицы открытых TCP- и UDP-портов
соответственно. Какая из функций какую таблицу возвращает, нетрудно
догадаться исходя из их имени. Остальные три функции реализованы в
kernel32.dll и пригодятся нам для определения процесса, который открыл
порт.
Напомню, что в прошлый раз мы писали программу TCPView с запасом на
будущее, а в главном окне даже подготовили отдельную колонку для
отображения имени процесса. Сегодня с помощью нескольких волшебных
движений тазом мы ее заполним.
Если ты читал предыдущую статью (а если не читал – вставляй DVD в
дисковод и бери ее оттуда), то открывай свой заголовочный файл, который
уже должен быть создан и начинай добавлять в него описания функций. Как
и в прошлый раз, мы будем объявлять функции в виде переменных, чтобы
загружать их динамически.
Состояния TCP
Двинемся по порядку, а значит, начнем с рассмотрения функции:
AllocateAndGetTcpExTableFromStack:
AllocateAndGetTcpExTableFromStack: function (
pTCPTable: PMIB_TCPEXTABLE;
bOrder: BOOL;
heap: THandle;
zero: DWORD;
flags: DWORD
): DWORD; stdcall;
Здесь мы объявляем переменную AllocateAndGetTcpExTableFromStack,
по сути, представляющую собой функцию, которая принимает, если я
правильно посчитал, пять параметров. До пяти я вроде бы считать умею, а
если что-то не так, то простите старика-ветерана клавиатурного труда.
Итак, функция получает следующие параметры:
- Указатель типа PMIB_TCPEXTABLE, через который нам вернут массив состояния TCP-портов;
- Булево значение, определяющее, нужно ли сортировать таблицу.
-
Куча (heap), в которой нужно выделить память для хранения
результирующей таблицы. Вполне логично хранить результат в куче своего
процесса, указатель на которую можно получить с помощью функции GetProcessHeap.
-
Флаги, определяющие, как себя будет вести функция с кучей. В утилите
Руссиновича здесь зачем-то указывается двойка, и если запустить поиск
по инету, то все найденные примеры будут автоматом указывать на это же
число. Зачем? Видимо, код копируется без понимания того, что он делает.
Нам никакие «специфические поведения» кучи не нужны, поэтому смело
поставим сюда 0.
- Последний флаг определяет IP-адреса, для которых нужно получать таблицу. Здесь можно указать флаг AF_INET или AF_INET6
для IP-протокола шестой версии. Интернетчики опять же копируют код один
к одному и явно указывают число 2 (значение константы AF_INET). Обе
константы объявлены в заголовочном файле Winsock… Хотя нет, константа AF_INET6 есть только в заголовочном файле второй версии, ведь первый Winsock ничего не знал о IPv6.
Запусти поиск в рунете по названию функции
AllocateAndGetTcpExTableFromStack и в большинстве случаев ты узнаешь,
что функция не документирована. Кем не документирована? В MSDN есть
подробное описание, просто искать его нужно умеючи :). Свежий msdn
всегда можно найти по адресу msdn.microsoft.com.
Да, он обновляется с задержкой и уже после выхода ОС, и чтобы быть
впереди всей планеты, просто нужно купить подписку за немалое
количество портретов американских лидеров. В общем, к чему я клоню:
если новой функции нет в старой версии справки, то это не значит, что
описание отсутствует вовсе ;).
Кстати, если верить MSDN, эта функция устарела и больше не
поддерживается в новоиспеченной Windows Vista! Я Висту пока еще не
ставил и не проверял, но если это так, наш универсальный пример будет
как раз кстати. Если посмотреть в SDK для Висты, можно заметить
интересный факт: функция там объявлена, но только для совместимости.
Так что не пытайся вызвать ее напрямую, иначе тебя ждет крах программы.
Что будет в качестве замены – еще неизвестно, а Майкрософт пока молчит.
Состояния UDP
Таблицу состояний UDP-портов можно узнать с помощью функции AllocateAndGetUdpExTableFromStack, которую необходимо объявить следующим образом:
AllocateAndGetUdpExTableFromStack: function (
pUDPTable: PMIB_UDPEXTABLE;
bOrder: BOOL;
heap: THandle;
zero: DWORD;
flags: DWORD
): DWORD; stdcall;
Ее параметры идентичны параметрам функции работы с TCP-портами, за исключением первого, который имеет тип PMIB_UDPEXTABLE. Порты UDP не имеют соединений, поэтому их таблица состояний немного отличается.
Структуры данных
Теперь поговорим о структурах данных, через которые мы будем
получать результирующие таблицы. Начнем с TCP-портов. Функция принимает
в качестве первого параметра тип данных PMIB_TCPEXTABLE, а, на самом деле, это структура следующего вида:
PMIB_TCPEXTABLE = ^TMIB_TCPEXTABLE;
TMIB_TCPEXTABLE = packed record
dwNumEntries: DWORD;
Table: array[0..0] of TMIB_TCPEXROW;
end;
В ней содержатся всего два параметра: количество элементов в таблице
и массив элементов таблицы состояний портов. Каждый элемент массива -
это тоже структура типа TMIB_TCPEXROW, представляющая собой вот что:
PMIB_TCPEXROW = ^TMIB_TCPEXROW;
TMIB_TCPEXROW = packed record
dwState: DWORD;
dwLocalAddr: DWORD;
dwLocalPort: DWORD;
dwRemoteAddr: DWORD;
dwRemotePort: DWORD;
dwProcessID: DWORD;
end;
Если ты не пропустил прошлый номер, то должен знать, что функция GetTcpTable
возвращает примерно такую же структуру. Здесь также присутствует
локальный адрес, локальный порт, удаленный адрес и удаленный порт.
Самое последнее поле является новым и определяет идентификатор
процесса, который открыл порт.
Теперь посмотрим на структуру PMIB_UDPEXTABLE, которая передается в качестве первого параметра функции получения состояний UDP-портов:
PMIB_UDPEXTABLE = ^TMIB_UDPEXTABLE;
TMIB_UDPEXTABLE = packed record
dwNumEntries: DWORD;
Table: array[0..0] of TMIB_UDPEXROW;
end;
Тут снова нас ожидает количество элементов в таблице состояний и массив из структур типа TMIB_UDPEXROW. Эта структура выглядит так:
PMIB_UDPEXROW = ^TMIB_UDPEXROW;
TMIB_UDPEXROW = packed record
dwLocalAddr: DWORD;
dwLocalPort: DWORD;
dwProcessID: DWORD;
end;
В ней мы видим локальный адрес, локальный порт и идентификатор процесса. Информации об удаленной машине нет и быть не может.
Вспомогательные функции
Для реализации примера нам понадобятся еще три системные функции из оконного ядра kernel32.dll:
CreateToolhelp32Snapshot: function (dwFlags, th32ProcessID: DWORD): THandle; stdcall;
{$EXTERNALSYM CreateToolhelp32Snapshot}
Process32First: function (hSnapshot: THandle; var lppe: TProcessEntry32): BOOL; stdcall;
{$EXTERNALSYM Process32First}
Process32Next: function (hSnapshot: THandle; var lppe: TProcessEntry32): BOOL; stdcall;
{$EXTERNALSYM Process32Next}
Давай кратко пробежимся по этим функциям:
- CreateToolhelp32Snapshot - создает снимок указанного процесса;
- Process32First - возвращает первый процесс из снимка;
- Process32Next - возвращает следующий процесс из снимка.
За более подробной информацией по этим функциям обращайся к MSDN. У
них достаточно много возможностей и различных флагов, поэтому описать
их в одной статье будет сложно.
Загрузка функций
Мы объявили переменные, через которые будем обращаться к системным
функциям, но все они являются указателями и на данном этапе указывают в
никуда. Теперь в них необходимо записать соответствующие адреса. В
прошлый раз для этого мы создавали функцию LoadAPIHelpAPI. Давай расширим ее и добавим следующие строки:
@AllocateAndGetTcpExTableFromStack:=GetProcAddress(HIpHlpApi, 'AllocateAndGetTcpExTableFromStack');
@AllocateAndGetUdpExTableFromStack:=GetProcAddress(HIpHlpApi, 'AllocateAndGetUdpExTableFromStack');
@CreateToolhelp32Snapshot := GetProcAddress(GetModuleHandle('kernel32.dll'), 'CreateToolhelp32Snapshot');
@Process32First := GetProcAddress(GetModuleHandle('kernel32.dll'), 'Process32First');
@Process32Next := GetProcAddress(GetModuleHandle('kernel32.dll'), 'Process32Next');
Теперь, после загрузки, каждая переменная будет указывать на
соответствующую функцию в системе. Если какая-то функция не будет
найдена, то соответствующая переменная будет равна нулю. Эту
особенность мы будем использовать для того, чтобы определить,
поддерживает ли ОС новые функции или необходимо использовать
универсальный код, который мы рассматривали в прошлый раз.
Реализация
Вот мы и подошли к самому интересному - реализации универсального примера. В нашем старом коде по событию OnShow вызывалась функция GetConnections, в которой и происходило определение состояний портов. Улучшим пример, поставив условие вместо безусловного вызова:
procedure TForm1.FormShow(Sender: TObject);
begin
if @AllocateAndGetTcpExTableFromStack=nil then
GetConnections
else
GetExConnections;
end;
Если указатель AllocateAndGetTcpExTableFromStack равен нулю, значит, соответствующей функции нет в системе, и нужно вызывать GetConnections.
Если он не равен нулю, то функция найдена в системе и можно
использовать расширенные функции, которые мы рассмотрели сегодня.
Полный код содержится в функции GetExConnections, которую мы поместили в листинге 1.
Логика GetExConnections практически не изменилась по сравнению с ранее написанной GetConnections.
Мы точно так же получаем таблицу состояний и выводим ее содержимое,
просто пользуясь при этом другими API функциями. Единственное, что
заслуживает отдельного внимания - это вызов функции ProcessPidToName,
которая должна переводить идентификатор процесса в удобочитаемое имя.
Эту функцию с подробнейшими комментариями ты можешь увидеть в листинге
2.
Итог
Программа готова. На DVD ты найдешь полноценную программку, которая
определяет порты и делает это универсально. Если система поддерживает
расширенные функции, то она использует их и отображает имена процессов.
Если нет, то ничего страшного, никакой ошибки не произойдет - прога
просто воспользуется старыми функциями и отобразит все то же самое,
исключая имя процесса.
Как видишь, все гениальное - в простоте и умении искать нужные
функции. Можешь улучшить этот пример, чтобы он обновлял таблицу по
таймеру и подсвечивал записи, состояние портов которых изменилось, или
новые записи в таблице. Можно добавить возможность уничтожения
выделенного процесса, ведь соответствующий идентификатор мы научились
определять. Только не забывай, что при использовании старых функций
процесс не определен. Кстати, написанный пример получает только
состояние IP-портов старой версии, а реализацию состояния IPv6 легко
организовать простым изменением последнего флага при вызове функций AllocateAndGetTcpExTableFromStack и AllocateAndGetUdpExTableFromStack.
На этом спешу откланяться. До новых встреч!
Листинг 1
procedure TForm1.GetExConnections;
var
TCPExTable: PMIB_TCPEXTABLE;
UDPExTable: PMIB_UDPEXTABLE;
hSnapshot: THandle;
i:Integer;
local_name:array[0..255]of char;
ExeName:String;
begin
lwTCP.Items.BeginUpdate;
lwTCP.Items.Clear;
// Получаем снимок процессов
hSnapshot := CreateToolhelp32Snapshot($2, 0);
try
// Определяем таблицу состояний TCP-портов
if AllocateAndGetTcpExTableFromStack(@TCPExTable, False, GetProcessHeap, 2, 2) = NO_ERROR then
begin
for i := 0 to TCPExTable.dwNumEntries - 1 do
begin
with lwTCP.Items.Add do
begin
Caption:='TCP';
if (hSnapshot = INVALID_HANDLE_VALUE) then
// Если не удалось получить снимок, то имя процесса оставить пустым
SubItems.Add('')
else
begin
// Снимок процессов был получен удачно, поэтому переводим ID в человеческое имя
ExeName:=ProcessPidToName(hSnapshot, tcpExTable.Table[i].dwProcessId);
SubItems.Add(ExeName);
end;
SubItems.Add(inet_ntoa(TInAddr(TCPExTable^.Table[I].dwLocalAddr)));
SubItems.Add(IntToStr(TCPExTable^.Table[I].dwLocalPort));
SubItems.Add(inet_ntoa(TInAddr(TCPExTable^.Table[I].dwRemoteAddr)));
SubItems.Add(IntToStr(TCPExTable^.Table[I].dwRemotePort));
SubItems.Add(TCPState[TCPExTable^.Table[I].dwState]);
end;
end;
end;
// Определяем таблицу состояний UPX-портов
if AllocateAndGetUdpExTableFromStack(@UdpExTable, False, GetProcessHeap, 2, 2) = NO_ERROR then
begin
for i := 0 to UDPExTable.dwNumEntries - 1 do
begin
with lwTCP.Items.Add do
begin
Caption:='UDP';
if (hSnapshot = INVALID_HANDLE_VALUE) then
// Если не удалось получить снимок, то имя процесса оставить пустым
SubItems.Add('')
else
// Снимок процессов был получен удачно, поэтому переводим ID в человеческое имя
SubItems.Add(ProcessPidToName(hSnapshot, UDPExTable.Table[i].dwProcessId));
gethostname(local_name, 255);
SubItems.Add(inet_ntoa(TInAddr(UDPExTable^.Table[I].dwLocalAddr)));
SubItems.Add(IntToStr(UDPExTable^.Table[I].dwLocalPort));
end;
end;
end;
finally
lwTCP.Items.EndUpdate;
end;
end;
Листинг 2
function TForm1.ProcessPidToName(hProcess: THandle; ProcID: DWORD): String;
var
procEntry:TPROCESSENTRY32;
ProcessName:String;
begin
procEntry.dwSize := sizeof(procEntry);
ProcessName:='???';
// Получаем первый процесс из снимка, который мы сделали в функции GetExConnections
if (Process32First(hProcess, procEntry)) then
begin
// Запускаем цикл перебора всех процессов
repeat
// Если текущий процесс равен искомому, то возвращаем его имя
if (procEntry.th32ProcessID = ProcId) then
begin
ProcessName:=procEntry.szExeFile;
Result:=ProcessName;
exit;
end;
// Цикл выполнять, пока функция Process32Next не вернет nil, то есть пока мы не достигнем конца снимка
until (not Process32Next(hProcess, &procEntry));
end;
Result:=ProcessName;
end;
|