내보내기(0) 인쇄
모두 확장
이 항목은 아직 평가되지 않았습니다.- 이 항목 평가

Windows Azure에서 Node.js의 libuv를 활용

저자: Christian Martinez

Node.js는 현재 가장 중요한 이슈 중 하나가 분명하며, Nathan Totten의 블로그 게시물을 포함해서 Windows Azure에서 실행되는 이와 관련된 블로그 게시물을 쉽게 찾아볼 수 있습니다. 이 게시물도 확실히 흥미롭고 뛰어난 내용을 담고 있긴 하지만 이 게시물은 Node를 만든 사람인 Ryan Dahl이 Node.js 블로그에 올린 이 게시물의 영향으로 작성되었습니다. 조금 더 구체적으로 말해서, 다음과 같은 매우 흥미로운 문장을 찾을 수 있습니다.

Node가 완전히 비차단적인 특성을 가지므로, libuv는 그 자체로 유용한 라이브러리인 것으로 판명됩니다. 이 라이브러리는 BSD 라이선스를 지원하고, 최소한의 특성과 고성능을 지원하는 플랫폼 간 네트워킹 라이브러리입니다.

필자는 완전한 기능으로 작동하고, 다중 MEP를 지원하며, 고성능 및 범용 메시징 라이브러리인 0MQ( http://www.zeromq.org/ )와 같이 뛰어난 네트워킹 라이브러리에 항상 많은 관심을 갖고 있습니다. Libuv는 0MQ와 종류가 매우 다르며, 목적도 다르지만 두 프로젝트 모두 오픈 소스를 지향하고 성능을 최우선으로 하는 공통적인 특성을 갖고 있습니다.

이 라이브러리를 살펴보기 전에 필자는 libuv 소스의 5.7 버전을 다운로드해서 vcbuild.bat 파일을 실행해보았습니다. 그런 다음 python과 subversion을 다운로드하고 gyp 스크립트를 확인해봤습니다. 어찌되었든 이러한 자잘한 이야기는 차치하고, 필자는 여러 과정을 거쳐서 결국 64비트 정적 라이브러리를 생성하도록 조정할 수 있는 Visual Studio 솔루션을 생성할 수 있었다는 것만 알아 주시기 바랍니다. libuv와 Node는 매우 활발한 프로젝트이기 때문에 이후에는 이 문서를 작성한 시점과도 이미 많은 것이 달라졌을 가능성이 있으므로, 지금 이 게시물의 내용은 이후 여러분이 직접 경험해볼 때 그 결과가 같을 수도 있고 다를 수도 있을 것입니다.

라이브러리가 준비된 다음, 필자는 이를 Azure에 어떻게 배포해야 할지 결정해야 했습니다. Libuv는 네이티브 C 코드이며, 필자가 SDK 1.5ProgramEntryPoint 요소를 사용해서 이론적으로 배포할 수 있었던 테스트 모음과 함께 제공됩니다. 하지만 이러한 테스트 모음은 이 라이브러리를 자신의 고유 관리 코드와 함께 사용하길 원하는 개발자들에게 있어서 지루하고 유용하지 않은 것으로 보여질 수 있으므로, 필자는 필자가 WorkerRole에서 실행되는 서버를 만들 수 있었던 C++/CLI로 된 프로토타입을 개발했습니다. 이 프로토타입은 다음과 같습니다.

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

이 프로토타입은 관리되는 개발자에게 매우 일반적인 것으로 보일 수 있으며, 이 프로토타입에서 필자가 한 모든 일은 증분 바이트 및 호출 수를 늘린 것이며, 여러 분도 제공된 대리자에서 원하는 모든 흥미로운 작업을 자유롭게 수행할 수 있습니다.

그러면 직관적인 C 코드를 사용해서 괜찮아 보이는 관리되는 환경을 어떻게 만들 수 있었을까요? 우선 처음에 해야 했던 일은 P/Invoke 또는 C++/CLI 중 무엇을 사용할지 결정하는 것이었습니다. libuv 소스를 살펴보고 모든 것이 콜백으로 구동된다는 것을 알게 된 다음 필자는 C++/CLI를 사용하기로 결정했습니다. 멤버 함수 구문에 대한 포인터는 항상 다룰 때마다 즐거움이 있지만, 필자는 여전히 DLLImport를 관리하고, P/Invoke를 사용해서 마샬링 및 매핑을 수행하는 것을 더 좋아합니다. 이 리소스는 콜백 구문을 생각할 때 많은 도움이 되었습니다. 기본적으로는 Delegate 선언, Delegate 인스턴스 변수 및 GCHandle만 있으면 됩니다. 그래서 필자의 헤더 파일은 결국 다음과 같은 형태를 갖게 되었습니다.

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

대리자 연결은 다음과 같습니다.

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

완벽하지는 않지만 그래도 작업은 완료되었습니다. 나머지 내부 사항은 게시된 소스로 제공됩니다.

서버가 준비된 다음에는 오래된 일반 C#으로 된 TcpClient를 사용해서 간단한 테스트 클라이언트를 만들고 이를 다른 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);
}

그런 다음 C 런타임을 사용할 수 있도록 내 "server" 프로젝트에 간단한 스크립트와 시작 작업을 추가했습니다.

"%~dps0vcredist_x64.exe" /q /norestart

그리고 마지막으로 프로젝트를 Windows Azure에서 배포하고 실행했습니다.

이 프로젝트의 전체 소스는 여기에서 찾을 수 있습니다.


빌드 날짜:

2013-11-22
이 정보가 도움이 되었습니까?
(1500자 남음)
의견을 주셔서 감사합니다.

커뮤니티 추가 항목

Microsoft는 MSDN 웹 사이트에 대한 귀하의 의견을 이해하기 위해 온라인 설문 조사를 진행하고 있습니다. 참여하도록 선택하시면 MSDN 웹 사이트에서 나가실 때 온라인 설문 조사가 표시됩니다.

참여하시겠습니까?
표시:
© 2014 Microsoft. All rights reserved.