DCSIMG
Real Time Chat Service για windows 8 store apps - nekgiann's blog - Site Root - StudentGuru

Real Time Chat Service για windows 8 store apps

 Ύστερα από μερικούς πειραματισμούς πάνω στα νέα windows 8, και αφού σε κάποια φάση έφτασε η ώρα και για εμένα να χρειαστεί να υλοποιήσω real time επικοινωνία μεταξύ δύο windows 8 εφαρμογών κάθισα και έκανα μια μικρή έρευνα πάνω στο θέμα.

Αφού βρήκα την λύση, αποφάσισα να φτιάξω αυτό εδώ το tutorial για τους επόμενους developers που θα χρειαστούν κάτι παρόμοιο.

Στο tutorial αυτό λοιπόν θα δούμε πως μπορούμε να δημιουργήσουμε μία wcf full duplex service, τις ρυθμίσεις που πρέπει να κάνουμε για να τρέξει και πως μπορούμε να την αξιοποιήσουμε άμεσα στις εφαρμογές μας.

Αρχικά θα δείξω την βασική λειτουργία των web services μέσω half duplex επικοινωνίας.

Μπορείτε να βρείτε το solution εδώ.

Δημιουργία του solution

Σε αυτό το project θα χρειαστεί να δημιουργήσουμε 2 projects. Ένα για το win 8 app και ένα στο οποίο θα κάνουμε host την web service.

-          Ξεκινάμε το visual studio και πατάμε file->new project / windows Store -> Blank App. Του δίνουμε το όνομα chatApp και πατάμε οκ.


-          Αμέσως στο Solution πατάμε δεξί κλικ και πατάμε Add->New Project και επιλέγουμε Web -> ASP.NET Empty Web Application. Δίνουμε όνομα chatServer και πατάμε οκ.

Προετοιμασία του chatApp

Πάμε στο project chatApp και πατάμε στο MainPage.xaml για να ανοίξει ο Designer.

Προσθέτουμε στην xaml τα παρακάτω:

       <StackPanel Margin="100">
            <TextBlock Text="Συζήτηση" FontSize="40"/>
            <ScrollViewer Height="400">
                <TextBlock x:Name="conversationT" Text="waiting..." FontSize="20" TextWrapping="Wrap" />
            </ScrollViewer>
            <TextBox x:Name="sendTextTB" />
            <Button x:Name="sendMsgB" Content="Στείλε" Width="300" />
        </StackPanel>

Όπως γίνεται φανερό, το TextBlock conversationT θα κρατάει το κείμενο του chat ενώ στο sendTextTB θα γράφουμε το κείμενο που θέλουμε να σταλεί.

Πατάμε διπλό κλικ στο κουμπί για να μας κάνει handle το  sendMsgB_Click.

Δημιουργία της Service

Πατάμε δεξί κλικ στο project chatServer,  add->new item, Web -> WCF Service και δίνουμε το όνομα chatService. Αυτό μας δημιούργησε 2 πράγματα, το chatService.svc και το IchatService.

Λίγη μπακαλίστικη θεωρία!
Σε μια κλασσική και απλή service έχουμε half duplex επικοινωνία. Αυτό σημαίνει πως η web service μας δέχεται αιτήσεις από έναν client και του στέλνει πίσω ένα αποτέλεσμα ή κάνει μια εργασία στον server. Σε αντιστοιχία, ο client απλά κάνει την αίτηση και μόνον τότε λαμβάνει δεδομένα τα οποία μπορεί να τον ενδιαφέρουν. Το μοντέλο λειτουργεί όπως με μια κλήση μιας function μέσα σε ένα πρόγραμμα με την διαφορά πως αυτή η κλήση γίνεται μέσα από το διαδίκτυο.

Πχ, αν είχαμε την εξής κλάση:

class communicate
    {
        public string getMessage()
        {
            return "hello";
        }
    }

Μέσα στο πρόγραμμα μας θα γράφαμε:

           communicate c=new communicate();
           c.getMessage();

Για να πάρουμε τα δεδομένα που θα είχε να μας δώσει η κλάση. Το ίδιο μπορεί να γίνει και μέσα από μια web service.
Πάμε να δούμε πως.

Υλοποίηση της half duplex επικοινωνίας

Στο project chatServer ανοίγουμε το IchatService.cs και το τροποποιούμε ως εξής:

    [ServiceContract]
    public interface IchatService
    {
        [OperationContract]
        void DoWork();
 
        [OperationContract]
        string getMsg();
    }

Μερικά σημαντικά στοιχεία.
Όσες μεθόδους θέλουμε να μπορεί να τις δει ο client  πρέπει να τις μαρκάρουμε με το [OperationContract].
H IchatService είναι μια Interface από την οποία κληρονομεί η κλάση chatService. Ανοίγουμε λοιπόν το αρχείο chatService.svc.cs και βλέπουμε να μας έχει υπογραμμιστεί με κόκκινο η κλάση. Αυτό γίνεται γιατί δεν υλοποιεί όλες τις μεθόδους όπου κληρονομεί. Πατάμε λοιπόν ακριβώς πάνω στο IchatService και επιλέγουμε Implement Interface.

Στην μέθοδο που μας δημιούργησε κάνουμε return ένα μήνυμα:

        public string getMsg()
        {
            return "hello from Server";
        }

Αυτό ήταν! Πλέον κάναμε διαθέσιμη την μέθοδο μας σε όποιον θέλει να την καλέσει.

Επικοινωνία της web service με το chatApp

Για να γίνει εφικτή η επικοινωνία θα πρέπει να τρέξουμε την web service και να πούμε στον client (chatApp) ότι θέλουμε να την χρησιμοποιήσουμε. Για να γίνει αυτό πατάμε δεξί κλικ πάνω στο chatService.svc και επιλέγουμε View in browser. Αν όλα έχουν πάει καλά θα μας ανοίξει ο browser με μια σελίδα που λέει chatService Service.


Πάμε να ενημερώσουμε τον client για την web service.
Στο project chatApp κάνουμε δεξί κλικ στο references και επιλέγουμε add service reference. Πατάμε discover. Θα μας εμφανιστεί η chatService. Την επιλέγουμε. Σαν Namespace δίνουμε chatService. Πατάμε οκ.


Αυτό θα μας δημιουργήσει αυτόματα όλη την υλοποίηση που χρειαζόμαστε για να καλέσουμε την chatService μέσα από το application μας. Πάμε να δούμε πως.

Κάλεσμα της chatService μέσα από το chatApp

Ανοίγουμε το MainPage.xaml.cs.
Προσθέτουμε το using chatApp.chatService;

Στην κλάση μέσα συμπληρώνουμε:
IchatServiceClient client = new IchatServiceClient();
για να αρχικοποιήσουμε την service.


Τροποποιούμε την sendMsgB_click ως εξής:
private async void sendMsgB_Click(object sender, RoutedEventArgs e)
        {
            var msg = await client.getMsgAsync();
            MessageDialog d=new MessageDialog(msg);
            d.ShowAsync();
        }

Τρέχουμε το project, πατάμε το κουμπί στείλε και βλέπουμε το μήνυμα από τον Server.

Προφανώς δεν στέλνουμε κάτι. Αυτό που λέμε στο κλικ είναι ‘φέρε μας ένα μήνυμα’ ασύγχρονα.
Για αυτό βάζουμε το keyword await. Η κλήση γίνεται μέσω ιντερνέτ, θα υπάρχει καθυστέρηση, οπότε θα πρέπει να περιμένουμε μέχρι να έρθει η απάντηση.

 Μέχρι τώρα είδαμε πως λέμε στον server φέρε μας ή κάνε κάτι. Προφανώς αυτό για το chatApp δεν είναι χρήσιμο από την στιγμή που θέλουμε να έχουμε realTime επικοινωνία. Προφανώς μπορούμε να ξεκινήσουμε να κάνουμε διαδοχικά αιτήσεις και να του λέμε φέρε μας μήνυμα αν έχεις ξανά και ξανά. Αυτό όπως αντιλαμβάνεστε δεν είναι καλή πρακτική μιας και επιβαρύνουμε το δίκτυο πάρα πολύ και το αποτέλεσμα δεν είναι και τόσο άμεσο.

Εδώ είναι που χρειαζόμαστε την full duplex επικοινωνία.
Στην full duplex επικοινωνία παρέχουμε έναν μηχανισμό ούτως ώστε ο server να μπορεί να στείλει ο ίδιος μήνυμα σε έναν client απ ευθείας.

Μέρος 2 – Υλοποίηση της Full duplex επικοινωνίας

Στο IchatService interface δηλώσαμε της μεθόδους όπου θα μπορεί να καλεί ο client προς τον server.
Πάμε να δούμε τι χρειάζεται να αλλάξουμε για να δηλώσουμε τις ειδοποιήσεις που θα μπορεί να στέλνει ο server προς έναν συνδεδεμένο client.

Ακριβώς κάτω από το τέλος του interface IchatSercice δηλώνουμε ένα δεύτερο ως εξής:

[ServiceContract]

    public interface IchatServiceCallback
    {
        [OperationContract(IsOneWay = true)]
        void sendMessage(string msg);
    }

Επίσης τροποποιούμε το ServiceContract του IchatService ως:
[
ServiceContract(CallbackContract = typeof(IchatServiceCallback))


Όλη η κλάση πρέπει να δείχνει έτσι:


namespace chatServer
{
    [ServiceContract(CallbackContract = typeof(IchatServiceCallback))]
    public interface IchatService
    {
        [OperationContract]
        void DoWork();
 
        [OperationContract]
        string getMsg();
    }
    [ServiceContract]
    public interface IchatServiceCallback
    {
        [OperationContract(IsOneWay = true)]
        void sendMessage(string msg);
    }

}

Τι κάναμε μόλις τώρα;
Φτιάξαμε ένα καινούριο interface. Το IchatServiceCallback . Ότι μέθοδο δηλώσουμε μέσα σε αυτό το interface θα είναι διαθέσιμη στον server ως κανάλι επικοινωνίας με τον client.

Σε αυτό το σημείο αν κάνουμε δεξί κλικ στο chatService.svc και πατήσουμε view in browser θα δούμε πως θα μας βγάλει ένα μήνυμα:

Contract requires Duplex, but Binding 'BasicHttpBinding' doesn't support it or isn't configured properly to support it.

 

Αυτό είναι το σημείο όπου πονοκεφάλιασα μέχρι να βρω μια εύκολη λύση! Την παραθέτω:

Ανοίγουμε το chatService.svc και προσθέτουμε την εξής μέθοδο:


public static void Configure(ServiceConfiguration config)
        {
            config.EnableMetadata();
            config.EnableProtocol(new NetHttpBinding());
        }

Το EnableMetadata(); Μας κοκκινίζει. Για να το λύσουμε:
δεξί κλικ στον chatServer->add->new item, Code -> class και δίνουμε το όνομα
ServiceConfigurationExtensions

Κάνουμε την κλάση static και προσθέτουμε την μέθοδο:

public static void EnableMetadata(this ServiceConfiguration config)
        {
            config.Description.Behaviors.Add(new ServiceDebugBehavior());
            config.Description.Behaviors.Add(new ServiceMetadataBehavior() { HttpGetEnabled = true });
        }

Προσθέτουμε και τα usings:
using System.ServiceModel;
using System.ServiceModel.Description;


Η κλάση πρέπει να δείχνει ως:

using System.ServiceModel;
using System.ServiceModel.Description;
 
namespace chatServer
{
    public static class ServiceConfigurationExtensions
    {
        public static void EnableMetadata(this ServiceConfiguration config)
        {
            config.Description.Behaviors.Add(new ServiceDebugBehavior());
            config.Description.Behaviors.Add(new ServiceMetadataBehavior() { HttpGetEnabled = true });
        }
    }
}

 Αν τα έχουμε κάνει όλα σωστά και πατήσουμε view in browser στο chatService.svc θα δούμε πως τρέχει χωρίς λάθη!

Αυτό ήταν! Πάμε να δούμε πως θα πάρουμε το μήνυμα στον client.

Client side full duplex – λήψη μηνύματος από τον server

Πρέπει να ανανεώσουμε την web service στον client. Αφού πατήσουμε view in browser στο chatService.svc πάμε στο chatApp project, ανοίγουμε το service references, πατάμε δεξί κλικ στο chatService και επιλέγουμε update Service references. Αυτό ανανεώνει την service στο project μας. Θα πρέπει να το κάνουμε κάθε φορά που αλλάζει κάτι στον server και θέλουμε να το δει ο client.

Ανοίγουμε το MainPage.xaml.cs αρχείο

Στον constructor πατάμε client. Για να ανοίξει ο Intellisense την λίστα, επιλέγουμε το sendMessageReceived πατάμε γρήγορα += tab tab και μας υλοποιεί έναν handler. Αυτό ήταν! Κάθε φορά που ο server στέλνει μήνυμα μέσω της sendMessage στον client θα τρέχει η client_sendMessageReceived!


Ας φτιάξουμε τον κώδικα της ως εξής:

        void client_sendMessageReceived(object sender, sendMessageReceivedEventArgs e)
        {
            conversationT.Text += e.msg + "\n";
        }

Όπου στο e.msg περιέχεται η παράμετρος msg!

Το μόνο που μένει είναι να δημιουργήσουμε την λογική στον server για να στέλνει τα μηνύματα!

Server side full duplex – διατήρηση καναλιού επικοινωνίας και αποστολή μηνυμάτων στον server

Κάθε φορά που ένας client κάνει αρχικά μια κλήση στον server, θέλουμε ο server να κρατάει ένα κανάλι επικοινωνίας ανοικτό για να μπορεί να του στέλνει πίσω μηνύματα. Πάμε να δούμε πως θα το κάνουμε:

Ανοίγουμε στο project chatServer το chatService.svc.cs

Δημιουργούμε τον contructor και προσθέτουμε τον κώδικα:

public chatService()
        {
            var context = OperationContext.Current;
            IchatServiceCallback callbackChannel = context.GetCallbackChannel<IchatServiceCallback>();
        }

Στην μεταβλητή callbackChannel κρατάμε με αυτόν τον τρόπο το κανάλι. Αν πατήσουμε τελεία στο callbackChannel θα δούμε πως υπάρχει το sendMessage(string msg).

Θέλουμε να διατηρήσουμε σε μια κοινή λίστα όλα τα ενεργά κανάλια όλων των συνδεδεμένων clients. Έτσι δημιουργούμε μια στατική λίστα όπου περιέχει αντικείμενα τύπου IchatServiceCallback.

Άρα προσθέτουμε την λογική:

static List<IchatServiceCallback> callbacks=new List<IchatServiceCallback>();
        public chatService()
        {
            var context = OperationContext.Current;
            IchatServiceCallback callbackChannel = context.GetCallbackChannel<IchatServiceCallback>();
            callbacks.Add(callbackChannel);
            sendMessageToAllConnected("συνδέθηκε ένας χρήστης.");
        }
        private void sendMessageToAllConnected(string msg)
        {
            foreach (var chatServiceCallback in callbacks)
            {
                chatServiceCallback.sendMessage(msg);
            }
        }
 

Τελειώνοντας το chatApp

Στο interface IchatService προσθέτουμε το

        [OperationContract]

        void sendMessageToAll(string msg);

Στο chatService.svc.cs την υλοποιούμε ως:

        public void sendMessageToAll(string msg)
        {
            sendMessageToAllConnected(msg);
        }

Ανανεώνουμε την service reference στον client αφού πρώτα τρέξουμε την service στον browser.

Στο project chatApp στην MainPage.xaml.cs τροποποιούμε την μέθοδο sendMsgB_click ως:

private void sendMsgB_Click(object sender, RoutedEventArgs e)
        {
            var s = sendTextTB.Text.Trim();
            if (!String.IsNullOrEmpty(s))
            {
                client.sendMessageToAllAsync(s);
            }
            sendTextTB.Text = "";
        }

Τρέχουμε το project. Συμπληρώνουμε ένα μήνυμα και πατάμε στείλε.
Το μήνυμα που μας ήρθε στην οθόνη ήταν αποτέλεσμα αυτού που έστειλε o server.

Για να ψηθούμε λίγο περισσότερο…

Τρέχουμε μια φορά το project στον simulator και μια φορά στο local machine. Και βλέπουμε πως πράγματι υπάρχει επικοινωνία!

Μερικές παρατηρήσεις:
Αν τρέξουμε 2-3 φορές το project φαινομενικά θα σταματήσει να δουλεύει η επικοινωνία. Πρακτικά όμως αυτό που γίνεται είναι να έχουν μείνει στην παλιά λίστα με τα callbacks οι παλιοί clients και να ρίχνει ένα ωραίο exception από την αρχή χωρίς να συνεχίζει την επικοινωνία με τους επόμενους νέους clients. Άλλος ένας μπακάλικος τρόπος για να το λύσουμε αυτό:

private void sendMessageToAllConnected(string msg)
        {
            foreach (var chatServiceCallback in callbacks)
            {
                try
                {
                    chatServiceCallback.sendMessage(msg);
                }
                catch
                {
                }
            }
        }

Προφανώς θα πρέπει να φροντίσουμε να αφαιρέσουμε τα νεκρά κανάλια από την λίστα μας!

Πώς να το κάνω host

Ωραία όλα αυτά αλλά πως θα το κάνω host κάπου και να παίζει;

Η διαδικασία είναι απλή.

Θα ανεβάσετε όλα τα αρχεία από το publish στο site και θα αλλάξετε τα bindings του client (από πού θα βαρά την service).

Για να το κάνετε αυτό πατάτε δεξί κλικ στο chatService, configure service reference… και βάζετε την νέα url όπου έχετε ανεβάσει την service

Για απορίες - σχόλια, μην διστάσετε!
Από κάτω! ;)