Respondiendo peticiones en NodeJS

El "responder" a las peticiones que llegan desde el cliente de nuestra aplicaci贸n, es poder reaccionar dependiendo de la URL que solicita el cliente.
Continuamos con la estructura de mi anterior publicacion:

  • index (Landing Page).
  • contact (Formulario de Contacto).
  • error (Pagina 404 Not Found).

Nada nuevo para nosotros.
Vamos a extender el c贸digo anterior, y lograremos que haga algo, por fin, con estas solicitudes entrantes...
En este punto, hay que comentar que hay dos caminos para cumplir nuestro objetivo, el correcto y el incorrecto.

El camino incorrecto:

La l贸gica nos dicta, que seria una buena idea el implementar las respuesta directamente en los manejadores, algo as铆 en nuestro requestHandlers.js, mediante el uso de returns:

exports.getIndex = () => {  
  console.log('Manejador de solicitud "index" solicitado')
  return 'Hola Index'
}

exports.getContact = () => {  
  console.log('Manejador de solicitud "contact" solicitado')
  return 'Hola Contacto'
}

Este raciocinio, es escalable desde el punto de vista de que cada manejador tendr铆a su l贸gica bien separada, lo cual nos permitir铆a tener un c贸digo escalable y muy mantenible.

En nuestro archivo server.js, tendr铆amos que adaptarlo a fin de que pueda responder al navegador, con el contenido que los manejadores retornan por v铆a de router.js:

import http from 'http'  
import url from 'url'

import config from './config'

exports.startServer = (route, handle) => {  
  const onRequest = (req, res) => {
    let pathname = url.parse(req.url).pathname

    res.writeHead(200, { 'Content-Type': 'text/html' })
    let content = route(handle, pathname)
    res.write(content)
    res.end()
  }

  http.createServer(onRequest).listen(config.port)

  console.log('El servidor esta corriendo en el puerto: ' + config.port)
}

Simplemente, le pasamos al m茅todo write() del objeto res, la respuesta del manejador que llega por medio de la funci贸n route().
Si probamos nuestra app en este punto, podemos observar que obtenemos las respuestas esperadas, en la ruta /contact, aparece el mensaje "Hola Contacto", y para una ruta /cualquiera, nos devuelve un mensaje "404 No Encontrado"; podr铆amos, incluso, haber enviado un archivo *.html directamente al navegador para que se renderizara, y tambi茅n habr铆a funcionado.

Entonces, por que este es el camino incorrecto?

La respuesta a esta pregunta se encuentra en el dise帽o de NodeJS como marco de trabajo para JavaScript en el servidor.

Node, a diferencia de otros lenguajes populares en el Back-End, como podr铆a ser php, java o ruby, utiliza un solo hilo de ejecuci贸n en todo el ciclo de vida de la aplicaci贸n.

Modelo Single-Threaded de NodeJS

La imagen pertenece a este articulo, muy recomendable para leer (En ingles).

Este es uno de los conceptos fundamentales de Node. Para abstraer un poco la idea, voy a decir que php por cada solicitud que recibe de un cliente, inicia un nuevo hilo de ejecuci贸n en el CPU del servidor, por lo tanto no importa si una operaci贸n de un cliente es muy lenta, supongamos una query a una base de datos enorme, solamente el cliente que esta haciendo esa operaci贸n se ver铆a relentizado y no el resto de usuarios de la app, pero en Node no es as铆. Al utilizar un solo hilo de ejecuci贸n, si un cliente realiza un proceso que sature ese hilo de ejecuci贸n, el resto de clientes no podr铆a tener acceso a nuestra aplicaci贸n, terrible.

El principal aliciente para utilizar este modelo single-threaded-process, es el rendimiento...
Imaginemos los siguiente, Node al solamente utilizar un solo hilo de ejecuci贸n, no se beneficia directamente de CPUs multi-core ya que no hace uso de dichos cores, por lo tanto dichos hardware se abarata en costo, y el escalamiento de una arquitectura en node solo se ve "penalizada" por su RAM o Espacio de almacenamiento en todo caso.

Pero, volviendo al tema que nos concierne en esta publicaci贸n, cual es el problema real en este punto? Al utilizar solamente un solo hilo de ejecuci贸n, si por alguna raz贸n alguna operaci贸n bloquea dicho hilo, este no va a estar disponible para ning煤n otro cliente, hasta que dicho hilo se desbloquee y pueda continuar respondiendo. A esto se le da el nombre de operaci贸n bloqueante, y es lo que como desarrolladores NodeJS tenemos que evitar.

Las operaciones bloqueantes y no bloqueantes, merecen una publicaci贸n especial, por ese motivo no voy a profundizar en dicho tema ac谩.

El camino correcto, o algo as铆...:

Lo ideal seria responder a las peticiones que llegan a nuestro manejador de solicitudes, es utilizar operaciones no bloqueantes. Node nos lo pone f谩cil, y provee en su API un objeto que nos da algunas esenciales... Este, es el objeto Child Process.

Node, tiene una api muy peque帽a y con una documentaci贸n realmente excelente.

Quiero hacer un par茅ntesis y explicar la arquitectura que vamos a seguir en este punto. Hasta ahora, por el camino malo seguimos el siguiente patr贸n:

Solicitud --> Handler --> Router --> Server --> Respuesta.

Lo que tenemos que hacer ahora es, en vez de llevar el contenido al servidor, llevar el servidor al contenido.
Vamos a utilizar el objeto res (de la funci贸n onRequest() de nuestro server.js), y lo vamos a enviar como par谩metro en nuestra funci贸n router() hasta nuestro requestHandlers.js. De este modo, los handlers van a poder manipular este objeto y de esta forma responder por ellos mismos.

La l贸gica que se pens贸 en un primer momento, de separar las acciones de cada handler por separado, continua siendo valida en este punto.

Entonces, modificamos en nuestro archivo server.js, la funci贸n res() de modo tal que aparte de handle y pathname tambi茅n lleve como par谩metro al objeto res:

let content = route(handle, pathname, res)  

Una excelente idea es eliminar cualquier tipo de interacci贸n que la funciona onRequest() este teniendo con el objeto res, de modo tal de hacer mas modular nuestro server.js. Finalmente, nuestro server deber铆a quedar muy parecido a lo siguiente:

import http from 'http'  
import url from 'url'

import config from './config'

exports.startServer = (route, handle) => {  
  const onRequest = (req, res) => {
    let pathname = url.parse(req.url).pathname
    route(handle, pathname, res)
  }

  http.createServer(onRequest).listen(config.port)

  console.log('El servidor esta corriendo en el puerto: ' + config.port)
}

En nuestro router.js, debemos actualizar la estructura del m茅todo route() de modo tal, que reciba el objeto res pasado por par谩metro, y a su vez, siguiendo la l贸gica que hablamos anteriormente, el router tendr铆a que pasarlo hacia el handler, para que este finalmente pueda manipularlo y realizar la respuesta. Nuestro router.js, quedar铆a as铆:

exports.route = (handle, pathname, res) => {  
  console.log('Solicitud sobre ruta: ' + pathname)
  if (typeof handle[pathname] === 'function') {
    return handle[pathname](res)
  } else {
    console.log('No se encontro manipulador para ' + pathname)
    res.writeHead(404, {'Content-Type': 'text/html'})
    res.write('404 No Encontrado')
    res.end()
  }
}

Se puede apreciar que en el bloque del else se agrego una manipulaci贸n directa al objeto res:

else {  
    console.log('No se encontro manipulador para ' + pathname)
    res.writeHead(404, {'Content-Type': 'text/html'})
    res.write('404 No Encontrado')
    res.end()
  }

Finalmente, hay que modificar a nuestro requestHandlers.js, para que manipule al objeto res directamente, algo as铆:

exports.getIndex = (res) => {  
  console.log('Handler solicitado: /index')
  res.writeHead(200, {'Content-Type': 'text/html'})
  res.write('Hola Index')
  res.end()
}

exports.getContact = (res) => {  
  console.log('Handler solicitado: /contact')
  res.writeHead(200, {'Content-Type': 'text/html'})
  res.write('Hola Contacto')
  res.end()
}

Si ahora probamos nuestra app, obtenemos el resultado correcto nuevamente. Quiz谩 sea l贸gico pensar que lo 煤nico que se hizo ac谩 fue replicar c贸digo, pero no es as铆. Se sentaron las bases par que nuestra app sea realmente modular y escalable, y sobre todo, se respeto un patr贸n de dise帽o de software para que el d铆a de ma帽ana tu yo futuro no quiera volver en el tiempo para asesinarte.
En pr贸ximas entradas continuaremos sobre esta base, y muchas cosas van a lograr entenderse completamente y cerrarse, pero por hoy creo que esta publicaci贸n se torno bastante larga y densa.

Como siempre, el c贸digo esta en mi GitHub.

Hasta la pr贸xima! :D

Show Comments

Get the latest posts delivered right to your inbox.