Алгоритм приема/передачи данных
В разд. 4.10.2 мы уже рассмотрели пример, в котором сервер асинхронно ожидает соединения с помощью функции select, и как только происходило подключение, создавался новый поток, который обменивался данными с клиентом. Самое узкое место в этом примере — второй поток, который работал только с одним клиентом.
Я уже говорил о том, что с помощью асинхронной работы сетевых функций можно легко реализовать возможность работы сразу с несколькими клиентами. Да и отдельный поток для обмена сообщениями в данном случае является излишним. В листинге 6.1 приведен пример, в котором сервер ожидает соединения и работает с клиентом в одной функции, но может обслуживать сразу несколько клиентов:
Листинг 6.1. Алгоритм асинхронной работы с клиентом |
struct sockaddr_in localaddr, clientaddr; int iSize;
sServerListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (sServerListen == SOCKET_ERROR) { MessageBox(0, "Can't load WinSock", "Error", 0); return 0; }
ULONG ulBlock; ulBlock = 1; if (ioctlsocket(sServerListen, FIONBIO, ulBlock) == SOCKET_ERROR) { return 0; }
localaddr.sin_addr.s_addr = htonl(INADDR_ANY); localaddr.sin_family = AF_INET; localaddr.sin_port = htons(5050);
if (bind(sServerListen, (struct sockaddr *)localaddr, sizeof(localaddr)) == SOCKET_ERROR) { MessageBox(0, "Can't bind", "Error", 0); return 1; }
MessageBox(0, "Bind OK", "Error", 0);
listen(sServerListen, 4);
MessageBox(0, "Listen OK", "Error", 0);
FD_SET ReadSet; int ReadySock;
while (1) { FD_ZERO(ReadSet); FD_SET(sServerListen, ReadSet);
for (int i=0; iTotalSocket; i++) if (ClientSockets[i] != INVALID_SOCKET) FD_SET(ClientSockets[i], ReadSet);
if ((ReadySock = select(0, ReadSet, NULL, NULL, NULL)) == SOCKET_ERROR) { MessageBox(0, "Select filed", "Error", 0); }
//We have new connection (Есть новые подключения) if (FD_ISSET(sServerListen, ReadSet)) { iSize = sizeof(clientaddr); ClientSockets[TotalSocket] = accept(sServerListen, (struct sockaddr *)clientaddr,iSize); if (ClientSockets[TotalSocket] == INVALID_SOCKET) { MessageBox(0, "Accept filed", "Error", 0); break; } TotalSocket++; } //We have data from client (Есть данные от клиента) for (int i=0; iTotalSocket; i++) { if (ClientSockets[i] == INVALID_SOCKET) continue; if (FD_ISSET(ClientSockets[i], ReadSet)) { char szRecvBuff[1024], szSendBuff[1024];
int ret = recv(ClientSockets[i], szRecvBuff, 1024, 0); if (ret == 0) { closesocket(ClientSockets[i]); ClientSockets[i]=INVALID_SOCKET; break; } else if (ret == SOCKET_ERROR) { MessageBox(0, "Recive data filed", "Error", 0); break; } szRecvBuff[ret] = '\0';
strcpy(szSendBuff, "Command get OK");
ret = send(ClientSockets[i], szSendBuff, sizeof(szSendBuff), 0); if (ret == SOCKET_ERROR) { break; } } }
} closesocket(sServerListen); return 0; }
Рассмотрим, как работает этот пример. Секрет заключается в том, что объявлено две переменные:
sServerListen — переменная типа socket, которая будет использоваться для прослушивания порта и ожидания соединения со стороны клиента;
ClientSockets — массив из 50 элементов типа ClientSockets. Этот массив будет использоваться для работы с клиентами, и именно 50 клиентов смогут обслуживаться одновременно. В данном примере каждому соединению будет выделяться очередной сокет, поэтому после пятидесятого произойдет ошибка. В реальной программе этот массив необходимо сделать динамическим, чтобы при отключении клиента можно было удалять из массива соответствующий сокет.
После этого создается сокет sServerListen для прослушивания сервера, переводится в асинхронный режим, связывается функцией bind с локальным адресом и запускается прослушивание. В этом участке кода никаких изменений не произошло.
Самое любопытное происходит в бесконечном цикле, который раньше просто ожидал соединения. Теперь в набор добавляется не только сокет сервера, но и активные клиентские сокеты. После этого функция select ожидает, когда какой-либо из этих сокетов будет готов к чтению данных.
Дальше — еще интереснее. Первым делом проверяется серверный сокет. Если он готов к чтению, то присоединился клиент. Соединение принимается с помощью функции accept, а результат (сокет для работы с клиентом) сохраняется в последнем (доступном) элементе массива ClientSockets. После этого функция select будет ожидать событий и от этого клиента.
На следующем этапе проверяются все сокеты из массива на готовность чтения данных с их стороны. Если какой-нибудь клиент готов, то читаются данные и отправляется ответ. Если при чтении данные не получены, и функция recv вернула нулевое значение, то клиент отключился от сервера.
Этот алгоритм достаточно быстрый и универсальный. А главное, позволяет с помощью одного цикла обрабатывать серверный и клиентские сокеты. Это очень удобно и эффективно. Если в вашей программе нужно обмениваться небольшими сообщениями, то программу можно использовать уже в таком виде. Если будет происходить обмен данными большого объема, то необходимо добавить возможность чтения и отправки всех пришедших данных.
Не забудьте только заменить массив клиентских сокетов на динамический. Если вы не хотите использовать динамические массивы, то можно поступить проще — перед каждым заполнением структуры FD_SET упорядочивать в ней элементы, чтобы убрать сокеты, равные INVALID_SOCKET. После этого необходимо установить переменную TotalSocket так, чтобы она указывала на следующий после последнего реально существующего элемента массива.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\AdvancedTCPServer. |
ARP-протокол
Я уже говорили о том, что перед обращением к компьютеру по локальной сети необходимо узнать его МАС-адрес. Для этого существует ARP-протокол, который по IP-адресу ищет МАС-адрес. Происходит это автоматически и незаметно для рядового пользователя, но иногда появляется возможность ручного управления таблицей ARP. В ОС Windows для этих целей есть утилита с одноименным названием аrр, но она консольная, и работать с ней не очень удобно. Сейчас я покажу простую графическую утилиту, на примере которой и объясню функции работы с данным протоколом.
Создайте новое MFC-приложение на основе диалога. Внешний вид главного диалогового окна представлен на 6.9. Здесь используются две строки ввода для указания IP- и МАС-адреса. По нажатии кнопки Add будет добавляться новая ARP-запись, в которой по указанному IP-адресу будет определяться МАС-адрес. По нажатии кнопки Update в списке List Box будет отображаться таблица ARP. Кнопка Delete будет использоваться для удаления записи из таблицы.
В обработчик события кнопки Update нужно написать код из листинга 6.10.
Листинг 6.10. Отображение таблицы ARP |
DWORD dwActualSize = 0; GetIpNetTable(pIpArpTab, dwActualSize, true);
pIpArpTab = (PMIB_IPNETTABLE) malloc(dwActualSize); if (GetIpNetTable(pIpArpTab, dwActualSize, true) != NO_ERROR) { if (pIpArpTab) free (pIpArpTab); return; }
DWORD i, dwCurrIndex; char sPhysAddr[256], sType[256], sAddr[256]; PMIB_IPADDRTABLE pIpAddrTable = NULL; char Str[255];
dwActualSize = 0; GetIpAddrTable(pIpAddrTable, dwActualSize, true); pIpAddrTable = (PMIB_IPADDRTABLE) malloc(dwActualSize); GetIpAddrTable(pIpAddrTable, dwActualSize, true);
dwCurrIndex = -100;
for (i = 0; i pIpArpTab-dwNumEntries; ++i) { if (pIpArpTab-table[i].dwIndex != dwCurrIndex) { dwCurrIndex = pIpArpTab-table[i].dwIndex;
struct in_addr in_ad; sAddr[0] = '\n'; for (int i = 0; i pIpAddrTable-dwNumEntries; i++) { if (dwCurrIndex != pIpAddrTable-table[i].dwIndex) continue;
in_ad.s_addr = pIpAddrTable-table[i].dwAddr; strcpy(sAddr, inet_ntoa(in_ad)); }
sprintf(Str,"Interface: %s on Interface 0x%X", sAddr, dwCurrIndex); lbMessages.AddString(Str); lbMessages.AddString(" Internet Address | Physical Address | Type"); }
AddrToStr(pIpArpTab-table[i].bPhysAddr, pIpArpTab-table[i].dwPhysAddrLen, sPhysAddr);
switch (pIpArpTab-table[i].dwType) { case 1: strcpy(sType,"Other"); break; case 2: strcpy(sType,"Invalidated"); break; case 3: strcpy(sType,"Dynamic"); break; case 4: strcpy(sType,"Static"); break; default: strcpy(sType,""); }
struct in_addr in_ad; in_ad.s_addr = pIpArpTab-table[i].dwAddr; sprintf(Str, " %-16s | %-17s | %-11s", inet_ntoa(in_ad), sPhysAddr, sType); lbMessages.AddString(Str); }
free(pIpArpTab); }
Посмотрите внимательно на код. Обратите внимание, что в данном случае не загружается библиотека WinSock. К проекту подключается заголовочный файл winsock.h, потому что используются типы данных, которые объявлены в нем. Но обращение к ним происходит только при компиляции. В данном случае не применяются библиотечные функции, необходимые на этапе выполнения.
Если вы добавите в программу код, который будет содержать хотя бы одну функцию из библиотеки WinSock (например, определение имени хоста по IP-адресу), то прежде чем ее использовать, необходимо будет загрузить библиотеку.
Первым делом из системы получена таблица соответствия IP-адресов их физическим МАС-адресам. Это и есть ничто иное, как ARP-таблица. Функция для получения этой таблицы имеет следующий вид:
DWORD GetIpNetTable( PMIB_IPNETTABLE pIpNetTable, PULONG pdwSize, BOOL border );
ARP-таблица описывается следующими параметрами:
указатель на структуру типа MIB_IPNETTABLE, в которой будет размещена таблица;
размер структуры. Если он нулевой, то функция вернет объем памяти, требуемый для таблицы;
сортировка — если параметр равен true, то таблица будет упорядоченной.
Структура MIB_IPNETTABLE, которая задается в качестве первого параметра, имеет следующий вид:
typedef struct _MIB_IPNETTABLE { DWORD dwNumEntries; MIB_IPNETROW table[ANY_SIZE]; } MIB_IPNETTABLE, *PMIB_IPNETTABLE;
Первый параметр указывает на количество записей в таблице, а второй — это структура, содержащая данные таблицы:
typedef struct _MIB_IPNETROW { DWORD dwIndex; DWORD dwPhysAddrLen; BYTE bPhysAddr[MAXLEN_PHYSADDR]; DWORD dwAddr; DWORD dwType; } MIB_IPNETROW, *PMIB_IPNETROW;
Структура с данными таблицы описывается набором параметров:
dwIndex — индекс адаптера;
dwPhysAddrLen — длина физического адреса;
bPhysAddr — физический адрес;
dwAddr — IP-адрес;
dwType — тип записи. В свою очередь может принимать такие значения:
4 — статический. Если запись добавлена вручную с помощью функций, которые будут рассмотрены позже;
3 — динамический. Записи с адресами, которые получены автоматически с помощью протокола ARP (действительны в течение определенного времени, а потом автоматически уничтожаются);
2 — неправильный. Записи с ошибками;
1 — другой.
В принципе, ARP-таблица уже создана. Если в компьютере установлена только одна сетевая карта, то этого достаточно, потому что все записи будут относиться к ней. Если установлена хотя бы две сетевые карты, то часть записей будет принадлежать одному интерфейсу, а остальные — второму.
Чтобы картина была полной, необходимо показать, какие записи к какому интерфейсу относятся. Для этого есть индекс интерфейса в структуре MIB_IPNETROW, но этот индекс абсолютно ничего не скажет конечному пользователю. Но в сочетании с IP-адресом адаптера это станет более информативно.
А вот IP-адреса адаптера у нас пока нет. Чтобы его узнать, нужно получить таблицу соответствия IP-адресов адаптерам. Это можно сделать с помощью функции GetIpAddrTable. Функция похожа на GetIpNetTable:
DWORD GetIpAddrTable ( PMIB_IPADDRTABLE pIpAddrTable, PULONG pdwSize, BOOL bOrder );
И так же имеет три параметра: указатель на структуру типа MIB_IPADDRTABLE (pIpAddrTable), размер структуры (pdwSize) и флаг сортировки (bOrder).
Первый параметр — это структура следующего вида:
typedef struct _MIB_IPADDRTABLE { DWORD dwNumEntries; MIB_IPADDRROW table[ANY_SIZE]; } MIB_IPADDRTABLE, *PMIB_IPADDRTABLE;
У этой структуры два параметра:
dwNumEntries — количество структур, указанных во втором параметре;
table — массив структур типа MIB_IPADDRROW.
Структура MIB_IPADDRROW описывается следующим образом:
typedef struct _MIB_IPADDRROW { DWORD dwAddr; DWORDIF_INDEX dwIndex; DWORD dwMask; DWORD dwBCastAddr; DWORD dwReasmSize; unsigned short unused1; unsigned short wType; } MIB_IPADDRROW, *PMIB_IPADDRROW;
В этой структуре доступны следующие параметры:
dwAddr — IP-адрес;
dwIndex — индекс адаптера, с которым связан IP-адрес;
dwMask — маска для IP-адреса;
dwBCastAddr — широковещательный адрес. Чаще всего это IP-адрес, в котором нулевое значение номера узла. Например, если у вас IP-адрес 192.168.4.7, то широковещательный адрес будет 192.168.4.0;
dwReasmSize — максимальный размер получаемых пакетов;
unused1 — зарезервировано;
wType — тип адреса, может принимать следующие значения:
MIB_IPADDR_PRIMARY — основной IP-адрес;
MIB_IPADDR_DYNAMIC — динамический адрес;
MIB_IPADDR_DISCONNECTED — адрес на отключенном интерфейсе, например, отсутствует сетевой кабель;
MIB_IPADDR_DELETED — адрес в процессе удаления;
MIB_IPADDR_TRANSIENT — временный адрес.
Когда получены необходимые данные, запускается цикл перебора всех строк в таблице ARP. Но прежде чем выводить на экран информацию о строке, надо проверить, к какому интерфейсу она относится.
При получении данных был выбран режим сортировки записей, поэтому можно надеяться, что вначале идут записи одного интерфейса, а потом другого. Поэтому перед циклом в переменную dwCurrIndex занесено значение —100. Интерфейса с таким номером точно не будет. На первом же шаге цикла будет видно, что запись из ARP-таблицы не относится к интерфейсу с номером —100, и необходимо вывести на экран IP-адрес сетевой карты, к которой относится эта запись. Для этого по параметру dwIndex ищется запись в таблице соответствия IP-адресов номерам интерфейса. Если запись найдена (а она должна быть найдена), то выводится заголовок таблицы, который будет выглядеть примерно так:
Interface: 192.168.1.100 on Interface 0x10000003
Internet Address | Physical Address | Type
Затем выводится информация из ARP-таблицы, пока не встретится запись, относящаяся к другому интерфейсу. Тогда снова выводится заголовок и т.д.
На 6.9 вы можете увидеть пример работы программы на моем компьютере. Обращаю ваше внимание, что у вас может и не быть ARP-записей, потому что они существуют только при работе в локальной сети. При выходе в Интернет по модему протокол ARP не используется.
6.9. Результат работы программы ARPApplication
Если вы пока не знаете, как применить программу, то у меня уже были случаи, когда она оказалась незаменима. Допустим, что нужно узнать МАС-адрес компьютера в локальной сети, который находится от вас очень далеко. Можно пойти к этому компьютеру и посмотреть адрес с помощью утилиты ipconfig, а можно произвести следующие действия:
Выполнить программу Ping для проверки связи с удаленным компьютером. В этот момент отсылаются эхо-пакеты, которым также нужно знать МАС-адрес, и для этого задействуется ARP-протокол.
Запустить программу просмотра ARP-таблицы и там посмотреть МАС-адрес нужного компьютера.
Теперь посмотрите, как можно добавлять новые записи в таблицу ARP. Напоминаю, что все записи, добавленные программно, становятся статичными и не уничтожаются автоматически на протяжении всей работы ОС. По нажатии кнопки Add будет выполняться код из листинга 6.11.
Листинг 6.11. Добавление новой записи в таблицу ARP |
edIPAddress.GetWindowText(sInetAddr, 255); edMacAddress.GetWindowText(sMacAddr, 255); edInterface.GetWindowText(sInterface, 255);
if (sInetAddr == NULL || sMacAddr == NULL || sInterface == NULL) { AfxMessageBox("Fill IP address, MAC address and Interface"); return; }
DWORD dwInetAddr; dwInetAddr = inet_addr(sInetAddr); if (dwInetAddr == INADDR_NONE) { AfxMessageBox("Bad IP Address"); return; }
StrToMACAddr(sMacAddr, sPhysAddr);
MIB_IPNETROW arpRow; sscanf(sInterface, "%X",(arpRow.dwIndex));
arpRow.dwPhysAddrLen = 6; memcpy(arpRow.bPhysAddr, sPhysAddr, 6); arpRow.dwAddr = dwInetAddr; arpRow.dwType = MIB_IPNET_TYPE_STATIC;
if (SetIpNetEntry(arpRow) != NO_ERROR) AfxMessageBox("Couldn't add ARP record"); }
Самое главное здесь — это функция SetIpNetEntry , которая добавляет новую ARP-запись и выглядит следующим образом:
DWORD SetIpNetEntry ( PMIB_IPNETROW pArpEntry );
В качестве единственного параметра функции указывается структура типа MIB_IPNETROW, которую мы уже использовали при получении данных ARP-таблицы. В этой структуре необходимо указать четыре параметра: интерфейс (dwIndex), МАС-адрес (bPhysAddr) и IP-адрес (dwInetAddr), запись которого надо добавить, и тип записи (в поле dwType значение MIB_IPNET_TYPE_STATIC). Остальные поля в этой функции не используются, и их заполнять не надо.
Теперь посмотрите на функцию удаления. По нажатии кнопки Delete выполняется код из листинга 6.12.
Листинг 6.12. Удаление записи из ARP-таблицы |
edIPAddress.GetWindowText(sInetAddr, 255); edInterface.GetWindowText(sInterface, 255);
if (sInetAddr == NULL || sInterface == NULL) { AfxMessageBox("Fill IP address and Interface"); return; }
DWORD dwInetAddr; dwInetAddr = inet_addr(sInetAddr); if (dwInetAddr == INADDR_NONE) { printf("IpArp: Bad Argument %s\n", sInetAddr); return; }
MIB_IPNETROW arpEntry;
sscanf(sInterface, "%X",(arpEntry.dwIndex)); arpEntry.dwAddr = dwInetAddr;
if (DeleteIpNetEntry(arpEntry) != NO_ERROR) AfxMessageBox("Couldn't delete ARP record"); }
Для удаления записи используется функция DeleteIpNetEntry, которая выглядит следующим образом:
DWORD DeleteIpNetEntry( PMIB_IPNETROW pArpEntry );
У нее один параметр в виде структуры PMIB_IPNETROW, в которой нужно указывать только интерфейс и IP-адрес, запись которого надо удалить.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\ARPApplication. |
DHCP-сервер
Если в вашей сети используется DHCP-сервер, то нельзя использовать удаление и добавление IP-адреса, которое мы рассматривали в главе 5. В этом случае адрес выдается и освобождается DHCP-сервером, и это нельзя делать вручную, иначе могут возникнуть проблемы и конфликты с другими компьютерами.
При использовании DHCP-адреса не удаляются из системы, а освобождаются. В этом случае сервер сможет отдать высвобожденный адрес другому компьютеру, если он жестко не привязан к определенному сетевому интерфейсу. Для освобождения используется функция IpReleaseAddress, которой надо передать нужный адаптер. Для получения адреса используется функция IpRenewAddress, которой также следует указать адаптер, нуждающийся в новом адресе.
6.6. Диалоговое окно будущей программы RenewIPAddress
Рассмотрю использование функций на примере. Для этого создайте новое MFC-приложение. Главное окно вы можете увидеть на 6.6. Для определения адаптера, нуждающегося в удалении, нужно знать его индекс. Для этого внизу окна расположен элемент управления List Box, в котором будет отображаться список установленных интерфейсов. Вывод списка адаптеров будет происходить по нажатии кнопки List Adapters. Код, который должен здесь выполняться, идентичен коду из листинга 5.2, где также выводилась информация об установленных адаптерах.
По нажатии кнопки Release освобождается IP-адрес. Код, который должен выполняться, приведен в листинге 6.5.
Листинг 6.5. Освобождение IP-адреса |
DWORD InterfaceInfoSize = 0; PIP_INTERFACE_INFO pInterfaceInfo;
if (GetInterfaceInfo(NULL, InterfaceInfoSize) != ERROR_INSUFFICIENT_BUFFER) { AfxMessageBox("Error sizing buffer"); return; }
if ((pInterfaceInfo = (PIP_INTERFACE_INFO) GlobalAlloc(GPTR, InterfaceInfoSize)) == NULL) { AfxMessageBox("Can't allocate memory"); return; }
if (GetInterfaceInfo(pInterfaceInfo, InterfaceInfoSize) != 0) { AfxMessageBox("GetInterfaceInfo failed"); return; }
for (int i = 0; i pInterfaceInfo-NumAdapters; i++) if (iIndex == pInterfaceInfo-Adapter[i].Index) { if (IpReleaseAddress(pInterfaceInfo-Adapter[i]) != 0) { AfxMessageBox("IpReleaseAddress failed"); return; } break; } }
В поле ввода на главном окне пользователь указывает индекс адаптера. Теперь надо только пролистать все интерфейсы. И если какой-либо из них принадлежит адаптеру, то вызвать для него функцию IpReleaseAddress.
Для получения списка интерфейсов используется функция GetInterfaceInfo, которая работает так же, как и уже знакомая вам GetAdaptersInfo. При первом вызове определяется необходимый размер памяти для хранения всей информации, после чего выделяется эта память, и функция вызывается снова.
Затем запускается цикл перебора всех полученных интерфейсов. Если интерфейс указанного адаптера найден, то освобождается адрес.
Получение адреса происходит подобным образом. В листинге 6.6 показан код, который должен выполняться по нажатии кнопки Renew.
Листинг 6.6. Запрос нового IP-адреса |
DWORD InterfaceInfoSize = 0; PIP_INTERFACE_INFO pInterfaceInfo;
if (GetInterfaceInfo(NULL, InterfaceInfoSize) != ERROR_INSUFFICIENT_BUFFER) { AfxMessageBox("Error sizing buffer"); return; }
if ((pInterfaceInfo = (PIP_INTERFACE_INFO) GlobalAlloc(GPTR, InterfaceInfoSize)) == NULL) { AfxMessageBox("Can't allocate memory"); return; }
if (GetInterfaceInfo(pInterfaceInfo, InterfaceInfoSize) != 0) { AfxMessageBox("GetInterfaceInfo failed"); return; }
for (int i=0; ipInterfaceInfo-NumAdapters; i++) if (iIndex == pInterfaceInfo-Adapter[i].Index) { if (IpRenewAddress(pInterfaceInfo-Adapter[i]) != 0) { AfxMessageBox("IpRenewAddress failed"); return; } break; } }
Код получения нового адреса идентичен освобождению (см. листинг 6.5). Разница только в том, что в данном случае вызывается функция IpRenewAddress.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\RenewIPAddress. |
Определение пути пакета
Как можно определить путь пакета, по которому он идет от нас до адресата? Если принять во внимание предназначение ICMP-сообщений, то проблема решается просто. Каждый пакет имеет поле TTL (Time To Leave, время жизни). Каждый маршрутизатор уменьшает значение поля на единицу, и когда оно становится равным нулю, пакет считается заблудившимся, и маршрутизатор возвращает ICMP-сообщение об ошибке. Использование этого поля еще упрощает проблему.
Надо направить пакет на сервер с временем жизни, равным 1. Первый же маршрутизатор уменьшит значение на 1 и увидит 0. Это заставит его вернуть ICMP-сообщение об ошибке, по которому можно узнать первый узел, через который проходит пакет. Затем отсылается пакет с временем жизни, равным 2, и определяется второй маршрутизатор (первый пропустит пакет, а второй вернет ICMP-сообщение). Таким образом можно отсылать множество пакетов, пока не достигнем адресата.
Стало быть, есть возможность узнать не только маршрут, но и время отклика каждого маршрутизатора, что позволяет определить слабое звено на пути следования пакета. Связь с каждым из устройств на пути пакета может изменяться в зависимости от нагрузки, поэтому желательно сделать несколько попыток соединения, чтобы определить среднее время отклика.
Конечно же, первый пакет может пойти одним маршрутом, а второй — другим, но чаще всего все пакеты движутся по одному и тому же маршруту.
Сейчас я покажу простейший пример определения пути следования маршрута. Но только ICMP-пакеты я буду посылать не через RAW-сокеты, а через библиотеку icmp.dll. В этой библиотеке есть все необходимые функции для создания сокета и отправки пакета. Нужно только указать адрес, содержимое пакета и его параметры, а все остальное сделают за вас. Таким образом, вы научитесь пользоваться библиотекой icmp.dll и сможете выяснить путь прохождения пакета.
Создайте новое MFC-приложение TraceRote. На главном окне вам понадобится одна строка ввода для указания адреса компьютера, связь с которым необходимо проверить, один компонент типа List Box для отображения информации и кнопка (например, Trace), по которой будет пинговаться удаленный компьютер. По нажатии кнопки будет выполняться код из листинга 6.9.
Листинг 6.9. Определение пути следования пакета |
hIcmp = LoadLibrary("ICMP.DLL"); if (hIcmp == NULL) { AfxMessageBox("Can't load ICMP DLL"); return; }
pIcmpCreateFile = (lpIcmpCreateFile) GetProcAddress(hIcmp, "IcmpCreateFile"); pIcmpSendEcho = (lpIcmpSendEcho) GetProcAddress(hIcmp, "IcmpSendEcho"); pIcmpCloseHandle = (lpIcmpCloseHandle) GetProcAddress(hIcmp, "IcmpCloseHandle");
in_addr Address; if (pIcmpCreateFile == NULL) { AfxMessageBox("ICMP library error"); return; }
char chHostName[255]; edHostName.GetWindowText(chHostName, 255); LPHOSTENT hp = gethostbyname(chHostName); if (hp== NULL) { AfxMessageBox("Host not found"); return; } unsigned long addr; memcpy(addr, hp-h_addr, hp-h_length);
BOOL bReachedHost = FALSE; for (UCHAR i=1; i=50 !bReachedHost; i++) { Address.S_un.S_addr = 0;
int iPacketSize=32; int iRTT;
HANDLE hIP = pIcmpCreateFile(); if (hIP == INVALID_HANDLE_VALUE) { AfxMessageBox("Could not get a valid ICMP handle"); return; }
unsigned char* pBuf = new unsigned char[iPacketSize]; FillMemory(pBuf, iPacketSize, 80);
int iReplySize = sizeof(ICMP_ECHO_REPLY) + iPacketSize; unsigned char* pReplyBuf = new unsigned char[iReplySize]; ICMP_ECHO_REPLY* pEchoReply = (ICMP_ECHO_REPLY*) pReplyBuf;
IP_OPTION_INFORMATION ipOptionInfo; ZeroMemory(ipOptionInfo, sizeof(IP_OPTION_INFORMATION)); ipOptionInfo.Ttl = i;
DWORD nRecvPackets = pIcmpSendEcho(hIP, addr, pBuf, iPacketSize, ipOptionInfo, pReplyBuf, iReplySize, 30000);
if (nRecvPackets != 1) { AfxMessageBox("Can't ping host"); return; } Address.S_un.S_addr = pEchoReply-Address; iRTT = pEchoReply-RoundTripTime;
pIcmpCloseHandle(hIP);
delete [] pReplyBuf; delete [] pBuf;
char lpszText[255];
hostent* phostent = NULL; phostent = gethostbyaddr((char *)Address.S_un.S_addr, 4, PF_INET);
if (phostent) sprintf(lpszText, "%d: %d ms [%s] (%d.%d.%d.%d)", i, iRTT, phostent-h_name, Address.S_un.S_un_b.s_b1, Address.S_un.S_un_b.s_b2, Address.S_un.S_un_b.s_b3, Address.S_un.S_un_b.s_b4); else sprintf(lpszText, "%d - %d ms (%d.%d.%d.%d)", i, iRTT, Address.S_un.S_un_b.s_b1, Address.S_un.S_un_b.s_b2, Address.S_un.S_un_b.s_b3, Address.S_un.S_un_b.s_b4);
lbMessages.AddString(lpszText);
if (addr == Address.S_un.S_addr) bReachedHost = TRUE; }
if (hIcmp) { FreeLibrary(hIcmp); hIcmp = NULL; }
WSACleanup(); }
Несмотря на то, что используется дополнительная библиотека icmp.dll, библиотеку WinSock надо загрузить в любом случае. К тому же будет использоваться функция gethostbyname для определения IP-адреса, если пользователь укажет символьное имя компьютера. В данном случае будет достаточно первой версии библиотеки, т. к. не будут применяться RAW-сокеты. Таким образом, программа сможет работать и в Windows 98 (без WinSock 2.0).
После этого нужно загрузить динамическую библиотеку icmp.dll с помощью функции LoadLibrary. Она находится в папке windows/system (или windows/system32), поэтому не надо указывать полный путь. Программа без проблем найдет и загрузит библиотеку.
В библиотеке нас будут интересовать следующие процедуры:
IcmpCreateFile — инициализация;
IcmpSendEcho — отправка эхо-пакета;
IcmpCloseHandle — закрытие ICMP.
Прежде чем посылать пакет, следует его проинициализировать с помощью функции IcmpCreateFile. По завершении работы с ICMP нужно вызвать функцию IcmpCloseHandle, чтобы закрыть его.
Теперь в заранее подготовленные переменные запоминаются адреса необходимых процедур из библиотеки:
pIcmpCreateFile=(lpIcmpCreateFile)GetProcAddress(hIcmp,"IcmpCreateFile");
pIcmpSendEcho=(lpIcmpSendEcho)GetProcAddress(hIcmp,"IcmpSendEcho");
pIcmpCloseHandle=(lpIcmpCloseHandle)GetProcAddress(hIcmp,"IcmpCloseHandle");
Если писать программу по всем правилам, то необходимо было бы проверить полученные адреса на равенство нулю. Если хотя бы один адрес функции нулевой, то она не найдена, и дальнейшее ее использование невозможно. Чаще всего такое бывает из-за неправильного написания имени функции. Но может случиться, когда программа загрузит другую библиотеку с таким же именем, в которой вообще нет таких функций. Чтобы этого не произошло, переменная pIcmpCreateFile (она должна содержать адрес функции IcmpCreateFile) проверяется на равенство нулю. Если это так, то загрузилась ошибочная библиотека, и об этом выводится соответствующее сообщение. Остальные переменные не проверяются (в надежде на правильное написание).
Следующим этапом определяется адрес компьютера, путь к которому надо найти, и переводится в IP-адрес. Если в этот момент произошла ошибка, то адрес указан неверно, и дальнейшее выполнение кода невозможно.
Вот теперь можно переходить к пингованию удаленного компьютера. Так как может возникнуть необходимость послать несколько пакетов с разным временем жизни, запускается цикл от 1 до 50. Использование в данном случае цикла while, который выполнялся бы, пока пинг не дойдет до нужного компьютера, не рекомендуется, т.к. появляется вероятность возникновения бесконечного цикла.
Внутри цикла инициализируется ICMP-пакет с помощью функции IcmpCreateFile. Результатом будет указатель на созданный объект, который понадобится при посылке эхо-пакета, поэтому он сохраняется в переменной hIP типа HANDLE:
HANDLE hIP = pIcmpCreateFile(); if (hIP == INVALID_HANDLE_VALUE) { AfxMessageBox("Could not get a valid ICMP handle"); return; }
Если результат равен INVALID_HANDLE_VALUE, то во время инициализации произошла ошибка, и дальнейшее выполнение невозможно.
После этого выделяется буфер для данных, который, как и в случае с пин-гом, заполняется символом с кодом 80. Далее создается пакет типа ICMP_ECHO_REPLY, в котором возвращается информация, полученная от маршрутизатора или компьютера. Нужно также создать пакет типа IP_OPTION_INFORMATION, в котором указывается время жизни пакета (параметр Ttl).
Когда все подготовлено, можно отправлять ICMP-пакет с помощью функции IcmpSendEcho, у которой 8 параметров:
указатель ICMP (получен во время инициализации);
адрес компьютера;
буфер с данными;
размер пакета (с учетом объема посылаемых данных);
IP-пакет с указанием времени жизни (на первом шаге он будет равен единице, потом двум и т.д.);
буфер для хранения структуры типа ICMP_ECHO_REPLY, в которую будет записан результирующий пакет;
размер буфера;
время ожидания ответа.
В качестве результата функция возвращает количество принятых пакетов. В нашем случае он один. Если возвращаемое значение равно нулю, то маршрутизатор или компьютер не ответили ICMP-пакетом, и невозможно выяснить его параметры.
Время ответа можно получить из параметра RoundTripTime структуры ICMP_ECHO_REPLY, а адрес сетевого устройства, ответившего на запрос, — из параметра Address.
После работы не забывайте закрывать указатель на созданный ICMP с помощью IcmpCloseHandle.
Теперь можно выводить полученную информацию. Для удобства восприятия в программе реализован перевод IP-адреса в символьное имя с помощью функции gethostbyaddr. У этой функции три параметра:
IP-адрес компьютера, символьное имя которого надо определить;
длина адреса;
семейство протокола. От этого зависит формат предоставляемого адреса.
Далее проверяется, если поступил ответ от искомого компьютера, то цикл прерывается, иначе нужно увеличить на единицу время жизни пакета и повторить посылку ICMP-пакета:
if (addr == Address.S_un.S_addr) bReachedHost = TRUE;
По окончании работы нужно выгрузить из памяти библиотеку icmp.dll и освободить библиотеку WinSock:
if (hIcmp) { FreeLibrary(hIcmp); hIcmp = NULL; } WSACleanup();
Запустите программу TraceRoute. На 6.8 показано окно с результатами ее работы.
6.8. Окно с результатом работы программы TraceRoute
Я постарался сделать пример простым, а логику — прямолинейной, чтобы вам легче было разобраться. При этом, если в процессе работы происходит ошибка, то по выходе из программы библиотеки icmp и winsock остаются загруженными. Самый простой способ избавиться от этого недостатка — производить загрузку библиотек при старте, а выгрузку — при выходе из программы, и если библиотеки не загружены, то можно отключать кнопки отправки пакета, чтобы пользователь не смог ими воспользоваться.
В Интернете можно найти заголовочные файлы для библиотеки icmp.dll, которые могут еще больше упростить этот пример. Но я не стал их использовать, чтобы ничего не ускользнуло от вашего внимания.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\TraceRoute. |
Полезные алгоритмы
В книге было приведено много шуточных задач и исследованы некоторые теоретические аспекты сетевого программирования. Теперь я продемонстрирую на примерах кое-какие полезные алгоритмы. С их помощью вы узнаете еще много любопытного о приемах хакеров, и заодно закрепим полученные теоретические знания.
При разборе сетевых функций в главах 4 и 5 были рассмотрены интересные примеры, но в них было несколько недостатков. Например, сканер портов был медленным, и на проверку 1000 портов уходило слишком много времени (см. разд. 4.4). Я уже упоминал о том, что необходимо для ускорения этого процесса. В этой главе я покажу самый быстрый сканер, который можно сделать очень гибким и универсальным.
Помимо этого, я покажу, как улучшить прием/передачу данных. Именно эта часть чаще всего является узким местом в обеспечении максимальной производительности при минимальной нагрузке на процессор.
В программировании очень много нюансов, и в разных ситуациях для достижения максимального эффекта можно поступить по-разному. Рассмотреть абсолютно все я не смогу, потому что на это понадобятся тысячи страниц и потребуются глубокие знания математики, поэтому затрону в основном сетевую часть.
Протокол ICMP
Я уже говорил о том, что протокол IP не обеспечивает надежность передачи данных, поэтому нельзя узнать о целостности принимаемых данных. Но с помощью протокола ICMP (Internet Control Message Protocol, интернет-протокол управляющих сообщений) можно узнать, достиг ли пакет адресата. Пакеты ICMP отправляются в тех случаях, когда адресат недоступен, буфер шлюза переполнен или недостаточен для отправки сообщения, или адресат требует передать данные по более короткому маршруту.
Протокол IСМР был разработан для того, чтобы информировать о возникающих проблемах во время приема/передачи и повысить надежность передачи информации по IP -протоколу, который изначально ненадежен. Но и на ICMP надеяться нельзя, потому что данные могут не дойти до адресата (заблудиться в сети), а вы не получите никаких сообщений. Именно поэтому используют протоколы более высокого уровня (например, TCP), имеющие свои методы обеспечения надежности.
Если вы хотите создать свой протокол на основе IP, то можете использовать сообщения ICMP для обеспечения определенной надежности. Но помните, что она не является достаточной. Сообщение ICMP отправляются, когда шлюз или компьютер не может обработать пакет. Но если он не дошел до компьютера из-за обрыва или по какой-то другой причине, никаких сообщений не будет, потому что системы подтверждений в протоколе IP нет.
Из-за малой надежности протокола ICMP программисты его редко используют в тех целях, для которых он создавался (для контроля доставки данных). Но у него есть другое предназначение, которое получило широкое распространение. Если отправить компьютеру ICMP-пакет, и он дойдет до адресата, то тот должен ответить. Таким способом можно легко проверить связь с удаленным компьютером. Именно таким образом реализованы программы Ping.
Для теста связи нужно знать открытый порт на удаленном компьютере, чтобы попытаться соединиться с ним. Если связь прошла успешно, то компьютер доступен. Не зная порта, можно просканировать весь диапазон, но это займет слишком много времени. Протокол ICMP позволяет избежать этой процедуры.
В некоторых сетях на все машины ставят брендмауэры, которые запрещают протокол ICMP, и в этом случае администраторы открывают на каком-то порту эхо-сервер (такой сервер получает данные и отвечает пакетом с этими же данными) и тестируют соединение через него. Такой способ хорош, но может возникнуть ситуация, когда связь есть, но эхо-сервер завис или его просто выключили, и администратор может подумать, что оборвалась связь. Но чаще всего ICMP-пакеты разрешены и работают нормально.
В теории все прекрасно, но на практике есть одна сложность — ICMP-протокол использует пакеты, отличные от TCP или UDP, поддержка которых есть в WinSock. Как же тогда отправить пакет с управляющим сообщением? Нужно самостоятельно сформировать пакет необходимого формата.
В WinSock1 не было возможности доступа напрямую к данным пакета. Функция select в качестве второго параметра (тип спецификации) могла принимать только значения SOCK_STREAM (для TCP-протокола) или SOCK_DGRAM (для UDP-протокола), и я об этом говорил. В WinSock2 появилась поддержка RAW-сокетов, которые позволяют получить низкоуровневый доступ к пакетам. Чтобы создать такой сокет (сырой), при вызове функции socket в качестве второго параметра нужно указать SOCK_RAW.
Рассмотрю, как программно реализована программа типа Ping. Это поможет вам понять, как работать с RAW-сокетами и как проверять связь с помощью ICMP-протокола. Создайте новое MFC-приложение. Главное диалоговое окно будущей программы можно увидеть на 6.7.
6.7. Диалоговое окно будущей программы Pinger
В строку Host будет вводиться имя или IP-адрес компьютера, с которым надо проверить связь. По нажатии кнопки будут отправляться и приниматься ICMP-пакеты и выводиться результат в List Box, который растянут по нижней части окна.
По нажатии кнопки Ping выполняется код из листинга 6.7.
Листинг 6.7. Использование пакетов ICMP |
WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { AfxMessageBox("Can't load WinSock"); return; }
// Create socket (Создание сокета) rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (rawSocket == SOCKET_ERROR) { AfxMessageBox("Socket error"); return; }
// Lookup host (Поиск хоста) char strHost[255]; edHost.GetWindowText(strHost, 255); lpHost = gethostbyname(strHost); if (lpHost == NULL) { AfxMessageBox("Host not found"); return; }
// Socket address (Адрес сокета) sDest.sin_addr.s_addr = *((u_long FAR *) (lpHost-h_addr)); sDest.sin_family = AF_INET; sDest.sin_port = 0;
str.Format("Pinging %s [%s]", strHost, inet_ntoa(sDest.sin_addr));
lMessages.AddString(str);
// Send ICMP echo request (Посылка эхо-запроса ICMP) static ECHOREQUEST echoReq;
echoReq.icmpHdr.Type = ICMP_ECHOREQ; echoReq.icmpHdr.Code = 0; echoReq.icmpHdr.ID = 0; echoReq.icmpHdr.Seq = 0; echoReq.dwTime = GetTickCount(); FillMemory(echoReq.cData, 64, 80); echoReq.icmpHdr.Checksum = CheckSum((u_short *)echoReq, sizeof(ECHOREQUEST));
// Send the echo request (Отправка эхо-запроса) sendto(rawSocket, (LPSTR)echoReq, sizeof(ECHOREQUEST), 0, (LPSOCKADDR)sDest, sizeof(SOCKADDR_IN));
struct timeval tVal; fd_set readfds; readfds.fd_count = 1; readfds.fd_array[0] = rawSocket; tVal.tv_sec = 1; tVal.tv_usec = 0;
iRet=select(1, readfds, NULL, NULL, tVal);
if (!iRet) { lMessages.AddString("Request Timed Out"); } else { // Receive reply (Получение ответа) ECHOREPLY echoReply; int nRet; int nAddrLen = sizeof(struct sockaddr_in);
// Receive the echo reply iRet = recvfrom(rawSocket, (LPSTR)echoReply, sizeof(ECHOREPLY), 0, (LPSOCKADDR)sSrc, nAddrLen);
if (iRet == SOCKET_ERROR) AfxMessageBox("Recvfrom Error");
// Calculate time (Расчет времени) dwElapsed = GetTickCount() - echoReply.echoRequest.dwTime; str.Format("Reply from: %s: bytes=%d time=%ldms TTL=%d", inet_ntoa(sSrc.sin_addr), 64, dwElapsed, echoReply.ipHdr.TTL); lMessages.AddString(str); }
iRet = closesocket(rawSocket); if (iRet == SOCKET_ERROR) AfxMessageBox("Closesocket error");
WSACleanup(); }
Прежде чем работать с сетью, необходимо загрузить библиотеку WinSock с помощью функции WSAStartup . Работа с RAW-сокетами не исключение, но загружать надо библиотеку WinSocket2, потому что в первой версии нет необходимых возможностей.
После этого можно создавать сокет с помощью функции socket со следующими параметрами:
семейство протокола — AF_INET (как всегда);
спецификация — SOCK_RAW для использования RAW-сокетов;
протокол — IPPROTO_ICMP.
Для отправки пакета с данными компьютеру необходимо знать его адрес. Если пользователь ввел символьное имя, то надо определить IP-адрес по имени с помощью функции gethostbyname.
После этого, как и в случае с другими протоколами, заполняется структура типа sockaddr_in, содержащая адрес компьютера, с которым нужно соединяться. ICMP-запрос не будет использовать портов, поэтому параметр Port установлен в 0.
Затем заполняется структура типа ECHOREQUEST. Эта структура является пакетом, который будет отправляться в сеть. Если при использовании протоколов TCP или UDP необходимо только указать данные, которые подлежат отправке, то в случае с ICMP нужно формировать полный пакет, который будет отправлен через IP-протокол. Структура echorequest имеет вид пакета и выглядит следующим образом:
typedef struct tagECHOREQUEST { ICMPHDR icmpHdr; DWORD dwTime; char cData[64]; }ECHOREQUEST, *PECHOREQUEST;
Параметр icmpHdr — это заголовок пакета, который необходимо самостоятельно заполнить, а параметр cData содержит отправляемые данные. В нашем случае будут отправляться пакеты по 64 байта, поэтому объявлен массив из 64 символов. В программе весь массив заполняется символом с кодом 80 с помощью функции FillChar. Для программы Ping не имеет значения, какие отправлять данные, потому что главное — проверить возможность связи с удаленным компьютером.
Параметр dwTime — это время, которое можно использовать на свое усмотрение. По нему чаще всего определяют время прохождения пакета.
Заголовок формируется в зависимости от принимаемого или отправляемого сообщения. Так как я для примера выбрал программу типа Ping, то буду рассматривать необходимые для нее данные. Более подробное описание протокола ICMP вы можете найти в документе RFC 792 по адресу http://info. internet.isi.edu/in-notes/rfc/files/rfc792.txt. Заголовок (параметр icmpHdr) — это структура следующего вида:
typedef struct tagICMPHDR { u_char Type; u_char Code; u_short Checksum; u_short ID; u_short Seq; char Data; }ICMPHDR, *PICMPHDR;
Рассмотрим эти параметры:
Tуре — тип пакета. В нашем случае это ICMP_ECHOREQ, который означает эхо-запрос ответа (сервер должен вернуть те же данные, которые принял). При ответе этот параметр должен быть нулевым;
Code — не используется в эхо-запросах и должен равняться нулю;
Checksum — контрольная сумма. RFC не накладывает жестких требований на алгоритм, и он может быть изменен. В данной программе я использовал упрощенный алгоритм, который вы можете увидеть в листинге 6.8;
ID — идентификатор. Для эхо-запроса должен быть обнулен, но может содержать и другие значения;
Seq — номер очереди, который должен быть обнулен, если код равен нулю.
Листинг 6.8. Функция подсчета контрольной суммы |
while( nleft 1 ) { sum += *addr++; nleft -= 1; }
sum += (sum 16); answer = ~sum; return (answer); }
После формирования пакета он отправляется с помощью функции sendto, потому что в качестве транспорта используется IP-протокол, который не поддерживает соединение как TCP, и по своей работе схож с UDP-протоколом.
Для ожидания ответа используется функция select, с помощью которой ждем в течение 1-й секунды возможности чтения с сокета. Если за это время ответ не получен, считается, что удаленный компьютер недоступен. Иначе читается пакет данных. В принципе, ответ уже известен, что связь между компьютерами работает, и читать пакет не обязательно, но я сделаю это, чтобы вы увидели весь цикл приема/передачи сообщений через RAW-сокеты. В реальном приложении чтение пакета необходимо, чтобы удостовериться в том, что получен пакет от того компьютера, которому отправлены данные (возможно, что совершенно другой компьютер посылал данные). Чтение пакета необходимо и в том случае, когда пингуется асинхронно сразу несколько компьютеров.
Для чтения пакета используется функция recvfrom, как и при работе с UDP-протоколом. Если при отправке посылается пакет данных в виде структуры ECHOREQUEST, то при чтении принимается пакет типа ECHOREPLY, который выглядит следующим образом:
typedef struct tagECHOREPLY { IPHDR ipHdr; ECHOREQUEST echoRequest; char cFiller[256]; }ECHOREPLY, *PECHOREPLY;
Первый параметр — это заголовок пришедшего пакета. Второй параметр — это заголовок пакета с данными, который был послан.
Заголовок принятого пакета отличается от отправленного и выглядит следующим образом:
typedef struct tagIPHDR { u_char VIHL; u_char TOS; short TotLen; short ID; short FlagOff; u_char TTL; u_char Protocol; u_short Checksum; struct in_addr iaSrc; struct in_addr iaDst; }IPHDR, *PIPHDR;
Это ничто иное, как заголовок протокола IP.
Все необходимые структуры должны быть описаны в заголовочном файле. Для приведенного примера я описал все в файле PingerDlg.h.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\Pinger. |
Самый быстрый сканер портов
Потоки — это очень мощная и удобная вещь, позволяющая создать многозадачность даже внутри отдельного приложения. Но у них есть один большой недостаток — программисты, познакомившись с потоками, начинают использовать их везде, где это надо и не надо.
Я видел много сканеров, которые используют по 20—50 потоков для одновременного сканирования большого количества портов. Я понимаю, что пример, который мы рассмотрели в главе 4, был очень медленным, и его надо ускорять, но не таким же методом. Попробуйте на досуге реализовать сканирование с помощью потоков. Вы увидите, что это не так уж и просто. Ну и, конечно же, вы уже знаете, что потоки излишне нагружают систему.
Сейчас вам предстоит увидеть, как можно реализовать быстрое сканирование портов без использования потоков. А тогда как? Конечно, с помощью асинхронной работы с сетью. Можно создать несколько асинхронных сокетов и запустить ожидание соединения. Потом собрать все сокеты в набор fd_set и выполнить функцию select в ожидании события соединения с сервером. По завершении ее выполнения необходимо проверить все сокеты на удачное соединение и вывести результат.
Давайте попробуем реализовать это на примере. Для иллюстрации сказанного создайте новое приложение MFC Application на основе диалогового окна. При этом не включайте опцию поддержки WinSock в разделе Advanced Features. В данном случае мы будем использовать некоторые функции WinSock2. Поэтому подключите заголовочный файл winsock2.h вручную и укажите в свойствах проекта необходимость использования библиотеки ws2_32.lib. Все это мы уже не раз делали, и это не должно вызвать затруднений.
Теперь откройте в редакторе ресурсов главное окно программы. Оформите его в соответствии с 6.1. Здесь необходимо добавить три поля ввода Edit Box, список List Box и кнопку, по нажатии которой будет происходить сканирование. Для всех полей ввода нужно создать следующие переменные:
chHostName — имя или IP-адрес сканируемого компьютера;
chStartPort — порт, с которого надо начать сканирование;
chEndPort — порт, до которого нужно сканировать.
Портов очень много, и даже наш быстрый сканер затратит на это немало времени.
6.1. Окно будущей программы FastScan
Теперь перейдем к программированию. Создайте обработчик события BN_CLICKED для кнопки, по нажатии которой должно начинаться сканирование. Код, который здесь нужно написать, достаточно большой (см. листинг 6.2), но несмотря на то, что он есть на компакт-диске, я советую набрать его вручную. Только в этом случае вы сможете разобраться в предназначении каждой строчки. Я же постараюсь дать вам всю необходимую информацию.
Листинг 6.2. Быстрое сканирование портов |
WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { SetDlgItemText(IDC_STATUSTEXT, "Can't load WinSock"); return; }
SetDlgItemText(IDC_STATUSTEXT, "Resolving host");
chStartPort.GetWindowText(tStr, 255); iStartPort = atoi(tStr); chEndPort.GetWindowText(tStr, 255); iEndPort = atoi(tStr);
chHostName.GetWindowText(tStr, 255);
struct hostent *host=NULL; host = gethostbyname(tStr); if (host == NULL) { SetDlgItemText(IDC_STATUSTEXT, "Unable to resolve host"); return; }
for (int i = 0; i MAX_SOCKETS; i++) busy[i] = 0;
SetDlgItemText(IDC_STATUSTEXT, "Scanning");
while (((iBusySocks) || (iStartPort = iEndPort))) { for (int i = 0; i MAX_SOCKETS; i++) { if (busy[i] == 0 iStartPort = iEndPort) { sock[i] = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock[i] 0) { SetDlgItemText(IDC_STATUSTEXT, "Socket filed"); return; } iBusySocks++; addr.sin_family = AF_INET; addr.sin_port = htons (iStartPort); CopyMemory(addr.sin_addr, host-h_addr_list[0], host-h_length);
ULONG ulBlock; ulBlock = 1; if (ioctlsocket(sock[i], FIONBIO, ulBlock) == SOCKET_ERROR) { return; }
connect(sock[i], (struct sockaddr *) addr, sizeof (addr)); if (WSAGetLastError() == WSAEINPROGRESS) { closesocket (sock[i]); iBusySocks--; } else { busy[i] = 1; port[i] = iStartPort; } iStartPort++; } } FD_ZERO (fdWaitSet); for (int i = 0; i MAX_SOCKETS; i++) { if (busy[i] == 1) FD_SET (sock[i], fdWaitSet); }
struct timeval tv; tv.tv_sec = 1; tv.tv_usec = 0;
if (select (1, NULL, fdWaitSet, NULL, tv) == SOCKET_ERROR) { SetDlgItemText(IDC_STATUSTEXT, "Select error"); return; }
for (int i = 0; i MAX_SOCKETS; i++) { if (busy[i] == 1) { if (FD_ISSET (sock[i], fdWaitSet)) { int opt; int Len = sizeof(opt); if (getsockopt(sock[i], SOL_SOCKET, SO_ERROR, (char*)opt, Len) == SOCKET_ERROR) SetDlgItemText(IDC_STATUSTEXT, "getsockopt error");
if (opt == 0) { struct servent *tec; itoa(port[i],tStr, 10); strcat(tStr, " ("); tec = getservbyport(htons (port[i]), "tcp"); if (tec==NULL) strcat(tStr, "Unknown"); else strcat(tStr, tec-s_name);
strcat(tStr, ") - open"); m_PortList.AddString(tStr); busy[i] = 0; shutdown(sock[i], SD_BOTH); closesocket(sock[i]); } busy[i] = 0; shutdown (sock[i], SD_BOTH); closesocket (sock[i]); iBusySocks--; } else { busy[i] = 0; closesocket(sock[i]); iBusySocks--; } } } ProcessMessages(); } WSACleanup(); SetDlgItemText(IDC_STATUSTEXT, "Scaning complete"); return; }
В данном примере для сканирования используются три массива:
sock — массив дескрипторов сокетов, которые ожидают соединения;
busy — состояние сканируемых портов. Любой из них может быть занят и вызвать ошибку. В файле помощи по WinSock написано, что не каждый порт можно использовать. Поэтому элемент массива, номер которого соответствует такому занятому (зарезервированному) порту, делается равным 1, в противном случае — присваивается 0;
port — массив сканируемых портов. В принципе, можно было бы обойтись и без этого массива, но для упрощения кода я его ввел.
В этом примере есть одна новая функция, которую мы не рассматривали, — getservbyport. Она выглядит следующим образом:
struct servent FAR * getservbyport ( int port, const char FAR * proto );
Функция возвращает информацию о сервисе, работающем на порту, указанном первым параметром. Второй параметр определяет протокол. В качестве результата возвращается структура типа servent, в которой поле s_name содержит символьное описание сервиса. Если функция вернет нулевое значение, то невозможно определить по номеру порта параметры работающего сервиса.
Данные, которые возвращает функция getservbyport, не являются точными, и ее легко обмануть. Например, для порта с номером 21 функция будет всегда возвращать информацию о протоколе FTP (File Transfer Protocol), но никто вам не мешает запустить на этом порту Web-сервер, и функция getservbyport не сможет этого определить.
Все остальное вам уже должно быть знакомо, но я подведу итоги, описав используемый алгоритм:
Загрузить сетевую библиотеку.
Определить адрес сканируемого компьютера до начала цикла. Этот адрес будет использоваться внутри цикла перебора портов в структуре sockaddr_in. Сама структура будет заполняться в цикле, потому что каждый раз будет новый порт, а адрес изменяться не будет, поэтому его определение вынесено за пределы цикла. Нет смысла на каждом этапе цикла делать одну и ту же операцию, тем более, что определение IP-адреса может занять время, если указано имя сканируемого компьютера.
Запустить цикл, который будет выполняться, пока начальный порт не превысит конечный. Внутри этого большого цикла выполняются следующие действия:
запустить цикл от 0 до значения MAX_SOCKETS. В этом цикле создается сокет, переводится в асинхронный режим и запускается функция connect. Так как сокеты находятся в асинхронном режиме, то не будет происходить ожидания соединения и замораживания программы, но при этом и неизвестно, произошло соединение или нет;
обнулить переменную fdWaitSet типа fd_set;
запустить цикл от 0 до значения MAX_SOCKETS. В этом цикле все сокеты помещаются в набор fd_set;
ожидать события от сокета с помощью функции select;
запустить цикл от 0 до значения MAX_SOCKETS. В этом цикле проверяется, какие сокеты удачно соединились с сервером. Если соединение прошло успешно, то получить символьное имя порта с помощью функции getsockopt. После этого сокет закрыть, чтобы разорвать соединение с сервером;
Выгрузить сетевую библиотеку.
Что такое MAX_SOCKETS? Это константа, которая определяет количество сканируемых сокетов. В данном примере она равна 40, и это оптимальное значение для различных сред. Чем больше количество сокетов, сканируемых за один проход, тем быстрее оно будет проходить.
Еще один недостаток — сканирование блокирует работу программы, поэтому открытые порты вы сможете увидеть только после окончания сканирования, когда программа освободится и перерисует окно. Чтобы избежать заморозки можно написать следующую процедуру:
void ProcessMessages() { MSG msg; while (PeekMessage(msg,NULL,0,0,PM_NOREMOVE)) { if (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } else return; } }
Эта функция содержит простой цикл — обработчик сообщений, который вы уже не раз видели в Win32-приложениях. В данном случае он не бесконечный, и обрабатывает все сообщения, накопившиеся в очереди. А когда они заканчиваются, цикл прерывается, и программа будет продолжать сканирование.
6.2. Результат сканирования моего компьютера
Напишите саму функцию где-нибудь в начале модуля и вставьте вызов ProcessMessages() в конце цикла поиска портов. В этом случае вы избавитесь от заморозки и сможете увидеть открытые порты сразу.
Стоит еще заметить, что в данном случае использовался протокол, который отображает открытые TCP-порты. Он никак не связан с UDP-портами. Чтобы сканировать UPD, необходимо создавать сокет (функция socket), ориентированный на сообщения.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\FastScan. |
Состояние локального компьютера
Если нужно узнать состояние портов локального компьютера, нет необходимости сканировать порты. Есть способ лучше — запросить состояние всех портов с помощью функции GetTcpTable. В этом случае вы получите более подробную информацию, которую можно свести в таблицу из следующих колонок:
локальный адрес — интерфейс, на котором открыт порт;
локальный порт — открытый порт;
удаленный адрес — адрес, с которого в данный момент установлено соединение с портом;
удаленный порт — порт на удаленной машине, через который происходит обращение к локальной машине;
состояние — может принимать различные значения: прослушивание, закрытие порта, принятие соединения и т. д.
Самое главное преимущество использования состояния локальной таблицы TCP — мгновенная работа. Сколько бы ни было открытых портов, их определение происходит в считанные миллисекунды.
Для иллюстрации примера работы с TCP-портом создайте MFC-приложение на основе диалогового окна с именем IPState. На главное диалоговое окно поместите один список типа List Box и кнопку с заголовком TCP Table. На 6.3 вы можете увидеть окно будущей программы.
6.3. Окно будущей программы IPState
По нажатии кнопки TCP Table должен выполняться код из листинга 6.3.
Листинг 6.3. Получение информации о ТСР-портах |
dwStatus = GetTcpTable(pTcpTable, dwActualSize, TRUE);
pTcpTable = (PMIB_TCPTABLE) malloc(dwActualSize); assert(pTcpTable);
dwStatus = GetTcpTable(pTcpTable, dwActualSize, TRUE); if (dwStatus != NO_ERROR) { AfxMessageBox("Couldn't get tcp connection table."); free(pTcpTable); return; }
CString strState; struct in_addr inadLocal, inadRemote; DWORD dwRemotePort = 0; char szLocalIp[1000]; char szRemIp[1000];
if (pTcpTable != NULL) { lList.AddString("================================================="); lList.AddString("TCP table:"); for (int i=0; ipTcpTable-dwNumEntries; i++) { dwRemotePort = 0; switch (pTcpTable-table[i].dwState) { case MIB_TCP_STATE_LISTEN: strState="Listen"; dwRemotePort = pTcpTable-table[i].dwRemotePort; break; case MIB_TCP_STATE_CLOSED: strState="Closed"; break; case MIB_TCP_STATE_TIME_WAIT: strState="Time wait"; break; case MIB_TCP_STATE_LAST_ACK: strState="Last ACK"; break; case MIB_TCP_STATE_CLOSING: strState="Closing"; break; case MIB_TCP_STATE_CLOSE_WAIT: strState="Close Wait"; break; case MIB_TCP_STATE_FIN_WAIT1: strState="FIN wait"; break; case MIB_TCP_STATE_ESTAB: strState="EStab"; break; case MIB_TCP_STATE_SYN_RCVD: strState="SYN Received"; break; case MIB_TCP_STATE_SYN_SENT: strState="SYN Sent"; break; case MIB_TCP_STATE_DELETE_TCB: strState="Delete"; break; } inadLocal.s_addr = pTcpTable-table[i].dwLocalAddr; inadRemote.s_addr = pTcpTable-table[i].dwRemoteAddr; strcpy(szLocalIp, inet_ntoa(inadLocal)); strcpy(szRemIp, inet_ntoa(inadRemote));
char prtStr[1000]; sprintf(prtStr, "Loc Addr %1s; Loc Port %1u; Rem Addr %1s; Rem Port %1u; State %s;", szLocalIp, ntohs((unsigned short) (0x0000FFFF pTcpTable-table[i].dwLocalPort)), szRemIp, ntohs((unsigned short)(0x0000FFFF dwRemotePort)), strState); lList.AddString(prtStr); } } free(pTcpTable); }
У функции GetTcpTable три параметра:
структура типа PMIB_TCPTABLE;
размер структуры, указанной в качестве первого параметра;
признак сортировки — если указано TRUE, то таблица будет отсортирована по номеру порта, иначе данные будут представлены в перемешанном виде.
Если в качестве первых двух параметров указать нулевое значение, то во втором параметре будет получен необходимый размер для хранения структур PMIB_TCPTABLE. Этот прием мы уже не раз использовали в главе 5.
Память определенного размера выделяется функцией malloc. В данном случае это необязательно делать в глобальной области.
Повторный вызов функции GetTcpTable позволяет через первый параметр (переменная рТсрTаblе типа PMIB_TCPTABLE) получить данные о состоянии всех TCP-портов. Их количество находится в параметре dwNumEntries структуры рТсрTаblе. Информацию об определенном порте можно узнать из параметра table[i], где i — номер порта. Этот параметр тоже является структурой, и в нем нас интересуют следующие элементы:
dwState — состояние порта. Этот параметр может принимать различные значения (MIB_TCP_STATE_LISTEN, MIB_TCP_STATE_CLOSED и т. д.). Список всех констант можно найти в коде программы или в справочной системе. Назначение констант просто определить, достаточно только внимательно посмотреть на код из листинга 6.3;
dwLocalPort — локальный порт;
dwRemotePort — удаленный порт;
dwLocalAddr — локальный адрес;
dwRemoteAddr — удаленный адрес.
В примере запускается бесконечный цикл, который перебирает все записи из параметра table, и информация добавляется в список List Box.
6.4. Результат работы программы IPState
Для правильной компиляции программы в начале модуля надо подключить три заголовочных файла:
#include iphlpapi.h
#include assert.h
#include winsock2.h
В свойствах проекта, в разделе Linker/Input в пункте Additional Dependencies нужно добавить две библиотеки IPHlpApi.lib и ws2_32.lib ( 6.5).
Для получения таблицы UDP- портов используется функция GetUdpTable. Она работает аналогично, но узнать можно только локальный адрес и локальный порт, потому что протокол UDP не устанавливает соединения, и нет сведений об удаленном компьютере.
Давайте добавим в программу кнопку UDP Table, а по ее нажатии должен будет выполняться код из листинга 6.4.
6.5. Подключение библиотек
Листинг 6.4. Получение таблицы состояния UDP-портов |
dwStatus = GetUdpTable(pUdpTable, dwActualSize, TRUE);
pUdpTable = (PMIB_UDPTABLE) malloc(dwActualSize); assert(pUdpTable);
dwStatus = GetUdpTable(pUdpTable, dwActualSize, TRUE);
if (dwStatus != NO_ERROR) { AfxMessageBox("Couldn't get udp connection table."); free(pUdpTable); return; }
struct in_addr inadLocal; if (pUdpTable != NULL) { lList.AddString("================================================="); lList.AddString("UDP table:"); for (UINT i = 0; i pUdpTable-dwNumEntries; ++i) { inadLocal.s_addr = pUdpTable-table[i].dwLocalAddr;
char prtStr[1000]; sprintf(prtStr, "Loc Addr %1s; Loc Port %1u", inet_ntoa(inadLocal), ntohs((unsigned short)(0x0000FFFF pUdpTable-table[i].dwLocalPort))); lList.AddString(prtStr); } } free(pUdpTable); }
Код для получения информации о UDP похож на тот, что использовался для протокола TCP, и вам не составит труда разобраться с происходящим.
Примечание |
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\IPState. |