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.