VENDAS: 1-800-867-1389

Aproveitando o libuv do Node.js no Azure

Atualizado: outubro de 2013

Autor: Christian Martinez

Node.js é definitivamente um dos itens mais interessantes do momento e você pode encontrar sobre como executá-lo no , como esse de Nathan Totten. Embora seja certamente interessante, o assunto desta postagem foi inspirado pela postagem no blog Node.js do autor do Node, Ryan Dahl. Mais especificamente, esta foi a citação que me interessou:

Como o Nó não tem nenhum bloqueio, libuv se torna uma biblioteca muito mais útil: uma biblioteca de rede de plataformas cruzadas, de alto desempenho, mínima e licenciada por BSD.

Sempre tenho interesse em bibliotecas de rede legais, sendo que a minha favorita é a 0MQ ( http://www.zeromq.org/ ) que é uma biblioteca de mensagens completa, de uso geral, de alto desempenho e com suporte para vários MEPs. Libuv é de um tipo muito diferente de 0MQ e tem metas diferentes, mas ambos os projetos têm software livre e um desempenho de primeira classe.

Para começar minha aventura, baixei a versão 5.7 do código-fonte de libuv e tentei executar o arquivo vcbuild.bat. Depois disso, baixei o python, depois a subversão e fiz check-out do gyp script e... bom, de qualquer forma todos esses detalhes provavelmente estão em um blog, mas por enquanto, é importante saber que isso acabou gerando uma solução do Visual Studio que eu pude ajustar para gerar uma biblioteca estática de 64 bits. Como o libuv e o Node são projetos altamente ativos, é provável que as coisas tenham mudado desde que fiz meu exercício, portanto, se você fizer também, talvez sua experiência seja diferente.

Assim que obtive a biblioteca, tive que decidir como queria fazer implantações no . Libuv é um código C nativo e é fornecido com um pacote de teste que eu poderia, em tese, ter implantado com o uso do elemento SDK 1.5ProgramEntryPoint. Porém, isso teria sido entediante e, provavelmente, não seria considerado muito útil pelos desenvolvedores que quisessem usar a biblioteca com seu próprio código gerenciado, então, em vez disso, desenvolvi um protótipo em C++/CLI que me permitiu criar um servidor executado em uma WorkerRole e parecido com este:

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

Isso deve parecer muito normal para qualquer desenvolvedor gerenciado e, embora tudo o que eu faça no protótipo seja incrementar bytes e o número de chamadas recebidas, você pode executar outras ações interessantes de sua escolha no representante exposto.

Então como comecei com código C e acabei com uma experiência gerenciada interessante? Bem, a primeira coisa que eu tive que fazer foi decidir se queria usar P/Invoke ou C++/CLI. Depois de examinar o código-fonte de libuv e perceber que era voltado para retorno de chamada, decidi optar pelo C++/CLI. Embora sempre seja um prazer lidar com a sintaxe de ponteiro para função membro, ainda prefiro lidar com isso do que gerenciar DLLImport e realizar marshaling e mapeamento usando P/Invoke. Esse recurso foi muito útil para descobrir a sintaxe do retorno de chamada. Basicamente, você acabará precisando de uma declaração Delegate, uma variável de instância Delegate e um GCHandle. O arquivo de cabeçalho ficou assim:

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

A conexão dos representantes ficou parecida com esta:

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

Não é muito bonita, mas fez o trabalho! Os outros itens internos estão disponíveis com o código postado.

Quando obtive um servidor, criei um cliente de teste simples usando C# antigo básico, TcpClient e o executei de outra WorkerRole:

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

Em seguida, adicionei um script simples e uma Tarefa de Inicialização ao meu projeto “server” para assegurar que o tempo de execução de C estava disponível:

"%~dps0vcredist_x64.exe" /q /norestart

E, finalmente, eu implantei o projeto e o executei no .

O código-fonte completo do projeto pode ser encontrado aqui.

Isso foi útil para você?
(1500 caracteres restantes)
Agradecemos os seus comentários
A Microsoft está realizando uma pesquisa online para saber sua opinião sobre o site do MSDN. Se você optar por participar, a pesquisa online lhe será apresentada quando você sair do site do MSDN.

Deseja participar?
Mostrar:
© 2014 Microsoft