Makale Özeti

Bu makalemizde Silverlight 2.0 ile beraber gelen Socket Programlama özelliklerine değinirken örnek bir Silverlight istemci ve Winforms sunucu uygulaması hazırlıyoruz.

Makale

Socket programlama Silverlight çıktığından beri biz yazılım geliştiricilerin en büyük hayali ve bu hayal gerçek oluyor. Silverlight 2.0 Beta 1 ile beraber Socket Programlama karşımızda. Yani artık istemci ile sunucu arasında TCP/IP ile haberleşmek mümkün. Tabi belirli kurallar var; bu kurallardan ilki sunucudaki uygulamanın istemci uygulamanın yüklendiği web sitesi ile aynı konumda olması. Yani sunucu uygulamanızın web siteniz ahmet.com ise ahmet.com'un reverse DNS Look-Up ile bakıldığında çıkan IP adresine sahip sunucuda bulunması gerekiyor. Bu durumun Silverlight'ın Beta 1 sonrası sürümlerinde policyfile gibi sistemlerde daha esnek hale getirileceği söylentiler arasında fakat baktığımızda şu anki hali ile bile süper bir potansiyel söz konusu.

Peki nedir bunun avantajı?

Diyorum ya, hayalimizdi diye, peken neden? Web sitelerinde güncel bilgi göstermek her zamanki en büyük derttir. Bunu yapabilmek için çok eskilere döndüğümüzde bazı meta tagları ile belirli aralıkla sayfanın refresh atmasını sağladığımız günler bile olurdu. IFRAME vs nin gelmesi ile en azından bunu sayfada kısmi bölümlerde uygulayabilir hale geldik. Sonrasında AJAX geldi ve çok daha sinsi bir şekilde kullanıcı farkında olmadan belirli aralıklarla sunucudan yeni veri talebinde bulunarak sayfa değişmeden yeni içeriği gösterebildik. Oysa hep bizi rahatsız eden bir nokta vardı, o da şu; sürekli istemciden sunucuya bağlanarak bir veri değişikliğinin olup olmadığını kontrol etmek durumunda kalıyorduk. Sunucuya "Yeni birşey var mı?" diye dakikada bir soruyor ve çoğunda da hüsran ile geri dönüyordu. Keşke sunucu bize bir "Alo" diyebilse ve değişiklik olduğunda istemciyi haberdar edebilse? Teknik olarak bu güvenlik sebepleri nedeniyle zaten mümkün değil çünkü bir istemci bilgisayara dışarıdan içeriye bağlantı kuramazsınız (kuramamanız gerekir). Peki nasıl oluyor da Socket Programming bunu aşıyor? Aslında aşmıyor, yine istemci sunucuya bağlanıyor fakat söz konusu bağlantı TCP bazında olduğu herhangi bir trafiğe neden olmadan sürekli açık tutulabiliyor. Durum böyle olunca sunucu kendisine bağlı istemciye istediğinde söz konusu bağlantı üzerinden rahatlıkla ulaşabiliyor.

Sunucu tarafından işe başlayalım.

İlk olarak sunucudaki programımızı hazırlayalım. Söz konusu program kendisine gelen tüm istekleri karşılayarak gerektiğinde istemcilere veri gönderecek. Bizim programımız içerisinde bir TextBox bulunacak ve kutu içerisine metin yazıldıkça kendisine bağlı tüm istemcilere bu metin sürekli gönderilecek.

    Dim Baglilar As New System.Collections.Generic.List(Of System.IO.StreamWriter)

    Dim yeniTR As System.Threading.Thread

    Dim TCPBaglantilari As New System.Threading.ManualResetEvent(True)

    Dim Dinleyici As New System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 4530)

İlk olarak global değişkenlerimizi tanımlıyoruz. Bunlardan ilki olan Baglilar değişkeni sunucuya bağlı olan istemcilere veri gönderecek olan StreamWriter nesnelerini bir listesini taşıyacak. Böylece istediğimizde bu liste içerisinde gezerek tüm bağlı olan istemcilere veri gönderebileceğiz. yeniTR adındaki değişkenimizi yeni bir Threat yaratmak ve her yerden kendisine ulaşabilmek için kullanacağız. TCPBaglantilari değişkenimiz var olan Threat'ın blocklanması ve tüm event-larının sıfırlanması için kullanılacak. Dinleyici adındaki değişkenimizi ise tüm istemcileri dinleyecek olan ve gelen bağlantıları algılayacak olan TCPListener nesnemizin ta kendisi. Gördüğünüz gibi bu nesneyi tanımlarken iki parametre aktarmışız. Bunlardan ilki herhangi bir IP adresi üzerinden bu uygulamaya bağlanılabileceği anlamına gelirken diğer ise sadece 4530 portu üzerinden bağlantı yapılabileceği anlamına geliyor. Silverlight 2.0 Beta 1 şu anda 4502-4532 aralığındaki portları kullanabiliyor.

    Private Sub btn_Basla_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_Basla.Click

        yeniTR = New System.Threading.Thread(AddressOf Bekle)

        yeniTR.Start()

    End Sub

Uygulamamızdaki düğmeye basıldığında dinleme işlemini başlatmak üzere yeni bir Thread yaratıyoruz. Söz konusu Thread Bekle Sub'ına bağlı. Yarattığımız thread'i hemen başlatıp yolumuza devam edelim.

    Sub Bekle()

        Dinleyici.Start()

        While True

            TCPBaglantilari.Reset()

            Dinleyici.BeginAcceptTcpClient(New System.AsyncCallback(AddressOf BaglantiGeliyor), Nothing)

            TCPBaglantilari.WaitOne()

        End While

    End Sub

Yeni Thread içerisinde hemen Dinleyici nesnemizi başlatıyoruz ve kısır bir döngüye giriyoruz. Sürekli olarak elimizdeki Threadi sıfırlayarak Dinleyici'nin BeginAcceptTcpClient metodu ile istemciden bir bağlantı geleceğini belirterek WaitOne metodu ile de bekliyoruz. Eğer burada bir bağlantı gelir ve başarılı veya başarısız şekilde sonuçlanırsa bu döngü başa gelerek tekrar yeni bir bağlantı bekleyecek. BeginAcceptTcpClient içerisinde parametre olarak verdiğimiz BaglantiGeliyor event-handları herhangi bir bağlantı geldiğinde çalıştırılacak.

    Private Sub BaglantiGeliyor(ByVal ar As System.IAsyncResult)

        TCPBaglantilari.Set()

        Dim Musteri As System.Net.Sockets.TcpClient = Dinleyici.EndAcceptTcpClient(ar)

        If Musteri.Connected Then

            Dim yazici As New System.IO.StreamWriter(Musteri.GetStream)

            yazici.AutoFlush = True

            Baglilar.Add(yazici)

            yazici.Write("Bağlandınız.")

        End If

    End Sub

Baglanti geldiği anda bekleyen Threat'leri devam ettirmek adına TCPBaglantilari.Set() metodunu çağırıyoruz. Unutmayalım ki programımız aynı anda sadece tek istemcinin bağlantısını authenticate edebilir, yani diğerleri bir önceki istemci bağlanıp bağlantısını oluşturana kadar bekleyecektir. Bu noktada artık istemci bağlantı kurma işlemini tamamladığı için diğerlerine yol veriyoruz. Musteri adında bir TCPClient yarattıktan sonra Dinleyici'nin EndAcceptTcpClient metodu ile args parametresi üzerinden gelen Request'i alıyoruz. Eğer Musteri bağlı ise, yani Connected ise artık sıra geldi ona veri göndermeye. Musteri'nin yani TCPClient'ın Stream'ini alarak bundan bir StreamWriter oluşturuyoruz. Bu Stream üzerinden artık istemciye istediğimiz veriyi gönderebiliriz.

Unutmayın ki uygulamamızda bir TextBox vardı ve içerisine birşey yazıldığında tüm bağlı kullanıcılara gönderecektik. Bunun için sonra kullanabilmek adına elimizdeki canlı Stream'leri saklamak sorundayız. Global olarak tanımladığımız Baglilar adında Generic.List'e elimizdeki Stream'i aktarıyoruz. Bu arada kullanıcıya "Bağlandınız" diye de bir metin gönderiyoruz.

    Private Sub TextBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged

        For Each x As System.IO.StreamWriter In Baglilar

            x.Write(TextBox1.Text)

        Next

    End Sub

Artık TextBox içerisinde değişiklik olunca bunu istemcilere göndermek çok kolay. Basit bir şekilde Generic.List içerisinde gezin ve her Stream'e elinizdeki veriyi gönderin.

İstemci tarafında neler olacak?

Silverlight tarafında çok basit görsellikte bir uygulamamız olacak. Sadece bir TextBlock! Uygulama tarayıcı içerisinde ilk açıldığında sunucuya bağlanacak ve gelen veriyi sürekli olarak söz konusu TextBlock içerisinde gösterecek.

<UserControl x:Class="SocketsClient.Page"

    xmlns="http://schemas.microsoft.com/client/2007"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Width="400" Height="300">

  <Grid x:Name="LayoutRoot" Background="White">

    <TextBlock Margin="42,46,37,125" Text="TextBlock" TextWrapping="Wrap" x:Name="Metin"/>

  </Grid>

</UserControl>

XAML kodumuzu yukarıdaki şekilde düzenledikten sonra hemen code-behind dosyasına geçerek bağlantı kodlarımızı yazmaya başlayalım.

    Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        Dim Hat As New System.Net.Sockets.Socket(Net.Sockets.AddressFamily.InterNetwork, Net.Sockets.SocketType.Stream, Net.Sockets.ProtocolType.Tcp)

        Dim Args As New System.Net.Sockets.SocketAsyncEventArgs

 

        Args.UserToken = Hat

        Args.RemoteEndPoint = New System.Net.DnsEndPoint("localhost", 4530)

        AddHandler Args.Completed, AddressOf Baglandi

        Hat.ConnectAsync(Args)

    End Sub

Silverlight uygulaması ilk yüklendiğinde hemen bir Socket bağlantısı yaratmak için System.Net.Sockets.Socket üzerinden ilerliyoruz. Hat adındaki değişkenimizi yaratırken verdiğimiz parametrelerden ilki olan InterNetwork bizim bağlantı için IPv4 kullanacağımızı ve ikinci parametre de TCP kullanacağımızı belirtiyor. Asenkron bir çalışma yapısı için bir de SocketAsyncEventArgs nesnesi yaratarak söz konusu nesneyi Hat adındaki Socket'imize bağlıyoruz. Args'ın RemoteEndPoint özelliği istemcinin bağlanacağı sunucunun adresini ve port bilgisini içeriyor. Bağlanti oluşturulduğunda elimizdeki SocketAsyncEventArgs nesnesi olan Args'ın Completed event-handları çalışacağı için ona da dinamik bir event-handler olarak Baglandi metodunu atıyoruz. Son olarak ConnectAsync diyerek Socket değişkenimizin eldeki SocketAsyncEventArgs ile sunucuya bağlanmasını sağlıyoruz.

    Private Sub Baglandi(ByVal sender As Object, ByVal e As System.Net.Sockets.SocketAsyncEventArgs)

        Dim Gelen(1024) As Byte

        e.SetBuffer(Gelen, 0, Gelen.Length)

        RemoveHandler e.Completed, AddressOf Baglandi

        AddHandler e.Completed, AddressOf Geldi

        Dim Baglanti As System.Net.Sockets.Socket = CType(e.UserToken, System.Net.Sockets.Socket)

        Baglanti.ReceiveAsync(e)

    End Sub

Artık istemci sunucuya bağlandığında göre sıra geldi karşı taraftan yeri geldiğinde veriyi almaya. Hatta ilk bağlantı esnasında hatırlarsanız bizim sunucumuzun "Bağlandınız" diye bir metin gönderiyordu. Gelen veriyi alabilmek için ve sürekli gelen veriyi dinlemek için eldeki Socket'i alarak sürekli dinleme durumunda olmamız şart. Kodumuzdaki event-handler içerisinde e parametresi aslında bizim bir önceki adımda tanımladığımız Args adaınki SocketAsyncEventArgs'ın ta kendisi. SetBuffer ile veriyi önbelleklemek için kullanacağımız ayarları da bir Byte değişkeni üzerinden aktardıktan sonra ilginç bir şekilde elimizdeki event-handlerları değiştiyoruz. Bundan sonra Args'ın Compeleted durumu yeni bir bağlantı oluştuğunu değil yeni veri geldiğini bildireceği için farklı bir event-handlerı bağlamamız gerekiyor. Baglandi adındaki metodumuzla Args'ın ilişkisi keserek Geldi adında farklı bir metoda bağlıyoruz. Son olarak Page.Load'a atadığımız ve e.UserToken üzerinden alabileceğimiz ana Socket değişkenimizi de yakalayarak ReceiveAsync metodu ile veri alımını başlatıyoruz.

Geldi ve Baglandi metodları aslında Silverlight içerisinde ayrı bir Thread içerisinde çalışıyor. Bu nedenle tüm bu işlemler yapılırken kullanıcının uygulama ile olan interaktivitesi kesinlikle kesilmiyor. Tabi ayrı bir Thread gibi davranıyor olmasını dezavantajı ise birazdan karşımıza çıkacak. Kısır döngü içerisinde süreki ReceiveAsync ile sunucuyu dinlerken istemci tarafında görsel arayüzde değişiklik yapamayacağız. Bu da bizim sunucudan veri alabilmemizi fakat ekranda göstermememize neden olacak. Tabi demokrasilerde çare tükenmez...

    Delegate Sub MyDelegate(ByVal myArg2 As String)

 

    Sub GelGel(ByVal x As String)

        Metin.Text = x

    End Sub

İlk olarak bir Delegate tanımlayacağız, söz konusu delegemiz sadece bir parametre alacak. Ayrıca bir de Sub yaratıyoruz. Aynı şekilde Sub'da bir metin parametresi alıyor ve bizim uygulamamızda adı Metin olan TextBlock içerisine yerleştiriyoruz. İşte bu yapı ile biraz önce bahsettiğimiz sorundan kurtuluyor olacağız.

    Private Sub Geldi(ByVal sender As Object, ByVal e As System.Net.Sockets.SocketAsyncEventArgs)

        Dim Gelen As String = System.Text.Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred)

        Me.Dispatcher.BeginInvoke(New MyDelegate(AddressOf GelGel), New String() {Gelen})

        Dim Baglanti As System.Net.Sockets.Socket = CType(e.UserToken, System.Net.Sockets.Socket)

        Baglanti.ReceiveAsync(e)

    End Sub

Aslında sunucudan gelen veriyi almak çok kolay. Kodumuz içerisindeki ilk satır bu işi hallediyoruz. Esas mesele veriyi aldıktan sonra sahnede göstermek. Dispatcher nesnesi belki de Silverlight içerisinde en ilginç yapılardan biri; Dispatcher ile mevcut Thread'i yakalayarak BeginInvoke ile başka bir metod çalıştırıyoruz. Çalıştıracağımız metodu sunucudan gelen veriyi parametre olarak vereceğiz ve söz konusu metod (GelGel) bu veriyi alarak sahnedeki Metin adındaki TextBlock içerisine yerleştirecek. BeginInvoke ile ilgili işimizi de tamamladıktan sonra artık tekrar sunucunun dinlenmeye başlanması için elimizdeki Socket'i yakalayarak ReciveAsync metodunu çalıştırıyoruz. Bu sistem böyle sonsuza tek dönecek ve sunucudan gelen veri sürekli olarak tüm istemcilerde anında gösterilecek.

Son olarak hem istemci hem de sunucu uygulamanın tam kodunu sizlerle paylaşmak istiyorum.

[İstemci: Silverlight uygulaması]

Partial Public Class Page

    Inherits UserControl

 

    Public Sub New()

        InitializeComponent()

    End Sub

 

    Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        Dim Hat As New System.Net.Sockets.Socket(Net.Sockets.AddressFamily.InterNetwork, Net.Sockets.SocketType.Stream, Net.Sockets.ProtocolType.Tcp)

        Dim Args As New System.Net.Sockets.SocketAsyncEventArgs

 

        Args.UserToken = Hat

        Args.RemoteEndPoint = New System.Net.DnsEndPoint("localhost", 4530)

        AddHandler Args.Completed, AddressOf Baglandi

        Hat.ConnectAsync(Args)

    End Sub

 

    Private Sub Baglandi(ByVal sender As Object, ByVal e As System.Net.Sockets.SocketAsyncEventArgs)

        Dim Gelen(1024) As Byte

        e.SetBuffer(Gelen, 0, Gelen.Length)

        RemoveHandler e.Completed, AddressOf Baglandi

        AddHandler e.Completed, AddressOf Geldi

        Dim Baglanti As System.Net.Sockets.Socket = CType(e.UserToken, System.Net.Sockets.Socket)

        Baglanti.ReceiveAsync(e)

    End Sub

 

    Private Sub Geldi(ByVal sender As Object, ByVal e As System.Net.Sockets.SocketAsyncEventArgs)

        Dim Gelen As String = System.Text.Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred)

        Me.Dispatcher.BeginInvoke(New MyDelegate(AddressOf GelGel), New String() {Gelen})

        Dim Baglanti As System.Net.Sockets.Socket = CType(e.UserToken, System.Net.Sockets.Socket)

        Baglanti.ReceiveAsync(e)

    End Sub

 

    Delegate Sub MyDelegate(ByVal myArg2 As String)

 

    Sub GelGel(ByVal x As String)

        Metin.Text = x

    End Sub

 

End Class

[Sunucu: Winforms Uygulaması]

Public Class Form1

 

    Dim Baglilar As New System.Collections.Generic.List(Of System.IO.StreamWriter)

    Dim yeniTR As System.Threading.Thread

    Dim TCPBaglantilari As New System.Threading.ManualResetEvent(True)

    Dim Dinleyici As New System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 4530)

 

    Private Sub btn_Basla_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_Basla.Click

        'İzin Verilen Port Aralığı 4502-4532

        yeniTR = New System.Threading.Thread(AddressOf Bekle)

        yeniTR.Start()

    End Sub

 

    Sub Bekle()

        Dinleyici.Start()

        While True

            TCPBaglantilari.Reset()

            Dinleyici.BeginAcceptTcpClient(New System.AsyncCallback(AddressOf BaglantiGeliyor), Nothing)

            TCPBaglantilari.WaitOne()

        End While

    End Sub

 

    Private Sub BaglantiGeliyor(ByVal ar As System.IAsyncResult)

        TCPBaglantilari.Set()

        Dim Musteri As System.Net.Sockets.TcpClient = Dinleyici.EndAcceptTcpClient(ar)

        If Musteri.Connected Then

            Dim yazici As New System.IO.StreamWriter(Musteri.GetStream)

            yazici.AutoFlush = True

            Baglilar.Add(yazici)

            yazici.Write("Bağlandınız.")

        End If

 

    End Sub

 

    Private Sub TextBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged

        For Each x As System.IO.StreamWriter In Baglilar

            x.Write(TextBox1.Text)

        Next

    End Sub

End Class

Hepinize kolay gelsin.