Программирование на C++ глазами хакера

         

Анализ примера


Если сделать сервер невидимым и наделить его возможностями отправки паролей или перезагрузки компьютера по внешнему запросу, то этот пример легко превратить в "трояна". Но я не буду этого делать, потому что это нарушит мои принципы. Все это рассматривалось только в познавательных целях.

Стоит также заметить, что после каждой операции при работе с сетью происходит проверка на ошибку. Если при создании сокета произошла какая-либо внештатная ситуация, то последующая работа бесполезна.

Давайте рассмотрим, как можно сделать описанный код более универсальным. В примере есть один недостаток. Если одна из сторон должна будет отправить данные слишком большого объема, то они будут отправлены/приняты не полностью. Это связано с тем, что данные уходят маленькими порциями (пакетами), и системный буфер для отправки данных не безграничен.

Допустим, что системный буфер равен 64 Кбайт. При попытке переслать по сети объем данных больше этого значения клиент получит только 64 Кбайт. Остальные данные просто пропадут. Чтобы этого не произошло, вы должны проверять, сколько реально было отправлено, и корректировать ваши действия.

В листинге 4.14 приведен пример, с помощью которого можно переслать клиенту любой объем данных, даже если он превышает размер буфера. Алгоритм достаточно прост, но давайте его подробно рассмотрим.

Листинг 4.14. Алгоритм отправки данных большого объема
char szBuff[4096]; szBuff = "Данные для отправки..."; int nSendSize = sizeof(szBuff); int iCurrPos = 0;

while(nSendSize 0) { int ret = send(sock, szBuff[iCurrPos], nSendSize, 0); if (ret == 0) break; else if (ret == SOCKET_ERROR) { // Произошла ошибка MessageBox(0, "Send failed", "Error", 0); break; } nSendSize -= ret; iCurrPos += ret; }

В данном примере определяется размер отсылаемых данных и сохраняется в переменной nSendSize. После этого запускается цикл, который будет выполняться, пока переменная больше нуля и еще есть данные для отправки. Переменная iCurrPos указывает на текущую позицию в буфере, а отправка начинается с нулевой позиции.


В функции send в качестве второго параметра передается буфер, содержащий данные для отправки, а в квадратных скобках указана позиция в буфере, начиная с которой нужно отсылать.

Функция возвращает количество реально отосланных байт. После проверки значения, которое вернула функция send, надо уменьшить размер данных в буфере, ожидающих отправки, и увеличить текущую позицию в буфере.

Если отправлены еще не все данные, то на следующем шаге функция попытается отправить следующую порцию.

Вы также не сможете и принять сразу большую порцию данных. Поэтому необходимо таким же образом запустить цикл, в котором будет приниматься большая порция данных. Но как определить, насколько велик этот кусок данных? Ведь при отправке известно количество данных, а при приеме — нет.

Решить эту проблему очень просто. Прежде чем отсылать данные, вы должны сообщить принимающей стороне количество байт, которые подлежат пересылке. Для этого должен быть заведомо определен протокол передачи данных. Например, когда приходит команда get, то после нее определенное количество байт можно отвести под значение размера отправляемых данных. Перед самими данными можно отправить команду data. Таким образом, клиент будет знать, сколько ему ожидать данных, и сможет получить их полностью. Код приема данных может выглядеть, как в листинге 4.15.



Листинг 4.15. Алгоритм получения данных большого объема
char szBuff[4096]; int nSendSize = 1000000; int iCurrPos = 0;

while(nSendSize 0) { int ret = recv(sock, szBuff[iCurrPos], nSendSize, 0); if (ret == 0) break; else if (ret == SOCKET_ERROR) { // Произошла ошибка MessageBox(0, "Send failed", "Error", 0); break; } nSendSize -= ret; iCurrPos += ret; }


Асинхронная работа через объект события


Если в программе нет процедуры обработки сообщений, то можно воспользоваться объектами событий. В этом случае алгоритм работы будет несколько иной:

Создать объект события с помощью функции WSACreateEvent.

Выбрать сокет с помощью функции WSAEventSelect.

Ожидать событие с помощью функции WSAWaitForMultipleEvents.

Давайте подробно рассмотрим все функции, необходимые для работы с объектами событий.

Первым делом следует создать событие с помощью функции WSACreateEvent. Функции не надо передавать никаких параметров, она просто возвращает новое событие типа WSAEVENT:

WSAEVENT WSACreateEvent(void);

Теперь нужно связать сокет с этим объектом и указать события, которые нам нужны. Для этого используется функция WSAEventSelect:

int WSAEventSelect ( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents )

Первый параметр — это сокет, события которого нас интересуют. Второй параметр — объект события. Последний параметр — это необходимые события. В качестве последнего параметра можно указывать те же константы, что рассматривались для функции WSAAsyncSelect (все они начинаются с префикса FD_).

Раньше вы уже встречались с функциями WaitForSingleObject и WaitForMultipleObjects, которые ожидают наступления события типа HANDLE. Для сетевых событий используется похожая функция с именем WSAWaitForMultipleEvents:

DWORD WSAWaitForMultipleEvents ( DWORD cEvents, const WSAEVENT FAR *lphEvents, BOOL fWaitAll, DWORD dwTimeOUT, BOOL fAlertable );

Давайте рассмотрим каждый параметр:

cEvents — количество объектов событий, изменение состояния которых нужно ожидать. Чтобы узнать максимальное число, воспользуйтесь константой WSA_MAXIMUM_WAIT_EVENTS;

lphEvents — массив объектов событий, которые нужно ожидать;

fWaitAll — режим ожидания событий. Если указано TRUE, то функция ожидает, пока все события не сработают, иначе — после первого передает управление программе;

dwTimeOUT — временной интервал в миллисекундах, в течение которого нужно ожидать события. Если в этом временном интервале не возникло события, то функция возвращает значение WSA_WAIT_TIMEOUT. Если нужно ожидать бесконечно, то можно указать константу WSA_INFINITE;


fAlertable — параметр используется при перекрестном вводе/выводе, который я не рассматриваю в этой книге, поэтому указан FALSE.

Чтобы узнать, какое событие из массива событий сработало, нужно вычесть из возвращенного функцией WSAWaitForMultipleEvents значения константу WSA_WAIT_EVENT_0.

Прежде чем вызывать функцию WSAWaitForMultipieEvents, все события в массиве должны быть пустыми. Если хотя бы одно из них будет занято, то функция сразу вернет управление программе, и не будет ожидания. После выполнения функции отработавшие события становятся занятыми, и после обработки их надо освободить. Для этого используется функция WSAResetEvent:

BOOL WSAResetEvent ( WSAEVENT hEvent );

Функция очищает состояние события, указанного в качестве единственного параметра.

Когда событие уже не нужно, его необходимо закрыть. Для этого используется функция WSACloseEvent. Функции следует передать объект события, который необходимо закрыть:

BOOL WSACloseEvent ( WSAEVENT hEvent );

Если закрытие прошло успешно, то функция возвращает TRUE, иначе — FALSE.


Быстрый UDP


Как и IP, протокол UDP для передачи данных не устанавливает соединения с сервером. Данные просто выбрасываются в сеть, и протокол даже не заботится о доставке пакета. Если данные на пути к серверу испортятся или вообще не дойдут, то отправляющая сторона об этом не узнает. Так что, по этому протоколу, как и по голому IP, не желательно передавать очень важные данные.

Благодаря тому, что протокол UDP не устанавливает соединения, он работает очень быстро (в несколько раз быстрее TCP, о котором чуть ниже). Из-за высокой скорости его очень удобно использовать там, где не нужно заботиться о целостности данных. Таким примером могут служить радиостанции в Интернете. Звуковые данные просто выплескиваются в глобальную сеть, и если слушатель не получит одного пакета, то максимум, что он заметит — небольшое заикание в месте потери. Но если учесть, что сетевые пакеты имеют небольшой размер, то эта задержка будет практически незаметна.

Большая скорость — большие проблемы с безопасностью. Так как нет соединения между сервером и клиентом, то нет никакой гарантии в достоверности данных. Протокол UDP больше подвержен спуфингу (spoofing , подмена адреса отправителя), поэтому построение на нем защищенных сетей затруднено.

Итак, UDP очень быстр, но его можно использовать только там, где данные не имеют высокой ценности (возможна потеря отдельных пакетов) и не секретны (UDP больше подвержен взлому).



Функция select


Еще в первой версии Winsock была очень интересная возможность управления неблокируемыми сокетами. Для этого используется функция select:

int select ( int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout );

Функция возвращает количество готовых к использованию дескрипторов Socket.

Теперь рассмотрим параметры этой функции:

nfds — игнорируется и служит только для совместимости с моделью сокетов Беркли;

readfds — возможность чтения (структура типа fd_set);

writefds — возможность записи (структура типа fd_set);

exceptfds — важность сообщения (структура типа fd_set);

timeout — максимальное время ожидания или null для блокирования дальнейшей работы (ожидать бесконечно).

Структура fd_set — набор сокетов, oт которых нужно ожидать разрешение на выполнение определенной операции. Например, если вам нужно дождаться прихода данных на один из двух сокетов, то вы можете сделать следующее:

добавить в набор fd_set два уже созданных сокета;

запустить функцию select и в качестве второго параметра указать набор с сокетами.

Функция select будет ожидать данные указанное время, после чего можно прочитать данные из сокета. Но данные могут прийти только на один из двух сокетов. Как узнать, на какой именно? Для начала с помощью функции FD_ISSET нужно обязательно проверить, входит ли сокет в набор.

При работе со структурой типа fd_set вам понадобятся следующие функции:

FD_ZERO — очищает набор. Прежде чем добавлять в набор новые сокеты, обязательно вызывайте эту функцию, чтобы проинициализировать набор. У этой функции только один параметр — указатель на переменную типа fd_set;

FD_SET — добавляет сокет в набор. У функции два параметра — сокет, который нужно добавить, и переменная типа fd_set, в набор которой нужно добавить сокет;

FD_CLR — удаляет сокет из набора. У этой функции два параметра — сокет, который надо удалить, и набор, из которого будет происходить удаление;

FD_ISSET — проверяет, входит ли сокет, определенный в первом параметре, в набор типа fd_set, указанный в качестве второго параметра.



Использование сокетов через события Windows


Функция select введена в библиотеку WinSock для совместимости с аналогичными библиотеками других платформ. Для программирования в Windows более мощной является функция WSAAsyncSelect, которая позволяет отслеживать состояние сокетов с помощью сообщений Windows. Таким образом, вы сможете получать сообщения в функции WndProc, и нет необходимости замораживать работу программы для ожидания доступности сокетов.

Функция выглядит следующим образом:

int WSAAsyncSelect ( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent );

Рассмотрим каждый параметр:

s — сокет, события которого необходимо ловить;

hWnd — окно, которому будут посылаться события при возникновении сетевых сообщений. Именно у этого окна (или родительского) должна быть функция WndProc, которая будет получать сообщения;

wMsg — сообщение, которое будет отсылаться окну. По его типу можно определить, что это событие сети;

lEvents — битовая маска сетевых событий, которые нас интересуют. Этот параметр может принимать любую комбинацию из следующих значений:

FD_READ — готовность к чтению;

FD_WRITE — готовность к записи;

FD_OOB — получение срочных данных;

FD_ACCEPT — подключение клиентов;

FD_CONNECT — соединение с сервером;

FD_CLOSE — закрытие соединения;

FD_QOS — изменения сервиса QoS (Quality of Service);

FD_GROUP_QOS — изменение группы QoS.

Если функция отработала успешно, то она вернет значение больше нуля, если произошла ошибка — SOCKET_ERROR.

Функция автоматически переводит сокет в неблокирующий режим, и нет смысла вызывать функцию ioctlsocket.

Вот простой пример использования WSAAsyncSelect:

WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_WRITE);

После выполнения этой строчки кода окно hWnd будет получать событие wMsg каждый раз, когда сокет s будет готов принимать и отправлять данные. Чтобы отменить работу события, необходимо вызвать эту же функцию, но в качестве четвертого параметра указать 0:

WSAAsyncSelect(s, hWnd, 0, 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; }

WSAAsyncSelect(sServerListen, hWnd, WM_USER+1, FD_ACCEPT); listen(sServerListen, 4);

// Main message loop: while (GetMessage(msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, msg)) { TranslateMessage(msg); DispatchMessage(msg); } }

closesocket(sServerListen); WSACleanup();

return (int) msg.wParam; }

Благодаря использованию функции WSAAsyncSelect весь код (без дополнительных потоков) можно расположить прямо в функции _tWinMain.

Код практически ничем не отличается от того, что был в проекте TCPServer (см. разд. 4.7.1). Единственное, перед запуском прослушивания (listen) вызывается функция WSAAsyncSelect, чтобы выбрать созданный сокет и перевести его в асинхронный режим. Здесь указываются следующие параметры:

sServerListen — переменная, которая указывает на созданный серверный сокет;

hWnd — указатель на главное окно программы, и именно ему будут передаваться сообщения;

WM_USER+1 — все пользовательские сообщения должны быть больше константы WM_USER. Меньшие значения могут использоваться системой и вызвать конфликт. Я использовал такую конструкцию, чтобы явно показать необходимость использования такого сообщения. В реальных приложениях я советую создавать для этого константу с понятным именем и использовать ее. Это можно сделать следующим образом: #define WM_NETMESSAGE WM_USER+1;

FD_ACCEPT — событие, которое нужно обрабатывать. Что может делать серверный сокет? Принимать соединения со стороны клиента. Именно это событие нас интересует.

Самое главное будет происходить в функции WndProc. Начало функции, где нужно добавить код, показано в листинге 4.20.



Листинг 4.20. Обработка сетевых сообщений в функции WndProc
LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc;

SOCKET ClientSocket; int ret; char szRecvBuff[1024], szSendBuff[1024];

switch (message) { case WM_USER+1: switch (WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: ClientSocket = accept(wParam, 0, 0); WSAAsyncSelect(ClientSocket, hWnd, WM_USER+1, FD_READ | FD_WRITE | FD_CLOSE); break;

case FD_READ: ret = recv(wParam, szRecvBuff, 1024, 0); if (ret == 0) break; else if (ret == SOCKET_ERROR) { MessageBox(0, "Recive data filed", "Error", 0); break; } szRecvBuff[ret] = '\0';

strcpy(szSendBuff, "Command get OK");

ret = send(wParam, szSendBuff, sizeof(szSendBuff), 0); break;

case FD_WRITE: //Ready to send data break;

case FD_CLOSE: closesocket(wParam); break; } case WM_COMMAND: ...

Здесь в самом начале добавлен новый оператор case , который проверяет, равно ли пойманное сообщение искомому сетевому сообщению WM_USER+1. Если это сетевое событие, то запускается перебор сетевых событий. Для этого используется оператор switch, который сравнивает указанное в скобках значение с поступающими событиями:

switch (WSAGETSELECTEVENT(lParam))

Как известно, в параметре lParam находятся код ошибки и тип события. Чтобы получить событие, используется функция WSAGETSELECTEVENT. А затем проверяются необходимые нам события. Если произошло соединение со стороны клиента, то выполняется следующий код:

case FD_ACCEPT: ClientSocket = accept(wParam, 0, 0); WSAAsyncSelect(ClientSocket, hWnd, WM_USER+1, FD_READ | FD_WRITE | FD_CLOSE); break;

Сначала принимается соединение с помощью функции accept. Результатом будет сокет, с помощью которого можно работать с клиентом. С этого сокета тоже нужно ловить события, поэтому вызываем функцию WSAAsyncSelect. Чтобы не плодить сообщения, используем в качестве третьего параметра значение WM_USER+1. Это не вызовет конфликтов, потому что серверный сокет обрабатывает только событие FD_ACCEPT, а у клиентского нас интересуют события чтения, записи данных и закрытия сокета.



Когда к серверу придут данные, поступит сообщение WM_USER+1, а функция WSAGETSELECTEVENT(lParam) вернет значение FD_READ. В этом случае читаются пришедшие данные, а клиенту посылается текст "Command get OK":

case FD_READ: ret = recv(wParam, szRecvBuff, 1024, 0); if (ret == 0) break; else if (ret == SOCKET_ERROR) { MessageBox(0, "Recive data filed", "Error", 0); break; } szRecvBuff[ret] = '\0'; strcpy(szSendBuff, "Command get OK"); ret = send(wParam, szSendBuff, sizeof(szSendBuff), 0); break;

Это тот же самый код, который использовался в приложении TCPServer для обмена данными между клиентом и сервером. Я намеренно не вносил изменений, чтобы сервер можно было протестировать программой TCPClient.

По событию FD_WRITE ничего не происходит, а только стоит комментарий. По событию FD_CLOSE закрывается сокет.

Рассмотренный пример с использованием функции select может работать только с одним клиентом. Чтобы добавить возможность одновременной обработки нескольких соединений, необходимо сформировать массив потоков, используемых для приема/передачи данных. В главе 6 я приведу пример с использованием функции select, который и без массивов потоков будет избавлен от этих недостатков.

Функция WSAAsyncSelect проще в программировании и изначально позволяет обрабатывать множество клиентов. Ну, а самое главное — нет ни одного дополнительного потока.

Чтобы протестировать пример, сначала запустите программу-сервер WSASel, а потом — программу-клиент TCPClient.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\WSASel.
Хочу обратить ваше внимание, что обмен информацией происходит асинхронно. Отправку и прием большого количества данных нужно будет делать порциями.

Допустим, что клиент должен передать серверу 1 Мбайт данных. Конечно же, за один прием это сделать нереально. Поэтому на стороне сервера вы должны действовать следующим образом:

сервер должен узнать (клиент должен сообщить серверу) количество передаваемых данных;



сервер должен выделить необходимый объем памяти или, если количество данных слишком большое, создать временный файл;

при получении события FD_READ сохранять принятые данные в буфере или в файле. Обрабатывать событие, пока данные не будут получены полностью, и клиент не пришлет определенную последовательность байт, определяющую завершение передачи данных.

Подобным способом должна происходить отправка с клиента:

сообщить серверу количество отправляемых данных;

открыть файл, из которого будут читаться данные;

послать первую порцию данных, остальные данные — по событию FD_WRITE;

по завершению отправки послать серверу последовательность байт, определяющую завершение передачи данных.

Использование сообщений Windows очень удобно, но вы теряете совместимость с UNIX-системами, где сообщения реализованы по-другому и нет функции WSAAsyncSelect. Поэтому при переносе такой программы на другую платформу возникнут большие проблемы и придется переписать слишком много кода. Но если перенос не планируется, то я всегда использую WSAAsyncSelect, что позволяет добиться максимальной производительности и удобства программирования.


Клиентские функции


В разд. 4.6.4 я познакомил вас с серверными функциями, и вы уже в состоянии написать сервер с помощью WinAPI-функций. Но мы пока не будем этого делать, потому что еще не можем написать клиентскую часть и протестировать пример. Так что теперь самое время заняться клиентскими функциями и функциями приема/передачи данных.

Для соединения с сервером нужны два этапа — создать сокет и подключиться к нему. Но это в идеальном случае. Как правило, добавляется еще один этап. Какой? Простым пользователям тяжело работать с IP-адресами, поэтому чаще всего они используют символьные имена серверов. В этом случае необходимо перед соединением с сервером выполнить еще одно действие — перевести символьное имя в IP-адрес.

Как создавать сокет, вы уже знаете. Теперь давайте разберемся с процессом определения IP-адреса. Для этого используется одна из двух функций: gethostbyname или WSAAsyncGetHostByName (в зависимости от версии WinSock). Для начала рассмотрим функцию gethostbyname:

struct hostent FAR * gethostbyname ( const char FAR * name );

В качестве единственного параметра нужно передать символьное имя сервера. Функция возвращает структуру типа hostent, которую рассмотрим чуть позже.

Теперь переходим к рассмотрению WSAAsyncGetHostByName:

HANDLE WSAAsyncGetHostByName ( HWND hWnd, unsigned int wMsg, const char FAR * name, char FAR * buf, int buflen );

Функция выполняется асинхронно, а это значит, что не блокируется выполнение программы при ее вызове. Программа будет работать дальше, но результат будет получен позже через сообщение Windows, указанное в качестве второго параметра. Это очень удобно, потому что процесс определения адреса может оказаться очень долгим, и блокирование программы на длительное время будет неэффективным. Процессорное время можно использовать для других целей.

Давайте разберем параметры подробнее:

hWnd — дескриптор окна, которому будет послано сообщение по завершении выполнения асинхронного запроса;

wMsg — сообщение Windows, которое будет сгенерировано после определения IP-адреса;


name — символьное имя компьютера, адрес которого надо определить;

buf — буфер, в который будет помещена структура hostent. Буфер должен иметь достаточный объем памяти. Максимальный размер можно определить с помощью макроса MAXGETHOSTSTRUCT;

buflen — длина буфера, указанного в четвертом параметре.

Теперь рассмотрим структуру hostent, с помощью которой получен результат:

struct hostent { char FAR * h_name; char FAR * FAR * h_aliases; short h_addrtype; short h_length; char FAR * FAR * h_addr_list; };

Проанализируем параметры структуры:

h_name — полное имя компьютера. Если в сети используется доменная система, то этот параметр будет содержать полное доменное имя;

h_aliases — дополнительное имя узла;

h_addrtype — тип возвращаемого адреса;

h_length — длина каждого адреса в списке адресов;

h_addr_list — список адресов компьютера.

Компьютер может иметь несколько адресов, поэтому структура возвращает полный список, который заканчивается нулем. В большинстве случаев достаточно выбрать первый адрес из списка. Если функция gethostbyname определила несколько адресов, то чаще всего по любому из них можно будет соединиться с искомым компьютером.

Теперь непосредственно функция соединения с сервером connect. Она выглядит следующим образом :

int connect ( SOCKET s, const struct sockaddr FAR* name, int namelen );

Параметры функции:

s — предварительно созданный сокет;

name — структура SOCKADDR, содержащая адрес сервера, к которому надо подключиться;

namelen — размер структуры SOCKADDR, указанной в качестве второго параметра.

Во второй версии WinSock появилась функция WSAConnect:

int WSAConnect ( SOCKET s, const struct sockaddr FAR * name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS );

Первые три параметра ничем не отличаются от параметров функции connect. Поэтому рассмотрим только новые:

lpCallerData — указатель на пользовательские данные, которые будут отправлены серверу во время установки соединения;

lpCalleeData — указатель на буфер, в который будут помещены переданные во время соединения данные.



Оба параметра имеют тип указателя на структуру WSABUF, которая выглядит следующим образом:

typedef struct _WSABUF { u_long len; char FAR * buf; } WSABUF, FAR * LPWSABUF;

Здесь первый параметр — размер буфера, а второй — указатель на сам буфер. Последние два параметра функции WSAConnect (lpSQOS и lpGQOS) являются указателями на структуры типа QoS. Они определяют требования к пропускной способности канала при приеме и передаче данных. Если указать нулевое значение, то это будет означать, что требования к качеству обслуживания не предъявляются.

Во время попытки соединения чаще всего могут встретиться следующие ошибки:

WSAETIMEDOUT — сервер недоступен. Возможна какая-то проблема на пути соединения;

WSAECONNREFUSED — на сервере не запущено прослушивание указанного порта;

WSAEADDRINUSE — указанный адрес уже используется;

WSAEAFNOSUPPORT — указанный адрес не может использоваться с данным сокетом. Эта ошибка возникает, когда указывается адрес в формате одного протокола, а производится попытка соединения по другому протоколу.


Медленный , но надежный TCP


Как я уже сказал, протокол TCP лежит на одном уровне с UDP и работает поверх IP, который используется для отправки данных. Именно поэтому протоколы TCP и IP неразрывно связаны и их часто объединяют одним названием TCP/IP.

В отличие от UDP-протокол TCP устраняет недостатки своего транспорта (IP). В этом протоколе заложены средства установления связи между приемником и передатчиком, обеспечение целостности данных и гарантии их доставки.

Когда данные отправляются в сеть по TCP, то на отправляющей стороне включается таймер. Если в течение определенного времени приемник не подтвердит получение данных, то будет предпринята еще одна попытки отправки данных. Если приемник получит испорченные данные, то он сообщит об этом источнику и попросит снова отправить испорченные пакеты. Благодаря этому обеспечивается гарантированная доставка данных.

Когда нужно отправить сразу большую порцию данных, не вмещающихся в один пакет, то они разбиваются на несколько TCP-пакетов. Пакеты отправляются порциями по несколько штук (зависит от настроек стека). Когда сервер получает порцию пакетов, то он восстанавливает их очередность и собирает данные вместе (даже если пакеты прибыли не в том порядке, в котором они отправлялись).

Из-за лишних накладных расходов на установку соединения подтверждение доставки и повторную пересылку испорченных данных протокол TCP намного медленней UDP. Зато TCP можно использовать там, где нужна гарантия доставки и большая надежность. Хотя надежность нельзя назвать сильной (нет шифрования, сохраняется возможность взлома), но она приемлемая и намного больше, чем у UDP. По крайней мере, тут спуфинг не может быть реализован так просто, как у UDP, и в этом вы убедитесь, когда прочтете про процесс установки соединения. Хотя возможно все, и хакеры умеют взламывать и ТСР-протокол.

Опасные связи TCP

Давайте посмотрим, как протокол TCP обеспечивает надежность соединения. Все начинается еще на этапе попытки соединения двух компьютеров в следующей последовательности:

Клиент, который хочет соединиться с сервером, отправляет SYN-запрос на сервер, указывая номер порта, к которому он хочет подсоединиться, и специальное число (чаще всего случайное).


Сервер отвечает своим сегментом SYN , содержащим специальное число сервера. Он также подтверждает приход SYN-пакета со стороны клиента с использованием АСК-ответа, где специальное число, отправленное клиентом, увеличено на 1.

Клиент должен подтвердить приход SYN от сервера с использованием АСК — специальное число сервера плюс 1.

Получается, что при соединении клиента с сервером они обмениваются специальными числами. Эти числа и используются в дальнейшем для обеспечения целостности и защищенности связи. Если кто-то другой захочет вклиниться в установленную связь (с помощью спуфинга), то ему надо будет подделать эти числа. Но так как они большие и выбираются случайным образом, то такая задача достаточно сложная, хотя Кевин Митник в свое время смог решить ее. Но это уже другая история, и не будем уходить далеко в сторону.

Стоит еще отметить, что приход любого пакета подтверждается АСК-ответом, что гарантирует доставку данных.


NetBEUI


В 1985 году уже сама IBM сделала попытку превратить NetBIOS в полноценный протокол, который умеет не только формировать данные для передачи, но и физически передавать их по сети. Для этого был разработан NetBEUI (NetBIOS Extended User Interface, расширенный пользовательский интерфейс NetBIOS). Он предназначен именно для описания физической части передачи данных протокола NetBIOS.

Сразу хочу отметить, что NetBEUI является немаршрутизируемым протоколом, и первый же маршрутизатор будет отбиваться от таких пакетов как теннисистка от мячиков :). Это значит, что если между двумя компьютерами стоит маршрутизатор и нет другого пути для связи, то им не удастся установить соединение через NetBEUI.



Обмен данными


Вы узнали, как создавать сервер, и познакомились с функциями соединения. Теперь необходимо научиться тому, ради чего все это задумывалось — передавать и принимать данные. Именно ради обмена данными между компьютерами мы рассматривали такое количество функций.

Сразу замечу, что функции создавались тогда, когда еще не было даже разговоров о UNICODE (универсальная кодировка, позволяющая работать с любым языком). Поэтому, чтобы отправить данные в этой кодировке, нужно привести их к типу char*, а длину умножить на 2, потому что каждый символ в UNICODE занимает 2 байта (в отличие от ASCII, где символ равен одному байту).

Чтобы принять данные, нужно их сначала отправить. Поэтому начну рассмотрение функций обмена данными с этого режима. Для передачи данных серверу существуют функции send и WSASend (для WinSock2). Функция send выглядит следующим образом:

int send ( SOCKET s, const char FAR * buf, int len, int flags );

Функция передает следующие параметры:

s — сокет, через который будет происходить отправка данных. В программе может быть открыто одновременно несколько соединений с разными серверами, и нужно четко определить, какой сокет надо использовать;

buf — буфер, содержащий данные, которые необходимо отправить;

len — длина буфера в параметре buf;

flags — флаги, определяющие метод отправки. Здесь можно указывать сочетание из следующих значений:

0 — флаги не указаны;

MSG_DONTROUTE — отправляемые пакеты не надо маршрутизировать. Если транспортный протокол, который отправляет данные, не поддерживает этот флаг, то он игнорируется;

MSG_OOB — данные должны быть отправлены вне очереди (out of band), т.е. срочно.

Функция WSASend выглядит следующим образом:

int WSASend ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE );

Рассмотрим параметры этой функции:

s — сокет, через который будет происходить отправка данных;

lpBuffers — структура или массив структур типа WSABUF. С этой структурой вы познакомились, когда рассматривали функцию connect. Эта же структура использовалась для отправки данных во время соединения;


dwBufferCount — количество структур в параметре lpBuffers;

lpNumberOfBytesSent — количество переданных байт для завершенной операции ввода/вывода;

dwFlags — определяет метод отправки и может принимать такие же значения, как и параметр dwFlags функции send;

pOverlapped и pCompletionRoutine — задаются при использовании пере-крытого ввода/вывода (overlapped I/O). Это одна из моделей асинхронной работы сети, поддерживаемой WinSock.

Если функция send (или WSASend) отработала успешно, то она вернет количество отправленных байт, иначе — значение —1 (или константу SOCKET_ERROR, которая равна -1). Получив ошибку, вы можете проанализировать ее с помощью функции WSAGetLastError:

WSAECONNABORTED — соединение было разорвано, или вышло время ожидания или произошла другая ошибка;

WSAECONNRESET — удаленный компьютер разорвал соединение, и вам необходимо закрыть сокет;

WSAENOTCONN — соединение не установлено;

WSAETIMEDOUT — время ожидания ответа вышло.

Для получения данных используются функции recv и WSARecv (для второй версии WinSock). Функция recv выглядит следующим образом:

int recv ( SOCKET s, char FAR * buf, int len, int flags );

Параметры очень похожи на те, которые описаны для функции send:

s — сокет, данные которого надо получить;

buf — буфер, в который будут помещены принятые данные;

len — длина буфера в параметре buf;

flags — флаги, определяющие метод получения. Здесь можно указывать сочетание из следующих значений:

0 — флаги не указаны;

MSG_PEEK — считать данные из системного буфера без удаления. По умолчанию считанные данные стираются из системного буфера;

MSG_OOB — обработать срочные данные out of band.

Использовать флаг MSG_PEEK не рекомендуется, потому что вы можете встретиться с множеством непредсказуемых проблем. В этом случае функцию recv придется вызывать второй раз (без этого флага), чтобы удалить данные из системного буфера. При повторном считывании в буфере может оказаться больше данных, чем в первый раз (за это время компьютер может получить на порт дополнительные пакеты), и вы рискуете обработать данные дважды или не обработать что-то вообще. Еще одна проблема заключается в том, что системная память не очищается, и с каждым разом остается меньше пространства для поступающих данных. Именно поэтому я рекомендую использовать флаг MSG_PEEK только при необходимости и очень аккуратно.



Функция WSARecv выглядит следующим образом:

int WSARecv ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE );

Здесь также бросается в глаза сходство в параметрах с функцией WSASend. Давайте рассмотрим их назначение:

s — сокет, через который будет происходить получение данных;

lpBuffers — структура или массив структур типа WSABUF. В эти буферы будут помещены полученные данные;

dwBufferCount — количество структур в параметре lpBuffers;

lpNumberOfBytesSent — количество полученных байт, если операции ввода/вывода уже завершились;

dwFlags — определяет метод отправки и может принимать такие же значения, как и параметр dwFlags функции recv. Но есть один новый флаг — MSG_PARTIAL. Его нужно указывать для протоколов, ориентированных на чтение сообщения в несколько приемов. В случае указания этого флага при каждом считывании можно получить только часть данных;

pOverlapped и pCompletionRoutine — устанавливаются при использова-нии перекрытого ввода/вывода (overlapped I/O). Это одна из моделей асинхронной работы сети, поддерживаемой WinSock.

Стоит заметить, что если вы используете протокол, ориентированный на передачу сообщений (UPD), и указали недостаточный размер буфера, то любая функция для получения данных вернет ошибку WSAEMSGSIZE. Если протокол потоковый (TCP), то такая ошибка не возникнет, потому что получаемые данные кэшируются в системе и предоставляются приложению полностью. В этом случае, если указан недостаточный буфер, то оставшиеся данные можно получить при следующем считывании.

Есть еще одна интересная сетевая функция, которая появилась в WinSock2. Если все рассмотренные в этой главе функции сетевой библиотеки (без префикса WSA) существуют не только в Windows, но и в UNIX-системах, то функция TransmitFile является расширением Microsoft и работает только в Windows.

Функция TransmitFile отсылает по сети целый файл. Это происходит достаточно быстро, потому что отправка идет через ядро библиотеки. Вам не надо заботиться о последовательном чтении и проверять количество отправленных данных, потому что это гарантируется библиотекой WinSock2.



Функция выглядит следующим образом:

BOOL TransmitFile( SOCKET hSocket, HANDLE hFile, DWORD nNumberOfBytesToWrite, DWORD nNumberOfBytesPerSend, LPOVERLAPPED lpOverlapped, LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, DWORD dwFlags );

Рассмотрим ее параметры:

hSocket — сокет, через который нужно отправить данные;

hFile — указатель на открытый файл, данные которого надо отправить;

nNumberOfBytesToWrite — количество отправляемых из файла байт. Если указать о, то будет отправлен весь файл;

nNumberOfBytesPerSend — размер пакета для отправки. Если указать 1024, то данные будут отправляться пакетами в 1024 байт данных. Если указать 0, то будет использовано значение по умолчанию;

lpOverlapped — используется при перекрестном вводе/выводе;

lpTransmitBuffers — содержит служебную информацию, которую надо послать до и после отправки файла. По этим данным на принимающей стороне можно определить начало или окончание передачи;

dwFlags — флаги. Здесь можно указать следующие значения:

TF_DISCONNECT — закрыть сокет после передачи данных;

TF_REUSE_SOCKET — подготовить сокет для повторного использования;

TF_WRITE_BEHIND — завершить работу, не дожидаясь подтверждения о получении данных со стороны клиента.

Параметр lpTransmitBuffers имеет тип структуры следующего вида:

typedef Struct _TRANSMIT_FILE_BUFFERS { PVOID Head; DWORD HeadLength; PVOID Tail; DWORD TailLength; } TRANSMIT_FILE_BUFFERS;

У этой структуры следующие параметры:

Head — указатель на буфер, содержащий данные, которые надо послать клиенту до начала отправки файла;

HeadLength — размер буфера Head;

Tail — указатель на буфер, содержащий данные, которые надо послать клиенту после завершения отправки файла;

TailLength — размер буфера Tail.


Обработка ошибок


В самом начале необходимо узнать, как можно определить ошибки, которые возникают при вызове сетевых функций. Правильная обработка ошибок для любого приложения является самым важным. Хотя сетевые функции не критичны для ОС, но могут повлиять на ход работы программы. А это в свою очередь может привести к понижению безопасности системы.

Сетевые приложения обмениваются данными с чужими компьютерами, а это значит, что в качестве стороннего клиента может выступить злоумышленник. Если не обработать ошибку, это может привести к доступу к важным данным или функциям.

Приведу простейший пример. У вас есть функция, которая вызывается каждый раз, когда программе пришли данные, и проверяет их на корректность и права доступа. Если все нормально, то функция выполняет критический код, который не должен быть доступен злоумышленнику. Контроль должен происходить на каждом этапе: получение данных, проверка их корректности и доступности, а также любой вызов сетевой функции. Помните, чтоэто придаст стабильность и надежность вашей программе и обеспечит безопасность всей системы.

Если во время выполнения какой-либо функции произошла ошибка, то она вернет константу SOCKET_ERROR или -1. Если вы получили такое значение, то можно воспользоваться функцией WSAGetLastError. Ей не надо передавать параметры, она просто возвратит код ошибки, которая произошла во время выполнения последней сетевой функции. Кодов ошибок очень много, и они зависят от функции, которая отработала последней. Я буду рассматривать их по мере необходимости.



Обработка принимаемых данных


Все принятые по сети данные следует тщательно верифицировать. Если необходимо разделить доступ к определенным возможностям по паролю, то я рекомендую первым делом контролировать права на выполнение команды. После этого проверяйте корректность указанной команды и переданные параметры.

Допустим, что клиент запрашивает у сервера какие-либо файлы. Если вы сначала проверите правильность указания пути и имени файла и в случае неудачи выведете сообщение об ошибке, то хакер будет знать, что файла в системе нет. Иногда хакеру этой информации может оказаться достаточно для поиска пути проникновения в систему. Именно поэтому сначала нужно проверять право на выполнение команды, а потом уже разбирать параметры и оценивать их корректность.

Если есть возможность, то команды лучше проверять жестко. Например, команду get filename необходимо проверять так, чтобы первые три буквы составляли слово "get". Нельзя делать поиск символов "get" во всем полученном тексте, т. к. для хакера открывается множество возможностей передать неверные данные. Большинство взломов происходит из-за неправильного анализа полученных данных и передачи некорректных данных серверу.

Если вы разрабатываете протокол обмена командами между клиентом и сервером, то делайте так, чтобы команда передавалась в самом начале, а все параметры шли в конце. Допустим, что у вас есть команда get. Она может работать в двух режимах:

Забрать файл со стороннего сервера — GET имя файла FROM Адрес

Забрать файл с клиента, который подключился к серверу — GET Имя файла

Первая команда с точки зрения безопасности неэффективна. Для определения наличия ключевого слова FROM придется делать поиск по строке. Этого делать нельзя. Все ключевые слова желательно искать в жестко определенной позиции. В данном случае первую команду желательно преобразовать к следующему виду:

GET FROM Имя файла, Адрес

В этом случае ключевые слова идут в начале команды, и вы жестко можете определить их наличие. Если хакер попытается использовать неправильные параметры, то у него ничего не выйдет.


Если передаваемые данные разнообразны, но могут быть подведены под какой-то шаблон, то обязательно используйте его при контроле. Это поможет вам сделать дополнительную проверку корректности данных, но не может обеспечить полную защиту. Именно в шаблонной проверке программисты чаще всего допускают ошибки. Чем сложнее проверка или шаблон, тем труднее учесть все ограничения на передаваемые данные. Прежде чем использовать программу в боевых условиях или в коммерческих целях, рекомендуется уделить тестированию этого участка максимально возможное время. Желательно, чтобы программу тестировал сторонний человек, потому что только конечный пользователь или хакер введет те данные, о которых вы даже не подозревали и не думали, что их вообще можно использовать.

Задача усложняется, если по этим данным будет происходить доступ к файловой системе. Это может привести к нерегламентированному доступу к вашему диску со всеми вытекающими последствиями. Когда в качестве параметра указывается путь к файлу, то его легко проверить по шаблону, но очень легко ошибиться. Большинство программистов просто проверяют начало пути, но это ошибка.

Допустим, что у вас открыт доступ только к папке interpub на диске С:. Если проверять только начало пути, то хакер сможет без проблем написать вот такой путь:

c:\interpub\..\winnt\system32\cmd.eхе\

Здесь, благодаря двойной точке, хакер выходит из папки interpub и получает доступ ко всему диску, в том числе и системным файлам.

Прежде чем писать проверку по шаблону, вы должны ознакомиться со всеми его исключительными ситуациями. И еще раз напоминаю, что вы должны максимально тестировать программу даже с самыми невероятными параметрами. Пользователи непредсказуемы, особенно неопытные, а хакеры достаточно умны и изучают систему со всех сторон, даже с тех, о которых вы не подозреваете.


Передача данных по сети с помощью CSocket


Как я уже говорил, работа с сокетами происходит по технологии "клиент-сервер". Сервер запускается на определенном порту и начинает ожидать соединение. Клиент подключается на этот порт, и после этого может обмениваться данными с сервером.

Посмотрим, как передача данных выглядит на практике. Создайте новый проект MFC Application и назовите его MFCSendText. В мастере измените параметры так же, как и в предыдущем примере со сканером портов (см. разд. 4.4). Точно так же добавьте класс от TSocket. Точнее сказать, два класса: один для клиента, а другой — для сервера, и будут они называться CClientSocket и CServerSocket соответственно. Как видите, из одного класса CSocket выводятся два класса: для сервера и для клиента.

Теперь оформим главное окно программы. Для этого откройте в редакторе ресурсов диалоговое окно IDD_MFCSENDTEXT_DIALOG и поместите на него четыре кнопки с заголовками Create Server (IDC_BUTTON1), Connect to Server (IDC_BUTTON2), Send Data (IDC_BUTTON3), Disconnect (IDC_BUTTON4). Внизу окна поместите Static Text для вывода сообщений.

Для кнопки Send Data создайте переменную. Для этого надо щелкнуть по ней правой кнопкой мышки и в появившемся меню выбрать пункт Add Variable. В окне Мастера создания переменной в поле Variable name укажите m_SendButton.

Теперь переходим к программированию. Для начала рассмотрим файл ServerSocket.h, в котором находится объявление класса CServerSocket. Его содержимое вы можете увидеть в листинге 4.6.

Листинг 4.6. Содержимое файла ServerSocket.h
#pragma once

#include "MFCSendTextDlg.h"

// CServerSocket command target // (Определение класса CServerSocket)

class CServerSocket : public CSocket { public: CServerSocket(CMFCSendTextDlg* Dlg); virtual ~CServerSocket(); virtual void OnAccept(int nErrorCode); virtual void OnConnect(int nErrorCode); protected: CMFCSendTextDlg* m_Dlg; public: virtual void OnClose(int nErrorCode); };

Первое, что я изменил — это конструктор. Теперь CServerSocket имеет один параметр Dlg типа CMFCSendTextDlg. Через этот параметр будет передаваться указатель на основной класс, чтобы была возможность обращаться к нему из класса CServerSocket. В разделе protected объявлена переменная для хранения указателя на класс главного окна.


class CClientSocket : public CSocket { public: CClientSocket(CMFCSendTextDlg* Dlg); virtual ~CClientSocket(); virtual void OnReceive(int nErrorCode); virtual void OnClose(int nErrorCode); protected: CMFCSendTextDlg* m_Dlg; };

Здесь также модифицирован конструктор, чтобы сохранять информацию о классе, создавшем класс клиента CClientSocket. Для этого заведена такая же переменная m_Dlg.

Помимо этого, введены два метода: OnReceive (вызывается, когда по сети пришли новые данные) и OnClose (вызывается, когда соединение завершено).

Теперь посмотрим, как все это реализовано в файле ClientSocket.cpp (листинг 4.9).

Листинг 4.9. Содержимое файла ClientSocket.cpp
// ClientSocket.cpp : implementation file

#include "stdafx.h" #include "MFCSendText.h" #include "ClientSocket.h"

// CClientSocket

CClientSocket::CClientSocket(CMFCSendTextDlg* Dlg) { m_Dlg = Dlg; }

CClientSocket::~CClientSocket() { }

void CClientSocket::OnReceive(int nErrorCode) { char recstr[1000]; int r=Receive(recstr,1000); recstr[r]='\0'; m_Dlg-SetDlgItemText(IDC_STATIC, recstr);

CSocket::OnReceive(nErrorCode); }

void CClientSocket::OnClose(int nErrorCode) { m_Dlg-m_SendButton.EnableWindow(FALSE);

CSocket::OnClose(nErrorCode); }

Самое важное находится в методе OnReceive. Он вызывается каждый раз, когда для клиента пришли по сети какие-то данные. Для чтения полученных данных используется метод Receive. У него два параметра:

буфер, в который будут записаны полученные данные, — переменная recstr;

размер буфера.

Метод возвращает количество полученных по сети данных. Это значение записывается в переменную r. Теперь в переменной recstr находятся полученные данные, но по правилам языка С строки должны заканчиваться нулевым символом. Добавим его в буфер за последним полученным символом:

recstr[r]='\0';

Теперь полученный текст копируем в компонент Static Text на диалоговом окне с помощью следующей строки кода:

m_Dlg-SetDlgItemText(IDC_STATIC, recstr);

Метод OnClose вызывается каждый раз, когда соединение завершено. В его коде кнопку Send Data надо сделать недоступной, потому что без соединения с сервером нельзя отправлять данные.



m_Dlg-m_SendButton.EnableWindow(FALSE);

Сейчас перейдем к рассмотрению главного модуля программы — MFCSendTextDlg. Начнем разбор с заголовочного файла (листинг 4.10).

Листинг 4.10. Заголовочный файл MFCSendTextDlg.h
// MFCSendTextDlg.h : header file

#pragma once #include "afxwin.h"

class CServerSocket; class CClientSocket;

class CMFCSendTextDlg : public CDialog { // Construction (Коструктор) public: // standard constructor // (стандартный конструктор) CMFCSendTextDlg(CWnd* pParent = NULL);

// Dialog Data (Данные диалога) enum { IDD = IDD_MFCSENDTEXT_DIALOG };

protected: // DDX/DDV support (Поддержка обмена данными) virtual void DoDataExchange(CDataExchange* pDX);

// Implementation protected: HICON m_hIcon; CServerSocket* m_sSocket; CClientSocket* m_cSocket; CClientSocket* m_scSocket;

// Generated message map functions // (Сгенерированные функции карты сообщений) virtual BOOL OnInitDialog(); afx_msg void OnSysCommand(UINT nID, LPARAM lParam); afx_msg void OnPaint(); afx_msg HCURSOR OnQueryDragIcon(); DECLARE_MESSAGE_MAP() public: afx_msg void OnBnClickedButton1(); afx_msg void OnBnClickedButton2(); afx_msg void OnBnClickedButton3(); CButton m_SendButton; afx_msg void OnBnClickedButton4(); void AddConnection(); };

Здесь введены три переменные в разделе protected:

m_sSocket — указатель на класс CServerSocket;

m_cSocket и m_scSocket — указатели на класс CClientSocket.

А в разделе public добавлен один метод void AddConnection().

Теперь создайте поочередно обработчики события для всех кнопок диалогового окна. Для этого необходимо щелкнуть на кнопке правой кнопкой мышки и в появившемся меню выбрать пункт Add Event Handler. Давайте рассмотрим каждый обработчик события в отдельности.

Для кнопки Create Server будет следующий обработчик:

void CMFCSendTextDlg::OnBnClickedButton1() { // TODO: Add your control notification handler code here m_sSocket=new CServerSocket(this); m_sSocket-Create(22345); m_sSocket-Listen(); SetDlgItemText(IDC_STATIC, "Server started"); }



Здесь необходимо создать сервер и запустить прослушивание порта (ожидание соединений клиентов). В первой строке инициализируется переменная m_sSocket. Она имеет тип класса CServerSocket, поэтому в качестве параметра надо передать конструктору указатель на текущий класс. Это делается с помощью ключевого слова this.

После этого вызывается метод Create, у которого в качестве единственного параметра необходимо указать номер порта, на котором будет работать сервер. Теперь можно запускать прослушивание с помощью метода Listen.

Сервер запущен, и через компонент Static Text в окне диалога выводится соответствующее сообщение.

В обработчике события для кнопки Connect To Server надо написать следующий код:

void CMFCSendTextDlg::OnBnClickedButton2() { // ТОDО: Add your control notification handler code here m_cSocket = new CClientSocket(this); m_cSocket-Create(); if (m_cSocket-Connect("127.0.0.1", 22345)) m_SendButton. EnableWindow( TRUE); }

В первой строке инициализируется переменная m_cSocket. Следующей строкой кода создается класс. Теперь можно соединяться с сервером. Для этого используется метод Connect. Существует несколько реализаций данного метода, и они отличаются количеством и типом передаваемых параметров. В нашем случае используются следующие параметры:

IP-адрес в виде строки;

порт, на который необходимо подключиться.

Если соединение прошло успешно, то метод вернет ненулевое значение. Проверяется результат, и если все нормально, то кнопка Send Data делается доступной.

Отправка данных происходит, когда пользователь нажимает кнопку Send Data. Код, который должен находиться в обработчике события, выглядит следующим образом:

void CMFCSendTextDlg::0nBnClickedButton3() { // TODO: Add your control notification handler code here m_cSocket-Send("Hello", 100);

int err=m_cSocket-GetLastError(); if(err0) { CString ErrStr; ErrStr.Format("errcode=%d",err); AfxMessageBox ( ErrStr ); } }

Отправка данных происходит с помощью метода Send объекта-клиента m_cSocket. У него два параметра:



данные, которые надо отправить, — строка "Hello";

размер данных. В данном случае нужно было бы указать 5, потому что отправляемое слово содержит 5 символов (5 байт), но в примере указано 100. Это не приведет к ошибке, но позволит вам легко изменять отправляемую строку. В реальных приложениях обязательно указывайте истинную длину строки.

До этого я практически не проверял производимые действия на ошибки. А при запуске сервера это делать необходимо, потому что, если сервер уже запущен, то повторная попытка приведет к ошибке. Кроме того, может возникнуть ситуация, когда на компьютере пользователя не установлен протокол TCP, тогда тоже будут ошибки.

При отправке данных такая проверка есть. С помощью метода GetLastError можно получить код ошибки последней операции в классе m_cSocket. Если результат метода GetLastError больше нуля, то была ошибка.

В обработчике события кнопки Disconnect выполняется следующий код:

void CMFCSendTextDlg::OnBnClickedButton4() { // ТODO: Add your control notification handler code here SetDlgItemText(IDC_STATIC, "Disconnected"); m_cSocket-Close(); }

Первой строкой в текстовое поле в окне диалога выводится сообщение о том, что соединение разорвано. Во второй строке вызывается метод Close, который закрывает соединение с сервером.

Теперь самое интересное — метод AddConnection, который я уже использовал, когда произошло соединение с сервером. Посмотрим, что в нем происходит:

void CMFCSendTextDlg::AddConnection() { m_scSocket = new CClientSocket(this); m_sSocket-Accept(*m_scSocket); }

Как видите, здесь создается новый объект типа CClientSocket. После этого он присоединяется к серверу m_sSocket методом Accept. Так переменная класса CClientSocket связывается с новым соединением. Именно через эту переменную сервер может отослать данные к клиенту, и именно через нее он принимает данные.

Получается, что один класс CClientSocket используется на клиенте для соединения с сервером и отправки ему данных, а на сервере — для получения и возврата данных. Класс CServerSocket используется только для прослушивания порта и получения соединения.



Данный пример будет хорошо работать только тогда, когда один клиент соединяется с сервером. Если второй клиент попытается соединиться, то переменная m_scSocket будет перезаписана для нового клиента. Именно поэтому на сервере вы должны хранить динамический массив классов типа CClientSocket. При подключении клиента вы должны создавать новый класс типа CClientSocket и сохранять его в массиве, а при отключении клиента соответствующий класс должен уничтожаться из массива.

Напоследок хочется заметить, что я нигде не указывал протокол, по которому будут работать клиент с сервером. По умолчанию класс CSocket использует TCP/IP.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\MFCSendText.

Прием и передача данных


Вы уже познакомились в теории и на практике, как принимать и передавать данные между компьютерами. Но искусство хакера состоит в том, чтобы правильно использовать различные методы и режимы передачи. Существует два режима работы сокетов, и вы должны научиться правильно их использовать, потому что это повысит эффективность и скорость ваших программ.

Применяют следующие режимы сетевого ввода/вывода (прием/передача данных):

Блокирующий (синхронный) — при вызове функции передачи программа останавливает выполнение и ожидает завершения операции.

Не блокирующий (асинхронный) — после вызова функции программа продолжает выполнение вне зависимости от того, закончена операция приема/передачи или нет.

При описании функций мы уже сталкивались с этими понятиями (см. разд. 4.6.5 и 4.6.6), а сейчас остановлюсь на них более подробно, потому что благодаря им можно значительно повысить скорость работы и максимально использовать ресурсы.

По умолчанию создаются блокирующие сокеты, поэтому во всех примерах, которые уже рассматривались в этой главе, использовался синхронный режим как наиболее простой. В этом случае приходится создавать потоки, внутри которых работают сетевые функции, чтобы главное окно программы не блокировалось и реагировало на события от пользователя.

Но это не самая главная проблема. Простота и надежность — несовместимые понятия. Допустим, что был вызов функции recv, но по каким-то причинам она не вернула данные. В этом случае она останется заблокированной навечно, и сервер больше не будет реагировать на действия пользователя. Во избежание этой проблемы некоторые программисты перед считыванием данных проверяют их корректность с помощью вызова функции recv с флагом MSG_PEEK. Но вы уже знаете, что это не безопасно, и доверять таким данным не стоит. К тому же этот метод нагружает систему лишними проверками буфера приема на наличие данных.

Неблокирующие сокеты сложнее в программировании, но лишены описанных недостатков. Чтобы перевести сокет в асинхронный режим, нужно воспользоваться функцией ioctlsocket, которая выглядит так:


int ioctisocket ( SOCKET s, long cmd, u_long FAR* argp );

У этой функции три параметра:

сокет, режим которого надо изменить;

команда, которую необходимо выполнить;

параметр для команды.

Изменение режима блокирования происходит при указании в качестве команды константы FIONBIO. При этом, если параметр команды имеет нулевое значение, то будет использоваться блокирующий режим, иначе — неблокирующий.

Давайте посмотрим на пример создания сокета и перевода его в неблокирующий режим:

SOCKET s; unsigned long ulMode;

s = socket(AS_INET, SOCK_STREAM, 0); ulMode = 1; ioctisocket(s, FIONBIO, (unsigned long*)ulMode);

Теперь все функции приема/передачи будут завершаться ошибкой. Это нормальная реакция, и вы должны это учитывать при создании сетевых приложений, работающих в неблокирующем режиме. Если функция ввода/вывода вернула ошибку WSAEWOULDBLOCK, то это не означает неправильную передачу. Все прошло успешно, просто используется неблокирующий режим. Если же действительно произошел сбой, то мы получим ошибку, отличную от WSAEWOULDBLOCK.

В неблокирующем режиме функция recv не будет дожидаться приема данных, а просто вернет ошибку WSAEWOULDBLOCK. Тогда как нам узнать, что данные поступили на порт? Некоторые запускают цикл с постоянным вызовом функции recv, пока она не вернет данные. Но это нецелесообразно, потому что происходит блокирование приложения и излишне загружается процессор.

Конечно же, вы можете в цикле между проверками выполнять какие-то действия и тем самым использовать процессор во время ожидания с пользой, но я не буду рассматривать этот вариант, потому что есть способ лучше.


Прикладные протоколы — загадочный NetBIOS


NetBIOS (Network Basic Input Output System, базовая система сетевого ввода/вывода) — это стандартный интерфейс прикладного программирования. А проще говоря, это всего лишь набор API-функций для работы с сетью (хотя весь NetBIOS состоит только из одной функции, но зато какой...). NetBIOS был разработан в 1983 году компанией Sytek Corporation специально для IBM.

Система NetBIOS определяет только программную часть передачи данных, т.е. как должна работать программа для передачи данных по сети. А вот как будут физически передаваться данные, в этом документе не говорится ни слова, да и в реализации отсутствует что-нибудь подобное.

Если посмотреть на 4.1, то можно увидеть, что NetBIOS находится в самом верху схемы. Он расположен на уровнях сеанса, представления и приложения. Такое его расположение — лишнее подтверждение моих слов.

NetBIOS только формирует данные для передачи, а физически передаваться они могут только с помощью других протоколов, например, TCP/IP, IPX/SPX и т.д. Это значит, что NetBIOS является независимым от транспорта. Если другие варианты протоколов верхнего уровня (только формирующие пакеты, но не передающие) привязаны к определенному транспортному протоколу, который должен передавать сформированные данные, то пакеты NetBIOS может передавать любой другой протокол. Прочувствовал силу? Представьте, что вы написали сетевую программу, работающую через NetBIOS. А если вы еще не знаете, то она будет прекрасно работать как в UNIX/Windows-сетях через TCP, так и в Novell-сетях через IPX.

С другой стороны, для того, чтобы два компьютера смогли соединиться друг с другом по NetBIOS, необходимо, чтобы на обоих стоял хотя бы один общий транспортный протокол. Если один компьютер будет посылать NetBIOS -пакеты через TCP, а другой — с помощью IPX, то эти компьютеры друг друга не поймут. Транспорт должен быть одинаковый.

Стоит сразу же отметить, что не все варианты транспортных протоколов по умолчанию могут передавать по сети NetBIOS-пакеты. Например, IPX/SPX сам по себе этого не умеет. Чтобы его обучить, нужно иметь "NWLink IPX/SPX/NetBIOS Compatible Transport Protocol".

Так как NetBIOS чаще всего использует в качестве транспорта протокол TCP, который работает с установкой виртуального соединения между клиентом и сервером, то по этому протоколу можно передавать достаточно важные данные. Целостность и надежность передачи будет осуществлять TCP/IP, a NetBIOS дает только удобную среду для работы с пакетами и программирования сетевых приложений. Так что если вам нужно отправить в сеть какие-либо файлы, то можно смело положиться на NetBIOS.



Пример работы TCP-клиента


Сервер готов, теперь можно приступить к написанию клиентской части. Для этого создайте новый проект Win32 Project и назовите его TCPClient.

Найдите функцию _tWinMain и до цикла обработки сообщений добавьте следующий код:

WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { MessageBox(0, "Can't load WinSock", "Error", 0); return 0; }

HANDLE hNetThread; DWORD dwNetThreadId; hNetThread = CreateThread(NULL, 0, NetThread, 0, 0, dwNetThreadId);

Здесь также загружается библиотека WinSock версии 2.2, хотя функции будут использоваться только из первой версии и достаточно было бы ее. Но я напоминаю, что в более новой версии могут быть исправлены какие-то ошибки, и в учебных целях я решил использовать эту версию.

Как и в случае с сервером, для работы с сетью будет использоваться отдельный поток, но для клиента достаточно только одного. Он также создается функцией CreateThread, а в качестве третьего параметра передается имя функции, которая будет выполняться в отдельном потоке — NetThread. Ее еще нет в созданном проекте, поэтому давайте введем сейчас. Добавьте до функции _tWinMain код из листинга 4.13.

Листинг 4.13. Поток работы с сетью
DWORD WINAPI NetThread(LPVOID lpParam) { SOCKET sClient; char szBuffer[1024]; int ret, i; struct sockaddr_in server; struct hostent *host = NULL; char szServerName[1024], szMessage[1024];

strcpy(szMessage, "get"); strcpy(szServerName, "127.0.0.1");

// Создание сокета sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sClient == INVALID_SOCKET) { MessageBox(0, "Can't create socket", "Error", 0); return 1; } // Заполнение структуры с адресом сервера и номером порта server.sin_family = AF_INET; server.sin_port = htons(5050); server.sin_addr.s_addr = inet_addr(szServerName);

// Если указано имя, то перевод символьного адреса сервера в IP if (server.sin_addr.s_addr == INADDR_NONE) { host = gethostbyname(szServerName); if (host == NULL) { MessageBox(0, "Unable to resolve server", "Error", 0); return 1; } CopyMemory(server.sin_addr, host-h_addr_list[0], host-h_length); } // Соединение с сервером if (connect(sClient, (struct sockaddr *)server, sizeof(server)) == SOCKET_ERROR) { MessageBox(0, "connect failed", "Error", 0); return 1; }


// Отправка и прием данных ret = send(sClient, szMessage, strlen(szMessage), 0); if (ret == SOCKET_ERROR) { MessageBox(0, "send failed", "Error", 0); }

// Задержка Sleep(1000);

// Получение данных char szRecvBuff[1024]; ret = recv(sClient, szRecvBuff, 1024, 0); if (ret == SOCKET_ERROR) { MessageBox(0, "recv failed", "Error", 0); } MessageBox(0, szRecvBuff, "Recived data", 0); closesocket(sClient); }

Давайте подробно рассмотрим, что здесь происходит. В переменной szMessage хранится текст сообщения, которое отправляется серверу. Для примера жестко определена строка "get". В переменной szServerName указывается адрес сервера, с которым нужно произвести соединение. В данном случае установлен адрес 127.0.0.1, что соответствует локальному компьютеру. Это значит, что серверная и клиентская программы должны запуститься на одном и том же компьютере. После этого создается сокет так же, как и при создании сервера.

Следующим этапом надо подготовить структуру типа sockaddr_in (в нашем случае это структура server), в которой нужно указать семейство протоколов, порт (у сервера мы использовали 5050) и адрес сервера.

В примере указан IP-адрес, но в реальной программе у вас может быть и имя удаленного компьютера, которое нужно привести к IP. Именно поэтому адрес проверяется на равенство константе INADDR_NONE:

if (server.sin_addr.s_addr == INADDR_ NONE)

Если условие выполняется, то в качестве адреса указано символьное имя, и тогда с помощью функции gethostbyname выполняется преобразование в IP-адрес. Результат записывается в переменную типа hostent. Как я уже говорил, компьютер может иметь несколько адресов, тогда результатом будет массив структур типа hostent. Чтобы не усложнять задачу, просто возьмите первый адрес, который можно получить так: host-h_addr_list[0].

Теперь все готово к соединению с сервером. Для этого будет использоваться функция connect. Ей указывается созданный сокет, структура с адресом и размер структуры. Если функция вернет значение, отличное от SOCKET_ERROR, т о соединение прошло успешно, иначе произошла ошибка.



Следующим этапом отправляются данные серверу с помощью функции send. Вроде бы все отправлено, и сервер должен ответить, но не стоит торопиться читать данные из буфера, потому что на передачу и обработку сервером информации нужно время. Если сразу после отправки попробовать вызвать функцию recv, то мы скорей всего получим ошибку, потому что данные еще не поступили. Именно поэтому после функции send нужно сделать задержку.

В реальной программе задержку делать не стоит, потому что можно поступить другим способом, например, запустить цикл получения сообщения и ожидать, пока функция recv вернет данные, а не ошибку. Это самый простой способ, который в данном случае будет работать корректно и достаточно эффективно.

Для компиляции проекта, как в случае с сервером, необходимо подключить модуль winsock2.h и библиотеку ws2_32.lib.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\TCPCIient.

Пример работы ТСР-сервера


Начну с разработки сервера. Создайте новый проект Win32 Project с именем TCPServer. Откройте файл TCPServer.cpp и после объявления всех глобальных переменных, но до функции _twinMain, напишите две процедуры из листинга 4.12. Функции должны быть именно в таком порядке: сначала ClientThread, а затем — NetThread.

Листинг 4.12. Функции работы с сетью
DWORD WINAPI ClientThread(LPVOID lpParam) { SOCKET sock=(SOCKET)lpParam; char szRecvBuff[1024], szSendBuff[1024]; int ret; // Запуск бесконечного цикла while(1) { // Получение данных ret = recv(sock, szRecvBuff, 1024, 0); // Проверка полученных данных if (ret == 0) break; else if (ret == SOCKET_ERROR) { MessageBox(0, "Recive data filed", "Error", 0); break; } szRecvBuff[ret] = '\0';

// Здесь можно поставить проверку принятого текста // в переменной szRecvBuffer

// Подготовка строки для отправки клиенту strcpy(szSendBuff, "Command get OK");

// Отправка содержимого переменной szSendBuff клиенту ret = send(sock, szSendBuff, sizeof(szSendBuff), 0); if (ret == SOCKET_ERROR) { break; } } return 0; }

DWORD WINAPI NetThread(LPVOID lpParam) { SOCKET sServerListen, sClient; struct sockaddr_in localaddr, clientaddr; HANDLE hThread; DWORD dwThreadId; int iSize;

// Создание сокета sServerListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (sServerListen == SOCKET_ERROR) { MessageBox(0, "Can't load WinSock", "Error", 0); return 0; } // Заполнение структуры localaddr типа sockaddr_in localaddr.sin_addr.s_addr = htonl(INADDR_ANY); localaddr.sin_family = AF_INET; localaddr.sin_port = htons(5050);

// Связывание адреса с переменной localaddr типа sockaddr_in if (bind(sServerListen, (struct sockaddr *)localaddr, sizeof(localaddr)) == SOCKET_ERROR) { MessageBox(0, "Can't bind", "Error", 0); return 1; }

// Вывод сообщения об удачной операции bind MessageBox(0, "Bind OK", "Error", 0);

// Запуск прослушивания порта listen(sServerListen, 4);

// Вывод сообщения об удачном начале операции прослушивания MessageBox(0, "Listen OK", "Error", 0);


// Запуск бесконечного цикла while (1) { iSize = sizeof(clientaddr); // Прием соединения из очереди. Если его нет, // то функция будет ожидать соединения клиента sClient = accept(sServerListen, (struct sockaddr *)clientaddr, iSize); //Проверка корректности идентификатора клиентского сокета if (sClient == INVALID_SOCKET) { MessageBox(0, "Accept filed", "Error", 0); break; }

// Создание нового потока для работы с клиентом hThread = CreateThread(NULL, 0, ClientThread, (LPVOID)sClient, 0, dwThreadId); if (hThread == NULL) { MessageBox(0, "Create thread filed", "Error", 0); break; } CloseHandle(hThread); } // Закрытие сокета после работы с потоком closesocket(sServerListen); return 0; }

Теперь перейдем к рассмотрению написанного. В функции _tWinMain происходит загрузка библиотеки WinSock версии 2.2.

После этого создается новый поток с помощью функции CreateThread. Серверная функция accept блокирует работу программы, и если ее вызвать в основном потоке, то окно заблокируется и не будет реагировать на сообщения. Именно поэтому для сервера создается отдельный поток, в котором он и будет работать. Получается, что основная программа работает в своем потоке, а параллельно ей работает еще один поток, в котором сервер прослушивает необходимый порт.

С точки зрения программирования, поток — это функция, которая будет работать параллельно с другими потоками ОС. Таким образом, в ОС Windows реализуется многозадачность. За более подробной информацией о потоках обратитесь к документации или специализированной литературе по Visual C++.

В качестве третьего параметра функции CreateThread, создающей новый поток, необходимо передать указатель на функцию, которая должна работать в отдельном потоке.

Самое интересное происходит в функции NetThread. Все функции, которые там используются, мы уже рассмотрели, и здесь я только собрал все сказанное в одно целое.

Первым делом создается сокет функцией socket. Затем корректными параметрами заполняется структура localaddr, которая имеет тип sockaddr_in. Для предложенного сервера заполняются три параметра:



locaiaddr.sin_addr.s_addr — указывается флаг INADDR_ANY, чтобы принимать подключения с любого интерфейса, установленного в компьютере;

locaiaddr.sin_family — AF_INET, т.е. интернет-протоколы из семейства используемых протоколов;

locaiaddr.sin_port — порт номер 5050. На большинстве компьютеров он свободен.

После этого связывается заполненная структура с сокетом с помощью функции bind.

Теперь сокет готов к началу прослушивания порта с помощью функции listen. В качестве второго параметра указано число 4, что соответствует очереди из четырех клиентов. Если одновременно попытаются подключиться больше клиентов, то только первые четыре попадут в очередь, а остальные получат сообщение об ошибке.

Чтобы принимать соединения клиентов, запускается бесконечный цикл, в котором и будут обрабатываться все подключения. Почему бесконечный? Сервер должен всегда находиться в памяти и принимать подключения от клиентов в любое время.

Внутри цикла вызывается функция accept, чтобы принять соединение клиента из очереди. Как только соединение произошло, функция создаст сокет и вернет на него указатель, который сохраняется в переменной sClient. Прежде чем использовать новый сокет, его необходимо проверить на корректность. Если переменная sSocket будет равна INVALID_SOCKET, то с таким сокетом работать нельзя.

Если сокет корректный, то запускается еще один поток, в котором уже происходит обмен информацией (чтение данных, которые прислал клиент, ответы на запросы). Поток создается уже знакомой вам функцией CreateThread, а в качестве третьего параметра указывается функция ClientThread, которая и будет работать параллельно основной программе.

В качестве четвертого параметра функции CreateThread можно указывать любой параметр, и он будет передан функции потока. Логично будет указать клиентский сокет, чтобы в функции ClientThread знать сокет, с которым происходит работа.

В функции ClientThread передается только один параметр, в котором хранится то, что мы указали в качестве четвертого параметра при создании потока. В данном случае это указатель на сокет, и первая строка кода функции дает этот сокет, который сохраняется в переменной sock:



SOCKET sock=(SOCKET)lpParam;

В этой функции также запускается бесконечный цикл на случай, если клиент будет присылать серверу множество команд, и на них надо будет отвечать.

Внутри цикла сначала принимается текст с помощью функции recv. После этого полученные данные проверяются на корректность.

Если контроль прошел успешно, то можно проверить присланную клиентом команду. При написании троянского коня клиент может использовать запросы на высылку паролей, на перезагрузку компьютера, а может и запустить какую-нибудь шутку. Необходимо проверить, какой запрос пришел от сервера, и в зависимости от этого выполнить какие-либо действия.

Запросы могут приходить как простые текстовые команды, например, restart или sendmepassword. Так как мы только разбираем принцип действия троянского коня, но не создаем его, то в примере клиент будет посылать текстовую команду get. Сервер же будет отсылать обратно клиенту текст Command get OK. Текст сохраняется в переменной, содержимое которой и отправляется клиенту с помощью функции send:

strcpy(szSendBuff, "Command get OK");

ret = send(sock, szSendBuff, sizeof(szSendBuff), 0); if (ret == SOCKET_ERROR) { break ; }

Затем цикл повторяется от начала, и если сервер считает еще одну команду, то он ответит на нее, иначе цикл прервется.

Как я уже говорил, все сетевые функции описаны в файле winsock2.h, и его необходимо подключать к своему проекту, чтобы компилятор не выдавал ошибок. В самом начале файла с исходным кодом нашей программы найдите следующею строку:

#include "stdafx.h"

После нее добавьте подключение модуля winsock2.h:

#include winsock2.h

Чтобы собрать проект без ошибок, необходимо подключить библиотеку ws2_32.lib. Для этого щелкните правой кнопкой мыши по имени проекта в окне Solution Explorer и в появившемся меню выберите пункт Properties.

Перед вами откроется окно свойств, в котором надо перейти в раздел Configuration Properties/Linker/Input. Здесь в строке Additional Dependencies напишите имя библиотеки ws2_32.lib.

Вот и все, что относится к серверной программе. Запустив ее, вы должны будете увидеть два сообщения Bind OK и Listen OK. Если сообщения появились, то сервер работает корректно и находится в ожидании соединения со стороны клиента.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\TCPServer.
Но чтобы окончательно протестировать пример, надо написать программу клиента, который будет соединяться с сервером и отправлять ему команды. Именно это я покажу в следующем разделе.


Пример работы UDP-клиента


Теперь опишу программу клиента, который будет отправлять данные на сервер. Создайте новый проект Win32 Project и назовите его UDPClient. В данном случае можно обойтись без дополнительных потоков и отправить данные прямо из функции _tWinMain. Это связано с тем, что передача по протоколу UDP не делает задержек, и данные могут отправляться практически мгновенно. Поэтому не имеет смысла делать многозадачное приложение, что значительно упрощает задачу.

Откройте файл UDPClient.cpp и перед циклом обработки событий напишите код из листинга 4.17.

Листинг 4.17. Отправка данных UDP-серверу
WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { MessageBox(0, "Can't load WinSock", "Error", 0); return 0; }

SOCKET sSocket; struct sockaddr_in servaddr; char szServerName[1024], szMessage[1024]; struct hostent *host = NULL;

strcpy(szMessage, "This is message from client"); strcpy(szServerName, "127.0.0.1");

sSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sSocket == INVALID_SOCKET) { MessageBox(0, "Can't create socket", "Error", 0); return 0; } servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5050); servaddr.sin_addr.s_addr = inet_addr(szServerName);

if (servaddr.sin_addr.s_addr == INADDR_NONE) { host = gethostbyname(szServerName); if (host == NULL) { MessageBox(0, "Unable to resolve server", "Error", 0); return 1; } CopyMemory(servaddr.sin_addr, host-h_addr_list[0], host-h_length); }

sendto(sSocket, szMessage, 30, 0, (struct sockaddr *)servaddr, sizeof(servaddr));

Как и в случае с сервером, необходимо создать сокет, у которого в качестве второго параметра указано значение SOCK_DGRAM. Третий параметр определяет протокол, в данном случае это IPPROTO_UDP.

После этого заполняется переменная servaddr типа sockaddr_in, которая содержит адрес и порт компьютера, которому нужно отправить данные. Если в качестве адреса указано символьное имя, то оно преобразуется в IP-адрес так же, как и при ТСР-клиенте.


Теперь можно напрямую без соединения с сервером отправлять данные функцией sendto. В данном случае серверу отправляется содержимое переменной szMessage.

Не забывайте, что для компиляции примеров, использующих работу с сетью, необходимо подключить библиотеку ws2_32.lib (см. разд. 4.7.1).

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\UDPCIient.

Пример работы UDP-сервера


Создайте новый проект Win32 Project и назовите его UDP Server. Откройте файл UDPServer . cpp и добавьте в функцию _tWinMain перед циклом обработки сообщений следующий код:

WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { MessageBox(0, "Can't load WinSock", "Error", 0); return 0; }

HANDLE hNetThread; DWORD dwNetThreadId; hNetThread = CreateThread(NULL, 0, NetThread, 0, 0, dwNetThreadId);

Как и в случае с TCP-сервером загружается библиотека WinSock и создается новый поток, в котором и будет происходить работа с сетью. Сама функция потока показана в листинге 4.16.

Листинг 4.16. Функция работы с сетью
DWORD WINAPI NetThread(LPVOID lpParam) { SOCKET sServerListen; struct sockaddr_in localaddr, clientaddr; int iSize;

sServerListen = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sServerListen == INVALID_SOCKET) { MessageBox(0, "Can't create socket", "Error", 0); 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", "Warning", 0);

char buf[1024];

while (1) { iSize = sizeof(clientaddr); int ret = recvfrom(sServerListen, buf, 1024, 0, (struct sockaddr *)clientaddr, iSize); MessageBox(0, buf, "Warning", 0); } closesocket(sServerListen); return 0; }

Во время создания сокета функцией socket, указывается параметр SOCK_DGRAM, что означает необходимость использования протокола, основанного на сообщениях. В качестве последнего параметра нужно указать константу, точно определяющую протокол. В данном случае можно явно указать UDP -протокол с помощью константы IPPROTO_UDP или просто указать значение 0.

Все остальное вам уже должно быть понятно. После создания сокета нужно привязать его к локальному адресу функцией bind. Для UDP-сервера этого достаточно. В примере после связывания сокета запускается бесконечный цикл, который вызывает функцию recvfrom для получения данных от клиента.

При получении данных сервер просто выводит на экран окно с полученной информацией. Адрес отправителя сохраняется в переменной clientaddr, и его можно использовать для отправки ответа клиенту.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\UDPServer.



Примеры работы по протоколу UDР


Как вы уже могли понять, работа с протоколами, к каким относится UDP , происходит несколько иначе. Так как нет соединения между клиентом и сервером, то не нужно использовать некоторые функции, а точнее сказать, их использовать нельзя.

Функции, необходимые для работы с протоколом UDP, я уже описал в разд. 4.6.8, и теперь вам предстоит увидеть реальный пример и применить полученные знания на практике.



Примеры работы с сетью по протоколу TCP


Теперь пора на практике увидеть, как можно организовать работу в сети с помощью функций библиотеки WinSock. Для этого продемонстрирую небольшую программу, в которой клиент будет посылать запросы серверу, а тот будет на них отвечать. На основе этого примера можно понять, как хакеры создают троянских коней и управляют или воруют данные с удаленного компьютера.



Принцип работы протоколов без установки соединения


Все описанное выше относится к протоколам с установкой соединения между клиентом и сервером (протокол TCP), но существуют протоколы без установки соединения (например, UDP). Там не нужна функция connect, а прием и передача данных происходят по-другому. Я специально не затрагивал эту тему, чтобы вы не запутались в функциях и их назначении.

При работе с протоколами, не требующими соединения, на сервере достаточно вызвать функцию socket, чтобы связать сокет с портом и адресом (связать сокет и bind). После этого нельзя вызывать функции listen или accept, потому что сервер получает данные от клиента без установки соединения. Вместо этого нужно просто ожидать прихода данных с помощью функции recvfrom, которая выглядит следующим образом:

int recvfrom ( SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen );

Первые четыре параметра такие же, как и у функции recv. Параметр from указывает на структуру sockaddr, в которой будет храниться IP-адрес компьютера, с которого пришли данные. В параметре fromlen хранится размер структуры.

Во второй версии WinSock появилась функция WSARecvFrom, которая похожа на WSARecv, только добавлены параметры recv и fromlen:

int WSARecvFrom ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, struct sockaddr FAR * lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE );

С точки зрения клиента все тоже очень просто. Достаточно только создать сокет, и можно напрямую направлять данные. Для передачи данных по сети используется функция sendto:

int sendto ( SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR * to, int tolen };

Первые четыре параметра соответствуют тем, что рассматривались в функции send. Параметр to — это структура типа sockaddr. Она содержит адрес и порт компьютера, которому нужно передать данные. Так как у нас нет соединения между клиентом и сервером, то эта информация должна указываться прямо в функции передачи данных. Последний параметр tolen — это размер структуры to.


Начиная со второй версии, мы можем пользоваться функцией WSASendTo. У нее параметры такие же, как и у WSASend, только добавлены два новых — lрTо и iToLen , хранящие соответственно структуру с адресом получателя и ее размер.

int WSASendTo ( SOCKET S, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const struct sockaddr FAR * lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTIME );

Как видите, работа с протоколами, не требующими соединения, еще проще. Не надо вызывать функции прослушивания порта и соединения с сервером. Если вы разберетесь с работой протокола TCP, то работа UDP вам будет уже понятна.


Простой пример использования функции select


Теперь применим все сказанное на практике. Откройте пример TCPServer из разд. 4.7.1 и после создания сокета добавьте следующий код:

ULONG ulBlock; ulBlock = 1; if (ioctlsocket(sServerListen, FIONBIO, ulBlock) == SOCKET_ERROR) { return 0; }

Таким образом сокет переводится в асинхронный режим. Теперь попробуйте запустить пример. Сначала вы должны увидеть два сообщения "Bind OK" и "Listen OK", после чего программа вернет ошибку "Accept filed". В асинхронном режиме функция accept не блокирует работу программы, а значит, не ожидает соединения. В этом случае, если в очереди нет ожидающих подключения клиентов, то функция вернет ошибку WSAEWOULDBLOCK.

Чтобы избавиться от этого недостатка, нужно подкорректировать цикл ожидания соединения (бесконечный цикл while, который идет после вызова функции listen). Для асинхронного варианта он должен выглядеть, как в листинге 4.18.

Листинг 4.18. Цикл ожидания соединения
FD_SET ReadSet; int ReadySock;

while (1) { FD_ZERO(ReadSet); FD_SET(sServerListen, ReadSet);

if ((ReadySock = select(0, ReadSet, NULL, NULL, NULL)) == SOCKET_ERROR) { MessageBox(0, "Select filed", "Error", 0); }

if (FD_ISSET(sServerListen, ReadSet)) { iSize = sizeof(clientaddr); sClient = accept(sServerListen, (struct sockaddr *)clientaddr, iSize); if (sClient == INVALID_SOCKET) { MessageBox(0, "Accept filed", "Error", 0); break; }

hThread = CreateThread(NULL, 0, ClientThread, (LPVOID)sClient, 0, dwThreadId); if (hThread == NULL) { MessageBox(0, "Create thread filed", "Error", 0); break; } CloseHandle(hThread); } }

Перед циклом добавлены две переменные: ReadSet типа FD_SET для хранения набора сокетов и ReadySock типа int для хранения количества готовых к использованию сокетов. На данный момент у нас только один сокет, поэтому эту переменную пока использовать не будем.

В самом начале цикла обнуляется набор с помощью функции FD_ZERO и добавляется созданный сокет, который ожидает подключения. После этого вызывается функция select. Для нее указан только второй параметр, а все остальные значения — нулевые. Если указан второй параметр, то функция ожидает возможности чтения для сокетов из набора. Параметр "время ожидания" тоже установлен в ноль, что соответствует бесконечному ожиданию.



к чтению. Когда от клиента


Итак, сокет сервера ожидает подключения и готов к чтению. Когда от клиента поступит запрос на подключение, сокет примет его. Но прежде чем выполнять какие-то действия, необходимо проверить вхождение сокета в набор с помощью функции FD_ISSET.

Остальной код не изменился. Мы принимаем входящее соединение с помощью функции accept, получаем новый сокет для работы с клиентом и сохраняем его в переменной sClient, После этого создается новый поток, в котором происходит обмен данными с клиентом.

Запустите пример и убедитесь, что он работает корректно. Теперь нет ошибок, и программа терпеливо ожидает соединения со стороны клиента.

Возникает вопрос, в каком режиме работает сокет sClient, который создан функцией accept. Я уже говорил, что по умолчанию сокеты работают в блокирующем режиме, и мы не изменяли это значение. Давайте проверим. Запустите приложение и попробуйте подсоединиться к серверу программой TCPClient, которая приведена в разд. 4.7.2. Клиент отправит данные, потом получит ответ " Command get OK " и после этого выдаст ошибку. Почему? Потому что мы в бесконечном цикле пытаемся получить данные от клиента, и первая попытка удачна, а вторая — нет. Значит, сокет sClient работает в том же режиме, что и сокет sServerListen.

С помощью функции select можно избавиться от второго потока, который используется для обмена данными между клиентом и сервером. Помимо этого, в примере в нынешнем виде для обработки нескольких клиентов нужно создавать множество потоков. Благодаря функции select можно все это сделать без потоков, намного проще и эффективнее. Но к этому я вернусь в главе 6, где будут рассматриваться интересные алгоритмы.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\Select.

Протокол IP


Если посмотреть на схему сетевой модели (см. 4.1), то можно увидеть, что протокол IP находится на сетевом уровне. Из этого можно сделать вывод, что IP выполняет сетевые функции — доставка пакета любому узлу в сетях произвольной топологии.

Протокол IP при передаче данных не устанавливает виртуального соединения и использует датаграммы (пакеты данных) для отправки информации от одного компьютера к другому. Это значит, что по протоколу IP пакеты просто отправляются в сеть без ожидания подтверждения о получении данных (АСК Acknowledgment), а значит, без гарантии доставки пакетов и, соответственно, без гарантии целостности данных. Если хотя бы один пакет из 100 необходимых не дойдет до адресата, то данные нарушатся, и собрать их в единое целое будет невозможно.

Все необходимые действия по подтверждению и обеспечению целостности данных должны обеспечивать протоколы, работающие на более высоком уровне.

Каждый IP-пакет содержит адреса отправителя и получателя, идентификатор протокола, TTL (время жизни пакета) и контрольную сумму для проверки целостности пакета. Как видите, здесь есть контрольная сумма, которая все же позволяет узнать целостность пакета. Но об этом узнает только получатель. Когда компьютер-получатель принял пакет, то он проверяет контрольную сумму только для себя. Если сумма сходится, то пакет обрабатывается, иначе просто игнорируется. А компьютер-отправитель не сможет узнать об ошибке, возникшей в пакете, и повторить посылку. Именно поэтому соединение по протоколу IP нельзя считать надежным.



Протоколы IPX/SPX


Осталось только рассказать еще о нескольких протоколах, которые встречаются в повседневной жизни чуть реже, но зато они не менее полезны. Первые на очереди — это IPX/SPX.

Протокол IPX (Internetwork Packet Exchange, межсетевой обмен пакетами) сейчас используется, наверно, только в сетях фирмы Novell. В наших любимых окошках есть специальная служба Клиент для сетей Novell, с помощью которой вы сможете работать в таких сетях. IPX работает подобно IP и UDP — без установления связи, а значит, без гарантии доставки и всех последующих достоинств и недостатков.

SPX (Sequence Packet Exchange, последовательный обмен пакетами) — это транспорт для IPX, который работает с установлением связи и обеспечивает целостность данных. Так что если вам понадобится надежность при использовании IPX, то используй связку IPX/SPX или IPX/SPX11.

Сейчас IPX уже теряет свою популярность, но еще помнятся времена DOS, когда все сетевые игры работали через этот протокол.

Как видите, в Интернете протоколов целое море, но большинство из них взаимосвязано, как, например, HTTP—TCP—IP. Протокол, предназначенный для одной цели, может оказаться абсолютно непригодным для другой, потому что создать что-то идеальное невозможно. У каждого будут свои достоинства и недостатки.

И все же модель OSI , принятая еще на заре Интернета, не утратила своей актуальности до сих пор. По ней работает все и вся. Главное ее достоинство — скрывать сложность сетевого общения между компьютерами, с чем старушка OSI справляется без особых проблем.



Работа напрямую с WinSock


Как видите, работа с сетью с использованием MFC-объектов, а именно CSocket, очень проста. Но вы не сможете таким образом написать маленькое приложение, потому что для этого надо отказаться от использования стандартных библиотек. Именно поэтому я рассмотрю работу с сетью напрямую достаточно подробно.

В Windows для работы с сетью используется библиотека WinSock. Существуют две версии этой библиотеки. Первая версия WinSock разрабатывалась на основе модели сокетов Беркли, используемой в UNIX-системах. Начиная с Windows 98 в ОС уже встроена вторая версия.

Библиотека WinSock обратно совместима. Это значит, что старые функции не изменились, и программы, написанные для первой версии, будут прекрасно работать во второй. В более поздних версиях Microsoft добавила новые функции, но они оказались несовместимы с сетевыми функциями на других платформах. Впервые новшества появились в версии 1.1, и это были WSAStartup, WSACleanup, WSAGetLastError, WSARecvEx (имена начинаются с "WSA"). В следующей версии таких функций стало намного больше.

Если вам доступна версия WinSock2, то не обязательно ее использовать. Посмотрите, может быть возможностей первой версии окажется достаточно, и тогда вашу программу будет легко адаптировать к компилированию на платформе UNIX.

Конечно, компьютеры с установленной Windows 95 встретить уже достаточно сложно, но они существуют. Если вы обладатель такой ОС, то вы можете скачать новую версию библиотеки с сайта www.microsoft.com.

Если вы решили использовать в своей программе первую версию, то необходимо подключить заголовочный файл winsock.h, иначе — winsock2.h.

Сразу предупрежу, что я буду использовать WinSock и WinSock2.



Работа с ресурсами сетевого окружения


В ОС Windows есть очень удобная возможность — обмениваться информацией между компьютерами через открытые ресурсы. Вы можете сделать какую-либо папку открытой для сетевого доступа, и любой пользователь в вашей сети, у которого есть соответствующие права, сможет обращаться к файлам этой папки. Можно также подключить открытую папку как локальный диск. В любом случае для доступа к таким ресурсам можно использовать стандартные функции для доступа к файлам.

Когда приложение использует файл, то ОС сначала определяет устройство, на котором находится необходимый ресурс. Если ресурс расположен на удаленном компьютере, то запрос на ввод/вывод передается по сети этому устройству. Таким образом, ОС при обращении к сетевому ресурсу занимается перенаправлением ввода/вывода (I/O redirection).

Допустим, что у вас диск Z: — это подключенная по сети папка с удаленного компьютера. Каждый раз, когда вы обращаетесь к ней, ОС будет переадресовывать запросы ввода/вывода перенаправителю (redirector), который создаст сетевой канал связи с удаленным компьютером для доступа к его ресурсам. Таким образом, можно использовать те же средства, что и для доступа к локальным ресурсам. Это сильно облегчает создание приложений, предназначенных для работы в локальной сети. Точнее сказать, никаких изменений вносить не надо. Если программа умеет работать с локальным диском, то сможет работать и с удаленными дисковыми ресурсами.

Для более подробной информации по работе редиректора можете обратиться к документации по Windows или специализированной литературе. Для простого пользователя и даже программиста эта информация не очень важна, потому что весь процесс перенаправления скрыт.

Чтобы обеспечить доступ к ресурсам другого компьютера в вашей сети, не обязательно подключать открытую папку как локальный диск. Достаточно правильно написать сетевой путь. Для этого надо знать универсальные правила именования (Universal Naming Conversion , UNC) — способ доступа к файлам и устройствам (например, к принтерам) без назначения им буквы локального диска. Тогда вы не будете зависеть от имен дисков, но нужно будет четко определить имя компьютера, на котором находится нужный объект.


Общий вид UNC-имени выглядит следующим образом:

\\компьютер\имя\путь

Имя начинается с двойной косой черты (\\). Затем идет имя компьютера или сервера, на котором расположен объект, имя — это имя сетевой папки. После этого нужно указать путь к объекту.

Допустим, что у вас есть компьютер Тоm, на котором открыта для общего доступа папка Sound. В этой папке есть файл MySound.wav. Для доступа к этому файлу необходимо использовать UNC-имя: \\Tom\Sound\MySound.wav.

В листинге 4.1 приведен пример создания файла в открытой папке компьютера с именем Notebook.

Листинг 4.1. Пример создания файла в открытой папке другого компьютера
void CreateNetFile() { HANDLE FileHandle; DWORD BWritten;

// Create file \\notebook\temp\myfile.txt // Создание файла \\notebook\temp\myfile.txt if ((FileHandle = CreateFile("\\\\notebook\\temp\\myfile.txt", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL )) == INVALID_HANDLE_VALUE) { MessageBox(0, "Create file error", "Error",0); return; }

// Write to file 9 symbols // (Записать в файл 9 символов) if (WriteFile(FileHandle, "Test line", 9, BWritten, NULL)== 0) { MessageBox(0, "Write to file error", "Error",0); return; }

// Close file (Закрыть файл) CloseHandle(FileHandle); }

Для начала создается файл с помощью стандартной WinAPI -функции CreateFile . У этой функции следующие параметры:

путь к создаваемому файлу;

режим доступа — файл открыт для чтения (GENERIC_READ) и записи (GENERIC_WRITE);

режим доступа к открытому файлу другим профаммам — другим приложениям разрешено чтение (FILE_SHARE_READ) и запись (FILE_SHARE_WRITE);

атрибуты безопасности — не использованы (NULL);

способ открытия файла — всегда создавать (CREATE_ALWAYS), если файл уже существует, то данные будут перезаписаны;

атрибуты создаваемого файла — нормальное состояние файла (FILE_ATTRIBUTE_NORMAL);

указатель на шаблон, который будет использоваться при создании файла.



Функция CreateFile возвращает указатель на открытый файл. Если результат равен INVALID_HANDLE_VALUE, то файл не был создан по каким-либо причинам.

Для записи используется функция WriteFile, у которой следующие параметры:

указатель на открытый файл;

данные, которые надо записать;

количество байт данных для записи;

количество записанных байт (переменную типа DWORD);

структура, которая необходима только при открытии файла в режиме наложения (overlapped I/O).

Если запись прошла успешно, то функция должна вернуть ненулевое значение.

После всех манипуляций с файлом его необходимо закрыть. Для этого вызывается функция CloseHandle, который нужно только передать указатель на файл, который надо закрыть.

Примечание
Исходный код примера вы можете найти на компакт - диске в каталоге \Demo\Chapter4\Network.

Работа с сетью


Я напомню, что первоначальный смысл слова "хакер" был больше связан с человеком, который хорошо знает программирование, внутренности ОС и сеть. Вопросам программирования посвящена вся книга. В предыдущих главах мы учились понимать внутренности ОС на интересных, шуточных примерах. Теперь перейдем к рассмотрению сети.

В этой главе я начну знакомить вас с сетевыми возможностями языка программирования C++. Я покажу, как написать множество простых, но очень эффективных утилит с помощью объектов Visual C++ и сетевой библиотеки WinSock.

Для начала я ограничусь использованием объектной модели, которую предоставляет среда разработки, а вот чуть позже мы познакомимся с низкоуровневым программированием сетей. Но это будет через несколько десятков страниц.

Я не захотел сразу загружать вас низкоуровневым программированием с использованием API-функций, потому что это только забьет голову, и может получиться переполнение мозгового буфера. Уж лучше мы будем все делать постепенно. Сначала познакомимся с простыми вещами, не заглядывая в дебри, а потом приступим к сложному.



Работа с сетью с помощью объектов Visual C++


При работе с сетью можно использовать возможности, которые предоставляет среда разработки Visual C++. Объекты упрощают программирование и скрывают некоторые особенности реализации протоколов и сети.

При использовании объектов проекты будут достаточно большими, потому что уже нельзя использовать приложения Win32 Project. Проекты надо создавать с помощью мастера MFC Application. Для начала этого будет достаточно, потому что основная цель сейчас — понять процесс программирования сетевых приложений. Чуть позже я познакомлю вас с сетевыми WinAPI-функциями, и тогда мы сможем написать те же приложения, но без использования объектов, и получить приложения маленького размера.

Для работы с сетью в MFC есть очень удобный класс — CSocket. В качестве предка у него выступает CAsyncSocket. Что это означает? Объект CAsyncSocket работает с сетью асинхронно. Отправив пакет в сеть, объект не ждет подтверждения, и программа может продолжать работать дальше. Об окончании действия мы можем узнать по событиям, которые для нас уже реализованы в объекте, и достаточно только написать их обработчики.

При синхронной работе каждая отправка пакета или соединение с сервером замораживает выполнение программы до окончания выполнения действия. Таким образом, процессорное время расходуется нерационально.

Объект CSocket является потомком объекта CAsyncSocket, а значит, получает все его возможности, свойства и методы. Его работа построена на основе технологии "клиент-сервер". Это значит, что один объект может быть сервером, который принимает соединения клиентов и работает с ними. Из этого следует, что в примерах для передачи данных понадобится создавать два объекта: CServerSocet (сервер) и CClientSocket (клиент для подключения к серверу).

Объект CServerSocet схож с CClientSocket. Сервер ожидает соединения на определенном порту, и когда клиент подключился, создается объект CClientSocket, с помощью которого можно отправлять и принимать данные на сервере.

Чтобы увидеть на практике работу с сетью, давайте напишем программу, которая будет сканировать указанный компьютер и искать на нем открытые порты (сканер портов). Как это работает? Для того чтобы узнать, какие порты открыты, достаточно только попробовать подсоединиться к порту. Если соединение пройдет успешно, то данный порт открыла какая-то программа.


Теперь откройте файл ресурсов и найдите диалоговое окно IDD_MFCSCAN_DIALOG. Дважды щелкните по нему, чтобы откорректировать в редакторе ресурсов. Удалите кнопки OК и Cancel, а поместите на окно диалога следующие компоненты:

Static Text — с надписью "Server address";

Edit Control — для ввода адреса сканируемого сервера (по умолчанию текст " Sample edit box ";

List Box — для сохранения открытых портов;

Button (кнопка) — с надписью "Scan" для запуска сканирования портов указанного компьютера.

У вас должно получиться нечто похожее на изображенное на 4.8.

Теперь необходимо создать переменную для списка, чтобы с ним потом работать. Для этого надо щелкнуть по компоненту List Box правой кнопкой мышки и в выпадающем меню выбрать пункт Add Variable. В появившемся окне ( 4.9) нужно ввести в поле Variable name имя переменной. Укажите имя PortsList.

Все подготовительные работы закончены. Можно приступать к написанию кода сканера портов. Необходимо создать обработчик события, который будет срабатывать при нажатии пользователем кнопки Scan, и написать в нем весь необходимый код. Для этого щелкните правой кнопкой мышки по компоненту Button и выберите в появившемся меню пункт Add Event Handler. Перед вами откроется окно мастера Event Handler Wizard ( 4.10). Согласитесь со всеми установками мастера и нажмите кнопку Add and Edit.

Мастер создаст заготовку в виде пустой функции для обработчика события. В ней нужно написать код из листинга 4.5.



4.8. Окно диалога для нашего будущего приложения



4.9. Окно создания переменной



4.10. Окно Мастера создания обработчика события

Листинг 4.5. Код сканера портов
void CMFCScanDlg::OnBnClickedScanButton() { // TODO: Add your control notification handler code here CClientSocket *pSocket; CString ip; CString messtr; int port;

pSocket=new CClientSocket(); pSocket-Create();

GetDlgItemText(IDC_EDIT1,ip); port=1; while (port100) { if(pSocket-Connect(ip, port)) { messtr.Format("Port=%d opened", port); PortsList.AddString(messtr); pSocket-Close(); pSocket-Create(); } port++; } }



Теперь разберемся с тем, что здесь происходит. В данном коде объявлена переменная pSocket типа CClientSocket. С ее помощью мы будем работать с объектом, который умеет общаться с сетью по протоколу TCP/IP. Но прежде чем начать работу, нужно выделить память и создать объект. Это делается в следующих двух строчках:

pSocket=new CClientSocket();

pSocket-Create();

После этого следует узнать, какой IP-адрес указал пользователь в поле ввода. Для этого используется функция GetDlgItemText, у которой два параметра: идентификатор компонента и переменная, в которой будет сохранен результат.

Можно получить данные и с помощью специальной переменной. Для этого нужно было бы щелкнуть по компоненту в редакторе ресурсов правой кнопкой мышки и создать переменную. Но так как мы получаем данные только один раз, заводить переменную не имеет смысла.

После этого в переменную port заносится начальное значение 1, с которого начинается сканирование. Затем запускается цикл, который будет выполняться, пока переменная port не станет больше 100.

Внутри цикла производится попытка соединения с сервером следующим образом:

pSocket-Connect(ip, port)

Здесь вызывается метод Connect объекта, на который указывает переменная pSocket. У этого метода два параметра: адрес компьютера, к которому надо подключиться, и порт. Если соединение прошло удачно, то результатом будет ненулевое значение. В этом случае надо добавить информационную строку в список PortsList. Очень важно закрыть соединение и проинициализиро-вать объект заново, иначе дальнейшие попытки соединения с сервером будут бесполезны, и вы увидите только первый открытый порт. Закрытие соединения и инициализация производятся методами Close и Create соответственно:

pSocket-Close();

pSocket-Create();

В конце цикла увеличивается переменная port, чтобы на следующем этапе цикла сканировать следующий порт.

Теперь вы готовы скомпилировать программу, но чтобы все прошло удачно, нужно перейти в начало модуля, где перечислены подключаемые заголовочные файлы, и добавить следующую строку:



#include "ClientSocket.h"

Был использован объект CClientSocket, который описан в файле ClientSocket.h, поэтому без подключения модуля код не скомпилируется.

Результат работы программы вы можете увидеть на 4.11. Запустите программу и, указав в качестве адреса 127.0.0.1, просканируйте порты своего компьютера, начиная с 0 до 99. Почему сканируем так мало портов? В Windows процесс сканирования 1000 портов происходит слишком медленно (может занять около 5 минут), поэтому сканировать лучше маленькими порциями.

Чуть позже я покажу более совершенный пример по сканированию портов, а данная программа является чисто познавательной и очень хорошо подходит для понимания алгоритма сканирования. Если у вас большой опыт программирования в среде Visual C++ и вы знакомы с потоками, то я все равно не советую вам создавать множество потоков, чтобы каждый из них сканировал свой порт. Таким способом вы ускорите программу, но во время сканирования система будет нагружена и, причем, бесполезно. Потерпите немного, и вы познакомитесь с реально быстрым сканером портов.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\Scan.

Серверные функции


Вы уже знаете, что протокол TCP работает по технологии "клиент-сервер". Чтобы два компьютера смогли установить соединение, один из них должен запустить прослушивание на определенном порту. И только после этого клиент может присоединиться к серверу.

Давайте рассмотрим функции, необходимые для создания сервера. Первым делом следует связать сетевой локальный адрес с уже созданным сокетом. Для этого используется функция bind:

int bind ( SOCKET s, const struct sockaddr FAR* name, int namelen );

Давайте посмотрим на параметры этой функции:

предварительно созданный сокет;

указатель на структуру типа sockaddr;

размер структуры sockaddr, указанной в качестве второго параметра.

Структура sockaddr предназначена для хранения адреса, а в разных протоколах используется своя адресация. Поэтому и структура sockaddr может выглядеть по-разному. Для интернет-протоколов структура имеет имя sockaddr_in и выглядит следующим образом:

struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };

Рассмотрим параметры этой структуры:

sin_family — семейство протоколов. Этот параметр схож с первым параметром функции socket. Для интернет-протоколов указывается константа AF_INET;

sin_port — порт для идентификации программы поступающими данными;

sin_addr — структура SOCKADDR_IN, которая хранит IP-адрес;

sin_zero — используется для выравнивания адреса из параметра sin_addr. Это необходимо, чтобы размер структуры SOCKADDR_IN равнялся размеру SOCKADDR.

Сейчас я хочу подробнее остановиться на портах. Вы должны быть очень внимательны при выборе порта, потому что если он уже занят какой-либо программой, то вторая попытка закончится ошибкой. Вы должны знать, что некоторые порты зарезервированы для определенных (наиболее популярных) служб. Номера этих портов распределяются центром Internet Assigned Numbers Authority. Существует три категории портов:

0—1023 — управляются IANA и зарезервированы для стандартных служб. Не рекомендуется использовать порты из этого диапазона;


1024—49151 — зарезервированы IANA, но могут использоваться процессами и программами. Большинство из этих портов можно использовать;

49152—65535 — частные порты, никем не зарезервированы.

Если во время выполнения функции bind выяснится, что порт уже используется какой-либо службой, то функция вернет ошибку WSAEADDRINUSE.

Давайте рассмотрим пример кода, который создает сокет и привязывает к нему сетевой локальный адрес:

SOCKET s=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(4888); addr.sin_addr.s_addr=htonl(INADDR_ANY);

bind(s, (SOCKADDR*)addr), sizeof(addr);

В данном примере создается сокет со следующими параметрами:

AF_INET — означает, что будет использоваться семейство интернет-протоколов;

SOCK_STREAM — указывает на протокол, устанавливающий соединение;

IPPROTO_TCP — используется протокол TCP.

Затем объявляется структура addr типа sockaddr_in. В параметре sin_family структуры также указывается семейство интернет-протоколов (AF_INET). В параметре sin_port указывается номер порта. Байты в номере должны следовать в определенном порядке, который несовместим с порядком байт в числовых переменных языка С, поэтому происходит преобразование с помощью функции htons.

В параметре sin_addr.s_addr указывается специальный адрес inaddr_any, который позволит в дальнейшем программе ожидать соединение на любом сетевом интерфейсе. Это значит, что если у вас две сетевые карты, соединенные с разными сетями, то программа будет ожидать соединения из обеих сетей. Есть еще один адрес, который можно указать, — INADDR_ANY. Позволяет рассылать широковещательные данные для всех компьютеров сети.

После того как локальный адрес и порт привязаны к сокету, можно приступить к прослушиванию порта в ожидании соединения со стороны клиента. Для этого служит функция listen, которая выглядит следующим образом:

int listen ( SOCKET s, int backlog );

Первый параметр — это все тот же сокет, который был создан и к которому привязан адрес. По этим данным функция определит, на каком порту нужно запустить прослушивание.



Второй параметр — это максимально допустимое число запросов, ожидающих обработки. Допустим, что вы указали здесь значение 3, а вам пришло 5 запросов на соединение от разных клиентов. Только первые три из них встанут в очередь, а остальные получат ошибку WSAECONNREFUSED, поэтому при написании клиента (в части соединения) обязательно должна быть проверка.

При вызове функции listen вы можете получить следующие основные ошибки:

WSAEINVAL — функция bind не была вызвана для данного сокета;

WSANOTINITIALISED — не загружена библиотека WinSock, т.е. не выполнена функция WSAStartup;

WSAENETDOWN — нарушена сетевая подсистема;

WSAEISCONN — сокет уже подключен.

Остальные ошибки возникают реже.

Когда клиент попадает в очередь на подключение к серверу, необходимо разрешить соединение с помощью функции accept. Она выглядит следующим образом:

SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );

Во второй версии есть функция WSAAccept, у которой первые три параметра такие же, как и у функции accept. Функция WSAAccept выглядит следующим образом:

SOCKET WSAAccept ( SOCKET s, struct sockaddr FAR * addr, LPINT addrlen, LPCONDITIONPROC lpfnCondition, DWORD dwCallbackData );

Давайте рассмотрим общие параметры для этих функций :

предварительно созданный и запущенный на прослушивание сокет;

указатель на структуру типа sockaddr;

размер структуры sockaddr, указанной в качестве второго параметра.

После выполнения функции accept второй параметр (addr) будет содержать сведения об IP-адресе клиента, который произвел подключение. Эти данные можно использовать для контроля доступа к серверу по IP-адресу. Но вы должны помнить, что злоумышленнику не составляет труда подделать IP-адрес, поэтому такую защиту нельзя назвать достаточной, но она может усложнить взлом сервера.

Функция accept возвращает указатель на новый сокет, который можно использовать для общения с клиентом. Старая переменная типа SOCKET продолжает слушать порт в ожидании новых соединений, и ее использовать нет смысла. Таким образом, для каждого подключенного клиента будет свой SOCKET, благодаря чему вы сможете работать с любым из них.

Если вы вспомните пример с передачей данных с использованием MFC-объектов (см. разд. 4.5), то там применялся тот же метод. Как только клиент подключался к серверу, мы создавали новый сокет, через который и происходила работа с подключившимся клиентом. Именно этот сокет принимал данные, пришедшие по сети, и мог их отправлять обратно программе на стороне клиента.


Сетевые порты


Прежде чем вы начнете писать собственные программы, надо разобраться с еще одним понятием — сетевой порт. Допустим, что вашему компьютеру на сетевую карту пришел пакет данных. Как операционная система должна определить, для какой программы пришли данные: для Internet Explorer, для почтового клиента или для вашей программы? Чтобы определить это, используются порты.

Когда программа соединяется с сервером, то она открывает на вашем компьютере какой-нибудь сетевой порт и сообщает серверу, что именно с этим портом она работает. После этого сервер будет посылать на ваш компьютер пакеты данных, в которых будет указан сетевой адрес компьютера и номер порта. По IP-адресу пакет будет доставлен до вашего компьютера, а по номеру порта операционная система определит, что именно для вашей программы предназначается пришедший пакет.

Для соединения с сервером вам надо знать не только IP-адрес сервера, но и порт, на котором работает программа, потому что на сервере может работать множество сетевых программ, и все они используют свои порты.

Из всего вышесказанного следует, что только одна программа может открыть определенный порт. Если бы две программы могли открывать, например, 21-й порт, то Windows (или любая другая операционная система) уже не смогла бы определить, какой из двух программ пришли данные.

Номер порта — это число от 1 до 65 535. Для передачи такого числа по сети достаточно всего лишь двух байт, поэтому это не будет накладно для сети. Я рекомендую использовать для своих целей порты с номерами более 1024, потому что среди меньших значений очень много зарегистрированных номеров, и у вашей программы увеличивается вероятность конфликта с другими сетевыми программами.

Теперь пора переходить к более подробному рассмотрению некоторых протоколов и сетевых возможностей Windows. Я не смогу объяснить абсолютно все, но постараюсь рассмотреть самое интересное в сетевом программировании и показать несколько полезных примеров.



Сетевые протоколы


Прежде чем начинать писать сетевые программы, необходимо разобраться с сетевыми протоколами, понять основу и принципы их работы. В этом разделе я остановлюсь на самых важных моментах, которые необходимо знать программисту для правильного принятия решения. Вы увидите основные различия и сможете понять, что нельзя просто взять первый попавшийся протокол и написать с его помощью любую программу. Иногда выбор бывает очень сложным, но от него зависит будущее программы.



Сокеты Windows


Сокеты (Sockets) — это всего лишь программный интерфейс, который облегчает взаимодействие между различными приложениями. Современные сокеты родились из программного сетевого интерфейса, реализованного в ОС BSD UNIX. Тогда этот интерфейс создавался для облегчения работы с TCP/IP на верхнем уровне.

С помощью сокетов легко реализовать большинство известных протоколов, которые используются каждый день при выходе в Интернет. Достаточно только назвать HTTP, FTP, POP3, SMTP и далее в том же духе. Все они используют для отправки своих данных или TCP, или UDP и легко программируются с помощью библиотеки sockets/winsock.



Сопоставление адреса ARP и RARP


Протокол ARP (Address Resolution Protocol, протокол определения адреса) предназначен для определения аппаратного (MAC) адреса компьютера в сети по его IP-адресу. Прежде чем данные смогут быть посланы на какой-нибудь компьютер, отправитель должен знать аппаратный адрес получателя. Именно для этого и предназначен ARP.

Когда компьютер посылает ARP-запрос на поиск аппаратного адреса, то протокол сначала ищет этот адрес в локальном кэше. Если уже были обращения по данному IP-адресу, то информация о МАС-адресе должна сохраниться в кэше. Если ничего не найдено, то в сеть посылается широковещательный запрос, который получат все компьютеры сети. Они получат этот пакет и проверят адрес. Тот, кому принадлежит искомый IP, ответит на запрос, указав свой МАС-адрес. Так как этот адрес должен быть уникальным (прошивается в сетевом устройстве на заводе-изготовителе), то и ответ должен быть один. Но вы должны учитывать, что есть средства подделки МАС-адресов (хакеры иногда используют этот прием в своих целях), и может возникнуть ситуация, когда ответ придет от двух машин.

Протокол RARP (Revers Address Resolution Protocol, обратный протокол определения адреса) определяет IP-адрес по известному МАС-адресу. Процесс поиска адресов абсолютно такой же.



Создание сокета


После загрузки библиотеки необходимо создать сокет, с помощью которого происходит работа с сетью. Для этого в первой версии библиотеки есть функция socket:

SOCKET socket ( int af, int type, int protocol );

В версии WinSock2 для создания сокета можно использовать функцию WSASocket.

SOCKET WSASocket ( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags );

Первые три параметра и возвращаемое значение для обеих функций одинаковы. И в том, и в другом случае функция возвращает созданный сокет, который будет использоваться в дальнейшем при работе с сетью. Давайте рассмотрим общие параметры:

af — семейство протоколов, которые можно использовать:

AF_UNSPEC — спецификация не указана;

AF_INET — интернет-протоколы TCP, UDP и т.д. В данной книге я буду использовать именно эти протоколы, как самые популярные и распространенные;

AF_IPX — протоколы IPX, SPX;

AF_APPLETALK — протокол AppleTalk;

AF_NETBIOS — протокол NetBIOS;

type — спецификация для нового сокета. Здесь можно указывать одно из следующих значений:

SOCK_STREAM — передача с установкой соединения. Для интернет-протоколов будет использоваться TCP;

SOCK_DGRAM — передача данных без установки соединения. Для интернет-протоколов будет использоваться UDP;

protocol — протокол для использования. Протоколов очень много, и вы можете узнать о используемых константах в справочной системе по программированию, а я чаще всего буду использовать константу IPPROTO_TCP, которая соответствует протоколу TCP.

В функции WSASocket добавлены еще три параметра:

lpProtocolInfo — указатель на структуру WSAPROTOCOL_INFO, в которой определяются характеристики создаваемого сокета;

g — идентификатор группы сокетов;

dwFlags — атрибуты сокета.

Более подробно с указанными параметрами вы познакомитесь в процессе написания примеров. Это поможет вам лучше понять их и сразу же увидеть результат работы.



Структура сети


Для того чтобы просмотреть доступные в вашей сети компьютеры, нужно воспользоваться сетевым окружением. Но что, если вам нужно в своей программе сделать просмотр сети? Это очень просто. Сейчас я продемонстрирую программу, с помощью которой можно будет в виде дерева увидеть все компьютеры в сети и их открытые ресурсы.

Создайте новое MFC-приложение в Visual C++ и назовите проект NetNeighbour. В Мастере создания приложений, в разделе Application Type выберите Dialog based, а в разделе Advanced Features — Windows sockets. На жмите кнопку Finish, чтобы среда разработки создала необходимый шаблон приложения.

Прежде чем приступать к программированию, необходимо оформить окно будущей программы. Откройте в редакторе ресурсов диалоговое окно IDD_NETNEIGHBOUR_DIALOG. Растяните по всей свободной поверхности компонент Tree Control ( 4.2).

Чтобы можно было работать с этим компонентом, щелкните по нему правой кнопкой мышки. В появившемся меню выберите пункт Add variable, а в поле Variable name укажите m_NetTree. Эта переменная понадобится для добавления в меню новых пунктов.

4.2. Использование компонента Tree Control

Теперь все готово для рассмотрения исходного кода. Перейдите в файл NetNeighbourDlg.cpp. Здесь найдите функцию OnInitDialog, которая вызывается во время инициализации окна. В этот момент необходимо создать корневой элемент дерева. Это должно происходить следующим образом:

m_hNetworkRoot = InsertTreeItem(TVI_ROOT, NULL, "My Net", DRIVE_RAMDISK +1);

В переменной m_hNetworkRoot сохраняется результат работы функции InsertTreeItem.

Придется несколько раз использовать такой же код для добавления элементов, и чтобы в одном модуле не повторять одни и те же действия, я все оформил отдельной функцией (листинг 4.2).

Листинг 4.2. Добавление нового элемента в дерево сети
HTREEITEM CNetNeighbourDlg::InsertTreeItem(HTREEITEM hParent, NETRESOURCE *const pNetResource, CString sText, int iImage) { TVINSERTSTRUCT InsertStruct; InsertStruct.hParent = hParent; InsertStruct.hInsertAfter = TVI_LAST; InsertStruct.itemex.mask = TVIF_IMAGE | TVIF_TEXT | TVIF_CHILDREN | TVIF_PARAM; InsertStruct.itemex.pszText = sText.GetBuffer(sText.GetLength()); InsertStruct.itemex.iImage = iImage; InsertStruct.itemex.cChildren = 1; InsertStruct.itemex.lParam = (LPARAM)pNetResource; sText.ReleaseBuffer(); return m_NetTree.InsertItem( InsertStruct ); }


Теперь программа выглядит должным образом и создает корневой элемент, но пока без поиска в сети. Когда программа запущена, и пользователь щелкнет мышкой по элементу дерева, нужно найти все, что есть доступного в сети, относящееся к этому элементу.

Для этого надо написать обработчик события ITEMEXPANDING и в нем производить поиск. Перейдите в редактор ресурсов и выделите компонент Tree Control . В окне Properties щелкните по кнопке Control Events, и вы увидите все события, которые может генерировать выделенный компонент. Щелкните напротив события TVN_ITEMEXPANDING и в выпадающем списке выберите пункт Add, чтобы добавить обработчик события. Код, который должен быть в этом обработчике, приведен в листинге 4.3.

Листинг 4.3. Обработчик события TVN_ITEMEXPANDING
void CNetNeighbourDlg::OnTvnItemexpandingTree1(NMHDR *pNMHDR, LRESULT *pResult) { LPNMTREEVIEW pNMTreeView = reinterpret_castLPNMTREEVIEW(pNMHDR); // TODO: Add your control notification handler code here // (Добавьте сюда ваш код обработки сообщения)

CWaitCursor CursorWaiting; ASSERT(pNMTreeView); ASSERT(pResult);

if (pNMTreeView-action == 2) { CString sPath = GetItemPath(pNMTreeView-itemNew.hItem);

if(!m_NetTree.GetChildItem(pNMTreeView-itemNew.hItem)) { EnumNetwork(pNMTreeView-itemNew.hItem); if( m_NetTree.GetSelectedItem( ) != pNMTreeView- itemNew.hItem) m_NetTree.SelectItem(pNMTreeView-itemNew.hItem); } }

*pResult = 0; }

Здесь у элемента, который в данный момент пытаются открыть, проверяется наличие дочерних элементов и организуется их поиск. Для этого вызывается функция EnumNetwork, которую можно увидеть в листинге 4.4.

Листинг 4.4. Функция EnumNetwork для просмотра сети
bool CNetNeighbourDlg::EnumNetwork(HTREEITEM hParent) { bool bGotChildren = false;

NETRESOURCE *const pNetResource = (NETRESOURCE *) (m_NetTree.GetItemData(hParent));

DWORD dwResult; HANDLE hEnum; DWORD cbBuffer = 16384; DWORD cEntries = 0xFFFFFFFF; LPNETRESOURCE lpnrDrv; DWORD i; dwResult = WNetOpenEnum(pNetResource ? RESOURCE_GLOBALNET : RESOURCE_CONTEXT, RESOURCETYPE_ANY, 0, pNetResource ? pNetResource: NULL, hEnum );



if (dwResult != NO_ERROR) { return false; }

do { lpnrDrv = (LPNETRESOURCE) GlobalAlloc(GPTR, cbBuffer); dwResult = WNetEnumResource(hEnum, cEntries, lpnrDrv, cbBuffer); if (dwResult == NO_ERROR) { for(i = 0; icEntries; i++) { CString sNameRemote = lpnrDrv[i].lpRemoteName; int nType = 9; if(sNameRemote.IsEmpty()) { sNameRemote = lpnrDrv[i].lpComment; nType = 8; } if (sNameRemote.GetLength() 0 sNameRemote[0] == _T('\\')) sNameRemote = sNameRemote.Mid(1); if (sNameRemote.GetLength() 0 sNameRemote[0] == _T('\\')) sNameRemote = sNameRemote.Mid(1);

if (lpnrDrv[i].dwDisplayType == RESOURCEDISPLAYTYPE_SHARE) { int nPos = sNameRemote.Find( _T('\\')); if(nPos = 0) sNameRemote = sNameRemote.Mid(nPos+1); InsertTreeItem(hParent, NULL, sNameRemote, DRIVE_NO_ROOT_DIR); } else { NETRESOURCE* pResource = new NETRESOURCE; ASSERT(pResource); *pResource = lpnrDrv[i]; pResource-lpLocalName = MakeDynamic(pResource-lpLocalName); pResource-lpRemoteName = MakeDynamic(pResource-lpRemoteName); pResource-lpComment = MakeDynamic(pResource-lpComment); pResource-lpProvider = MakeDynamic(pResource-lpProvider); InsertTreeItem(hParent, pResource, sNameRemote, pResource-dwDisplayType+7); } bGotChildren = true; } } GlobalFree((HGLOBAL)lpnrDrv); if (dwResult != ERROR_NO_MORE_ITEMS) break; } while (dwResult != ERROR_NO_MORE_ITEMS);

WNetCloseEnum(hEnum); return bGotChildren; }

Логика поиска сетевых ресурсов достаточно проста. Для начала нужно открыть поиск функцией WNetOpenEnum, которая выглядит следующим образом:

DWORD WNetOpenEnum( DWORD dwScope, // scope of enumeration DWORD dwType, // resource types to list DWORD dwUsage, // resource usage to list LPNETRESOURCE lpNetResource, // pointer to resource structure LPHANDLE lphEnum // pointer to enumeration handle buffer );

Функция открывает перечисление сетевых устройств в локальной сети. Рассмотрим передаваемые ей параметры:

dwScope — ресурсы, включаемые в перечисление. Возможны комбинации следующих значений:



RESOURCE_GLOBALNET — все ресурсы сети;

RESOURCE_CONNECTED — подключенные ресурсы;

RESOURCE_REMEMBERED — запомненные ресурсы;

dwType — тип ресурсов, включаемых в перечисление. Возможны комбинации следующих значений:

RESOURCETYPE_ANY — все ресурсы сети;

RESOURCETYPE_DISK — сетевые диски;

RESOURCETYPE_PRINT — сетевые принтеры;

dwUsage — использование ресурсов, включаемых в перечисления. Возможны следующие значения:

0 — все ресурсы сети;

RESOURCEUSAGE_CONNECTABLE — подключаемые;

RESOURCEUSAGE_CONTAINER — контейнерные;

lpNetResource — указатель на структуру NETRESOURCE. Если этот параметр равен нулю, то перечисление начинается с самой верхней ступени иерархии сетевых ресурсов. Ноль ставится для того, чтобы получить самый первый ресурс. Потом я передаю в качестве этого параметра указатель на уже найденный ресурс. Тогда перечисление начнется с него и продолжится дальше. Так я повторяю, пока не найдутся все ресурсы;

lphEnum — указатель, который понадобится В функции WnetEnumResource.

Теперь нужно рассмотреть структуру NETRESOURCE:

typedef struct _NETRESOURCE { DWORD dwScope; DWORD dwType; DWORD dwDisplayType; DWORD dwUsage; LPTSTR lpLocalName; LPTSTR lpRemoteName; LPTSTR lpComment; LPTSTR lpProvider; } NETRESOURCE;

Что такое dwScope, dwType и dwUsage, вы уже знаете. А вот остальные рассмотрим подробнее:

dwDisplayType — способ отображения ресурса:

RESOURCEDISPLAYTYPE_DOMAIN — это домен;

RESOURCEDISPLAYTYPE_GENERIC — нет значения;

RESOURCEDISPLAYTYPE_SERVER — сервер;

RESOURCEDISPLAYTYPE_SHARE — разделяемый ресурс;

lpLocalName — локальное имя;

lpRemoteName — удаленное имя;

lpComment — комментарий;

lpProvider — хозяин ресурса. Параметр может быть равен нулю, если хозяин неизвестен.

Теперь можно переходить к следующей функции:

DWORD WNetEnumResource( HANDLE hEnum, // handle to enumeration LPDWORD lpcCount, // pointer to entries to list LPVOID lpBuffer, // pointer to buffer for results LPDWORD lpBufferSize // pointer to buffer size variable );



Параметры функции WnetEnumResource:

hEnum — указатель на возвращенное функцией wNetopenEnum значение;

lpcCount — максимальное количество возвращаемых значений. Не стесняйтесь, ставьте 2000. Если вы зададите 0xFFFFFFFF, то перечислятся все ресурсы. После выполнения функция передаст сюда фактическое число найденных ресурсов;

lpBuffer — указатель на буфер, в который будет помещен результат;

lpBuffersize — размер буфера.

После окончания перечисления вызывается функция WNetCloseEnum, которая закрывает начатое функцией WNetOpenEnum перечисление сетевых ресурсов. В качестве единственного параметра нужно передать указатель на возвращенное функцией WNetOpenEnum значение.

Это все, что касается поиска открытых сетевых ресурсов. Осталось только сделать одно замечание. Функция поиска WNetOpenEnum и соответствующие ей структуры находятся в библиотеке mpr.lib, которая по умолчанию не линкуется к проекту. Чтобы собрать проект без ошибок, необходимо подключить эту библиотеку. Для этого щелкните правой кнопкой мышки по имени проекта в окне Solution Explorer и в появившемся меню выберите пункт Properties. Перед вами откроется окно свойств, в котором надо перейти в раздел Configuration Properties/Linker/Input. Здесь в строке Additional Dependencies напишите имя библиотеки mpr.lib ( 4.3).



4.3. Добавление библиотеки, содержащей функцию WNetOpenEnum

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\NetNeighbour.

Теория сетей и сетевых протоколов


Прежде чем я покажу первый пример, придется немного заняться теорией. Это не займет много времени, но потом нам будет легче понимать друг друга. Для лучшего восприятия материала этой главы вам желательно знать основы сетей и протоколов.

Каждый раз, когда вы передаете данные по сети, они как-то перетекают от вашего компьютера к серверу или другому компьютеру. Как это происходит? Вы, наверно, скажете, что с помощью специального сетевого протокола, и будете правы. Но существует множество разновидностей протоколов.

Какой и когда используется? Зачем они нужны? Как они работают? Вот на эти вопросы я сейчас постараюсь дать ответ.

Прежде чем разбираться с протоколами, нам необходимо узнать, что такое модель взаимодействия открытых систем (OSI — Open Systems Interconnection), которая была разработана Международной организацией по стандартам (ISO — International Organization for Standardization). В соответствии с этой моделью, сетевое взаимодействие делится на семь уровней.

Физический уровень— передача битов по физическим каналам (коаксиальный кабель, витая пара, оптоволоконный кабель). Здесь определяются характеристики физических сред и параметры электрических сигналов.

Канальный уровень — передача кадра данных между любыми узлами в сетях типовой топологии или соседними узлами в сетях произвольной топологии. В качестве адресов на канальном уровне используются МАС-адреса.

Сетевой уровень — доставка пакета любому узлу в сетях произвольной топологии. На этом уровне нет никаких гарантий доставки пакета.

Транспортный уровень — доставка пакета любому узлу с любой топологией сети и заданным уровнем надежности доставки. На этом уровне имеются средства для установления соединения, буферизации, нумерации и упорядочивания пакетов.

Уровень сеанса — управление диалогом между узлами. Обеспечена возможность фиксации активной на данный момент стороны.

Уровень представления — предоставляется возможность преобразования данных (шифрование, сжатие).

Прикладной уровень — набор сетевых сервисов (FTP, E-mail и др.) для пользователя и приложения.


Если вы внимательно прочитали обо всех уровнях, то, наверно, заметили, что первые три уровня обеспечиваются оборудованием, таким как сетевые карты, маршрутизаторы, концентраторы, мосты и др. Последние три — операционной системой или приложением. Четвертый уровень является промежуточным.

Как работает протокол по этой модели? Все начинается с прикладного уровня. Пакет попадает на этот уровень, и к нему добавляется заголовок. После этого прикладной уровень отправляет этот пакет на следующий уровень (уровень представления). Здесь ему также добавляется свой собственный заголовок, и пакет отправляется дальше. И так до физического уровня, который занимается непосредственно передачей данных и отправляет пакет в сеть.

Другая машина, получив пакет, начинает обратный отсчет. Пакет с физического уровня попадает на канальный. Канальный уровень убирает свой заголовок и поднимает пакет выше (на уровень сети). Уровень сети убирает свой заголовок и поднимает пакет выше. Так пакет поднимается до уровня приложения, где остается чистый пакет без служебной информации, которая была прикреплена на исходном компьютере перед отправкой пакета.

Передача данных не обязательно должна начинаться с седьмого уровня. Если используемый протокол работает на четвертом уровне, то процесс передачи начнется с него, и пакет будет подниматься вверх до физического уровня для отправки. Количество уровней в протоколе определяет его потребности и возможности при передаче данных.

Чем ниже находится протокол (ближе к прикладному уровню), тем больше у него возможностей и больше накладных расходов при передаче данных (длиннее и сложнее заголовок). Рассматриваемые в данной книге протоколы будут находиться на разных уровнях, поэтому будут иметь разные возможности.

Корпорация Microsoft реализовала протокол TCP/IP в модели OSI по-своему (с небольшими отклонениями от стандарта). Я понимаю, что модель OSI справочная, и предназначена только в качестве рекомендации, но нельзя же было так ее изменять, ведь принцип оставлен тот же, хотя изменились названия и количество уровней.



4.1. Модель OSI и вариант от MS

У MS TCP/IP вместо семи уровней есть только четыре. Но это не значит, что остальные уровни позабыты и позаброшены, просто один уровень может выполнять все, что в OSI делают три уровня. Например, уровень приложения у Microsoft выполняет все, что делают уровень приложения, уровень представления и уровень сеанса, вместе взятые.

На 4.1 схематично сопоставлены MS TCP/IP-модель и справочная модель OSI. Слева указаны названия уровней по методу MS, а справа — уровни OSI. В центре показаны протоколы. Я постарался разместить их именно на том уровне, на котором они работают, впоследствии нам это пригодится.


Транспортные протоколы


На транспортном уровне мы имеем два протокола: UDP и TCP. Оба они работают поверх IP. Это значит, что, когда пакет TCP или UDP опускается на уровень ниже для отправки в сеть, он попадает на уровень сети прямо в лапы протокола IP. Здесь пакету добавляется сетевой адрес, TTL и другие атрибуты протокола IP. После этого пакет идет дальше вниз для физической отправки в сеть. Голый пакет TCP не может быть отправлен в сеть, потому что он не имеет информации о получателе, эта информация добавляется к пакету с IP-заголовком на уровне сети.

Давайте теперь рассмотрим каждый протокол в отдельности.



Запуск библиотеки


Прежде чем начать работу с сетью, нужно загрузить необходимую версию библиотеки. В зависимости от этого изменяется набор доступных функций. Если использовать первую версию библиотеки, а вызвать функцию из второй, то произойдет сбой в работе программы. Если не загрузить библиотеку, то любой вызов сетевой функции вернет ошибку WSANOTINITIALISED.

Для загрузки библиотеки используется функция WSAStartup, которая выглядит следующим образом:

int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );

Первый параметр (wVersionRequested) — это запрашиваемая версия библиотеки. Младший байт указываемого числа определяет основной номер версии, а старший байт — дополнительный номер. Чтобы легче было работать с этим параметром, я советую использовать макрос MAKEWORD(i, j), где i — это старший байт, a j — младший.

Второй параметр функции WSAStartup — это указатель на структуру WSADATA, в которой после выполнения функции будет находиться информация о библиотеке.

Если загрузка прошла успешно, то результат будет нулевым, иначе — произошла ошибка.

Посмотрите пример использования функции WSAStartup для загрузки библиотеки WinSock 2.0:

WSADATA wsaData;

int err = WSAStartup(MAKEWORD(2, 0), wsaData); if (err != 0) { // Tell the user that WinSock not loaded // ( Сказать пользователю, что библиотека не загружена ) return; }

Обратите внимание, что сразу после попытки загрузить библиотеку идет проверка возвращенного значения. Если функция отработала правильно, то она должна вернуть нулевое значение. Приведу основные коды ошибок:

WSASYSNOTREADY — основная сетевая подсистема не готова к сетевому соединению;

WSAVERNOTSUPPORTED — запрашиваемая версия библиотеки не поддерживается;

WSAEPROCLIM — превышен предел поддерживаемых ОС задач;

WSAEFAULT — неправильный указатель на структуру WSAData.

Структура WSADATA выглядит следующим образом:

typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION_LEN+l]; char szSystemStatus[WSASYS_STATOS_LEN+l]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADATA, FAR * LPWSADATA;


Разберем каждый параметр в отдельности:

wVersion — версия загруженной библиотеки WinSock;

wHighVersion — последняя версия;

szDescription — текстовое описание, которое заполняется не всеми версиями;

szSystemStatus — текстовое описание состояния, которое заполняется не всеми версиями;

iMaxSockets — максимальное количество открываемых соединений. Эта информация не соответствует действительности, потому что максимальное число зависит только от доступных ресурсов. Параметр остался только для совместимости с первоначальной спецификацией;

iMaxUdpDg — максимальный размер дейтаграммы (пакета). Информация не соответствует действительности, потому что размер зависит от протокола;

lpVendorInfо — информация о производителе.

Давайте рассмотрим небольшой пример, с помощью которого будет загружаться библиотека WinSock из структуры WSAData . Создайте новый проект MFC Application. В Мастере создания приложений, в разделе Application Type выберите Dialog based, а в разделе Advanced Features — Windows sockets. Это уже знакомый вам тип приложения.

Откройте в редакторе ресурсов диалоговое окно и оформите, как на 4.11. На форме должно быть 4 поля Edit Control для вывода информации о загруженной библиотеке и кнопка Get WinSock Info, по нажатию которой будут загружаться данные.

Для каждого поля вводятся переменные:

номер версии — mVersion;

последняя версия — mHighVersion;

описание — mDescription;

состояние — mSystemStatus.

Создайте обработчик события для кнопки Get WinSock Info и напишите в нем код из листинга 4.11.



4.11. Окно будущей программы WinSockInfo

Листинг 4.11. Получение информации о WinSock
void CWinSockInfoDlg::OnBnClickedButton1() { // TODO: Add your control notification handler code here WSADATA wsaData;

int err = WSAStartup(MAKEWORD(2, 0), wsaData); if (err != 0) { // Tell the user that WinSock not loaded return; }

char mText[255]; mVersion.SetWindowText(itoa(wsaData.wVersion, mText, 10)); mHighVersion.SetWindowText(itoa(wsaData.wHighVersion, mText, 10)); if (wsaData.szDescription) mDescription.SetWindowText(wsaData.szDescription); if (wsaData.szSystemStatus) mSystemStatus.SetWindowText(wsaData.szSystemStatus); }



В самом начале запускается WinSock (код, который я уже приводил). После этого полученная информация просто выводится в поля на форме диалога.



4.12. Результат работы программы WinSockInfo

На 4.12 вы можете увидеть результат работы программы на моем компьютере.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter4\WinSockInfo.
В приведенном примере есть один недостаток — не выгружается библиотека. для освобождения библиотеки используется функция WSACleanup:

int WSACleanup(void);

Функции не нужны параметры, она просто освобождает библиотеку, после чего работа с сетевыми функциями становится недоступной.


Завершение соединения


Для завершения сеанса сначала необходимо проинформировать партнера, с которым происходило соединение, об окончании передачи данных. Для этого используется функция shutdown , которая выглядит следующим образом:

int shutdown ( SOCKET s, int how );

Первый параметр — это сокет, соединение которого необходимо закрыть. Второй параметр может принимать одно из следующих значений:

SD_RECEIVE — запретить любые функции приема данных. На протоколы нижнего уровня этот параметр не действует. Если используется потоковый протокол (например, TCP) и в очереди есть данные, ожидающие чтение функцией recv, или они пришли позже, то соединение сбрасывается. Если используется UDP-протокол, то сообщения продолжают поступать;

SD_SEND — запретить все функции отправки данных;

SD_BOTH — запретить прием и отправку данных.

После того как партнер проинформирован о завершении работы, можно закрывать сокет. Для этого используется функция closesocket, которая выглядит так:

int closesocket ( SOCKET s );

После этого указанный в качестве единственного параметра сокет будет закрыт. Если вы попытаетесь использовать его в какой-нибудь функции, то получите ошибку WSAENOTSOCK — дескриптор не является сокетом. Любые пакеты, ожидающие отправку, прерываются или отменяются.