Алибек Болатов
Пример написания приложений "клиент-сервер"
Немного теории
Для начала, рассмотрим схему будущей системы "клиент-сервер".
Упрощенная схема:
- Запускается серверная подсистема
- Запускается клиентская подсистема
- Клиент пытается подключиться к серверу
- Сервер проверяет подключение (например, по IP-адресу) и подключает клиента
- Сервер подтверждает подключение
- Клиент посылает идентификационную строку (если имеются клиенты различных типов)
- Сервер проверяет идентификационную строку и отправляет клиенту контрольный запрос (авторизация приложения)
- Клиент принимает контрольный запрос и сообщает ответ
- Сервер проверяет корректность ответа и авторизует приложение
- Клиент передает аутентификационные данные пользователя
- Сервер проверяет аутентификационные данные пользователя
- Если были пройдены все проверки, то соединение между клиентом и сервером установлено.
Пояснения:
- Идентификационная строка
-
Как правило, в системах "клиент-сервер" клиенты бывают различных типов. Например, одна программа позволяет проводить управление и администрирование, другая позволяет производить мониторинг, третья предназначена для основной работы.
- Контрольный запрос и ответ
-
Используется, чтобы предотвратить подключение в качестве клиентов неавторизованных (самодельно написанных) программ. Например, сервер отправляет тестовую строку, которую клиент должен преобразовать согласно некоторому алгоритму. Затем сервер сравнивает полученный результат с эталонным (который вычисляется на сервере) и если они совпадают, то авторизует приложение.
- Аутентификационные данные пользователя
-
После авторизации приложения имеет смысл шифровать весь трафик между клиентом и сервером. Кроме того, лучше не передавать пароль пользователя по сети (даже по зашифрованному каналу), более предпочтительным будет передавать хэш пароля пользователя. После успешной авторизации пользователя сервер проверяет, имеет ли указанный пользователь право пользоваться данной клиентской программой, в противном случае соединение закрывается.
Чем шифровать и чем вычислять хэши - дело второстепенное, можно вообще отказаться от криптографии. Но в данном примере будет использовать хэши MD5 и шифрование по алгоритму "Энигма". И хеширование, и шифрование реализованно в классах, для использования своих алгоритмов достаточно заменить прилагаемые классы своими.
Более подробно информация о подключении и о структуре пакетов данных будет представлена в следующей главе.
Общее описание
Прежде чем писать код, вначале более детально рассмотрим этапы подключения клиента к серверу и разработаем структуру пакетов данных. Для конкретных нужд имеет смысл разрабатывать структуру пакета индивидуально, чтобы оптимизировать дальнейшее программирование. В данном примере будем использовать структуру, которая подходит для большинства случаев.
В подавляющем большинстве случаев схема обмена данными между клиентом и сервером выглядит так: клиент подает запрос, сервер его обрабатывает и присылает ответ. В некоторых случаях сервер сам отправляет какие-либо данные клиенту (уведомления о различных событиях). И практически никогда не требуется обратный обмен данными, т.е. сервер самостоятельно делает запрос клиенту, а тот отправляет ему ответ. Исходя из этого будем учитывать что в ответе, отсылаемом сервером, обязательно должно быть поле, которое подтверждает успешное выполнение запроса или возвращает код ошибки запроса (например, ошибочный запрос, или на выполнение указанной операции недостаточно прав).
Кроме того, имеется еще один ньюанс. Дело в том, что клиент не знает, когда сервер подготовит ответ и даже придет ли ответ вообще (т.к. данные принимаются и отправляются асинхронно). Кроме того, пользователю будет удобнее, если в то время, пока будут подготавливаться данные для первого запроса, он (пользователь) сможет отправить второй запрос.
В свете указанного встает проблема, как определить, на какой запрос пришел ответ от сервера. Самый простой путь (и на мой взгляд, оптимальный) - каждый запрос будет предваряться неким идентификатором (например, счетчиком), и когда сервер отправляет ответ клиенту, он добавляет к этому ответу ижентификатор запроса. Разумеется, на клиенте необходимо реализовать способ, чтобы по идентификатору запроса можно было определить сам запрос, т.е. на клиенте будет некий буфер или очередь, в которую будут добавляться элементы-запросы (при отправке запроса), а при получении ответа на запрос элементы будут удаляться из очереди.
Кроме того, было бы неплохо предусмотреть механизм, который бы затруднял подделку пакетов данных. Способов для этого много, в данном примере выбран механизм цифровой подписи (к пакету данных добавляется хэш, потом полученный блок данных шифруется и снова хэшируется).
Есть и еще один ньюанс. Шифровать трафик следует после авторизации приложения (не пользователя). Причин для этого несколько, самая очевидная -- такой механизм позволяет использовать динамический пароль и алгоритм шифрования. Другая причина, менее очевидная -- если достоверность клиента не подтверждена, то не следует ему (клиенту) знать о способах шифрования данных (при наличии большого количества зашифрованной информации вероятность ее расшифровки повышается).
На основании указанного была разработана следующая схема обмена данными.
Расшифровка:
- <MsgID>
- Идентификатор сообщения, Hex-dump, 8 байт.
- <Data>
- Данные (команда, текст запроса или любая другая информация), произвольной длины.
- <Param>
- Параметры, требуемые для запроса или команды. Поле необязательное, если указывается, то должно отделяться от данных символом табуляции (0x09 ASCII).
- <RetCode>
- Код возврата. В общем случае, код ошибки, возвращаемый сервером в ответ за запрос. В случае уведомлений является кодом уведомления. Hex-dump, 4 байта
- <Tab>
- Символ табуляции, разделитель полей между данными и параметрами. ASCII 0x09 (Dec 9).
- <@>
- Разделитель между идентификатором сообщения и кодом возврата. Символ "at", ASCII 0x40 (Dec 64).
Кодирование:
- При шифровании передаваемых данных пакет данных кодируется следующим способом:
- <Data>
- Пакет данных (с идентификатором, кодом возврата, данными и пр.).
- <Hash>
- Хэш на пакет данных, Hex-dump, 32 байта (128 бит).
- <EncodeData>
- Зашифрованный пакет данных (хэш + пакет данных).
- <HashEncode>
- Хэш на зашифрованный пакет данных, Hex-dump, 32 байта (128 бит).
- <0>
- Разделитель между хэшем на зашифрованный пакет данных и самими зашифрованными данными. Символ "null", ASCII 0x00 (Dec 0).
Блоки данных, передаваемые между сервером и клиентом, разделяются двойным "null" (ASCII 0x0000); это требуется из-за неизбежной фрагментации пакетов при передаче их через WinSock. Вследствии того, что символ "null" имеет особое значение (разделитель полей), следует исключить вероятность появления данного символа внутри блока данных. Один из способов -- преобразовывать двоичные данные в Hex-dump. Другой способ -- переделать программный код приема/отправки данных, например, добавляя перед каждым блоком данных его длину в байтах и загружая требуемое число байт.
Такая схема позволяет использовать одну и ту же подпрограмму для обработки пакета данных в любом режиме (прием/ответ, зашифрованный канал/открытый канал). В статье код этой подпрограммы не приводится, т.к. сам по себе он ничего не дает (помимо этой подпрограммы используется еще несколько подпрограмм), их лучше смотреть непосредственно в примере.
Теперь о том, как реализован обмен данными. Клиент формирует запрос (предварительно назначив ему идентификатор), помещает его в очередь (обработка очереди должна быть реализована в клиенте). К этому запросу при необходимости добавляются параметры. Полученный результат упаковывается в строку, к этой строке добавляется хэш. Полученный блок данных шифруется и к зашифрованному блоку также добавляется хэш (подписывающий уже зашифрованный пакет данных). Этот хэш и сами зашифрованные данные разделяются символом "null", к концу блока добавляется двойной "null".
Сервер принимает пакеты данных и помещает их в буфер. Как только в буфере находится последовательность <NULL><NULL>, сервер извлекает пакет данных и удаляет его из буфера. Полученный пакет данных проверяется на валидность и расшифровывается. Затем сервер выполняет запрос, полученный от клиента, и передает клиенту идентификатор сообщения (чтобы клиент мог определить, на какой запрос пришел ответ), код возврата, уведомляющий о успешном или неуспешном выполнении запроса, и данные, полученные в случае успешного выполнения запроса (при неуспешном выполнении запроса вместо данных приходит текст ошибки). Эти данные также упаковываются в строку, шифруются и подписываются и отсылаются клиенту.
Если клиент зарегистрирован, как получатель уведомлений, то сервер будет посылать клиенту уведомления о различных событиях, в этом случае код возврата является кодом события. Поскольку уведомление не является ответом на какой-либо запрос, то в качестве идентификатора используется последовательность "********". В текущей реализации клиент должен зарегистрироваться, как получатель уведомлений, при необходимости можно регистрировать в качестве получателей всех клиентов, которые подключаются к серверу.
Реализация сервера
Для сервера используется два сокета, wsServer (который при запуске сервера переводится в режим LISTEN) и индексированный wsClients(0), к копиям которого будут подключаться клиенты. К клиентским сокетам дополнительно создается массив пользовательского типа, в котором будет фиксироваться дополнительная информация.
Private Enum ClientTypeEnum
ctGeneral = 1
ctOther2 = 2
...
End Enum
Private Enum ClientErrorCodes
cerrSuccess = &H0&
cerrServerBusy = &HFF&
cerrAuth_WrongAppID = &H100&
cerrAuth_WrongAppPassword = &H101&
cerrAuth_WrongUserData = &H110&
cerrAuth_WrongUserLocked = &H111&
cerrAuth_WrongUserAccess = &H112&
cerrGeneralError = &HFFFF&
End Enum
Private Const EventMsgID As String = "********"
Private Enum ClientConnectStates
ccsNotConnect = 0
ccsConnecting = 10
ccsConnecting_WaitAppID = 11
ccsConnecting_WrongAppID = 12
ccsConnecting_SendPassword = 13
ccsConnecting_ReceivePassword = 14
ccsConnecting_WrongPassword = 15
ccsConnecting_Complete = 19
ccsAuthorizing = 20
ccsAuthorizing_WaitUser = 21
ccsAuthorizing_WrongUser = 22
ccsAuthorizing_WrongAccess = 23
ccsAuthorizing_Complete = 29
ccsConnect = 30
ccsDisconnecting = 90
End Enum
Private Type WinSockInfo
ConnectClock As Date
ConnectState As ClientConnectStates
Crypto As Crypt
Client As ClientTypeEnum
User As String
Data As String
Buffer As String
End Type
Для сокета wsServer требуется такой код:
Private Sub wsServer_ConnectionRequest(ByVal requestID As Long)
MakeNewConnection requestID, wsServer.RemoteHostIP, wsServer.RemotePort
End Sub
Процедура MakeNewConnection должна делать следующее:
MakeNewConnection
- Проверить, имеется ли возможность создать новый сокет
- Проверить RemoteHostIP и RemotePort (если требуется)
- Создать новый сокет и подключить к нему клиента
- Для нового сокета создать элемент WinSockInfo
- Перевести .ConnectState в состояние ccsConnecting_WaitAppID
Private Sub MakeNewConnection(ByVal requestID As Long, Optional ByVal RemoteHostIP As String,_
Optional ByVal RemotePort As Long)
Dim I As Long
I = GetFreeSocket()
If I = 0 Then
wsClients(0).LocalPort = 0
wsClients(0).Accept requestID
ClientSend 0, EventMsgID, cerrServerBusy, "Server busy."
Exit Sub
End If
ws(I).ConnectClock = Now()
ws(I).ConnectState = ccsConnecting
Set ws(I).Crypto = New Crypt
Load wsClients(I)
wsClients(I).Accept requestID
ws(I).ConnectState = ccsConnecting_WaitAppID
End Sub
Фактически, клиент уже подключен, но он не будет работать с сервером, пока не пройдет авторизацию. Для этого на клиентском сокете имеется такой код:
Private Sub wsClients_DataArrival(Index As Integer, ByVal bytesTotal As Long)
Dim I As Long, C As DataCheckResult, Data As String, MsgID As String, MsgBody As String, _
MsgParam As String
If Index = 0 Then Exit Sub
Data = Space$(bytesTotal)
wsClients(Index).GetData Data, vbString, bytesTotal
ws(Index).Buffer = ws(Index).Buffer & Data
Do
I = InStr(ws(Index).Buffer, vbNullCharDbl)
If I = 0 Then Exit Do
Data = Left$(ws(Index).Buffer, I - 1)
ws(Index).Buffer = Mid$(ws(Index).Buffer, I + Len(vbNullCharDbl))
Select Case ws(Index).ConnectState
Case ccsConnecting_WaitAppID
C = ExtractDataString(Data, MsgID, MsgBody, MsgParam)
Case ccsConnecting_ReceivePassword
C = ExtractDataString(Data, MsgID, MsgBody, MsgParam)
Case ccsAuthorizing_WaitUser
C = ExtractDataString(Data, MsgID, MsgBody, MsgParam, ws(Index).Crypto)
Case ccsConnect
C = ExtractDataString(Data, MsgID, MsgBody, MsgParam, ws(Index).Crypto)
Case Else
C = -1
MsgID = vbNullString
MsgBody = vbNullString
MsgParam = vbNullString
End Select
If C = dcrSuccess Then
If ws(Index).ConnectState = ccsConnect Then
ClientRequest Index, MsgID, MsgBody, MsgParam
Else
ClientAuth Index, MsgBody
End If
End If
Loop
End Sub
Авторизацией пользователя занимается процедура ClientAuth. Функция ExtractDataString принимает зашифрованные данные и расшифровывает их, заодно проверяя валидность. Код процедуры ClientAuth схематично показан ниже:
Private Sub ClientAuth(ByVal ClientIndex As Long, Message As String)
Dim S As String
Select Case ws(ClientIndex).ConnectState
Case ccsNotConnect
Case ccsConnecting_WaitAppID
'В Message будет находится идентификатор типа клиента
'В данном случае используется Select Case, но лучше использовать базу данных
Select Case Message
Case "WSCLIENT"
If vServerOptions.IPFilterClient Then
If Not CheckIPRange(wsClients(ClientIndex).RemoteHostIP, IPFilterClientList()) Then
ws(ClientIndex).ConnectState = ccsConnecting_WrongAppID
ws(ClientIndex).ConnectState = ccsDisconnecting
ClientSend ClientIndex, EventMsgID, _
cerrAuth_WrongAppID, "IP-address in not valid range for this application."
Exit Sub
End If
End If
ws(ClientIndex).Client = ctGeneral
ws(ClientIndex).ConnectState = ccsConnecting_SendPassword
ws(ClientIndex).Data = GenerateKeyPhrase()
ClientSend ClientIndex, EventMsgID, cerrSuccess, _
ws(ClientIndex).Data
Set ws(ClientIndex).Crypto = New Crypt
ws(ClientIndex).Crypto.KeyString = "wscs demo"
ws(ClientIndex).ConnectState = _
ccsConnecting_ReceivePassword
Case Else
ws(ClientIndex).ConnectState = _
ccsConnecting_WrongAppID
ws(ClientIndex).ConnectState = ccsDisconnecting
ClientSend ClientIndex, EventMsgID, _
cerrAuth_WrongAppID, "Application not registered in the database."
End Select
Case ccsConnecting_ReceivePassword
'Клиент должен преобразовать (зашифровать) полученную строку.
'В Message находится хэш на преобразованную строку.
S = ws(ClientIndex).Crypto.Encrypt(ws(ClientIndex).Data)
If Message = md5.DigestStrToHexStr(S) Then
ws(ClientIndex).ConnectState = ccsConnecting_Complete
ws(ClientIndex).Crypto.KeyString = ws(ClientIndex).Data
ws(ClientIndex).Data = vbNullString
ClientSend ClientIndex, EventMsgID, cerrSuccess, ws(ClientIndex).Data
Select Case ws(ClientIndex).Client
Case ctGeneral
ws(ClientIndex).ConnectState = ccsAuthorizing
ws(ClientIndex).ConnectState = ccsAuthorizing_WaitUser
End Select
Else
ws(ClientIndex).ConnectState = ccsConnecting_WrongPassword
ws(ClientIndex).ConnectState = ccsDisconnecting
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongAppPassword, "Wrong control phrase."
End If
Case ccsAuthorizing_WaitUser
'Авторизация приложения завершена, проводится авторизация пользователя
'В Message находится аутентификационная информация вида AUTH: @
ws(ClientIndex).User = vbNullString
If Left$(Message, 6) = "AUTH: " Then
S = Mid$(Message, 7)
If InStrRev(S, "@") > 0 Then
ws(ClientIndex).User = Left$(S, InStrRev(S, "@") - 1)
ws(ClientIndex).Data = UCase$(Mid$(S, InStrRev(S, "@") + 1))
End If
End If
If Len(ws(ClientIndex).User) = 0 Then
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongUserData, _
"Invalid user login/password.", ws(ClientIndex).Crypto
Else
'В данном случае используется Select Case, но, конечно, следует работать с БД
Select Case ws(ClientIndex).User
Case "wscs"
S = md5.DigestStrToHexStr("admin")
If S = ws(ClientIndex).Data Then
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongUserLocked, _
"User is locked.", ws(ClientIndex).Crypto
Else
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongUserData, _
"Invalid user login/password.", ws(ClientIndex).Crypto
End If
Case "demo"
S = md5.DigestStrToHexStr("demo")
If S = ws(ClientIndex).Data Then
If ws(ClientIndex).Client <> ctGeneral Then
ws(ClientIndex).ConnectState = ccsAuthorizing_WrongAccess
ws(ClientIndex).ConnectState = ccsDisconnecting
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongUserAccess, _
"Wrong user access.", ws(ClientIndex).Crypto
Else
ws(ClientIndex).ConnectState = ccsAuthorizing_Complete
ClientSend ClientIndex, EventMsgID, cerrSuccess, "Access granted.",_
ws(ClientIndex).Crypto
ws(ClientIndex).ConnectState = ccsConnect
End If
Else
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongUserData, _
"Invalid user login/password.", ws(ClientIndex).Crypto
End If
Case Else
ClientSend ClientIndex, EventMsgID, cerrAuth_WrongUserData, _
"Invalid user login/password.", ws(ClientIndex).Crypto
End Select
End If
End Select
End Sub
Процедура ClientRequest получает все запросы всех клиентов.
Private Sub ClientRequest(ByVal ClientIndex As Long, MsgID As String, Data As String, _
Param As String)
Dim msg As String, N() As String, V() As String, I As Long
If ClientIndex = 0 Then Exit Sub
If ws(ClientIndex).ConnectState <> ccsConnect Then Exit Sub
'Check request syntax
'Check user access
'Check client access
'If Check = False Then
' ClientSend ClientIndex, MsgID, cerrGeneralError, "General error", ws(ClientIndex).Crypto
' Exit Sub
'End If
Call GetParams(Param, N(), V())
Select Case Data
Case wsmcCommon_GetServerTime
ClientSend ClientIndex, MsgID, cerrSuccess, ServerClock(), ws(ClientIndex).Crypto
...
Case wsmcCommon_RegisterNotice
msg = NoticeClientRegister(ClientIndex)
If Len(msg) = 0 Then
ClientSend ClientIndex, MsgID, cerrSuccess, , ws(ClientIndex).Crypto
Else
ClientSend ClientIndex, MsgID, cerrGeneralError, msg, ws(ClientIndex).Crypto
End If
Case wsmcCommon_UnRegisterNotice
msg = NoticeClientUnregister(ClientIndex)
If Len(msg) = 0 Then
ClientSend ClientIndex, MsgID, cerrSuccess, , ws(ClientIndex).Crypto
Else
ClientSend ClientIndex, MsgID, cerrGeneralError, msg, ws(ClientIndex).Crypto
End If
End Select
End Sub
Функция ClientSend используется для передачи данных клиенту. В ней реализуется шифрование и подписывание данных, после чего они отправляются на сокет.
Private Sub ClientSend(ByVal ClientIndex As Long, ByVal MsgID As String,_
ByVal Code As ClientErrorCodes, Optional ByVal Data As String,_
Optional Crypto As Crypt)
Dim msg As String
If ClientIndex > 0 Then
If ws(ClientIndex).ConnectState = ccsNotConnect Then Exit Sub
End If
msg = Hex$(Code)
If Len(msg) < 4 Then msg = String$(4 - Len(msg), "0") & msg
If Len(Data) = 0 Then
msg = MsgID & "@" & msg
Else
msg = MsgID & "@" & msg & Data
End If
If Not (Crypto Is Nothing) Then
msg = Crypto.Encrypt(md5.DigestStrToHexStr(msg) & msg)
msg = md5.DigestStrToHexStr(msg) & vbNullChar & msg
End If
wsClients(ClientIndex).SendData msg & vbNullCharDbl
End Sub
Разумеется, это не весь код серверной подсистемы. Но его вполне достаточно, чтобы представить, как протекают рабочие процессы.
Реализация клиента
Для клиента код выглядит проще. Кроме того, поскольку клиентской подсистеме требуется авторизоваться только однажды, то имеет смысл разделить авторизацию и передачу данных и реализовать авторизацию в одном модуле. Это упростит обновление авторизации в случае нескольких типов клиентов (они будут работать с одним и тем же модулем и достаточно будет просто перекомпилировать проект).
Будем исходить из того, что на всех клиентских подсистемах есть форма frmMAIN, на которой имеется сокет wsClient, через который и будет происходит прием и передача данных.
Ниже приводится упрощенный модуль Authorize.
Option Explicit
Public Enum ClientConnectStates
ccsNotConnect = 0
ccsConnecting = 10
ccsConnecting_WaitAppID = 11
ccsConnecting_WrongAppID = 12
ccsConnecting_SendPassword = 13
ccsConnecting_ReceivePassword = 14
ccsConnecting_WrongPassword = 15
ccsConnecting_Complete = 19
ccsAuthorizing = 20
ccsAuthorizing_WaitUser = 21
ccsAuthorizing_WrongUser = 22
ccsAuthorizing_Complete = 29
ccsConnect = 30
ccsDisconnecting = 90
End Enum
Public CurrentConnectState As ClientConnectStates
Private AuthBuffer As String
Public Const EventMsgID As String = "********"
Public Enum ClientErrorCodes
cerrSuccess = &H0&
cerrServerBusy = &HFF&
cerrAuth_WrongAppID = &H100&
cerrAuth_WrongAppPassword = &H101&
cerrAuth_WrongUserData = &H110&
cerrAuth_WrongUserLocked = &H111&
cerrAuth_WrongUserAccess = &H112&
cerrGeneralError = &HFFFF&
End Enum
Sub ClientAuth_Recv(ByVal Code As ClientErrorCodes, ByVal Message As String)
Dim C As String
C = Hex$(Code): If Len(C) < 4 Then C = String$(4 - Len(C), "0") & C
Select Case CurrentConnectState
Case ccsConnecting
Select Case Code
Case cerrSuccess
CurrentConnectState = ccsConnecting_WaitAppID
ClientAuth_Send prj_ApplCode
Case cerrServerBusy
CurrentConnectState = ccsNotConnect
MsgBox "Сервер перегружен"
Case Else
CurrentConnectState = ccsNotConnect
MsgBox "При подключении произошла ошибка!"
End Select
Case ccsConnecting_WaitAppID
Select Case Code
Case cerrSuccess
Crypt.KeyString = prj_ApplPassword
AuthBuffer = Message
CurrentConnectState = ccsConnecting_ReceivePassword
ClientAuth_Send md5.DigestStrToHexStr(Crypt.Encrypt(Message))
Case cerrAuth_WrongAppID
CurrentConnectState = ccsNotConnect
MsgBox "Приложение '" & prj_ProductNameEng & "' не зарегистрировано на сервере."
Case Else
CurrentConnectState = ccsNotConnect
MsgBox "При подключении произошла ошибка!"
End Select
Case ccsConnecting_ReceivePassword
Select Case Code
Case cerrSuccess
Crypt.KeyString = AuthBuffer
CurrentConnectState = ccsAuthorizing
Call ClientAuth_Logon
Case cerrAuth_WrongAppPassword
CurrentConnectState = ccsNotConnect
MsgBox "Невозможно зарегистрировать приложение"
Case Else
CurrentConnectState = ccsNotConnect
MsgBox "При подключении произошла ошибка!"
End Select
Case ccsAuthorizing
Case ccsAuthorizing_WaitUser
Select Case Code
Case cerrSuccess
CurrentConnectState = ccsConnect
Case cerrAuth_WrongUserData
MsgBox "Невозможно войти в систему, неверные учетные данные."
Call ClientAuth_Logon
Case cerrAuth_WrongUserLocked
CurrentConnectState = ccsNotConnect
MsgBox "Невозможно войти в систему, учетная запись заблокированна."
Case cerrAuth_WrongUserAccess
CurrentConnectState = ccsNotConnect
MsgBox "Невозможно войти в систему, доступ к подсистеме не разрешен."
Case Else
CurrentConnectState = ccsNotConnect
MsgBox "При подключении произошла ошибка!"
End Select
End Select
End Sub
Sub ClientAuth_Send(ByVal Message As String)
If CurrentConnectState = ccsNotConnect Then Exit Sub
Message = EventMsgID & Message
If CurrentConnectState > ccsConnecting_Complete Then
Message = Crypt.Encrypt(md5.DigestStrToHexStr(Message) & Message)
Message = md5.DigestStrToHexStr(Message) & vbNullChar & Message
End If
frmMAIN.wsClient.SendData Message & vbNullCharDbl
End Sub
Sub ClientAuth_Logon()
'Здесь отображается диалоговое окно, в котором пользователь вводит логин и пароль.
'После ввода данных на сервер отсылается строка вида: AUTH: <LOGIN>@<PWDHASH>
'где <LOGIN> - логин, а <PWDHASH> - хэш на пароль.
CurrentConnectState = ccsAuthorizing_WaitUser
ClientAuth_Send "AUTH: " & LOGIN & "@" & PWDHASH
End Sub
Для сокета имеется такой код:
Private Sub wsClient_DataArrival(ByVal bytesTotal As Long)
Dim I As Long, msg As String, MsgID As String, MsgCode As ClientErrorCodes, MsgBody As String
msg = Space$(bytesTotal)
wsClient.GetData msg, vbString, bytesTotal
MsgBuff = MsgBuff & msg
Do
I = InStr(MsgBuff, vbNullCharDbl)
If I = 0 Then Exit Do
msg = Left$(MsgBuff, I - 1)
MsgBuff = Mid$(MsgBuff, I + Len(vbNullCharDbl))
If CurrentConnectState = ccsConnect Then
If ExtractDataString(msg, MsgID, MsgCode, MsgBody, Crypt) = dcrSuccess Then
If MsgID = EventMsgID Then
WinSock_Event (MsgCode), MsgBody
Else
WinSock_Processing MsgID, MsgCode, MsgBody
End If
End If
Else
If ExtractDataString(msg, MsgID, MsgCode, MsgBody) = dcrSuccess Then
ClientAuth_Recv MsgCode, MsgBody
End If
End If
Loop
End Sub
Функция ClientAuth_Recv вызывается в процессе авторизации, функции WinSock_Event и WinSock_Processing вызываются при получении уведомлений и ответов соответственно. Сами эти функции не приводятся, т.к. их содержимое будет зависеть от требований к подсистеме. Процедура WinSock_Event получает код уведомления и данные. Процедура WinSock_Processing получает идентификатор сообщений (по которому можно будет определить, каков был запрос), код возврата и содержимое сообщения.
Сложности и проблемы
Большинство проблем связаны с клиентской подсистемой. На серверной части сложность только в одном -- обеспечить асинхронность обработки запросов от разных клиентов. Желательно было бы реализовать асинхронность даже на уровне запросов для одного клиента, т.е. чтобы два разных запроса от одного и того же клиента обрабатывались независимо друг от друга. Одно из решений в подобных случаях -- создавать потоки для обработки каждого запроса.
Но обычно достаточно реализовать асинхронность для каждого подключения, а обработку внутри подключений сделать синхронной. Для этих целей проект переделывать не потребуется, т.к. прием/передача данных по WinSock происходит асинхронно, а регистрация событий уже реализована.
С клиентом же дело обстоит сложнее, приходится реализовывать (в самом клиенте) очередь сообщений и усложнять код. В прилагаемом примере реализован один из вариантов организации очереди; на современных машинах он работает достаточно быстро.
Еще один момент, который следовало бы отметить -- передача больших объемов данных. Данная реализация клиент-серверной платформы мало подходит для постоянной передачи данных объемом свыше 10-15 Кб. Тем не менее, нет никаких ограничений на передачу данных, которые могут уместиться в тип данных VB String (около двух гигабайт). Для передачи данных, объем которых превышает 32 Кб нужно будет переделать класс MD5Hash; в данном классе длина текста запоминается в Integer, которое не может принимать значения, выходящие за пределы -32767...+32767. По всем вопросам, связанным с исправлением класса MD5Hash следует обращаться к его автору, Robert M. Hubley (e-mail).
Заключение
Использованный пример был вырезан из проекта, который еще не завершен, поэтому возможны некоторые ошибки и недоработки. Указания на эти ошибки, а также пожелания и рекомендации приветствуются, пишите о них сюда.
Использованный пример не имеет никакого практического значения, он предназначен только для демонстрации и пояснения. Вы можете использовать его в качестве основы для своих клиент-серверных систем. Кроме того, в примере использована библиотека функций (модули modCommon.bas и modWinAPI.bas), которые могут пригодится в разработке своих программ.
Как запустить проект. Запустить сервер (wscs_s.exe), стартовать сервер. Запустить клиент (wscs_c.exe). На сервере созданы два пользователя, demo и wscs (пароли совпадают с логином), второй пользователь заблокирован (т.е. для входа использовать demo/demo). На сервере можно вызвать окно журнала, чтобы видеть протокол авторизации.
Все ссылки представляют собой ZIP-архив, на архив установлен пароль "alibek09.narod.ru".
Исходный код
Материалы данной статьи не допускается размещать без указания на
данную страницу.