Silverlight 2.0 ve Socket Programlama Mucizesi

0 dakikada yazıldı

7089 defa okundu

Düzenle

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.