Socket vs Socket часть 2, или скажем “нет” протоколу TCP

Дата публикации 9 авг 2004

Socket vs Socket часть 2, или скажем “нет” протоколу TCP — Архив WASM.RU

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

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

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

Слышу, слышу хор подсказок, что мол, используй встроенные возможности Windows, типа:

net send 192.168.0.4 Женя шлет тебе привет!

         или

net send Node4 Жду ответа!

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

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

         Ответ, как всегда, дает WindowsAPI. Ищем и находим. Функция sendto() – посылает данные по указанному адресу. А в чем же тогда ее отличие от уже изученной в первой части функции send()? Оказывается, что sendto() может осуществлять широковещательную передачу по специальному IP – адресу. Но, внимание, это работает только для сокетов типа SOCK_DGRAM! А сокеты, при открытии которых в качестве параметра типа сокета использовалось значение SOCK_DGRAM работают через протокол UDP, а не TCP! Отсюда становится ясно значение подзаголовка этой статьи… Конечно, это всего лишь литературный прием, ни один протокол не лучше и не хуже другого, просто они… разные, вот и все. Хотя оба – это протоколы транспортного уровня, которые “…обеспечивают передачу данных между прикладными процессами”. Оба обращаются к протоколу сетевого уровня, такому, как IP для передачи (приема) данных. Через который далее они (данные) попадают на физический уровень, т.е. в среду передачи… А что там за среда, кто его знает. Может это медный кабель, а может и не среда вовсе, а четверг, и не медный кабель, а эфир…

Схема взаимодействия сетевых протоколов.

UDP User Datagram Protocol

TCP – Transmission Control Protocol

ICMP – Internet Control Message Protocol (протоколобменауправляющимисообщениями)

ARP – Address Resolution Protocol (протокол определения адресов)

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

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

         Для демонстрации всего материала, как обычно, используется учебный пример, который можно скачать <здесь>. Пропускаем общую для всех Windows приложений часть и описываем только то, что касается работы сокетов. Сначала необходимо инициализировать Windows Sockets DLL с помощью функции WSAStartup(), которая вернет ноль в случае успешного выполнения, либо, в противном случае, один из кодов ошибки. Затем при инициализации главного окна приложения открываем сокет для приема cообщений:

Код (Text):
  1.  
  2.     invoke socket, AF_INET, \  
  3.                    SOCK_DGRAM, \    ; задает тип сокета - протокол UDP!
  4.     0   ; тип протокола

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

Код (Text):
  1.  
  2.         .if eax != INVALID_SOCKET   ; если нет ошибки
  3.             mov hSocket, eax    ; запомнить дескриптор

После этого, как обычно, надо указать Windows посылать сообщения заданному окну от открытого нами сокета:

Код (Text):
  1.  
  2.     invoke WSAAsyncSelect, hSocket, hWnd, WM_SOCKET, FD_READ

где    hSocket       - дескриптор сокета
hWnd          - дескриптор окна, процедуре которого будут посылаться сообщения
WM_SOCKET     - сообщение, нами же определенное в секции .const
FD_READ – маска, задающая интересующие нас события, в данном случае это готовность данных от сокета для чтения.

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

Код (Text):
  1.  
  2.     invoke ShowWindow, hwnd, SW_SHOWNORMAL

или, что более правильно, используйте:

Код (Text):
  1.  
  2.     invoke ShowWindow, hwnd, SW_HIDE   

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

         Далее. Необходимо заполнить структуру <sin> с локальным адресом...

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

Код (Text):
  1.  
  2.         invoke htons, Port
  3.         mov sin.sin_port, ax
  4.         mov sin.sin_family, AF_INET
  5.         mov sin.sin_addr, INADDR_ANY

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

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

через протоколTCP: 20, 21 – ftp;     23 – telnet;      25 – smtp;       80 – http;         139 - NetBIOS session service;

через протоколUDP: 53 – DNS; 137, 138 – NetBIOS; 161 – SNMP;

Конечно, в составе API есть специальная функция getservbyport(), которая по заданному номеру порта возвращает имя соответствующего ему сервиса. Вернее, сама функция возвращает указатель на структуру, внутри которой есть указатель на это имя...

Вызвать ее можно так:

Код (Text):
  1.  
  2.      invoke htons, Port; преобразуем номер порта в сетевой порядок байт
  3.      invoke getservbyport, ax, 0;

Обратите внимание на то, что сообщает Win32 Programmer’sReference по поводу getservbyport:

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

А вот и сама структура:

Код (Text):
  1.  
  2. servent STRUCT
  3.     s_name  DWORD   ?; указатель на строку с именем сервиса
  4.     s_aliases   DWORD   ?;
  5.     s_port      WORD    ?; номер порта
  6.     s_proto DWORD   ?;
  7. servent ENDS

Есть в API, так сказать, и “парная” функция: getservbyname(), которая по имени сервиса возвращает информацию о номере используемого порта.

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

Едем далее и ассоциируем локальный адрес, представленный в структуре <sin>, с открытым нами ранее сокетом:

Код (Text):
  1.  
  2.         invoke bind, hSocket, addr sin, sizeof sin
  3.             .if eax == SOCKET_ERROR; если ошибка
  4.                 invoke MessageBox, NULL, addr …
  5.             .endif

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

Код (Text):
  1.  
  2. ; если получено сообщение от сокета (hSocket)
  3. .elseif uMsg == WM_SOCKET
  4.     mov eax, lParam
  5.     .if ax == FD_READ;
  6.             HIWORD lParam
  7.             .if ax == NULL  ; отсутствует ошибка
  8. ; принять данные (64 байта) от сокета в буфер BytRecu
  9.         invoke recv, hSocket, addr BytRecu, 64, 0;
  10.     …

Теперь о том, как открыть сокет для передачи сообщений. Вот все необходимые действия программы:

Код (Text):
  1.  
  2.         invoke socket, AF_INET, SOCK_DGRAM, 0
  3.             mov hSocket1, eax

Далее идет уже знакомая нам конвертация номера порта и строкового формата IP- адреса назначения в сетевой порядок байт. И заполняем структуру типа sockaddr_in <>.

Код (Text):
  1.  
  2.             invoke htons, Port
  3.                 mov sin_to.sin_port, ax
  4.                 mov sin_to.sin_family, AF_INET       
  5.             invoke inet_addr, addr AdresIP
  6.                 mov sin_to.sin_addr, eax

Когда дело доходит до передачи данных, достаточно сделать cледующее:

Код (Text):
  1.  
  2.             invoke sendto, hSocket1, addr BytSend1, 64, 0, \
  3.                     addr sin_to,    sizeof sin_to

Значения параметров при вызове этой функции APIследующие:

hSocket1     - дескриптор ранее открытого сокета
addrBytSend1 - адрес буфера, содержащего данные на передачу
64 - размер данных вбуфере, в байтах
         0 - индикатор…, в примере MSDNэто просто 0
addrsin_to - указатель наструктуру, которая содержит адрес назначения
sizeofsin_to – размер этой структуры в байтах.

Если при выполнении функции sendto() не возникло ошибок, то она возвращает число переданных байт, иначе на выходе имеем в eaxзначение SOCKET_ERROR.

         Теперь самое время поговорить о том самом широковещательном адресе, о котором упоминалось вначале. В структуре <sin_to> мы предварительно заполнили поле с IP - адресом назначения, указывая, куда, собственно, отправлять данные. Если это адрес 127.0.0.1 – естественно, никуда дальше собственного компьютера наши данные не уйдут. В литературе четко сказано, что пакет, посланный в сеть с адресом 127.x.x.x, не будет передаваться ни по какой сети. Более того, маршрутизатор или шлюз никогда не должен распространять информацию о маршрутах для сети с номером 127 - этот адрес не является адресом сети. Чтобы отправить  “передачку” сразу всем компьютерам локальной сети нужно использовать адрес, сформированный из нашего собственного IP – адреса, но имеющий все единицы в младшем октете, что-нибудь типа 192.168.0.255.

Вот, собственно, и все. В момент закрытия программы необходимо закрыть сокеты и освободить ресурсы Sockets DLL, делается это просто:

Код (Text):
  1.  
  2.     invoke closesocket, hSocket
  3.     invoke closesocket, hSocket1
  4.     invoke WSACleanup

Для мультипотоковых приложений после WSACleanup завершаются операции с сокетами для всех потоков.

Самым трудным в этой статье было для меня решить, как лучше всего проиллюстрировать использование Windows Sockets API. Один подход вы, наверное, уже увидели, когда в едином приложении одновременно использовался и сокет на прием, и сокет на передачу сообщений. Не менее привлекательным кажется и другой способ, когда код для одного и другого четко разделен, вплоть то того, что существует в разных приложениях. В конце концов, я реализовал еще и этот способ, который может оказаться для понимания начинающими несколько проще. Во втором <архиве> лежат папки:

\SocSocDR – тестовая программа, только приемная часть

\SocSocDW – соответственно, только передающая часть

Отличия, кроме уже упомянутых, заключаются в том, что в передающей программе при выборе пункта “Передать тест…” вместо функции sendto() используется привычная нам по первой статье функция  send():

Код (Text):
  1.  
  2.             invoke send, hSocket1, addr BytSend1, eax, 0

Правда, чтобы так делать, нужно, все-таки, предварительно выполнить подключение созданного сокета к указанному в <sin_to> IP- адресу:

Код (Text):
  1.  
  2.             invoke connect, hSocket, addr sin_to, sizeof sin_to

Безэтогофункцияsend()выдаст SOCKET_ERROR!

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

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

Во втором методе обработки сообщений для их получения приложение создает скрытое окно. Оно служит для отделения главной оконной процедуры приложения от обработки сетевых сообщений. Этот подход может упростить главное приложение и облегчить использование имеющегося сетевого кода в других программах. Отрицательной стороной такого подхода является чрезмерное использование Windows - user памяти, т.к. для каждого созданного окна резервируется довольно большой его объем.

Какой способ выбрать - решайте сами. И еще одно. На время экспериментов, возможно, придется отключить  ваш персональный firewall. Так, например, Outpost Pro 2.1.275 в режиме обучения реагировал на попытку передачи в сокет, но, когда передача вручную разрешалась, данные все равно не доходили. Вот вам и UDP. Хотя дело может быть и не в этом. Проблем с моим ZoneAlarmPro 5.0.590 в такой же ситуации не было.

P.S. Заканчивая вторую часть статьи случайно наткнулся в сети на исходники трояна на нашем любимом языке MASM. Все компилируется и запускается,  одно но, клиент не хочет коннектиться с сервером, да еще под Windows 2000 sp4 иногда вылетает с ошибкой, мол, приложение будет закрыто и все такое… Лично мне в этом трояне нравится, что программа не просто там ведет лог нажатий, или “выдирает” файл с паролями и отсылает его по электронке, а имеет широкий набор управляемых дистанционно функций, дольно оригинально реализованных. Если получится привести все это хозяйство в чувство, то, возможно, скоро появится и третья часть, посвященная описанию конкретной реализации… Для тех, кто внимательно прочитал обе статьи и разобрался с работой функций сокет API, там нет ничего сложного. Вроде бы… Кстати, сам автор пишет в readme, что написал его (троян) в образовательных целях. Ну-ну. Этим и воспользуемся.

© DirectOr

0 1.686
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532