Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Topshelf y Katana: una arquitectura web y de servicios unificada
Descargar el código de ejemplo
Usar IIS para hospedar aplicaciones web de ASP.NET ha sido la norma de hecho durante más de una década. Crear estas aplicaciones es un proceso relativamente sencillo, no así implementarlas. La implementación requiere un vasto conocimiento de las jerarquías de configuración de aplicaciones y los matices de la historia de IIS, así como el tedioso aprovisionamiento de sitios, aplicaciones y directorios virtuales. Muchas de las piezas fundamentales de la infraestructura terminan fuera de la aplicación en componentes IIS configurados manualmente.
Cuando las aplicaciones crecen por encima de las simples solicitudes web y deben admitir solicitudes de ejecución prolongada, tareas recurrentes y otras tareas de procesamiento, se vuelven difíciles de admitir en IIS. A menudo, la solución es crear un servicio de Windows independiente para hospedar estos componentes. Pero esto requiere un proceso de implementación totalmente independiente, lo que duplica el esfuerzo necesario. Por último, los procesos web y de servicios deben comunicarse. Lo que podría ser una aplicación muy sencilla se convierte en algo muy complejo.
La figura 1 muestra cómo se vería normalmente esta arquitectura. La capa web es responsable de controlar las solicitudes rápidas y proporcionar una interfaz de usuario al sistema. Las solicitudes de ejecución prolongadas se delegan al servicio, que también controla las tareas recurrentes y el procesamiento. Además, el servicio proporciona el estado sobre el trabajo actual y futuro a la capa web que se incluirá en la interfaz de usuario.
Figura 1 Arquitectura web y de servicios tradicional independiente
Un nuevo enfoque
Afortunadamente, están surgiendo nuevas tecnologías que pueden simplificar mucho el desarrollo y la implementación de aplicaciones web y de servicios. Gracias al proyecto Katana (katanaproject.codeplex.com) y las especificaciones proporcionadas por OWIN (owin.org), ahora es posible autohospedar aplicaciones web, lo que deja a IIS fuera de la ecuación, y aún así admitir muchos de los componentes omnipresentes de ASP.NET, como WebApi y SignalR. El host web automático se puede incrustar en una aplicación de consola rudimentaria junto con Topshelf (topshelf-project.com) para crear un servicio de Windows con facilidad. Como resultado, los componentes web y de servicios pueden existir paralelamente en el mismo proceso, como se ve en la figura 2. Esto elimina la sobrecarga de desarrollar capas de comunicación extrañas, proyectos independientes y procedimientos de implementación independientes.
Figura 2 Arquitectura web y de servicios unificada con Katana y Topshelf
Esta capacidad no es totalmente nueva. Topshelf ha estado disponible durante años, ayudando a simplificar el desarrollo de servicios de Windows, y hay muchos marcos web de host automático de código abierto, como Nancy. Sin embargo, hasta que OWIN no floreció en el proyecto Katana, nada se había mostrado tan prometedor para convertirse en la alternativa de hecho para hospedar aplicaciones web en IIS. Además, Nancy y muchos componentes de código abierto trabajan con el proyecto Katana, lo que te permite gozar de un marco ecléctico y flexible.
Topshelf puede parecer opcional, pero no es así. Sin la capacidad de simplificar el desarrollo de servicios, el desarrollo web autohospedado puede ser sumamente engorroso. Topshelf simplifica el desarrollo del servicio al tratarlo como una aplicación de consola y abstraer el hecho de que se hospedará como servicio. En el momento de la implementación, Topshelf controla automáticamente la instalación y el inicio de la aplicación como un servicio de Windows, todo sin la sobrecarga de tener que tratar con InstallUntil, los matices de un proyecto de servicio y los componentes del servicio, ni tener que adjuntar depuradores a los servicios cuando algo sale mal. Topshelf también permite especificar muchos parámetros, como el nombre del servicio, en el código o configurarlos durante la instalación con una línea de comando.
Para ilustrar cómo unificar componentes web y de servicios con Katana y Topshelf, crearé una aplicación de mensajes SMS sencilla. Comenzaré con una API para recibir mensajes y ponerlos en una cola para enviarlos. Esto demostrará lo fácil que es controlar las solicitudes de ejecución prolongada. Luego, agregaré un método de consulta a API para devolver el recuento de mensajes pendientes, lo que también mostrará lo fácil que es consultar el estado del servicio desde los componentes web.
Más adelante, agregaré una interfaz administrativa para demostrar que los componentes web autohospedados aún admiten la creación de interfaces web enriquecidas. Para redondear el procesamiento de los mensajes, agregaré un componente para enviar mensajes a medida que entren en la cola, para presentar incluso los componentes del servicio.
Y para destacar una de las mejores partes de esta arquitectura, crearé un script psake para exponer la simplicidad de las implementaciones.
Para concentrarnos en las ventajas combinadas de Katana y Topshelf, no voy a entrar en los detalles de ninguno de los proyectos. Consulta “Getting Started with the Katana Project” (Introducción al proyecto Katana, bit.ly/1h9XaBL) y “Create Windows Services Easily with Topshelf” (Crear servicios de Windows fácilmente con Topshelf, bit.ly/1h9XReh) para más información.
Una aplicación de consola es lo único que se necesita para comenzar
Topshelf tiene como objetivo desarrollar e implementar de manera fácil un servicio de Windows desde el punto de partida de una simple aplicación de consola. Para comenzar con la aplicación de mensajes SMS, creo una aplicación de consola en C# y luego instalo el paquete NuGet de Topshelf desde la Consola del Administrador de paquetes.
Cuando se inicia la aplicación de consola, tengo que configurar HostFactory de Topshelf para el hospedaje abstracto de la aplicación como consola en el desarrollo y como servicio en la producción:
private static int Main() { var exitCode = HostFactory.Run(host => { }); return (int) exitCode; }
HostFactory devolverá un código de salida, que es útil durante la instalación del servicio para detectar y detener en caso de error. El configurador de host proporciona un método de servicio para especificar un tipo personalizado que representa el punto de entrada al código de tu aplicación. Topshelf hace referencia a esto como el servicio que hospeda, porque Topshelf es un marco para simplificar la creación del servicio de Windows:
host.Service<SmsApplication>(service => { });
Luego, creo un tipo SmsApplication que contenga la lógica para acelerar el servidor web con host automático y los componentes tradicionales del servicio de Windows. Como mínimo, este tipo de columna contendrá el comportamiento que se ejecutará cuando se inicie o se detenga la aplicación:
public class SmsApplication { public void Start() { } public void Stop() { } }
Dado que elijo usar un viejo objeto CLR simple (POCO) para el tipo de servicio, proporciono una expresión lambda a Topshelf para construir una instancia del tipo SmsApplication y especifico los métodos de inicio y detención:
service.ConstructUsing(() => new SmsApplication()); service.WhenStarted(a => a.Start()); service.WhenStopped(a => a.Stop());
Topshelf permite configurar muchos parámetros del servicio en código, así que uso SetDescription, SetDisplayName y SetServiceName para describir y nombrar el servicio que se instalará en la producción:
host.SetDescription("An application to manage sending sms messages and provide message status."); host.SetDisplayName("Sms Messaging"); host.SetServiceName("SmsMessaging"); host.RunAsNetworkService();
Finalmente, uso RunAsNetworkService para instruir a Topshelf a que configure el servicio para que se ejecute como la cuenta de servicio de red. Puedes cambiar esto a cualquier cuenta que se ajuste a tu entorno. Para ver más opciones de servicio, consulta la documentación de configuración de Topshelf en bit.ly/1rAfMiQ.
Ejecutar el servicio como una aplicación de consola es tan sencillo como iniciar el ejecutable. La figura 3 muestra el resultado de iniciar y detener el ejecutable de SMS. Dado que esta es una aplicación de consola, cuando inicias la aplicación en Visual Studio, verás el mismo comportamiento.
Figura 3 Ejecutar el servicio como una aplicación de consola
Incorporar una API
Una vez aplicada la asociación de Topshelf, puedo comenzar a trabajar en la API de la aplicación. El proyecto Katana ofrece componentes para hospedar automáticamente una canalización de OWIN, así que instalo el paquete Microsoft.Owin.SelfHost para incorporar los componentes de host automático. Este paquete hace referencia a varios paquetes, dos de ellos son importantes para el host automático. El primero, Microsoft.Owin.Hosting, proporciona un conjunto de componentes para hospedar y ejecutar una canalización de OWIN. El segundo, Microsoft.Owin.Host.HttpListener, suministra una implementación de un servidor HTTP.
Dentro de SmsApplication, creo una aplicación web autohospedada usando el tipo WebApp proporcionado por el paquete Hosting:
protected IDisposable WebApplication; public void Start() { WebApplication = WebApp.Start<WebPipeline>("http://localhost:5000"); }
El método WebApp Start requiere dos parámetros, un parámetro genérico para especificar un tipo que configurará la canalización de OWIN y una URL para escuchar las solicitudes. La aplicación web es un recurso desechable. Cuando se detiene la instancia de SmsApplication, desecho la aplicación web:
public void Stop() { WebApplication.Dispose(); }
Una ventaja de usar OWIN es que puedo aprovechar una variedad de componentes conocidos. En primer lugar, usaré WebApi para crear la API. Debo instalar el paquete Microsoft.AspNet.WebApi.Owin para incorporar WebApi a la canalización de OWIN. Luego creo el tipo WebPipeline para configurar la canalización de OWIN e insertar el middleware WebApi. Además, configuraré WebApi para usar el enrutamiento de atributos:
public class WebPipeline { public void Configuration(IAppBuilder application) { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); application.UseWebApi(config); } }
Ahora puedo crear un método de API para recibir mensajes y ponerlos en la cola para enviarlos:
public class MessageController : ApiController { [Route("api/messages/send")] public void Send([FromUri] SmsMessageDetails message) { MessageQueue.Messages.Add(message); } }
SmsMessageDetails contiene la carga útil del mensaje. La acción de envío agrega el mensaje a una cola que se procesará asincrónicamente más tarde. MessageQueue es una BlockingCollection global. En una aplicación real, esto puede significar que se tengan en cuenta otros factores, como la durabilidad y la escalabilidad:
public static readonly BlockingCollection<SmsMessageDetails> Messages;
En una arquitectura web y de servicio independiente, la entrega del procesamiento asincrónico de solicitudes de ejecución prolongada, como enviar un mensaje, requiere la comunicación entre los procesos web y de servicios. Y agregar métodos de API para consultar el estado del servicio implica una sobrecarga mayor en las comunicaciones. Un enfoque unificado simplifica compartir la información del estado entre los componentes web y de servicios. Para demostrarlo, agrego una consulta PendingCount a la API:
[Route("api/messages/pending")] public int PendingCount() { return MessageQueue.Messages.Count; }
Creación de una interfaz de usuario enriquecida
Las API son prácticas, pero las aplicaciones web autohospedadas aún necesitan admitir una interfaz visual. Creo que en el futuro el marco ASP.NET MVC o un derivado estarán disponibles como middleware de OWIN. Por ahora, Nancy es compatible y tiene un paquete para admitir lo básico del motor de vistas Razor.
Instalaré el paquete Nancy.Owin para agregar compatibilidad para Nancy, y Nancy.Viewengines.Razor para incorporar el motor de vistas Razor. Para conectar Nancy a la canalización de OWIN, debo registrarla después de registrar WebApi para que no capture las rutas que asigné a la API. De forma predeterminada, Nancy devuelve un error si no encuentra un recurso, mientras que WebApi pasa a la canalización las solicitudes que no puede manejar:
application.UseNancy();
Para obtener información sobre cómo usar Nancy con una canalización de OWIN, consulta “Hosting Nancy with OWIN” (Hospedar Nancy con OWIN) en bit.ly/1gqjIye.
Para crear una interfaz administrativa de estado, agrego un módulo de Nancy y asigno una ruta de estado para procesar una vista de estado y paso el recuento de mensajes pendientes como modelo de vista:
public class StatusModule : NancyModule { public StatusModule() { Get["/status"] = _ => View["status", MessageQueue.Messages.Count]; } }
La vista no es muy elegante en este momento, simplemente un recuento de los mensajes pendientes:
<h2>Status</h2> There are <strong>@Model</strong> messages pending.
Voy a ambientar la vista un poco con una barra de navegación sencilla de Bootstrap, como se muestra en la figura 4. Usar Bootstrap requiere hospedar el contenido estático para la hoja de estilo de Bootstrap.
Figura 4 Página administrativa de estado
Podría usar Nancy para hospedar contenido estático, pero la ventaja de OWIN es mezclar y combinar middleware, así que, en su lugar, voy a usar el nuevo paquete Microsoft.Owin.StaticFiles, que es parte del proyecto Katana. El paquete StaticFiles proporciona middleware para el servicio de archivos. Lo agregaré al comienzo de la canalización de OWIN para que el servicio de archivos estáticos de Nancy no participe.
application.UseFileServer(new FileServerOptions { FileSystem = new PhysicalFileSystem("static"), RequestPath = new PathString("/static") });
El parámetro FileSystem le dice al servidor de archivos dónde buscar archivos para enviar. Uso una carpeta llamada static. RequestPath especifica el prefijo de ruta para escuchar las solicitudes de este contenido. En este caso, elijo reflejar el nombre static, pero no tienen por qué coincidir. Uso el vínculo siguiente en el diseño para hacer referencia a la hoja de estilo de Bootstrap (naturalmente, coloco la hoja de estilo de Bootstrap en una carpeta CSS dentro de la carpeta static):
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
Unas palabras sobre contenido estático y vistas
Antes de continuar, quiero darte un consejo que me resulta útil cuando desarrollo una aplicación web autohospedada. Normalmente, configurarías el contenido estático y las vistas de MVC para que se copien en el directorio de salida para que los componentes web autohospedados se encuentren a sí mismos en relación con el ensamblado que se está ejecutando actualmente. Esto no solo es pesado y fácil de olvidar, sino que cambiar las vistas y el contenido estático requiere volver a compilar la aplicación, lo que lastra totalmente la productividad. Por eso recomiendo no solo copiar el contenido estático y las vistas en el directorio de salida, sino configurar el middleware, como Nancy y FileServer, para asignarlo a las carpetas de desarrollo.
De forma predeterminada, el directorio de salida de depuración de una aplicación de consola es bin/Debug, así que durante el desarrollo le digo a FileServer que busque dos directorios encima del directorio actual para encontrar la carpeta estática que contiene la hoja de estilo de Bootstrap:
FileSystem = new PhysicalFileSystem(IsDevelopment() ? "../../static" : "static")
Luego, para decirle a Nancy dónde buscar las vistas, crearé un NancyPathProvider personalizado:
public class NancyPathProvider : IRootPathProvider { public string GetRootPath() { return WebPipeline.IsDevelopment() ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\") : AppDomain.CurrentDomain.BaseDirectory; } }
De nuevo, uso la misma comprobación para buscar dos directorios encima del directorio base si ejecuté el modo de desarrollo en Visual Studio. Hasta ahora, he dejado la implementación de IsDevelopment a tu elección; podría ser un simple valor de configuración o se puede escribir código para detectar en qué momento Visual Studio inicia la aplicación.
Para registrar a este proveedor de ruta raíz personalizado, creo un NancyBootstrapper personalizado y reemplazo la propiedad RootPathProvider predeterminada para crear una instancia de NancyPathProvider:
public class NancyBootstrapper : DefaultNancyBootstrapper { protected override IRootPathProvider RootPathProvider { get { return new NancyPathProvider(); } } }
Y cuando agrego Nancy a la canalización de OWIN, paso una instancia de NancyBootstrapper en las opciones:
application.UseNancy(options => options.Bootstrapper = new NancyBootstrapper());
Envío de mensajes
Recibir mensajes es la mitad del trabajo, pero la aplicación todavía necesita un proceso para enviarlos. Este es un proceso que normalmente residiría en un servicio aislado. En esta solución unificada, simplemente agrego un SmsSender que se inicie cuando se inicie la aplicación. Voy a agregarlo al método SmsApplication Start (en una aplicación real, debo agregar la capacidad de detener y desechar este recurso):
public void Start() { WebApplication = WebApp.Start<WebPipeline>("http://localhost:5000"); new SmsSender().Start(); }
Dentro del método Start en SmsSender, inicio una tarea de ejecución prolongada para enviar mensajes:
public class SmsSender { public void Start() { Task.Factory.StartNew(SendMessages, TaskCreationOptions.LongRunning); } }
Cuando la acción de envío de WebApi recibe un mensaje, lo agrega a una cola de mensajes que es una colección de bloqueo. Creo el método SendMessages de modo que se bloquee hasta que lleguen mensajes. Esto es posible gracias a las abstracciones detrás de GetConsumingEnumerable. Cuando llega un conjunto de mensajes, comienza a enviarlos de inmediato:
private static void SendMessages() { foreach (var message in MessageQueue.Messages.GetConsumingEnumerable()) { Console.WriteLine("Sending: " + message.Text); } }
Sería trivial acelerar varias instancias de SmsSender para ampliar la capacidad de enviar mensajes. En una aplicación real, querría pasar un CancellationToken a GetConsumingEnumerable para detener la enumeración de forma segura. Si quieres saber más sobre las colecciones de bloqueo, encontrarás información útil en bit.ly/QgiCM7 y bit.ly/1m6sqlI.
Implementaciones fáciles y rápidas
Desarrollar una aplicación web y de servicios combinada es bastante sencillo y directo con Katana y Topshelf. Una de las increíbles ventajas de esta potente combinación es un proceso de implementación irrisoriamente fácil. Voy a mostrarte una implementación sencilla de dos pasos usando psake (github.com/psake/psake). No está prevista para usarse como script robusto para el uso real en producción; solo quiero demostrar la sencillez del proceso, independientemente de la herramienta que uses.
El primer paso es compilar la aplicación. Creo una tarea de compilación que se llamara msbuild con la ruta a la solución y creo una compilación de versión (la salida terminará en bin/Release):
properties { $solution_file = "Sms.sln" } task build { exec { msbuild $solution_file /t:Clean /t:Build /p:Configuration=Release /v:q } }
El segundo paso es implementar la aplicación como servicio. Creo una tarea de implementación que dependa de la tarea de compilación y declaro un directorio de entrega para mantener una ruta a la ubicación de instalación. Por simplicidad, solo implemento en un directorio local. Luego creo una variable ejecutable que apunte al ejecutable de la aplicación de consola en el directorio de entrega:
task deploy -depends build { $delivery_directory = "C:\delivery" $executable = join-path $delivery_directory 'Sms.exe'
Primero, la tarea de implementación comprobará si existe el directorio de entrega. Si encuentra un directorio de entrega, supondrá que el servicio ya se ha implementado. En este caso, la tarea de implementación desinstalará el servicio y eliminará el directorio de entrega:
if (test-path $delivery_directory) { exec { & $executable uninstall } rd $delivery_directory -rec -force }
A continuación, la tarea de implementación copia la salida de la compilación al directorio de entrega para implementar el nuevo código y, más adelante, copia las vistas y las carpetas estáticas en el directorio de entrega:
copy-item 'Sms\bin\Release' $delivery_directory -force -recurse -verbose copy-item 'Sms\views' $delivery_directory -force -recurse -verbose copy-item 'Sms\static' $delivery_directory -force -recurse –verbose
Finalmente, la tarea de implementación instala e inicia el servicio:
exec { & $executable install start }
Al implementar el servicio, asegúrate de que tu implementación de IsDevelopment devuelva false o, de lo contrario, recibirás una excepción de acceso denegado si el servidor de archivos no encuentra la carpeta estática. Además, en ocasiones reinstalar el servicio en cada implementación puede generar problemas. Otra táctica es detener el servicio, actualizarlo y luego iniciarlo si ya está instalado.
Como ves, la implementación es tremendamente sencilla. IIS e InstallUtil se eliminan por completo de la ecuación; es decir, un proceso de implementación en lugar de dos, y no hay por qué preocuparse por cómo se comunicarán la capa web y de servicios. Esta tarea de implementación se puede ejecutar varias veces a medida que compilas tu aplicación web y de servicios unificada.
Mirando hacia el futuro
La mejor manera de saber si este modelo combinado te funcionará es buscar un proyecto de bajo riesgo y probar. Es muy fácil desarrollar e implementar una aplicación con esta arquitectura. Existe una curva de aprendizaje ocasional (si usas Nancy para MVC, por ejemplo). Pero lo mejor de usar OWIN, incluso si el enfoque combinado no funciona, es que aún puedes hospedar la canalización de OWIN en IIS usando el host ASP.NET (Microsoft.Owin.Host.SystemWeb). Pruébalo y ve qué te parece.
Wes McClure aprovecha su experiencia para ayudar a los clientes a entregar de manera rápida software de alta calidad con el fin de aumentar de manera exponencial el valor que crean para sus clientes. Disfruta hablando sobre todo lo relacionado al desarrollo de software, es autor en Pluralsight y escribe sobre sus experiencias en devblog.wesmcclure.com. Puedes ponerte en contacto con él en wes.mcclure@gmail.com.
Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Howard Dierking (Microsoft), Damian Hickey, Chris Patterson (RelayHealth), Chris Ross (Microsoft) y Travis Smith
Howard Dierking es jefe de programas en el equipo de marcos y herramientas para Azure, donde se concentra en ASP.NET, NuGet y API web. Previamente, Dierking actuó como redactor jefe de MSDN Magazine y también estaba a cargo del programa de certificación de desarrolladores de Microsoft Learning. Antes de entrar a Microsoft, trabajó durante diez años como desarrollador y arquitecto de aplicaciones con un enfoque en sistemas distribuidos.
Chris Ross es ingeniero en diseño de software en Microsoft, donde trabaja en todo lo relacionado con conexión a redes y OWIN.
Chris Patterson es arquitecto en RelayHealth, la empresa de conectividad de McKesson Corporation, y es responsable de la arquitectura y el desarrollo de aplicaciones y servicios que aceleran la prestación de atención al conectar a pacientes, proveedores, farmacias e instituciones financieras. Chris es uno de los principales contribuyentes a Topshelf y MassTransit, y ha recibido el premio al Profesional más valioso, entregado por Microsoft, por sus contribuciones a la comunidad técnica.
Damian Hickey es desarrollador de software y se centra en aplicaciones basadas en DDD\CQRS\ES. Es un defensor del software .NET de código abierto y contribuye a varios proyectos, como Nancy, NEventStore y otros. Habla esporádicamente, cuando la gente se molesta en escuchar, y en ocasiones escribe en el blog http://dhickey.ie. Puedes ponerte en contacto con él en dhickey@gmail.com / randompunter
Travis Smith es un desarrollador que defiende Atlassian y Atlassian Marketplace. Travis ayuda a fomentar una web abierta, el poliglotismo y las tecnologías web emergentes. Travis contribuye a varios proyectos de código abierto, como Topshelf y MassTransit. Puedes encontrarlo en eventos para desarrolladores, donde habla con pasión sobre la creación de programas de software increíbles o en Internet en http://travisthetechie.com.