(NET) NET (2005 год)

Программирование в сетях

Моя последняя прога 2005-го года - многопоточный клиент-серверный Messenger. Эта прога основана на нескольких довольно хитрых технологиях:

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


Здесь будут описаны и выложены реально работающие проекты, расширяя которые, можно легко двигатся в любом направлении дальше. Реальная же система с применением этих технологий СУЩЕСТВЕННО сложнее - хотя бы потому что демонстрационный SQL-скрипт (выложенный ниже) разбивается на множество триггеров, сложным образом срабатывающих от действий клиентов - действия которых (опять же) зависят как от работы этих триггеров, так и от обработки событий, передаваемых с SQL-сервера по TCP/IP.

Ну и естественно, в реальном приложении нет никаких диагностических сообщений с SQL-сервера, наглухо его подвешивающих, и нет никаких конфигураций C:\Program Files\Microsoft SQL Server\MSSQL\Binn\sqlservr.exe.config - все ведется в таблах.

Ну и еще самое главное отличие реальной системы от рассмотренной ниже - SQL-сервер не передает сообщения триггеров не непосредственно приложению (т.к. приложение может быть за брендмауэром), а передает промежуточному прокcи-серверу, который проксирует пакеты из внутреннего во внешний интерфейс).

Для начала посмотрим как клиентское приложение на VB6 слушает сообщения с SQL-сервера (минуя собственно коннект к SQL-серверу). Для этого делается вот такое демонстрационное приложение на VB6:

00001: Option Explicit
00002: Dim WithEvents Net As ComComponent.NET_Listener
00003: '
00004: Private Sub Form_Load()
00005: On Error GoTo Msg1
00006: Set Net = New ComComponent.NET_Listener
00007: Exit Sub
00008: Msg1: MsgBox ("Ошибка создания обьекта прослушки сети" & vbCrLf & Err.Description)
00009: End Sub
00010: Private Sub Command1_Click()
00011: On Error GoTo Msg2
00012: Net.Hello ("Привет из Net!")
00013: Exit Sub
00014: Msg2: MsgBox ("Ошибка создания обьекта прослушки сети. " & vbCrLf & Err.Description)
00015: End Sub
00016: Private Sub Command2_Click()
00017: On Error GoTo Msg3
00018: Call Net.Start
00019: Exit Sub
00020: Msg3: MsgBox ("Ошибка начала прослушики сети." & vbCrLf & Err.Description)
00021: End Sub
00022: Private Sub Command3_Click()
00023: On Error GoTo Msg4
00024: Call Net.Abort
00025: Exit Sub
00026: Msg4: MsgBox ("Ошибка остановки прослушки сети." & vbCrLf & Err.Description)
00027: End Sub
00028: Private Sub Net_MyEvent(ByVal sender As Variant, ByVal e As mscorlib.EventArgs)
00029: Text1 = sender
00030: End Sub

Как видите в этом приложении в строке 6 создается экземпляр проги ComComponent.NET_Listener, которая и будет прослушивать сеть минуя основной поток VB6 и когда из сети будут поступать сообщения - ComComponent.NET_Listener будет вызывать в строке 28 событие Net_MyEvent и эта прога будет показывать у себя на форме то, что ComComponent.NET_Listener принял из сети. Теперь собственно прога NET_LISTENER:

00001: Imports System.Net
00002: Imports System.Net.Sockets
00003: Imports System.IO
00004: 
00005: <ComClass()> _
00006: Public Class NET_Listener
00007:     Public Event MyEvent As EventHandler
00008:     Dim ValidIP As New System.Text.RegularExpressions.Regex("[1-2]\d{1,2}\.[1-2]?\d{1,2}\.[1-2]?\d{1,2}\.[1-2]?\d{1,2}")
00009:     Dim NewThread As Threading.Thread
00010:     Dim ClientIP As System.Net.IPAddress
00011:     Dim Listener As System.Net.Sockets.TcpListener
00012:     Dim BinReader As System.IO.BinaryReader
00013:     Dim TCPClient As System.Net.Sockets.TcpClient
00014:     Dim NetStream As System.Net.Sockets.NetworkStream
00015:     Dim Config As System.Collections.Specialized.NameValueCollection
00016:     Dim Result As Int32
00017: 
00018:     Public Sub New()
00019:         MyBase.new()
00020:     End Sub
00021: 
00022:     Public Function Hello(ByVal b As String)
00023:         MsgBox(b & vbCrLf & "Поток : " & AppDomain.GetCurrentThreadId.ToString & vbCrLf & _
00024:         "Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString)
00025:     End Function
00026: 
00027:     Public Function Start()
00028:         If NewThread Is Nothing Then  'нам лишних потоков не надо
00029:             Config = System.Configuration.ConfigurationSettings.AppSettings
00030:             If Config.Count = 0 Then
00031:                 MsgBox("Ошибка. Нет файла конфигурации " & vbCrLf & _
00032:                 System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile & vbCrLf & _
00033:                 "в каталоге " & vbCrLf & _
00034:                 System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase)
00035:             Else
00036:                 If Len(Config("IP")) = 0 Or Len(Config("PORT")) = 0 Then
00037:                     MsgBox("Ошибка. В файле конфигурации " & vbCrLf & _
00038:                    System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile & vbCrLf & _
00039:                    "в каталоге " & vbCrLf & _
00040:                    System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase & vbCrLf & _
00041:                    "Нет ключей - ""IP"" или ""PORT"" ")
00042:                 Else
00043:                     If ValidIP.IsMatch(Config("IP")) And Val(Config("PORT")) < 65535 Then
00044:                         NewThread = New Threading.Thread(AddressOf TCP_Listener)
00045:                         NewThread.Start()
00046:                     Else
00047:                         MsgBox("Ошибка. В файле конфигурации " & vbCrLf & _
00048:                         System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile & vbCrLf & _
00049:                         "в каталоге " & vbCrLf & _
00050:                         System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase & vbCrLf & _
00051:                         "Неверно заданы ключи - ""IP"" или ""PORT"" ")
00052:                     End If
00053:                 End If
00054:             End If
00055:         End If
00056:     End Function
00057: 
00058:     Public Sub Abort()
00059:         TCPClient.Close()
00060:         Listener.Stop()
00061:         'динамические обьекты из стека - остальные все вроде статические
00062:         Listener = Nothing
00063:         BinReader = Nothing
00064:         TCPClient = Nothing
00065:         '
00066:         NewThread.Abort()
00067:         NewThread = Nothing
00068:     End Sub
00069: 
00070:     Private Sub TCP_Listener()
00071:         Try
00072:             ClientIP = IPAddress.Parse(Config("IP"))
00073:             Listener = New System.Net.Sockets.TcpListener(ClientIP, Val(Config("PORT")))
00074: 
00075:             Listener.Start()
00076:             '
00077:             'Теперь либо ждем вручную по Pending либо останавливаемся до получения пакета по AcceptSocket или AcceptTcpClient 
00078:             'System.Windows.Forms.Application.DoEvents()  'можно добавить чтобы не подвисали формы 
00079:             '
00080:             TCPClient = Listener.AcceptTcpClient()
00081:             '
00082:             'Здесь поток ЖДЕТ первого пакета из сети чтобы все проинициализировалось
00083:             '
00084:             NetStream = TCPClient.GetStream
00085:             '
00086:             'Иначе можно работать прямо по сырым сокетам без потоков
00087:             'Dim MySocket As Socket = Listener.AcceptSocket()
00088:             'MySocket.Receive(....)  Encoding... и т.д.
00089:             '
00090:             BinReader = New System.IO.BinaryReader(NetStream)
00091:             '
00092:             Do While True
00093:                 '
00094:                 Result = BinReader.ReadInt32  'пока данных нет - поток в этом месте подвисает
00095:                 '
00096:                 'MsgBox("Привет из потока " & AppDomain.GetCurrentThreadId.ToString & vbCrLf & _
00097:                 '"Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString & vbCrLf & _
00098:                 '"Из сети получено - " & Reader.ReadUInt32.ToString & vbCrLf & _
00099:                 '"Press any key чтобы передать событие в VB")
00100:                 '
00101:                 RaiseEvent MyEvent(Result, System.EventArgs.Empty)
00102:                 '
00103:             Loop
00104: 
00105:         Catch x As System.Exception
00106:             If TypeName(x) = "ThreadAbortException" Then
00107:                 'сообщений при закрытии потока нам не надо
00108:             Else
00109:                 MsgBox("Ошибка TCP/IP" & vbCrLf & x.Message & vbCrLf & _
00110:                 "Поток : " & AppDomain.GetCurrentThreadId.ToString & vbCrLf & _
00111:                 "Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString)
00112:             End If
00113:         End Try
00114: 
00115:     End Sub

Как видите в строке 5 - это класс на NET эмулирует COM (только не забудьте поставить соответсвующую галку в свойствах проекта, чтобы .NET-студия сама регистрировала классы в реестре) - что и позволяет прицепить эту прогу к VB6. Строки 23-24 полволяют увидеть номер основного потока VB6 и убедится что он STA, в отличие от потока TCP_Listener, создаваемого в строке 45 (который имеет MTA - Thread Model). Далше управление возвращается в VB6 - а здесь остается Listener, создается указатель на поток данные из сети и Reader (в строке 94) читает данные - пока не прервется коннект и вызывает события в VB6.

Большая часть текста (29-51) тут посвящена загрузки конфигурации из внешнего файла и разбору заданных там параметров с помощью регулярных выражений. Понятно что эти параметры будут вводит какой-нибудь менеджер, думающий только о своих процентах, и органически не способный думать ни о чем другом, кроме денег. К тому же хитрость в том, что конфигурационный файл имеет имя в зависимости не от имени NET-класса, а от имени EXE-файла приложения на VB6, что тоже вносит свою путаницу. Поэтому столько кода уделено конфигурационному файлу. Полный текст этого проекта можно сгрузить отсюда.


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



Теперь рассмотрим сторону SQL-сервера. Я написал достаточно универсальное приложение (которое в публикуемом варианте) скорее более виндузовое, чем SQL-серверное расширение. Оно состоит из двух проектов. Один проект - библиотека передачи сообщений в сеть, другой вот такая форма для тестирования первого проекта, с вот таким текстом:

00001: Public Class Form1
00002:     Inherits System.Windows.Forms.Form
00003:     Dim Sender1 As TCP_SENDER1.NET_Sender
00004:     Dim I As Int32
00005: 
00006: #Region " Windows Form Designer generated code "
00007: #End Region
00008: 
00009:     Private Sub btStart_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btStart.Click
00010:         Sender1 = New TCP_SENDER1.NET_Sender()
00011:         Sender1.Hello("Тест TCP/IP передатчика.")
00012:         If Sender1.Start() > 0 Then
00013:             MsgBox("Не получилось инициализировать передатчик.")
00014:         Else
00015:             btConnect.Enabled = True
00016:         End If
00017:     End Sub
00018: 
00019:     Private Sub btConnect_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btConnect.Click
00020:         If Sender1.TCP_Connect() > 0 Then
00021:             MsgBox("Не получилось приконнектится.")
00022:         Else
00023:             btStart.Enabled = False
00024:             btWrite.Enabled = True
00025:             btConnect.Enabled = False
00026:             btStop.Enabled = True
00027:         End If
00028:     End Sub
00029: 
00030:     Private Sub btWrite_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btWrite.Click
00031:         I += 1
00032:         TextBox1.Text = I
00033:         If Sender1.TCP_Write(I) > 0 Then
00034:             MsgBox("Не получилось передать данные.")
00035:         End If
00036:     End Sub
00037: 
00038:     Private Sub btStop_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btStop.Click
00039:         Sender1.Abort()
00040:         btStart.Enabled = True
00041:         btStop.Enabled = False
00042:         btConnect.Enabled = False
00043:         btWrite.Enabled = False
00044:     End Sub
00045: End Class

Как видите - это просто вызовы методов TCP-передатчика по кнопкам формы. У TCP-передатчика есть и настроечный модуль, описывающий конечную точку TCP/IP - т.е. в точности ту же точку, что описывал предыдущий конфигурационный файл. Локальный порт TCP/IP выбирается передатчиком по умолчанию случайным образом - что в большинстве случаев не требует изменения. Теперь собственно текст NET_Sender:

00001: Imports System.Net
00002: Imports System.Net.Sockets
00003: Imports System.IO
00004: 
00005: 
00006: <ComClass()> _
00007: Public Class NET_Sender
00008:     Public Event MyEvent As EventHandler
00009:     Dim ValidIP As New System.Text.RegularExpressions.Regex("[1-2]\d{1,2}\.[1-2]?\d{1,2}\.[1-2]?\d{1,2}\.[1-2]?\d{1,2}")
00010:     Dim ClientIP As System.Net.IPAddress
00011:     Dim TcpClient As System.Net.Sockets.TcpClient
00012:     Dim NetWriter As System.IO.BinaryWriter
00013:     Dim Config As System.Collections.Specialized.NameValueCollection
00014: 
00015:     Public Sub New()
00016:         MyBase.new()
00017:     End Sub
00018: 
00019:     Public Function Hello(Optional ByVal b As String = "")
00020:         System.Windows.Forms.MessageBox.Show(b & vbCrLf & "Поток : " & AppDomain.GetCurrentThreadId.ToString & _
00021:         ". Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString, "NET_Sender", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Information, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00022:     End Function
00023: 
00024:     Public Function Start()
00025:         Config = System.Configuration.ConfigurationSettings.AppSettings
00026:         If Config.Count = 0 Then
00027:             Try
00028:                 System.Windows.Forms.MessageBox.Show("Ошибка. Нет файла конфигурации " & vbCrLf & _
00029:                 System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile & vbCrLf & _
00030:                 "в каталоге " & vbCrLf & _
00031:                 System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase _
00032:                 , "NET_Sender", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Information, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00033:             Catch x As System.Exception
00034:                 Start = 1
00035:             End Try
00036:         Else
00037:             If Len(Config("IP")) = 0 Or Len(Config("PORT")) = 0 Then
00038:                 Try
00039:                     System.Windows.Forms.MessageBox.Show("Ошибка. В файле конфигурации " & vbCrLf & _
00040:                     System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile & vbCrLf & _
00041:                     "в каталоге " & vbCrLf & _
00042:                     System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase & vbCrLf & _
00043:                     "Нет ключей - ""IP"" или ""PORT"" " _
00044:                     , "NET_Sender", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Information, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00045:                 Catch x As System.Exception
00046:                     Start = 2
00047:                 End Try
00048:             Else
00049:                 If ValidIP.IsMatch(Config("IP")) And Val(Config("PORT")) < 65535 Then
00050:                     Try
00051:                         TcpClient = New System.Net.Sockets.TcpClient()
00052:                         ClientIP = IPAddress.Parse(Config("IP"))
00053:                         Start = 0
00054:                     Catch x As System.Exception
00055:                         Start = 4
00056:                     End Try
00057:                 Else
00058:                     Try
00059:                         System.Windows.Forms.MessageBox.Show("Ошибка. В файле конфигурации " & vbCrLf & _
00060:                         System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile & vbCrLf & _
00061:                         "в каталоге " & vbCrLf & _
00062:                         System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase & vbCrLf & _
00063:                         "Неверно заданы ключи - ""IP"" или ""PORT"" " _
00064:                         , "NET_Sender", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Information, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00065:                     Catch x As System.Exception
00066:                         Start = 3
00067:                     End Try
00068:                 End If
00069:             End If
00070:         End If
00071:     End Function
00072: 
00073:     Public Function TCP_Connect()
00074:         If TcpClient Is Nothing Then
00075:             'Старта еще не было
00076:             TCP_Connect = 1
00077:         Else
00078:             Try
00079:                 TcpClient.Connect(System.Net.IPAddress.Parse(Config("IP")), Val(Config("PORT")))
00080:                 Dim NetStream = TcpClient.GetStream               'указатель потока в сети
00081:                 NetWriter = New System.IO.BinaryWriter(NetStream) 'обьект, который пишет в поток
00082:                 TCP_Connect = 0
00083:             Catch x As System.Exception
00084:                 System.Windows.Forms.MessageBox.Show("Ошибка коннекта в порт TCP/IP " & Config("IP") & ":" & Config("Port") & vbCrLf & _
00085:                 x.Message & vbCrLf & _
00086:                 "Поток : " & AppDomain.GetCurrentThreadId.ToString & _
00087:                 ". Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString _
00088:                 , "NET_Sender", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Information, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00089:                 TCP_Connect = 1
00090:             End Try
00091:         End If
00092:     End Function
00093: 
00094:     Public Function TCP_Write(Optional ByVal Value As Int32 = 0)
00095:         If NetWriter Is Nothing Then
00096:             'Коннекта еще не было
00097:             TCP_Write = 1
00098:         Else
00099: 
00100:             Try
00101:                 NetWriter.Write(Value)
00102:                 TCP_Write = 0
00103:             Catch x As System.Exception
00104:                 MsgBox("Ошибка записи в порт TCP/IP " & Config("IP") & ":" & Config("Port") & vbCrLf & _
00105:                 x.Message & vbCrLf & _
00106:                 "Поток : " & AppDomain.GetCurrentThreadId.ToString & _
00107:                 ". Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString)
00108:                 TCP_Write = 1
00109:             End Try
00110:         End If
00111:     End Function
00112: 
00113:     Public Sub Abort()
00114:         If TcpClient Is Nothing Then
00115:         Else
00116:             TcpClient.Close()
00117:             TcpClient = Nothing
00118:         End If
00119:         If NetWriter Is Nothing Then
00120:         Else
00121:             NetWriter.Close()
00122:             NetWriter = Nothing
00123:         End If
00124:     End Sub
00125: End Class

Как видите - тут тот же принцип. Грузится конфигурационный файл - разбирается по регулярным выражениям. Создается экземпляр TCP-клиента (указатель на который давал TCP-Listener в серверном варианте) - TcpClient.GetStream дает указатель на поток в сетит (строка 80), строка 81 - создаем BinaryWriter в этом потоке, и пишем в сеть пока не надоест - строка 101. Полный текст проекта можно сгрузить отсюда.

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






Ну а вот собственно последний рисунок - это апогей демонстрации всей этой технологии. Из SQL-сервера мы отправили число 5 и на удаленном TCP/IP-узле получили это число (и событие SQL-сервера) непосредственно в проге на VB6... При этом наша VB6-прога не подвисала в ожидании сообщений из сети и ничего не опрашивала на SQL-сервере. Просто триггер SQL-сервера вызвал событие у клиентской проги на VB6...


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


Единственно серьезное отличие (и сложность) при работе на передающей стороне не просто описанной выше проги-Sender, а обьекта, подгружаемого из SQL-сервера по sp_OACreate - то, что SQL-сервер уничтожает экземпляр обьекта, после завершения SQL-BATCH. Естественно виндузня не дает использовать TCP/IP канал сопоставленный уже удаленному классу. Иначе говоря - так как на левом рисунке - работать будет, а так как на правом - нет.




При наличии на передающей части TCP/IP канала SQL-сервера алгоритм использования вышеприведенных прог получается такой:



Comments ( )
Link to this page: //www.vb-net.com/dotnet/tour14/index.htm
< THANKS ME>