Рейтинг@Mail.ru

Интерфейс Mailslot на примере работы чата Intranet Chat

автор:
Be the first to comment! Программирование
Print Friendly, PDF & Email
Рассматривается механизм Mailslot на примере работы чата для локальной сети “Intranet Chat”. Попутно описывается алгоритм шифрования RC4 и его реализация на VB.NET.

Довольно популярная в начале 2000-ых годов программа “Intranet Chat” или просто iChat предназначена для обмена текстовыми сообщениями в локальной сети. В работе она использует клиент-серверный интерфейс mailslot. Кроме того, передаваемые данные программа шифрует, используя криптографический алгоритм RC4. Поэтому сначала рассмотрим этот алгоритм шифрования.

1Алгоритм шифрования RC4и его реализация на VB.NET

Алгоритм RC4 или, точнее, его открытая предполагаемая реализация ARC4, довольно прост в своей идее и прост в реализации. Коротко, суть в том, что биты исходного сообщения через операцию «исключающего или» (XOR) объединяется с битами ключа, и таким образом получается зашифрованное сообщение. Если зашифрованное сообщение пропустить через тот же алгоритм с тем же ключом, то получается расшифрованное сообщение. Длина ключа равняется длине сообщения, поэтому теоретически взломать такой алгоритм практически невозможно. Однако на практике для генерации ключа используются механизмы, называемые генераторами псевдослучайных последовательностей (ПСП).

Псевдослучайная последовательность – это такая последовательность, которая содержит в себе периодически повторяющуюся последовательность битов. Преимущество ПСП в том, что они легко получаются аппаратным или программным способом.

Не будем изобретать велосипед, возьмём за основу вот эту реализацию кодера и декодера алгоритма RC4, перепишем её на язык VB.NET и введём несколько улучшений.

Кодер и декодер RC4 на VB.NET (разворачивается)
Option Strict On
Option Explicit On
Option Infer On

Imports System.Runtime.CompilerServices

''' <summary>
''' Алгоритм шифрования и расшифрования RC4.
''' </summary>
Public Class Rc4

    ''' <summary>
    ''' Ключ шифрования (и расшифрования).
    ''' </summary>
    Public ReadOnly Property Key As Byte()

#Region "CTOR"

    ''' <summary>
    ''' Инициализация алгоритма RC4 заданным ключом.
    ''' </summary>
    ''' <param name="key"></param>
    Public Sub New(key As Byte())
        Me.Key = key
    End Sub

    Private S(255) As Integer 'Вектор перестановки.
    Private X As Integer 'Счётчик.
    Private Y As Integer 'Счётчик.

    ''' <summary>
    ''' Инициализирует вектор перестановки заданным ключом
    ''' (алгоритм ключевого расписания, key-scheduling algorithm).
    ''' </summary>
    ''' <param name="key">Байты ключа.</param>
    Private Sub Init(key() As Byte)
        X = 0
        Y = 0
        For i As Integer = 0 To 255
            S(i) = i
        Next
        Dim j As Integer = 0
        Dim keyLen As Integer = key.Length
        For i As Integer = 0 To 255
            j = (j + S(i) + CInt(key(i Mod keyLen))) Mod 256
            S.Swap(i, j)
        Next
    End Sub

#End Region '/CTOR

#Region "OPEN METHODS"

    ''' <summary>
    ''' Кодирует массив.
    ''' </summary>
    ''' <param name="openTextBytes">Массив байтов открытого текста.</param>
    Public Function Encode(openTextBytes As Byte()) As Byte()
        Init(Key)
        Dim ecnode As Byte() = New Byte(openTextBytes.Length - 1) {}
        For i As Integer = 0 To openTextBytes.Length - 1
            ecnode(i) = (openTextBytes(i) Xor GetNextItem())
        Next
        Return ecnode
    End Function

    ''' <summary>
    ''' Декодирует массив.
    ''' </summary>
    ''' <param name="encodedBytes">Зашифрованный массив.</param>
    Public Function Decode(encodedBytes As Byte()) As Byte()
        Return Encode(encodedBytes)
    End Function

#End Region '/OPEN METHODS

    ''' <summary>
    ''' Возвращает следующий элемент ПСП.
    ''' </summary>
    Private Function GetNextItem() As Byte
        X = (X + 1) Mod 256
        Y = (Y + S(X)) Mod 256
        S.Swap(X, Y)
        Dim ind As Integer = (S(X) + S(Y)) Mod 256
        Return CByte(S(ind))
    End Function

End Class

''' <summary>
''' Класс, расширяющий функциональность заданных типов.
''' </summary>
<Extension()>
Friend Module SwapExt

    ''' <summary>
    ''' Меняет местами заданные элементы массива.
    ''' </summary>
    ''' <typeparam name="T">Тип массива.</typeparam>
    ''' <param name="arr">Массив.</param>
    ''' <param name="index1">Индекс 1-го элемента.</param>
    ''' <param name="index2">Индекс 2-го элемента.</param>
    <Extension()>
    Public Sub Swap(Of T)(arr() As T, index1 As Integer, index2 As Integer)
        Dim temp As T = arr(index1)
        arr(index1) = arr(index2)
        arr(index2) = temp
    End Sub

End Module

Пользоваться нашим кодером и декодером очень просто: объявляем объект типа RC4, передавая ему ключ шифрования. Далее вызываем один из двух открытых методов: Encode() или Decode(). Первый – для зашифрования, второй – для расшифрования сообщения. Например, так:

Dim key As Byte() = Text.Encoding.UTF8.GetBytes("Мой супер секретный ключ")
Dim rc As New Rc4(key)
Dim message as Byte() = Text.Encoding.UTF8.GetBytes("Моё сообщение, которое будет зашифровано алгоритмом RC4.")
Dim encoded As Byte() = rc.Encode(message) 'байты зашифрованного сообщения
Dim restored as Byte() = rc.Decode(encoded)
Dim restoredText as String = Text.Encoding.UTF8.GetString(restored) 'восстановленный текст

Скомпилированная программа для расшифрования зашифрованных сообщений "Intranet Chat" – в приложении к статье внизу. Она написана на основе того кода, который здесь приведён.

2Реализация mailslot на языке VB.NET

Mailslot (мэйлслот) – это клиент-серверный интерфейс, с помощью которого можно передавать данные от одного процесса другому в операционной системе и по сети. Мэйлслот рассчитан на передачу небольших (до 424 байтов) сообщений, в том числе широковещательных (один отправитель, а получателей – много).

В системе должен быть один сервер мэйлслотов, который создаёт мэйлслот (локально). Клиентов может быть один или несколько. Данные передаются только в одну сторону: от клиента – к серверу. Для работы данного механизма сервером используются функции CreateMailslot() (создать мэйлслот) и ReadFile() (прочитать данные от клиентов), а клиентом – CreateFile() (создать файл) и WriteFile() (записать данные в файл), а также несколько других, менее интересных для нас. Обращение к серверу мэйлслотов осуществляется по имени. Зная имя мэйлслота клиенты могут записывать в него данные. Сервер мэйлслотов хранит эти данные, а при остановке сервера все данные теряются.

На самом деле, никакие файлы при использовании мэйлслотов не создаются: данные записываются в псевдофайл, расположенный в памяти компьютера.

Имена мэйлслотов образуются по такому принципу. Если мэйлслот расположен на локальном компьютере (регистр не учитывается):

\\.\mailslot\[путь\]имяМэйлслота

Если мэйлслот расположен на удалённом компьютере:

\\имяКомпьютера\mailslot\[путь\]имяМэйлслота

Для записи во все мэйлслоты с данным именем:

\\*\mailslot\[путь\]имяМэйлслота

В ОС Windows работа с мэйлслотами реализуется в системной библиотеке kernel32.dll. А описание данной технологии можно найти на сайте Microsoft.

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

Вспомогательный класс для Mailslot на VB.NET (разворачивается)
Imports Microsoft.Win32.SafeHandles
Imports System.Runtime.ConstrainedExecution
Imports System.Runtime.InteropServices
Imports System.Security
Imports System.Security.Permissions

Namespace Mailslots

    <SuppressUnmanagedCodeSecurity()>
    <SecurityCritical(SecurityCriticalScope.Everything)>
    <HostProtection(SecurityAction.LinkDemand, MayLeakOnAbort:=True), SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode:=True)>
    Public Class SafeMailslotHandle
        Inherits SafeHandleZeroOrMinusOneIsInvalid

#Region "NATIVE"

        <ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)>
        <DllImport("kernel32.dll")>
        Private Shared Function CloseHandle(handle As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

#End Region '/NATIVE

#Region "CTOR"

        Private Sub New()
            MyBase.New(True)
        End Sub

        Public Sub New(preexistingHandle As IntPtr, ownsHandle As Boolean)
            MyBase.New(ownsHandle)
            MyBase.SetHandle(preexistingHandle)
        End Sub

#End Region '/CTOR

        Protected Overrides Function ReleaseHandle() As Boolean
            Return SafeMailslotHandle.CloseHandle(Me.handle)
        End Function

    End Class

End Namespace

Сервер Mailslot непосредственно в данной работе нам не потребуется, но для полноты изложения прикладываю и его код. Если вы захотите написать полноценую замену для чата Intranet Chat, этот код вам понадобится.

Сервер Mailslot на VB.NET (разворачивается)
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Security
Imports System.Text

Namespace Mailslots

    ''' <summary>
    ''' Сервер Mailslot.
    ''' </summary>
    <SuppressUnmanagedCodeSecurity()>
    Public Class MailslotServer
        Implements IDisposable

#Region "NATIVE"

        <DllImport("kernel32.dll")>
        Private Shared Function CreateMailslot(mailslotName As String, nMaxMessageSize As UInteger, lReadTimeout As Integer, securityAttributes As IntPtr) As SafeMailslotHandle
        End Function

        <DllImport("kernel32.dll")>
        Private Shared Function GetMailslotInfo(hMailslot As SafeMailslotHandle, lpMaxMessageSize As IntPtr, <Out()> ByRef lpNextSize As Integer, <Out()> ByRef lpMessageCount As Integer, lpReadTimeout As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("kernel32.dll")>
        Private Shared Function ReadFile(handle As SafeMailslotHandle, bytes As Byte(), numBytesToRead As Integer, <Out()> ByRef numBytesRead As Integer, overlapped As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

#End Region '/NATIVE

        ''' <summary>
        ''' Число ожидающих сообщений. Обновляется только после вызова <see cref="GetNextMessage()"/>.
        ''' </summary>
        Public ReadOnly Property NumberMessages As Integer
            Get
                Return _NumberMessages
            End Get
        End Property
        Private _NumberMessages As Integer = 0

#Region "CTOR"

        Private Handle As SafeMailslotHandle

        Public Sub New(name As String)
            Handle = CreateMailslot($"\\.\mailslot\{name}", 0, 0, IntPtr.Zero) 'обратите внимание: данный синтаксис поддерживается только начиная с VS2019, в более ранних версиях используйте метод String.Format()
            If Handle.IsInvalid Then
                Throw New Win32Exception("Ошибка создания Mailslot.CreateMailslot()")
            End If
        End Sub

        Private Sub Dispose() Implements IDisposable.Dispose
            If (Handle IsNot Nothing) Then
                Handle.Close()
                Handle = Nothing
            End If
        End Sub

#End Region '/CTOR

#Region "METHODS"

        Private Const NO_MESSAGE As Integer = -1

        ''' <summary>
        ''' Получает следующее сообщение.
        ''' </summary>
        Public Function GetNextMessage() As String
            Dim messageBytes As Integer
            If (Not GetMailslotInfo(Handle, IntPtr.Zero, messageBytes, _NumberMessages, IntPtr.Zero)) Then
                Throw New Win32Exception("Ошибка в Mailslot.GetMailslotInfo()")
            End If
            If (messageBytes = NO_MESSAGE) Then
                Return String.Empty
            End If
            Dim buffer As Byte() = New Byte(messageBytes - 1) {}
            Dim bytesRead As Integer
            Dim succeeded As Boolean = ReadFile(Handle, buffer, messageBytes, bytesRead, IntPtr.Zero)
            If (Not succeeded) OrElse (bytesRead = 0) Then
                Throw New Win32Exception("Ошибка в Mailslot.ReadFile()")
            End If
            Dim result As String = Encoding.Unicode.GetString(buffer)
            Return result
        End Function

#End Region '/METHODS

    End Class '/MailslotServer

End Namespace

Теперь напишем клиент протокола Mailsot:

Клиент Mailslot на VB.NET (разворачивается)
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Security
Imports System.Text

Namespace Mailslots

    ''' <summary>
    ''' Клиент Mailslot.
    ''' </summary>
    <SuppressUnmanagedCodeSecurity()>
    Public Class MailslotClient
        Implements IDisposable

        Private Handle As SafeMailslotHandle
        Private ReadOnly Name As String
        Private ReadOnly Machine As String

#Region "ENUMS"

        <Flags()>
        Public Enum FileDesiredAccess As UInteger
            GenericRead = &H80000000UI
            GenericWrite = &H40000000UI
            GenericExecute = &H20000000UI
            GenericAll = &H10000000UI
        End Enum

        <Flags()>
        Public Enum FileShareMode As UInteger
            Zero = 0UI
            FileShareRead = 1UI
            FileShareWrite = 2UI
            FileShareDelete = 4UI
        End Enum

        Public Enum FileCreationDisposition As UInteger
            CreateNew = 1UI
            CreateAlways
            OpenExisting
            OpenAlways
            TruncateExisting
        End Enum

#End Region '/ENUMS

#Region "NATIVE"

        <DllImport("kernel32.dll")>
        Private Shared Function CreateFile(fileName As String, desiredAccess As FileDesiredAccess, shareMode As FileShareMode, securityAttributes As IntPtr, creationDisposition As FileCreationDisposition, flagsAndAttributes As Integer, hTemplateFile As IntPtr) As SafeMailslotHandle
        End Function

        <DllImport("kernel32.dll")>
        Private Shared Function WriteFile(handle As SafeMailslotHandle, bytes As Byte(), numBytesToWrite As Integer, <Out()> ByRef numBytesWritten As Integer, overlapped As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

#End Region '/NATIVE

#Region "CTOR"

        Public Sub New(name As String)
            Me.New(name, ".")
        End Sub

        Public Sub New(name As String, machine As String)
            Me.Name = name
            Me.Machine = machine
        End Sub

        Private Sub CreateHandle()
            Dim fileName As String = $"\\{Machine}\MAILSLOT\{Name}"
            Handle = CreateFile(fileName, FileDesiredAccess.GenericWrite, FileShareMode.FileShareRead, IntPtr.Zero, FileCreationDisposition.OpenExisting, 0, IntPtr.Zero)
            If Handle.IsInvalid Then
                Throw New Win32Exception("Ошибка создания указателя CreateHandle()")
            End If
        End Sub

        Private Sub Dispose() Implements IDisposable.Dispose
            If (Handle IsNot Nothing) Then
                Handle.Close()
                Handle = Nothing
            End If
        End Sub

#End Region '/CTOR

#Region "METHODS"

        ''' <summary>
        ''' Отправляет сообщение.
        ''' </summary>
        ''' <param name="msg">Байты сообщения.</param>
        Public Overridable Sub SendMessage(msg As String)
            Dim message As Byte() = Encoding.UTF8.GetBytes(msg)
            SendMessage(message)
        End Sub

        ''' <summary>
        ''' Отправляет сообщение.
        ''' </summary>
        ''' <param name="msg">Текст сообщения.</param>
        Public Overridable Sub SendMessage(msg As Byte())
            If (Handle Is Nothing) Then
                CreateHandle()
            End If
            Dim bytesWritten As Integer
            Dim succeeded As Boolean = WriteFile(Handle, msg, msg.Length, bytesWritten, IntPtr.Zero)
            If (Not succeeded) OrElse (msg.Length <> bytesWritten) Then
                Me.Dispose()
                Throw New Win32Exception("Ошибка отправки сообщения SendMessage()")
            End If
        End Sub

#End Region '/METHODS

    End Class '/MailslotClient

End Namespace

Использовать напрямую данный клиент с программой IChat мы пока ещё не можем, т.к. прежде нужно изучить протокол, по которому разные экземпляры программы «общаются» друг с другом в сети.

3Изучение работы программы "Intranet Chat" с помощью Wireshark

Теперь, когда мы умеем расшифровывать зашифрованные алгоритмом RC4 сообщения и имеем представление о протоколе Mailslot, можем приступить к более пристальному изучению программы "Intranet Chat". Для изучения протокола обмена клиентов программы iChat между собой воспользуется знаменитой программой Wireshark – сниффером сетевого трафика.

Сниффер – это программа захвата сетевого трафика. Зачастую снифферы обладают функциональностью анализа принимемых сетевых пакетов, а также их удобного представления, сохранения и другими возможностями.

Запустим Wireshark, выберем сетевой интерфейс, по которому будут идти данные и запустим мониторинг сетевых пакетов. Чтобы он показывал нам только интересующие данные протокола Mailslot, применим следующий фильтр (поле "1" на рисунке):

mailslot && !browser

Здесь мы включаем все сообщения протокола Mailslot, за исключением сообщений браузера, которые используются операционнной системой. Теперь запустим программу "Intranet Chat" и сразу же увидим в сниффере «улов». В анализаторе заголовков сетевых пакетов, в разделе SMB Mailslot Protocol можно сразу же увидеть, какое имя у мэйлслота программы Intranet Chat: ichat047.

Сетевые пакеты протокола Mailslot программы Intranet Chat в сниффере пакетов Wireshark
Сетевые пакеты протокола Mailslot программы "Intranet Chat" в сниффере пакетов Wireshark

Теперь поставим курсор мыши на поле данных Data (2 на рисунке), откроем правой кнопкой мыши контекстное меню и скопируем (3) в буфер обмена данные (4) пакета. Вставим скопированное в наш декодер и получим команду, которую отправляет программа "Intranet Chat".

Расшифрование сообщений программы IChat с помощью декодера алгортима RC4
Расшифрование сообщений программы IChat с помощью декодера алгортима RC4

Чтобы вставить текст из буфера обмена в консоль, нужно кликнуть мышью в заголовке окна консоли на иконку в левом верхнем углу, а затем выбрать Правка - Вставить.

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

Изучив собранные сниффером пакеты, мы увидим, что при запуске программа iChat отправляет два сообщения:

‼iChat‼‼1‼‼AAVE‼‼DISCONNECT‼‼iTCniaM‼
‼iChat‼‼2‼‼AAVE‼‼CONNECT‼‼iTCniaM‼‼1‼‼AAVE‼‼‼‼Hello all!‼‼*‼‼1.21b4‼‼0‼

Здесь на себя обращают внимание слова DISCONNECT и CONNECT; вполне очевидно, что это команды отключения клиента (видимо, для предотвращения коллизий) и затем подключения к чату. "1.21b4" – это конечно же версия IChat клиента.

Теперь отправим сообщение из программы iChat в общий чат и посмотрим, какие пакеты нам покажет Wireshark. Вот пример стандартного сообщения всем участникам чата:

‼iChat‼‼3‼‼AAVE‼‼TEXT‼‼iTCniaM‼‼Hello, Soltau.ru‼‼‼

Здесь можно отметить следующие группы:

ГруппаНазначение
Разделитель между группами; это байт 0x13
iChatПризнак сообщения программы "Intranet Chat"
3Порядковый номер сообщения от данного клиента; начинается с 1 и увеличивается на 1 после каждого сообщения от данного отправителя
AAVEИмя компьютера, с которого отправлено сообщение
TEXTПризнак текстового сообщения
iTCniaMИмя общего чата в программе iChat
Hello, Soltau.ruТекст сообщения

Отправим приватное сообщение кому-нибудь в чате. При этом программа Intranet Chat передаст следующее:

‼iChat‼‼46‼‼AAVE‼‼TEXT‼‼gsMTCI‼‼ Hello, Soltau.ru‼‼VIRPC‼

Здесь всё аналогично, кроме того, что признак приватного сообщения – строка "gsMTCI" и адрес получателя в конце. В данном случае это VIRPC.

Если мы немного понаблюдаем за сетевым трафиком, то отметим ещё одну интересную особенность. С определённым периодом (обычно от 1-2 до нескольких минут) программа Intranet Chat каждого из пользователей в сети «отмечается» в общем чате, посылая сообщение «я тут!», которые выглядят так:

‼iChat‼‼12‼‼AAVE‼‼REFRESH‼‼iTCniaM‼‼1‼‼AAVE‼‼‼‼Hello All!‼‼*‼‼1.21b4‼‼0‼

Думаю, тут комментарии излишни. Если какой-то из участников чата слишком долго не посылает поднобное сообщение, он будет отмечен как выбывший.

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

4Создание клиента чата IChat на языке VB.NET

Теперь мы знаем достаточно чтобы написать минимально действующий клиент для программы Intranet Chat, который будет отправлять сообщения в общий чат и в приватный чат заданному адресату. Напишем класс, реализующий клиент мэйлслотов для IChat, который будет производным от нашего ранее написанного класса MailslotClient:

Клиент для программы "Intranet Chat" на VB.NET (разворачивается)
Namespace Mailslots

    ''' <summary>
    ''' Клиент для передачи сообщений в общий чат программы "Intranet Chat".
    ''' </summary>
    Public Class IChatClient
        Inherits MailslotClient
        Implements IDisposable

#Region "ПОЛЯ"

        ''' <summary>
        ''' Кодер/декодер алгоритма RC4.
        ''' </summary>
        Private ReadOnly Rc As New Rc4(GetBytes("tahci"))

        ''' <summary>
        ''' Счётчик сообщений, отправленных клиентом. Начинается с 1 и увеличивается на 1 с каждым сообщением.
        ''' </summary>
        Private MessageCounter As Integer = 0

#End Region '/ПОЛЯ

#Region "КОНСТРУКТОРЫ"

        ''' <summary>
        ''' Создаёт клиента для обмена сообщениями в общем чате.
        ''' </summary>
        Public Sub New()
            MyBase.New("ichat047", "*")
            Disconnect()
            Connect()
        End Sub

        Protected Sub New(machine As String)
            MyBase.New("ichat047", machine)
            Disconnect()
            Connect()
        End Sub

#End Region '/КОНСТРУКТОРЫ

#Region "OPEN METHODS"

        ''' <summary>
        ''' Отправляет сообщение в общий чат.
        ''' </summary>
        ''' <param name="msg">Текст сообщения.</param>
        Public Overrides Sub SendMessage(msg As String)
            MessageCounter += 1
            Dim wrapped As Byte() = GetWrappedMessage(msg)
            Dim encoded As Byte() = Rc.Encode(wrapped)
            MyBase.SendMessage(encoded)
        End Sub

#End Region '/OPEN METHODS

#Region "ЗАЩИЩЁННЫЕ МЕТОДЫ"

        ''' <summary>
        ''' Возвращает сообщение с нужным для публичного сообщения обрамлением.
        ''' </summary>
        ''' <param name="msg">Текст сообщения.</param>
        ''' <remarks>Вот как выглядит сообщение чата: 
        ''' ‼iChat‼‼48‼‼AAVE‼‼TEXT‼‼iTCniaM‼‼Message text‼‼‼
        ''' </remarks>
        Protected Overridable Function GetWrappedMessage(msg As String) As Byte()
            Dim wrapped As New List(Of Byte)
            wrapped.AddRange(GetCommon())
            wrapped.AddRange(GetBytes("TEXT"))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetLineName())) '"iTCniaM")) 'имя линии для общего чата.
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(msg))
            wrapped.AddRange(GetDelim(3))
            Debug.WriteLine("Message=" & Text.Encoding.GetEncoding(1251).GetString(wrapped.ToArray()))
            Return wrapped.ToArray()
        End Function

        ''' <summary>
        ''' Возвращает часть сообщения, общую для всех: счётчик сообщений и имя отправителя.
        ''' </summary>
        Protected Function GetCommon() As List(Of Byte)
            Dim common As New List(Of Byte)
            common.AddRange(GetDelim(1))
            common.AddRange(GetBytes("iChat"))
            common.AddRange(GetDelim(2))
            common.AddRange(GetBytes(MessageCounter.ToString()))
            common.AddRange(GetDelim(2))
            Dim sender As String = GetComputerName()
            common.AddRange(GetBytes(sender))
            common.AddRange(GetDelim(2))
            Return common
        End Function

        ''' <summary>
        ''' Преобразует строку в массив байтов в кодировке Windows-1251.
        ''' </summary>
        Protected Function GetBytes(s As String) As Byte()
            Return Text.Encoding.GetEncoding(1251).GetBytes(s)
        End Function

        ''' <summary>
        ''' Возвращает заданное количество разделителей.
        ''' </summary>
        ''' <param name="num">Необходимое количество разделителей.</param>
        ''' <remarks>В программе iChat разделителем является байт 0x13.</remarks>
        Protected Function GetDelim(num As Integer) As List(Of Byte)
            Dim d As New List(Of Byte)
            For i As Integer = 0 To num - 1
                d.Add(&H13)
            Next
            Return d
        End Function

#End Region '/ЗАЩИЩЁННЫЕ МЕТОДЫ

#Region "ЗАКРЫТЫЕ МЕТОДЫ"

        ''' <summary>
        ''' Отключается от общего чата.
        ''' </summary>
        Private Sub Disconnect()
            MessageCounter += 1
            Dim wrapped As New List(Of Byte)
            wrapped.AddRange(GetCommon())
            wrapped.AddRange(GetBytes("DISCONNECT"))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetLineName())) 'имя линии чата
            wrapped.AddRange(GetDelim(1))
            Debug.WriteLine("Disconnect=" & Text.Encoding.GetEncoding(1251).GetString(wrapped.ToArray()))
            Dim encoded As Byte() = Rc.Encode(wrapped.ToArray())
            MyBase.SendMessage(encoded)
        End Sub

        ''' <summary>
        ''' Подключиться к общему чату.
        ''' </summary>
        Private Sub Connect()
            MessageCounter += 1
            Dim wrapped As New List(Of Byte)
            wrapped.AddRange(GetCommon())
            wrapped.AddRange(GetBytes("CONNECT"))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetLineName())) 'имя линии чата
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetUserName()))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetComputerName()))
            wrapped.AddRange(GetDelim(4))
            wrapped.AddRange(GetBytes("Hello all!"))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetDestination())) 'получатель или "*"
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetVersion())) 'версия программы
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetStatus()))
            wrapped.AddRange(GetDelim(1))
            Debug.WriteLine("Connect=" & Text.Encoding.GetEncoding(1251).GetString(wrapped.ToArray()))
            Dim encoded As Byte() = Rc.Encode(wrapped.ToArray())
            MyBase.SendMessage(encoded)
        End Sub

        ''' <summary>
        ''' Возвращает имя линии общего чата.
        ''' </summary>
        Private Function GetLineName() As String
            Return "iTCniaM"
        End Function

        ''' <summary>
        ''' Возвращает статус.
        ''' </summary>
        Private Function GetStatus() As String
            Return "0"
        End Function

        ''' <summary>
        ''' Возвращает версию программы iChat.
        ''' </summary>
        Private Function GetVersion() As String
            Return "1.21b4"
        End Function

        ''' <summary>
        ''' Получатель. Для публичного чата это "*".
        ''' </summary>
        Private Function GetDestination() As String
            Return "*"
        End Function

        ''' <summary>
        ''' Возвращает DNS имя компьютера, на котором запущен клиент iChat.
        ''' </summary>
        Private Function GetComputerName() As String
            Return System.Net.Dns.GetHostName()
        End Function

        ''' <summary>
        ''' Возвращает имя текущего пользователя Windows.
        ''' </summary>
        Private Function GetUserName() As String
            Return Environment.UserName
        End Function

#End Region '/ЗАКРЫТЫЕ МЕТОДЫ

        Protected Overloads Sub Dispose() Implements IDisposable.Dispose
            Disconnect() 'не забудем отключиться от чата при завершении работы
            MyBase.Dispose()
        End Sub

    End Class

End Namespace

Также напишем клиент для отправки приватных сообщений в чат iChat. Он будет наследоваться от класса публичного клиента, но будет лишь переопределять метод GetWrappedMessage(), который генерирует «обвязку» сообщения, а остальное практически целиком берётся из базового класса.

Приватный клиент для программы "Intranet Chat" на VB.NET (разворачивается)
Namespace Mailslots

    ''' <summary>
    ''' Клиент для передачи приватных сообщений программы "Intranet Chat".
    ''' </summary>
    Public Class IChatPrivateClient
        Inherits IChatClient

#Region "КОНСТРУКТОР"

        ''' <summary>
        ''' Имя компьютера адресата.
        ''' </summary>
        Private ReadOnly Destination As String

        ''' <summary>
        ''' Создаёт клиента для обмена приватными сообщениями с заданным компьютером.
        ''' </summary>
        ''' <param name="dest">Имя адресата.</param>
        Public Sub New(dest As String)
            MyBase.New(dest)
            Destination = dest
        End Sub

#End Region '/КОНСТРУКТОР

#Region "ЗАЩИЩЁННЫЕ МЕТОДЫ"

        ''' <summary>
        ''' Возвращает сообщение с нужным для приватного сообщения обрамлением.
        ''' </summary>
        ''' <param name="msg">Текст сообщения.</param>
        ''' <remarks>Вот как выглядит приватное сообщение чата: 
        ''' ‼iChat‼‼49‼‼AAVE‼‼TEXT‼‼gsMTCI‼‼ Hello‼‼VIRPC‼
        ''' </remarks>
        Protected Overrides Function GetWrappedMessage(msg As String) As Byte()
            Dim wrapped As New List(Of Byte)
            wrapped.AddRange(GetCommon())
            wrapped.AddRange(GetBytes("TEXT"))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(GetLineName())) 'имя линии приватного чата.
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(msg))
            wrapped.AddRange(GetDelim(2))
            wrapped.AddRange(GetBytes(Destination))
            wrapped.AddRange(GetDelim(1))
            Return wrapped.ToArray()
        End Function

        ''' <summary>
        ''' Возвращает имя линии приватного чата.
        ''' </summary>
        Private Function GetLineName() As String
            Return "gsMTCI"
        End Function

#End Region '/ЗАЩИЩЁННЫЕ МЕТОДЫ

    End Class

End Namespace

Использовать написанный клиент будем, например, так:

Using iChat As New IChatClient()
    While True
        Console.Write("Введите сообщение: ")
        Dim msg As String = Console.ReadLine()
        iChat.SendMessage(msg)
    End While
End Using

Конечно, данный клиент не обладает полнотой функциональности программы IChat. Например, мы ещё не научили его обновлять список компьютеров в сети, докладывать о своём статусе и т.д. Но мы взяли нужное направление, и если интересно – теперь вы знаете, что делать дальше.

Давайте проверим, как всё это работает. Запустим в локальной сети несколько экземпляров программы IChat.

Для имитации локальной сети я буду использовать виртуальную машину с установленной ОС Windows 7, которая находится в той же рабочей группе, что и основной компьютер. Таким образом у меня будет сеть, состоящая из двух машин.

На отдельном компьютере запустим нашу тестовую программу с клиентом iChat.

Отправка сообщений из собственного клиента в публичный чат программы Intranet Chat
Отправка сообщений из собственного клиента в публичный чат программы Intranet Chat

Обратите внимание, что при запущенной программе "Intranet Chat" написанный нами клиент работать не будет. Это связано с тем, что возникает коллизия в нумерации сообщений в чате. Как мы помним, при запуске программа iChat отправляет несколько сообщений на отключение и подключение к чату. Наш клиент при запуске также отправляет сообщения, и они имеют те же порядковые номера. Сервер программы iChat отвергнет пакеты и не отобразит их в чате.

Если при запуске нашего клиента указать имя адресата, то мы получим возможность отправлять ему приватные сообщения.

Отправка приватного сообщения через клиент Intranet Chat
Отправка приватного сообщения через клиент Intranet Chat

Читать сообщения чата мы, к сожалению, не можем, потому в нашей тестовой программе отсутствует сервер мэйлслотов. Но если мы добавим его и реализуем логику обработки сообщений, то получим собственную полноценную реализацию чата, совместимого с программой "Intranet Chat".

Last modified onВторник, 02 Июнь 2020 19:52 Read 4476 times
Ключевые слова: :

Поблагодарить автора:

Поделиться

Print Friendly, PDF & Email

Leave a comment