Дата публикации статьи: 12.08.2004 15:24

Семчуков Валерий
Работа с сокетами в Visual Basic используя Wsock32.dll, ws2_32.dll

Предисловие

    В один прекрасный день, когда я сам того не ожидал, передо мной встала большая проблема. Естественно я попытался найти интересующую себя информацию в русскоязычном интернете, но, к моему большому сожалению, там я ничего не нашел. Тогда я обратился за помощью к остальному интернет пространству, а именно к его англоязычной части, и тут я, к удивлению, не извлек для себя ничего нового. Что же это за проблема такая нерешимая? Как оказалась, этой проблемой является программирование сокетов под Visual Basic.
    С чего все началось: Как-то передо мной встала задача написать сетевую программу. Программа, естественно, состояла из клиентской и серверных частей. По моей задумке эта программа должна была стать сетевым терминалом, если быть точнее, она должна была определять какие процессы запущены на какой либо машине в сети и по желанию мы должны были бы отключать любой из этих процессов. Вначале я, как и полагается, выбрал для себя самый легкий путь, но как выяснилось позже, не самый оптимальный, я просто подключил ActiveX компоненту winsock.ocx. Порадовавшись жизни, что все так просто, я написал нужную мне программу, но как выяснилось позже, программа написанная таким образом меня сильно разочаровала. В чем же заключалось разочарование? Я разместил клиентскую часть на другом компьютере и попытался подключиться с серверной части, каково же было мое удивление, когда при подключении все процессорное время было занято обработкой моего приложения. Тогда я решил идти другим, далеко не самым легким путем.    
    Я написал приложение, используя библиотеку Wsock32.dll, ws2_32.dll. Но прежде чем это сделать, я долго помаялся в поисках информации о программировании сокетов в Visual Basic. Эта статья была написана именно для того, чтобы сэкономить ваше время и деньги при написании сетевых программ, используя функции библиотеки Wsock32.dll.

Глава 1. Маленькое Введение

    Сокет (Socket) – это точка сетевой коммуникации. Это понятие используется во многих протоколах транспортного уровня, таких как TCP и SPX, а также и в протоколе IPX. Сокеты делятся на два типа: 1. Сокеты для потокоориентированных протоколов и 2. Сокеты для датаграммных протоколов. Первый тип сокетов делится еще на два подтипа: активные и пассивные сокеты. Активный сокет соединен с удаленным активным сокетом через открытое соединение данных. Закрытие соединения приводит к уничтожению активных сокетов в обоих точках соединения. Пассивный сокет ни с чем не соединен, но он ждет запроса на соединение. Приход запроса на соединение и дальнейшее подтверждение этого запроса приводит к образованию коммуникационного потокового канала связи и созданию новых двух активных сокетов на обоих концах коммуникационного канала. Рассмотрим образование коммуникационного канала.

  1. Сервер при помощи функции socket создает пассивный сокет и привязывает его к какому-то локальному адресу и порту.
  2. При помощи функции listen сервер переводит пассивный сокет в состояние ожидания входящих сообщений и дальше занимается какой-то другой работой.
  3. Клиент, который хочет наладить коммуникационный канал с сервером, также создает пассивный сокет при помощи функции Socket и привязывает его к какому-нибудь локальному адресу и порту.
  4. При помощи функции connect пользователь пытается установить соединение с сервером. В качестве параметров к этой функции пользователь передает созданный пассивный сокет и адрес и порт сервера, то есть удаленного пассивного сокета.
  5. Сервер, узнав что кто-то пытается присоединиться к его пассивному сокету и разрешая это сделать, вызывает функцию Accept. Эта функция создает копию пассивного сокета, находящегося на прослушивании входящих сообщений, и переводит созданный сокет в активное состояние. Теперь сервер может использовать этот активный сокет для приема/передачи потоковых данных, а пассивный сокет продолжает слушать новые запросы на соединение.
  6. Если сервер вызвал функцию Accept в ответ на клиентскую функцию connect, то функция connect успешно отрабатывает и пассивный сокет клиента переводится в активное состояние. Теперь клиент через обновленный сокет может осуществлять прием/передачу потоковых данных.

  7. Потоковый канал связи налажен.

    Все бы было ничего, если бы не одно большое, НО. Как известно, VB программистам, прежде чем использовать какую-либо API функцию, ее необходимо вначале объявить (продекларировать). Это же необходимо сделать с функциями из Wsock32.dll, ws2_32.dll

Глава 2. Работа с сокетами

      Итак, перейдем к программированию.
    Прежде чем воспользоваться функциями, находящихся в Wsock32.dll, ws2_32.dll нам необходимо создать обычный модуль(*.bas) в котором разместятся все объявления этих функций.
Запишем описание необходимых функций:

Public Declare Function WSAStartup Lib "ws2_32.dll" (ByVal wVR As Long, lpWSAD As WSA_Data) As Long

    Для инициализации библиотеки и для проверки ее версии нам необходима будет эта функция, где wVR-это необходимая минимальная версия библиотеки, при присутствии которой ваше приложение будет корректно работать, как правило в качестве этого параметра передают 1. Объявим эту константу

Public Const WINSOCK_VERSION = 1 
'эта константа применяется при вызове WSAStartup
' в качестве первого параметра

    Второй параметр LpWSAD-это указатель на структуру WSA_Data. Ее необходимо объявить перед объявлением функции в следующем виде:

Public Type WSA_Data
    wVersion       As Integer
    wHighVersion   As Integer
    szDescription  As String * WSADESCRIPTION_LEN
    szSystemStatus As String * WSASYS_STATUS_LEN
    iMaxSockets    As Integer
    iMaxUdpDg      As Integer
    lpVendorInfo   As Long
End Type

где WSADESCRIPTION_LEN, WSASYS_STATUS_LEN это константы которые тоже необходимо обьявить будет выше:

Public Const WSADESCRIPTION_LEN = 257
Public Const WSASYS_STATUS_LEN = 129

Поля iMaxSockets и iMaxUdpDg в версии 2.0 не используются и остались только для совместимости с версией 1.1. Следующая функция - наверное самая главная функция, которую надо обьявить

Public Declare Function socket Lib "wsock32.dll" (ByVal _
af As Long, ByVal s_type As Long, ByVal protocol_ As Long) As Long

af – семейство протоколов (AF_INET, AF_IPX), type – тип протокола (SOCK_STREAM, SOCK_DGRAM) и протокол – указывает конкретный протокол (обычно указывается в 0 для TCP/IP или в NSPROTO_IPX или NSPROTO_SPX)
    Перед этой функцией обьявим следующие константы, необходимые нам для дальнейшей работы, значение их будет описано позже, хотя и без описания по-моему все и так ясно

'---------------Address families------------
Public Enum ADDRESS_FAMILIES
  AF_INET = 2
  AF_NS = 6
  AF_IPX = AF_NS
  PF_INET = 2
End Enum

'---------------Socket Types----------------
Public Enum SOCKET_TYPES
  SOCK_STREAM = 1
  SOCK_DGRAM = 2
End Enum
'---------------Protocols-------------------
Public Enum PROTOCOLS
   IPPROTO_TCP = 6
   IPPROTO_IP = 0
End Enum

    Чтобы работать дальше с созданным сокетом его нужно привязать к какому-нибудь локальному адресу и порту. Эта процедура выполняется функцией:

Public Declare Function bind Lib "wsock32" (ByVal socket As Long, addr As sockaddr, ByVal namelen As Long) As Long

В общем виде структура sockaddr имеет следующий вид:

Type sockaddr
    sin_family As Integer
    sin_port As Integer
    sin_addr As Long
    sin_zero As String * 8
End Type

Поле sin_family определяет используемый формат адреса (набор протоколов), в нашем случае (для TCP/IP) оно должно иметь значение AF_INET.
Поле sin_addr содержит адрес (номер) узла сети.
Поле sin_port содержит номер порта на узле сети.
Поле sin_zero не используется.

    Для установления связи "клиент-сервер" используются системные вызовы listen и accept (на стороне сервера), а также connect (на стороне клиента). Для заполнения полей структуры socaddr_in, используемой в вызове connect, обычно используется библиотечная функция gethostbyname, транслирующая символическое имя узла сети в его номер (адрес).

Public Declare Function listen Lib "wsock32.dll" _
(ByVal s As Long, ByVal backlog As Long) As Long

    Аргумент s задает дескриптор socket'а, через который программа будет ожидать запросы к ней от клиентов
backlog – это максимальный размер очереди входящих сообщений на соединение.
В качестве backlog будем передавать следующую константу, объявим ее:

Public Const QUEUE_SIZE = 5

    Если получен запрос на соединение, то мы можем подтвердить и установить соединение при помощи функции accept:

Public Declare Function Accept Lib "wsock32.dll" _
Alias "accept" (ByVal s As Long, addr As sockaddr, _
addrlen As Long) As Long

Аргумент s задает дескриптор socket'а, через который программа-сервер получила запрос на соединение (посредством системного запроса listen ).
Аргумент addr должен указывать на область памяти, размер которой позволял бы разместить в ней структуру данных, содержащую адрес socket'а программы-клиента, сделавшей запрос на соединение. Никакой инициализации этой области не требуется.
Аргумент p_addrlen должен указывать на область памяти в виде целого числа, задающего размер (в байтах) области памяти, указываемой аргументом addr.
Системный вызов accept извлекает из очереди, организованной системным вызовом listen, первый запрос на соединение и возвращает дескриптор нового (автоматически созданного) socket'а с теми же свойствами, что и socket, задаваемый аргументом s. Этот новый дескриптор необходимо использовать во всех последующих операциях обмена данными.
Кроме того после удачного завершения accept:

  1. область памяти, указываемая аргументом addr, будет содержать структуру данных (для сетей TCP/IP это sockaddr_in), описывающую адрес socket'а программы-клиента, через который она сделала свой запрос на соединение;

  2. целое число, на которое указывает аргумент p_addrlen, будет равно размеру этой структуры данных.

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

Public Declare Function recv Lib "wsock32.dll" _
(ByVal s As Long, buf As Any, ByVal buflen As Long, ByVal flags _
As Long) As Long

Аргумент s задает дескриптор socket'а, через который принимаются данные.
Аргумент buf указывает на область памяти, предназначенную для размещения принимаемых данных.
Аргумент buflen задает длину (в байтах) этой области.
Аргумент flags модифицирует исполнение системного вызова recv. При нулевом значении этого аргумента вызов recv полностью аналогичен системному вызову read.
При успешном завершении send возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается "переполненным", то send переводит программу в состояние ожидания до момента его освобождения.

Public Declare Function send Lib "wsock32.dll" _
(ByVal s As Long, buf As Any, ByVal buflen As Long, ByVal _
flags As Long) As Long

    Для закрытия ранее созданного socket'а используется обычный системный вызов closesocket, применяемый в ОС UNIX для закрытия ранее открытых файлов и имеющий следующий вид
    Примечание: Все функции приема/передачи потоковых данных являются блокирующими. Т.е. в периоды ожиданий ваше приложение будет просто висеть. Поэтому рекомендуется создавать для этих комманд отдкльные потоки: Threads.

Public Declare Function CreateThread Lib "kernel32" (lpThreadAttributes As Any, _
ByVal dwStackSize As Long, ByVal lpStartAddress As Long, _
lpParameter As Any, ByVal dwCreationFlags As Long, _
lpThreadID As Long) As Long

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

Public Declare Function htons _
Lib "ws2_32.dll" (ByVal hostshort As Integer) As Integer

    Для преобразования строки с IP-адресом в формате десятичное с точкой в 32-разрядное двоичное число (с сетевым порядком байтов).

Public Declare Function inet_addr _
Lib "wsock32" (ByVal cp As String) As Long

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

Public Declare Function WSAAsyncSelect Lib "wsock32" (ByVal socket As Long, _
ByVal hwnd As Long, ByVal iMsg As Long, ByVal lEvent As_
Long) As Long

socket идентификатор сокета, для которого требуется уведомление о произошедшем событии.
iMsg сообщение, посылаемое по событию.
lEvent битовая маска, определяющая комбинацию сетевых событий, в которых заинтересовано приложение.
Public Const FD_ACCEPT = &H8
Возвращаемое значение
В случае удачного завершения WSAAsyncSelect возвращает 0. В противном случае возвращается SOCKET_ERROR и устанавливается конкретный код ошибки, который может быть получен функцией WSAGetLastError.

    Функция WSAAsyncSelect используется для того, чтобы указать Wsock32.dll на необходимость посылки сообщения окну hWnd всякий раз, когда она обнаруживает на сокете любое из сетевых событий, указанных параметром lEvent. Сообщение, которое должно быть послано, определяется параметром iMsg. Сокет, для которого требуется уведомление, идентифицируется параметром socket.
    Функция WSAAsyncSelect автоматически устанавливает сокет s в неблокируемый режим, независимо от значения lEvent. См. функцию ioctlsocket для получения информации о том, как установить неблокируемый сокет обратно в блокируемый режим.

Public Declare Function closesocket _
Lib "wsock32.dll" (ByVal s As Long) As Long

    Итак преступим к самому интересному, а именно применим все выше описанное. Начнем с серверной части:

Public Sub Form_Load()
Dim wsaData As WSA_Data

If (WSAStartup(WINSOCK_VERSION, wsaData)) Then
   MsgBox "Can't init"
   Exit Sub
  Else
 
'-----------Create-Socket---------------------
 s = socket(PF_INET, SOCK_STREAM, 0)
 
  If (s = INVALID_SOCKET) Then
        MsgBox "Error create socket"
        Exit Sub
  End If


'--------------Bind
Dim socketaddr As sockaddr Dim Port As Integer Port = 123 socketaddr = saZero socketaddr.sin_family = AF_INET socketaddr.sin_addr = inet_addr("127.0.1.1") socketaddr.sin_port = htons(Port) 'socketaddr.sin_zero = String(8, vbNullChar) If (bind(s, socketaddr, sockaddr_size) = SOCKET_ERROR) Then MsgBox "Bad bind" Exit Sub Else 'MsgBox "Good bind" End If End If '--------------Listen
Dim ERR As Integer ERR = listen(s, QUEUE_SIZE) If (ERR = SOCKET_ERROR) Then MsgBox " Listen BAD !!! " Exit Sub Else 'MsgBox "God Listen " 'MsgBox "Wait to connected" End If Call Accept_s(s, socketaddr) End Sub ‘----------Это необходимо разместить в модуле Public Sub Accept_s(ByVal SockNum&, addr As sockaddr) s1 = Accept(SockNum, addr, Len(addr)) Dim ERRORS As Integer ERRORS = WSAAsyncSelect(SockNum, frmServ.hwnd, WM_SERVER_ACCEPT, FD_ACCEPT) If (ERRORS = SOCKET_ERROR) Then MsgBox " AsyncSelect BAD " Exit Sub Else Debug.Print "Good AsyncSelect" End If Call Recive(s1) End Sub Public Sub Recive(ByVal SockNm&) Dim buf As Byte r = recv(SockNm, buf, 1, 0) send(SockNm, buf, 1, 0) end sub Public Sub Close_Server() closesocket (s) If (WSACleanup()) Then MsgBox "Error Cleapir" Else Debug.Print "Cleapir ok" End If Unload frmServ End Sub Теперь клиентская часть: Dim wsaData As WSA_Data Dim IPADDR As String IPADDR = “127.0.0.1” ‘петля If (WSAStartup(WINSOCK_VERSION, wsaData)) Then MsgBox "Can't init" Exit Sub Else '-----------Create-Socket s = socket(PF_INET, SOCK_STREAM, 0) If (s = INVALID_SOCKET) Then MsgBox "Error create socket" Exit Sub End If '--------------Bind Dim socketaddr As sockaddr Dim Port As Integer Port = 123 socketaddr = saZero socketaddr.sin_family = AF_INET socketaddr.sin_addr = inet_addr(IPADDR) socketaddr.sin_port = htons(Port) 'socketaddr.sin_zero = String(8, vbNullChar) If (connect(s, socketaddr, Len(socketaddr)) = SOCKET_ERROR) Then MsgBox "Bad connect" Exit Sub End If End If 'WORK_FLAG = 0 '--------------Send'
Call send_data(s, WORK_FLAG) '------------Close End Sub Private Sub Command2_Click() List1.Clear WORK_FLAG = 1 Call send_data(s, WORK_FLAG) End Sub Private Sub Command3_Click() WORK_FLAG = 2 Call send_data(s, WORK_FLAG) End Sub Private Sub Form_Unload(Cancel As Integer) closesocket (s) If (WSACleanup()) Then MsgBox "Error Cleapir" Else Debug.Print "Cleapir ok" End If End Sub ‘----------Это необходимо разместить в модуле Public Sub send_data(ByVal sck&, flg As Integer) Dim buf As Byte Buf=321 sd = send(sck, buf, 1, 0) End Sub Public Sub receive(ByVal sock&) Dim buf1(10) As Byte rd = recv(sock, buf1(0), 150, 0) End Sub

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

Благодарности

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


Автор: Семчуков Валерий e-mail: ValeryS@alsi-astana.kz ICQ: 328874607