Семчуков Валерий
Работа с сокетами в 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. Сокеты для датаграммных протоколов. Первый тип сокетов делится еще на два подтипа: активные и пассивные сокеты. Активный сокет соединен с удаленным активным сокетом через открытое соединение данных. Закрытие соединения приводит к уничтожению активных сокетов в обоих точках соединения. Пассивный сокет ни с чем не соединен, но он ждет запроса на соединение. Приход запроса на соединение и дальнейшее подтверждение этого запроса приводит к образованию коммуникационного потокового канала связи и созданию новых двух активных сокетов на обоих концах коммуникационного канала. Рассмотрим образование коммуникационного канала.
- Сервер при помощи функции socket создает пассивный сокет и привязывает его к какому-то локальному адресу и порту.
- При помощи функции listen сервер переводит пассивный сокет в состояние ожидания входящих сообщений и дальше занимается какой-то другой работой.
- Клиент, который хочет наладить коммуникационный канал с сервером, также создает пассивный сокет при помощи функции Socket и привязывает его к какому-нибудь локальному адресу и порту.
- При помощи функции connect пользователь пытается установить соединение с сервером. В качестве параметров к этой функции пользователь передает созданный пассивный сокет и адрес и порт сервера, то есть удаленного пассивного сокета.
- Сервер, узнав что кто-то пытается присоединиться к его пассивному сокету и разрешая это сделать, вызывает функцию Accept. Эта функция создает копию пассивного сокета, находящегося на прослушивании входящих сообщений, и переводит созданный сокет в активное состояние. Теперь сервер может использовать этот активный сокет для приема/передачи потоковых данных, а пассивный сокет продолжает слушать новые запросы на соединение.
-
Если сервер вызвал функцию Accept в ответ на клиентскую функцию connect, то функция connect успешно отрабатывает и пассивный сокет клиента переводится в активное состояние. Теперь клиент через обновленный сокет может осуществлять прием/передачу потоковых данных.
- Потоковый канал связи налажен.
Все бы было ничего, если бы не одно большое, НО. Как известно, 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:
-
область памяти, указываемая аргументом addr, будет содержать структуру данных (для сетей TCP/IP это sockaddr_in), описывающую адрес socket'а программы-клиента, через который она сделала свой запрос на соединение;
-
целое число, на которое указывает аргумент 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