VENTAS: 1-800-867-1389

Aprovechar libuv de Node.js en Azure

Actualizado: octubre de 2013

Autor: Christian Martinez

Node.js es sin duda alguna uno de los temas más candentes ahora mismo y puede encontrar multitud de blogs sobre su ejecución en como este de Nathan Totten. Aunque es realmente interesante, el asunto de esta entrada está inspirado por esta entrada del blog de Node.js realizada por el creador de Node, Ryan Dahl. Más concretamente, esta cita fue la que atrajo mi interés:

Puesto que Node es completamente antibloqueo, libuv resulta ser una biblioteca bastante útil por sí misma: una biblioteca de red con licencia de BSD, mínima, de alto rendimiento y multiplataforma.

Siempre me interesan las buenas bibliotecas de red y mi favorita actualmente es 0MQ (http://www.zeromq.org/), que es una biblioteca de mensajería de uso general totalmente equipada, que admite MEP múltiple y de alto rendimiento. Libuv es una bestia muy diferente de 0MQ y tiene objetivos diferentes, pero ambos proyectos comparten el vínculo común de ser de código abierto y de hacer del rendimiento un ciudadano de primera clase.

Para dar comienzo a mi aventura me descargué la versión 5.7 del código fuente de libuv e intenté ejecutar el archivo vcbuild.bat. Después de tirar la toalla, descargué python, luego la subversión y examiné el script de gyp y, de todas formas, estas travesuras probablemente podrían ser un blog por sí mismas, pero por ahora lo único que sé es que al final lo que se generó fue una solución de Visual Studio que fui capaz de retocar para generar una biblioteca estática de 64 bits. Como libuv y Node son proyectos muy activos, es probable que las cosas hayan cambiado desde la primera vez que los probé; así que si lo prueba usted puede que su experiencia sea diferente o no.

Una vez que tenía la biblioteca, tuve que decidir cómo deseaba implementar las cosas en . Libuv es código C nativo e incluye un conjunto de pruebas que podría haber implementado teóricamente mediante el elemento ProgramEntryPoint del SDK 1.5. Pero eso habría sido aburrido y probablemente no les parecería muy útil a los desarrolladores que desearan usar la biblioteca con su propio código administrado, así que lo que hice en su lugar fue desarrollar un prototipo en C++/CLI que me permitiera crear un servidor que se ejecutara en un rol de trabajo y es similar a lo siguiente:

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
      }  
}

Eso debe ser muy normal para todos los desarrolladores de código administrado, y aunque todo lo que hago en el prototipo es incrementar los bytes y el número de llamadas recibidas, usted puede hacer cualquier cosa interesante que desee en el delegado expuesto.

Entonces ¿cómo obtuve una buena experiencia administrada a partir de código C puro? Bien, lo primero que tenía que hacer era decidir si deseaba utilizar P/Invoke o C++/CLI. Después de examinar detenidamente el código fuente de libuv y darme cuenta de que todo estaba controlado por devoluciones de llamada, decidí quedarme con C++/CLI. Si bien siempre es agradable trabajar con la sintaxis de las funciones de puntero a miembro, todavía prefiero administrar DLLImport y calcular referencias y asignar una mediante P/Invoke. Este recurso fue de gran ayuda para averiguar la sintaxis de devolución de llamada. Básicamente, lo que se necesita al final es una declaración de Delegate, una variable de instancia de Delegate y un GCHandle. Mi archivo de encabezado acabó teniendo una apariencia similar a esta:

// 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
    };
}

La conexión de los delegados es parecida a esto:

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");}

No es lo más bonito del mundo, pero lo conseguí. Todos los demás datos internos están disponibles con el código fuente publicado.

Una vez que tenía un servidor, compilé un cliente de prueba simple utilizando C# antiguo, TcpClient, y lo ejecuté desde otro rol de trabajo:

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);
}

A continuación, agregué un script simple y una tarea de inicio a mi proyecto de “servidor” para asegurarme de que el runtime de C estaba disponible:

"%~dps0vcredist_x64.exe" /q /norestart

Por último, implementé el proyecto y lo ejecuté en .

Todo el código fuente del proyecto se encuentra aquí.

¿Te ha resultado útil?
(Caracteres restantes: 1500)
Gracias por sus comentarios
Mostrar:
© 2014 Microsoft