Code modifié côté client, le 15/01/2020
Bonjour à tous,
ceci n'est pas une question mais un sujet pour venir en aide si à l'occasion comme moi vous veniez à galérer pour avoir l'équivalent d'un callback (comme on peu avoir sur #WCF) avec #GRPC.
Etant un nul moi même, logiquement vous devriez me comprendre car je vais tenter de simplifier grandement la mise en place, quand vous aurez fini vous pourrez ensuite aller voir le tuto de MS pour passer sur une version plus élaborée en ayant compris la base. Je me dis que des gens plus habitués pourront toujours partir ensuite de cette base pour qu'on améliore, et sinon... poubelle.
Testé en .Net Core 3.1
Avant de commencer un énorme merci à Pol63, pour cette fois comme pour les autres, il est toujours là pour aider et surtout il est limpide quand il s'exprime.
Pour cet exemple, je suis parti de la solution que l'on peut trouver sur la page de Microsoft mais qui pour un nul comme moi m'a semblé parfois abscons, à la fin j'ai fini par éplucher le code de celui qui avait posté pour décrypter totalement la mise en place. Donc cet exemple va reprendre la même chose car il est fort probable que vous aussi vous ayez tendance à partir d'un serveur GRPC qui va faire tout le boulot, pour ma part c'est l'inverse, le serveur #GRPC est secondaire dans l'application que je suis en train de développer. Pour simplifier je ne gère pas les Tokens permettant l'annulation, de sorte d'avoir un code vraiment minimaliste, concentré uniquement sur le callback qui aura un timer...
Vous allez pouvoir commencer déjà à réfléchir aussi sur la durée de vie de la méthode proposée sur le site MS et donc selon adapter en conséquence votre code...
Au niveau du fonctionnement, ici le stream n'est que dans dans un seul sens, le client appelle pour s'abonner, le serveur:
- Crée une instance de subscriber par le biais d'une factory qui va tourner en boucle en émettant des signaux.
- Abonne les signaux de ce subscriber à une fonction asynchrone qui va répondre au client (et ça s'arrête là en temps normal);
- On poursuit en bloquant un certain temps cet appel dans cet exemple, juste pour facilement comprendre le fonctionnement et gérer le minimum (sur le tuto MS on utilise await AwaitCancellation(context.CancellationToken) à la place du Task.Delay(10000))
Commençons donc par créer une fabrique à Subscriber... donc sur l'exemple MS ça sera en anglais SubscriberFactory, celle ci sera passée par votre service et permettra au moment de la connexion du client de lancer un subscriber, qui va tourner en boucle indéfiniment en asynchrone pour nous. Il nous faut une interface ISubscriberFactory et une class SubscriberFactory;
L'interface
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 public interface ISubscriberFactory { //.... ? ISubscriber GetSubscriber(string[] Subscribers); }
La class:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 /// <summary> /// Renvoie un NOUVEAU Subscriber /// </summary> /// <param name="level"></param> /// <returns></returns> public ISubscriber GetSubscriber(string[] level) { return new Subscriber(level); }
Maintenant pour l'intégrer au serveur GRPC nous allons faire quelques modifications sur la class Startup que vous utilisez pour lancer votre serveur, dans la méthode ConfigureServices on rajoute une ligne (elle est commentée)
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); services.AddSingleton<ISubscriberFactory, SubscriberFactory>(); // On rajoute cette ligne }
On va modifier également notre service, on ajoute une variable et on modifie le constructeur pour l'initialiser;
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 private readonly ILogger<GreeterService> _logger; private readonly ISubscriberFactory _subscriberFactory; public GreeterService(ILogger<GreeterService> logger, ISubscriberFactory fac) { _subscriberFactory = fac; _logger = logger; }
Penchons nous maintenant sur le subscriber qui va nécessite trois choses d'autant qu'ici on essaie de rester assez fidèle au tuto de Microsoft pour faciliter la transition et la compréhension
- Une interface
- Une classe
- Un eventargs
Pour l'eventargs j'ai simplifié au maximum comme pour tout le reste pour maximiser la lisibilité et la compréhension
Pour l'interface elle va porter un nom un peu différent vu que l'appli n'est pas la même, ayant été elle aussi totalement simplifiée. Elle contraindra la classe à implémenter une méthode Dispose
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 public class LambdaEventArgs: EventArgs { public LambdaEventArgs(string m) { Message = m; } public string Message { get; } }
Enfin la classe, un timer de 500 histoire d'y aller tranquillement
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 public interface ISubscriber: IDisposable { /// <summary> /// Evenement /// </summary> public event EventHandler<LambdaEventArgs> Update; }
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 public class Subscriber : ISubscriber { public event EventHandler<LambdaEventArgs> Update; public Subscriber(string[] symbol) { RunAsync(); } private async Task RunAsync() { try { while (true) { await Task.Delay(500); if (Update != null) { Console.WriteLine("Et pourtant elle tourne..."); Update?.Invoke(this, new LambdaEventArgs("it's Alive !!! Alive !!!")); } } } catch (OperationCanceledException) { // ... } } public void Dispose() { //... }
Il ne reste plus qu'à s'occuper des fichiers proto et de la méthode chargée d'abonner le client, en réalité concernant l'implantation en elle même ça ne s'est borné qu'à modifier le startup et le service... Et si vous avez déjà une classe que vous vouliez appeler, vous aurez donc à créer pour coller à cet exemple une classe factory, une interface pour cette classe factory, et une interface pour votre propre classe... et bien sûr un event si vous n'en aviez pas.
Côté proto, comme pour le reste je fais très simple, on peut optimiser, implanter d'autres choses etc... (je sais je ne suis pas inspiré pour nommer)
Pas grand chose à expliquer, juste faites bien attention à "stream" c'est ce qui permettra à grpc de fonctionner sur ce mode.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 service MyService { rpc SubscribeMessage (Subscribe) returns (stream Blabla); } message Subscribe{ repeated string LevelLog = 1; } message Blabla{ string malife = 1; }
Bien, jusque ici rien de compliqué en fait.... La suite va demander un peu plus de concentration donc si vous aviez prévu de lire en diagonale, ce que je fais aussi régulièrement quand je consulte des dizaines de pages ou des tutos déjà vu, c'est là ou il faudra stopper.
Vous ouvrez de nouveau votre service pour implémenter votre méthode SubscribeMessage.
L'inscription aux events de Subscriber n'est pas le plus compliqué, par contre vous pourriez vous casser les dents pour Désinscrire, en effet ce code génèrera pas mal d'erreurs sans cela.
- Solution 1, une petite classe qu'on instancie et à qui on confie de répondre, vous pourriez préférer cette solution à un moment.
- Solution 2, que j'ai choisi ici, un eventhandler qui de fait me permettra ensuite de désabonner quand j'aurai terminé.
Comme expliqué on fait donc très simple, un Task.Delay stoppe l'abonnement, sur le tuto plus avancé de MS on utilisera await AwaitCancellation(context.CancellationToken); et on implémentera un vrai système pour stopper;
Nous allons implémenter aussi une méthode asynchrone chargée de répondre, comme expliqué le principe est d'abonner l'event à cette fonction puis de bloquer la méthode selon les besoins et ce que l'on veut faire, le serveur émettra durant.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 public override async Task SubscribeMessage(Subscribe request, IServerStreamWriter<Blabla> responseStream, ServerCallContext context) { // Rappelez vous, cette méthode renvoie un nouveau Subscriber qui va donc se mettre à tourner en boucle; ISubscriber subscriber = _subscriberFactory.GetSubscriber(request.LevelLog.ToArray()); //Abonnement à l'évènement + appel de méthode asynchrone (Plante sur l'exemple sans ce moyen) EventHandler<LambdaEventArgs> messageTarget = new EventHandler<LambdaEventArgs>(async delegate (object sender, LambdaEventArgs args) { await WriteUpdateAsync(responseStream, args.Message); }); subscriber.Update += messageTarget; _logger.LogInformation("Subscription started."); await Task.Delay(5000); subscriber.Update -= messageTarget; _logger.LogInformation("Subscription stopped."); //await AwaitCancellation(context.CancellationToken); } private async Task WriteUpdateAsync(IServerStreamWriter<Blabla> stream, string paraMessage) { try { await stream.WriteAsync(new Blabla { Malife = paraMessage }); } catch (Exception e) { // Handle any errors due to broken connection etc. _logger.LogError($"Failed to write message: {e.Message}"); } }
C'est fini coté serveur Comme vous pouvez le constater ce n'est pas si compliqué à mettre en oeuvre... Toutefois comme ce code n'implémente pas toutes les fonctions pour bien gérer les déconnexions, nous appellerons un arrêt du serveur.. le code est dissocié toujours dans un but de compréhension.
------------------------------------- Implémenter un arrêt serveur -------------------------------------
Donc au niveau du code proto, pour STOPPER le serveur il faut rajouter, un import import pour ne pas renvoyer quoique ce soit au client.
Et côté service on implémente la méthode, et on modifie aussi le constructeur
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import "google/protobuf/empty.proto"; service MyService { rpc SubscribeMessage (Subscribe) returns (stream Blabla); rpc DistantStopServer (StopServer) returns (google.protobuf.Empty); } message StopServer{ } message Subscribe{ repeated string LevelLog = 1; } message Blabla{ string malife = 1; }
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 //... Propriétés implémentées précedemment. IHostApplicationLifetime applicationLifetime; public GreeterService( ILogger<GreeterService> logger, IHostApplicationLifetime app , ISubscriberFactory fac) { _subscriberFactory = fac; _logger = logger; applicationLifetime = app; } public override Task<Empty> DistantStopServer(StopServer request, ServerCallContext context) { applicationLifetime.StopApplication(); CancellationToken dd = applicationLifetime.ApplicationStopped; return Task.FromResult(new Empty()); } //.... Reste du code implémenté précédemment.
Sauf oubli tout est normalement ok.
------------------------------------- Côté Client -------------------------------------
En premier lieu il ne faut pas copier le fichier proto mais prendre l'habitude d'ajouter une référence de service, sur le fichier proto du serveur, vous aurez ainsi toujours une synchronisation entre le serveur et le client. Si jamais le proto n'est pas défini comme client côté client (ici ça sera une console très basique) , clique droit dessus pour régler le problème ou édition du fichier du projet.
Dans votre class Program, vu que c'est parfois capricieux de lancer les deux en mode débug il y aura simplement des arrêt pour vous aider, vous pouvez les lever si vous le souhaitez. De même, j'aime bien diss
La fonction qui va traiter en asynchrone les messages du serveur une fois abonné est celle ci.
Et pour la fonction principale.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 private static async Task StreamHandle(IAsyncStreamReader<Blabla> stream) { await foreach (var reply in stream.ReadAllAsync()) { Console.WriteLine(reply.Malife); } Console.WriteLine("Abonnement terminé"); }
Voilà c'est terminé, mais comme vous pourrez vous en rendre compte si vous fermez brutalement le client, l'objet Subscriber lancé lorsque vous avez souscris à l'abonnement va continuer d'exister pour sa part, ce qui va amener donc à devoir implémenter de quoi annuler les fonctions asynchrones lancées (voir côté CancellationTokenSource ), l'exemple de MS les implémente vous lèverez donc le Task.Delay qui n'avait ici qu'un but pédagogique. A noter que cette méthode ne permet pas de stopper réellement côté client l'abonnement au streaming d'après ce que j'ai pu en juger,
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 static void Main(string[] args) { Console.WriteLine("Pressez entreé pour démarrer"); Console.ReadLine(); // obligatoire en cas de connexion non sécurisée AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); // The port number(5001) must match the port of the gRPC server. var channel = GrpcChannel.ForAddress("https://localhost:5001"); MyService.MyServiceClient client = new MyService.MyServiceClient(channel); try { //StreamStream(client); <- obsolete StreamHandle(streamingCall.ResponseStream); Console.ReadLine(); Console.WriteLine("Requête d'arrêt serveur émise..."); await client.DistantStopServerAsync(new StopServer()); Console.WriteLine("Serveur stoppé"); Console.ReadLine(); } catch (Exception exc) { Console.WriteLine(exc.Message); Console.ReadKey(); } }
en effet le Cancellation Token Source du "context" ne se déclenche réellement que sur une déconnexion du client, on peut s'en rendre compte au travers de cet exemple justement, qui est très pratique pour faire des essais. C'est suffisant si l'on désire faire une application qui va demander un fichier, s'abonner à un stream jusqu'à se fermer.
N'hésitez pas à me faire un feedback s'il y a un soucis que j'améliore, je n'ai pas fait de tuto depuis +15 anset puis je suis loin d'être un as là dessus, mais si j'arrive à rendre un peu de ce qu'on m'a donné ici car c'est vraiment une communauté exceptionnelle.
Je vais le redire mais c'est basique le but c'est vraiment d'abonner une fonction asynchrone sur un event. A partir de là vous pouvez vous rendre sur le tuto de MS , vous devriez avoir compris l'essentiel du fonctionnement et pouvoir étoffer la fonction pour mieux gérer les déconnexions clients.
Le code source que j'ai décortiqué et qui sert pour est celui ci : le tuto déposé sur microsoft et moi je vais faire un café avant de me jeter la tête contre un mur ...![]()
Partager