VENTES: 1-800-867-1389

Exploitation de la bibliothèque Libuv de Node.js dans Azure

Mis à jour: octobre 2013

Auteur : Christian Martinez

Node.js est certainement un des outils les plus marquants en ce moment et vous trouverez des blogs sur son exécution dans Microsoft Azure, notamment celui-ci, rédigé par Nathan Totten. Intéressant et branché, le sujet de cette publication a été inspiré par cette publication sur le blog Node.js de Ryan Dahl, créateur de Node. Plus spécifiquement, c'est la citation suivante qui m'a intéressé :

Comme Node est complètement non bloquant, Libuv s'avère être une bibliothèque plutôt utile : une bibliothèque minimale de mise en réseau multiplateforme, hautes performances, sous licence BSD.

Je suis toujours intéressé par les bibliothèques de mise en réseau, ma préférée étant 0MQ ( http://www.zeromq.org/ ) qui est une bibliothèque de messagerie complète à usage général, hautes performances, prenant en charge plusieurs MEP. Libuv est très différente d'0MQ et a différents objectifs, mais les deux projets ont comme point commun d'être open source et de mettre les performances en avant.

Pour commencer, j'ai téléchargé la version 5.7 de la source libuv et tenté d'exécuter le fichier vcbuild.bat. Après cet échec, j'ai téléchargé Python, puis une sous-version et j'ai vérifié le script gyp. Quoi qu'il en soit ces manigances pourraient probablement constituer un blog à elles-seules, mais pour l'instant tout ce que j'ai réussi à générer est une solution Visual Studio impossible à optimiser pour générer une bibliothèque statique 64 bits. Étant donné que Libuv et Node sont des projets très actifs, il est probable que les choses ont évolué depuis ma tentative. En conséquence, votre expérience pourra être différente.

Une fois la bibliothèque à ma disposition, j'ai dû décider comment effectuer le déploiement dans Azure. Libuv est en code C natif et intègre une suite de tests que j'aurais pu déployer à l'aide de l'élément SDK 1.5ProgramEntryPoint. Cependant, cela aurait été ennuyeux et probablement pas considéré comme étant utile par les développeurs qui souhaitent utiliser la bibliothèque avec leur propre code managé. J'ai donc développé un prototype en C++/CLI qui m'a permis de créer un serveur qui s'exécute dans un rôle de travail et ressemble au suivant :

public override void Run()
{
     long cnt =0;
     long byteCnt = 0;
     using (var server = new TcpServer())
     {
        var ep = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["listen"];
        //must assign callback before calling listen
        server.OnBytesReceived += (arg) =>      
         {
          Interlocked.Increment(ref cnt);
          Interlocked.Add(ref byteCnt, arg.Length);
          //Trace.WriteLine(cnt);
         };
        server.Listen(ep.IPEndpoint.Address.ToString(), 
                          ep.IPEndpoint.Port); 
       //if this is successful libuv uv_run blocks exit
      }  
}

Cela doit sembler normal à tout développeur de code managé. Et bien que tout ce que je fasse dans le prototype est d'incrémenter les octets et le nombre d'appels reçus, vous êtes libre de faire d'autres choses intéressantes que vous choisissez dans le délégué exposé.

Comment suis-je passer du code C à une expérience managée ? La première chose que j'ai dû faire a été de déterminer si je souhaitais utiliser P/Invoke ou C++/CLI. Après avoir pris connaissance de la source libuv et réalisé que l'ensemble était piloté par des rappels, j'ai choisi d'utiliser C++/CLI. Alors qu'un pointeur vers la syntaxe de fonction membre est toujours facile à utiliser, je préfère utiliser C++/CLI plutôt que gérer DLLImport, le marshalling et le mappage à l'aide de P/Invoke. Cette ressource a été d'une aide précieuse pour déterminer la syntaxe de rappel. Fondamentalement ce dont vous avez besoin est une déclaration Delegate, une variable d'instance Delegate et un GCHandle. Mon fichier d'en-tête ressemble au suivant :

// LibuvWrapper.h
#pragma once
#include "include\uv.h"
using namespace System;
using namespace System::Collections::Concurrent;
using namespace System::Runtime::InteropServices;

namespace LibuvWrapper {
    
    public delegate void BytesReceived(array<Byte>^ stuff);
    
    public ref class TcpServer sealed 
    {
        public:
            
            TcpServer();
            ~TcpServer();
            !TcpServer();
            void Listen(String^ address, int port);
            //public delegate for passing bits sent 
            //from client onto implementation code
            BytesReceived^ OnBytesReceived; 
            private:
            //delegate declarations. to convert to native callbacks you
            //need the delegate declaration, delegate instance, 
            //gchandle and preferably a typedef
            delegate void AfterRead(uv_stream_t* handle, ssize_t nread, 
                                                        uv_buf_t buf);
            delegate void OnConnection(uv_stream_t* server, 
                                                        int status);
            delegate void AfterWrite(uv_write_t* req, int status);
            delegate void OnClose(uv_handle_t* peer);
            delegate void OnServerClose(uv_handle_t* handle);

            //callback methods
            void AfterReadImpl(uv_stream_t* handle, ssize_t nread, 
                                                uv_buf_t buf);
            void OnConnectionImpl(uv_stream_t* server, int status);
            void AfterWriteImpl(uv_write_t* req, int status);
            void OnCloseImpl(uv_handle_t* peer);
            void OnServerCloseImpl(uv_handle_t* handle);
            

            //handles and delegate members that will be 
            //converted to native callbacks
            AfterRead^ afterReadFunc;
            GCHandle afterReadHandle;
            OnConnection^ onConnectionFunc;
            GCHandle onConnectionHandle;
            AfterWrite^ afterWriteFunc;
            GCHandle afterWriteHandle;
            OnClose^ onCloseFunc;
            GCHandle onCloseHandle;
            OnServerClose^ onServerCloseFunc;
            GCHandle onServerCloseHandle;
            
            
            //libuv members
            uv_tcp_t * server;
            uv_loop_t * loop;
            uv_buf_t * ack; 
            //this is the same response sent to all 
            //clients as ack. created once and never 
            //deleted until object destruction
    };
}

L'écriture des délégués ressemble à la suivante :

afterReadFunc = gcnew AfterRead(this, &LibuvWrapper::TcpServer::AfterReadImpl);
afterReadHandle = GCHandle::Alloc(afterReadFunc);

The typedef and passing the callback look like this:
typedef void (__stdcall * AFTERREAD_CB)(uv_stream_t* handle,
                         ssize_t nread, uv_buf_t buf);

...

if(uv_read_start(stream, echo_alloc,static_cast<AFTERREAD_CB>
  (Marshal::GetFunctionPointerForDelegate(afterReadFunc).ToPointer())))
{
    throw gcnew System::Exception(
                 "Error on setting up after read\n");}

Ce n'est pas esthétique, mais le travail est fait ! Les autres éléments internes sont disponibles avec la source publiée.

Une fois que j'ai eu un serveur, j'ai créé un client de test simple en utilisant l'ancien code C# ordinaire, TcpClient et je les ai exécuté à partir d'un autre rôle de travail :

var ep = RoleEnvironment.Roles["LibuvServer"].Instances.Select(
               i => i.InstanceEndpoints ["listen"]).ToArray();
var cl = new TcpClient();
byte[] msg = Encoding.ASCII.GetBytes(new string('x',msgSize));
byte[] gbye = Encoding.ASCII.GetBytes("|^|");
byte[] ackBuf = new byte[1];

//it's easy for the client to start before server so wait 
//forever until we connect. 
//for something more sophisticated AKA "the greatest retry library 
//in the history of man" see 
//http://code.msdn.microsoft.com/Transient-Fault-Handling-b209151fwhile (!cl.Connected)
{
    try
    {
        cl.Connect(ep[0].IPEndpoint);
    }
    catch (Exception)
    {
        Thread.Sleep(100);
    }
    
}
using(var stream = cl.GetStream())
{
    var watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < numTimes ; i++)
    {

    stream.Write(msg, 0, msg.Length);
    stream.Read(ackBuf, 0, ackBuf.Length);
}
watch.Stop();
log = string.Format("Sent {0} messages of size {1} and received  {0} acks in {2} milliseconds", 
                                            numTimes, msgSize, watch.ElapsedMilliseconds);
Trace.WriteLine(log, "Information");
stream.Write(gbye,0,gbye.Length);
}

Ensuite, j'ai ajouté un script simple et la tâche de démarrage à mon projet de « serveur » afin de garantir que le runtime C était disponible :

"%~dps0vcredist_x64.exe" /q /norestart

Enfin, j'ai déployé le projet et je l'ai exécuté dans .

La source complète du projet est disponible ici.

Cela vous a-t-il été utile ?
(1500 caractères restants)
Merci pour vos suggestions.
Afficher:
© 2014 Microsoft