8 Aralık 2017 Cuma

Mesaj Simülatörü

Giriş
Aşağıda genel bir mesajlaşma protokolü için simülatör geliştirirken aldığım notlar var. Simülator için iki ana yöntem var. İlk yöntem GUI kullanmak. Böylece kullanıcıya standart bir arayüz sağlanır. Diğeri ise scripting dili ile GUI'siz bir ortam sağlamak.

Mimari
Kabaca şöyle
Network Layer <--(Network Packet)--> Structured  Message Layer <-> Simulated Entity
Network Layer - NL
Dış dünyaya açılan kanallar temsil eder. TCP UDP İstemcisi/Sunucusu olabilir. Bu katman sadece byte'ları bilir. Network Layer klasik iletişim protokolleri dışında 3. partiler tarafından sağlanan kütüphaneleri de kullanabilmelidir. Bu yüzden kendi içinde bölümlenmiştir.
Reactor <--->ReactorEventDispatcher <---> IOAdapter -> ReceiveStreamBuffer
NetworkEventType Enum
Reactor Sınıfının tespit ettiği olayları temsil ederi. Tipler şöyle. Tipler amaca ve detaya göre derinleştirilebilir.
RECEIVE,SEND,CONNECT,DISCONNECT
Olayların tetiklenmesi şöyle
Channel Started------> Client Connected --------> Packet Received ---------->
      |                       |                        |
      |                       |                        |
      \---Notify              \--->Notify              \----> Notify
                              
                              

Packet sent -------------> Disconnected ---------> Channel Stopped
     |                            |                      |
     |                            |                      |
     \---> Notify                 \---> Notify           \---> Notify

NetworkEventListener Arayüzü
İmzası şöyle
void networkEventOccurred (NetworkEvent event)
Reactor Sınıfı
select() işlemlerini yapar.
Reactor direkt ReactorEventDispatcher ile konuşmak yerine ReactorEventListener nesnelerini tetikler. Reactor DATA taşıyan NetworkEvent nesneleri dışında, port açıldı, port kapandı şeklinde bilgi taşıyan paketler de gönderebilir.
Reactor -> ReactorEventListener (aka ReactorEventDispatcher)
Reactor gönderdiği paketin byte[] haline getirilmiş halini tekrar ReactorEventDispatcher'a gönderir. Böylece uygulama gönderdiği paketin sarmalanmış halini de gösterebilir. Örneğin uygulama A mesajını göndermek istesin aslında IOChannel bu mesajı B ile sarmalar yani B(A) haline getirir. Reactor sarmalanmış halini uygulamaya gönderdiği için tüm mesaj gösterilebilir.
ReactorEventDispacther Sınıfı
Reactor'den gelen byte'ları hemen kuyruğa atar. Böylece Reactor meşgül edilmez. Daha sonra byte'ları IOChannel'a geçer.
ReactorEventDispatcher direkt IOChannel ile konuşmak yerine ReactorEventListener nesnelerini tetikler.
ReactorEventDispatcher -> NetworkEventListener (aka IOChannel)

Structured Message Layer - SML
Field Sınıfı
parent Alanı
Field kendisini sarmalayan Composite nesneyi bilir. Bu alan için getter() ve setter() vardır.

meaning Alanı
Her alan verilen değeri gösterimsel amaçlı başka bir nesneye çevirebilir. Gösterimsel amaçlı nesneden alan değerine çevirebilir.

Composite Sınıfı
Bu sınıf Parser'ın çıktısıdır.

fieldList Alanı
fieldsList tüm ilk seviye alanları tutan listedir.

addField metodu
FieldData nesnesi ekler.

toBytes metodu
Composite nesnesini byte dizine çevirir. Böylece ağdan gönderilebilir.

Parser Facade Sınıfı
parse metodu
Bazı mesaj zarflarında mesajın hangi aileye ait olduğunu anlamak mümkündür. Bu bilgiyi dikkate alara bir Composite döndürür.

Simulated Entity - SML
IOChannel Arayüzü
IOChannel tipine göre farklı Reactor veya kütüphane türleri takılabilir

Paket okurken ağa bakan arayüzden byte[] alır. Yukarı katmana bakan arayüze Composite verir.
Mesaj gönderirken yukarı katmandan Composite alır ağa bakan arayüze byte [] verir.
byte [] <->IOChannel <-> Composite
addMessageListener
IOChannel parse edebildiği mesajları tüm listener'lara gönderir.
IOChannel -> Composite -> Simulated Entity + Sniffer
networkEventOccured metodu
Olay tipi örneğin DISCONNECT ise ReceiveStreamBuffer temizlenir.
Olay tipi örneğin RECEIVE ise receive() metodu çağrılır.

receive metodu
Parse edilen byte'ları composite çevirir. Her IOChannel gelen mesajı önce bir ReceiveStreamBuffer'a yazar. Bu StreamBuffer byte'ları verildiği gibi yazan veya flip() ederek yazan olarak iki tipe sahiptir.
IOChannel -> ReceiveStreamBuffer
removeMessageListener
Listener'ı siler.

IOChannelReactor Sınıfı
I/O için Reactor kullanan sınıftır. Reactor ile haberleşmek için kuyruk veya direk reactor nesnesinin kendisini kullanır.

MessageFilter Sınıfı
Simüle edilen nesneye gelen Composite mesajın filtre edilerek işlenmemesini sağlar. Sniffer halen mesajı okuyabilir.

SimulatedEntity Sınıfı
IOChannel'dan composite alır.
Input IOChannel -> Composite -> Simulated Entity -> Output IOChannel
addIOChannel metodu
İsmi olan yeni bir IOChannel ekler. Ayrıca IOChannel'ı dinlemeye başlar.

removeIOChannel metodu
İsmi belirtilen IOChannel'ı siler.

start metodu
Simüle edilen işi yapan thread'i başlatır. Bu thread Composite'i işler.

stop metodu
Simüle edilen işi yapan thread'i durdurur.

SimulatedEntityEventType Sınıfı
Simüle edilen nesnenin durumunu bildirir.
ENTITY_STARTED, ENTITY_STOPPED
Message Generator
Mesaj alanlarını doldururken girilen değerleri formatlayarak göstermek gerekir. Formatter bir field'ı alır.
EnumeratedFormatter fiedl'ın alabileceği değerleri ComboBox'ta gösterir.

--------
1. Network Packet Consumer
NL ile SML Network Paketleri kullanarak haberleşirler. SML içinde her bir kanal için bir Network Packet Consumer bulunur.  Consumer gelen paketleri bir stream içinde bekletir. Stream bize byte'ları bekletme,gerekiyorsa byte'ları değiştirme

Parser - Ekran
Parser'ın okuyabildiği mesajları ekranda ağaç yapısı şeklinde için ProtocolTree şeklinde bir nesne döndürebilir. Ağaçtaki ana düğümler şöyle gösterilir.
"name - long name" (ana düğüm)
   -A - A message
  - B - B message
Bu agaç yapısı XML'den gelebilir.

Parser - Stream 
Bu katman gelen mesajı wireshark'taki gibi iç içe zarflara ayırır. Zarflara ayırma için Parser yapısının tanımlaması gerekir. Parser mesaj tanımlarını XML'den okur.

Mesaj alanlarının tanımı için kullanılabilecek hiyerarşi şöyledir.
FieldDefinition <- CompositeFieldDefinition
Mesaj alanlarını saklamak için kullanılabilecek hiyerarşi şöyledir.
Field <- CompositeField
parse metodu 
Şöyledir.
Field parse (Stream)

Simülatör İle Beslenen Yazılımı Doğrulama
Simülatör ile beslenen yazılımı doğrulamak gerekir. Bunun için iki temel yöntem kullanıldığını gördüm.

İlkinde yazılımın çıktısı (eğer varsa) simülatör tarafından otomatik olarak doğrulanır. Yani A girdisine karşılık B çıktısını bekleme işi script veya ekrandan simülatöre verilir. Simülatöre tüm adımları çalıştırır ve başarısız olanları listeler. 

İkinci yöntemde ise bir insan görsel doğrulama yapar. Görsel doğrulamanın en ilkel yöntemi yazılımın üretttiği log satırlarını kullanarak doğrulama yapmaya çalışmaktır. Biraz daha gelişmiş yazılımlarda logları görselleştiren eklentiler kullanılarak doğrulama işi basitleştirilir. İnsanın görsel doğrulaması tercih edilmez ancak bazı durumlarda kaçınmak mümkün olmayabilir.

GUI Kullan Mesaj Simülatörü

Kullanıcı göndermek istediği mesajları bir listeden veya menüden seçer ve ekrana sıralar. Mesajların içini doldurur. Bu noktada dikkat edilmesi gereken şey, senaryo nesne modeli ile gönderilecek mesaj nesnelerini birbirlerinden düzgünce ayırabilmek.

Örneğin ekrandan girilen ve senaryonun çalışması için gerekli olan "Gönderme Zamanı" senaryo nesne modeli içinde olmalı. Mesaj nesnelerine iletişim protokolü içinde tanımlı olmayan bilgileri eklememeliyiz.

Dinamik Olarak Doldurulması Gereken Alanlar
Bazı alanların değerlerinin, dinamik olarak doldurulmalı gerekebilir. Örneğin, mesaj içinde zaman bilgisi varsa, sistem saatinin okunması ve mesaja doldurulması gerekebilir. Bu tür alanlar için mesaj yapısına alanın dinamik atanması gerektiğini belirten bir bayrak eklemek gerekir.

Birden Fazla Kaynağı Simüle Etmek
Mesaj simülatörü tercihen birden fazla mesaj kaynağını simüle edebilmelidir.

Gönderme Zamanı
Senaryodaki her mesaj gönderme zamanı bilgisini içerebilmeli. Böylece simülasyon başladıktan ne kadar zaman sonra mesajın gönderileceği belirtilir.

İletişim Protokolünün Formatı
Mesajlar belli bir formata çevirlerek gönderilebilmeli. Format ikilik (binary) veya metin (http) olabilir.

İletişim Kanalı
Kanal ve gönderilecek mesaj verisi birbirinden tamamen bağımsız olmalı. Böylece simülasyon etkilenmeden, farklı bir kanal seçilebilir. Mesaj ile gönderilecek kanalın eşleştirilmesi için kullanılabilecek en basit yöntem, her kanala mesaj ile aynı ismi vermek olabilir. Böylelikle, konfigürasyon dosyası kullanmadan ve simülatörün kodunu değiştirmeden mesajı istediğimiz kanala yönlendirebiliriz. Örnekte Senaryo için ICommand, mesaj kanalı için ICommunicator, senaryo ile kanalı birleştirmek için IScriptContext arayüzleri kullanılmış.

interface ICommand {
    void Execute(IScriptContext context);
}

interface IScriptContext {
    ICommunicator Channel { get; set; }
}

interface ICommunicator{
    int Read(byte[] buffer, int offset, int count);
    int Write(byte[] buffer, int offset, int count);
    int GetBytesAvailable();
}

class RS232CommunicationChannel : ICommunicator...
class TcpIpCommunicationChannel : ICommunicator...
class SharedMemoryCommunicationChannel : ICommunicator...

class ConnectRS232Command : ICommand {
    void Execute(IScriptContext context) {
        context.CommunicationChannel = new RS232CommunicationChannel();
    }
}

Gelen Mesajları Almak
İletişim kanalından gelen mesajlar bir çok işlemden geçirilebilir.

Gelen Mesajları Göstermek
Test edilen birim bize cevap gönderiyorsa, gelen mesajlar alınıp başka bir ekranda gösterilebilir.

Gelen Mesajları Kaydetmek
Aynı zamanda bu mesajlar kaydedilebilir. Kullanılan yöntemlerden bir tanesi IChannel nesnesini sarmalayan bir RecordingChannel kullanmak.

Gelen mesaja göre davranmak
Sabit senaryolar yerine daha dinamik bir yapı olan gelen mesaja göre davranış, geliştirmesi en zor simülatörlerden birisi. Sadece GUI'den bu işi yapabilmek imkansız. Senaryo oynatıcısının bir "script engine" çalıştırarak, onMessage gibi bir metodun içinde gelen mesajı işleyip cevap vermesi gerek.

Scripting Dili Kullanan Mesaj Simülatörü

Mesaj simülatörüne script dili desteği eklemek sadece görsel arayüzlere bağımlı kalınmamasını sağlar. Böylece GUI ile çözülemeyecek kadar karmaşık işler halledilebilir.

C#
//Location of assemblies referenced by our executable
var assemblyReferences = AppDomain.CurrentDomain.GetAssemblies()
    .Where (a => ! a.IsDynamic)
    .Select (a => a.Location)
    .ToArray();

//Populate compiler options
CompilerParameters parameters = new CompilerParameters ();
parameters.GenerateInMemory = true;
parameters.GenerateExecutable = false;
parameters.IncludeDebugInformation = true;
parameters.ReferencedAssemblies.AddRange (assemblyReferences);


IronPython
IronPython scriptine .Net nesnesini geçme örneği aşağıda.

string script = ""; // TODO - get Iron Python script
var engine = Python.CreateEngine();
var scope =  engine.CreateScope();

//Add object to scope
string variableName = "myObject";
object myObject = new Object();
scope.SetVariable(name, value); 
//Burada SourceCodeKind.SingleStatement da kullanılabilir.
var source = engine.CreateScriptSourceFromString(script, SourceCodeKind.Statements);
var compiled = source.Compile();
var result = compiled.Execute(scope); 

//Python'dan .Net nesnesini kullanma örneği
def getMyObject():
    return myObject;
obj = getMyObject()
print obj.ToString()

Eğer IronPython içinden bir .Net nesnesi kullanmak isteseydik aşağıdaki gibi assembly dosyasını import etmek yeterli olurdu.
import clr
clr.AddReference('MyAssembly.dll')
import MyClass
obj = MyClass("Hello, World!")
obj.Write()
Ya da IronPython engine sınıfını başlatırken assembly dosyasını verebilirdik.
engine.Runtime.LoadAssembly(typeof(MyClass).Assembly);

Hiç yorum yok:

Yorum Gönder