Criando um Chat com Reactor e WebSockets
Posted on January 12, 2010
Obs: Este artigo tem a ver com a prova-de-conceito Cramp Chat Demo disponível no Github.
Existe um caso de uso de aplicações web que é o envio frequente de conteúdo adicional. Por exemplo uma conversa de chat, ou um livestream de feeds, ou até mesmo APIs públicas muito acessadas. O padrão de solução costuma ser um javascript que, em intervalos regulares, faz requisições Ajax a um servidor que retorna o conteúdo adicional a ser acrescentado na página. Isso é um polling.
Outro padrão é manter uma conexão HTTP aberta, recebendo conteúdo continuamente num stream. Para isso temos termos/técnicas guarda-chuva como Comet, Ajax Push, Reverse Ajax, HTTP server push, etc.

As técnicas no cliente variam bastante. Mas os backends não variam muito. Por exemplo, uma aplicação Rails é limitada ao número de conexões simultâneas (ativas exatamente ao mesmo tempo) pelo número de processos carregados. Se tivermos 10 processos Rails de pé, só podemos ter 10 conexões simultâneas. Essa característica é mais importante se o tempo de resposta por conexão for muito alto (e é por isso que sempre falamos em fazer o mínimo possível numa requisição e deferir processamentos mais pesados para tarefas em background, usando tecnologias simples como Delayed Job, Resque, ou se a coisa for mais complexa, servidores de fila e mensagens como RabbitMQ).
Se possível, alguns trechos da aplicação que são carregados o tempo todo, por exemplo, por causa do efeito de polling de clientes fazendo requisições muito frequentes, é importante que seja possível maximizar o uso de um processo diminuindo o tempo de resposta. Para isso existem soluções como Rails Metal, Sinatra ou algo ainda mais leve usando Rack puro. A aplicação Campfire da 37signals, por exemplo, tinha um componente feito em C++ que depois foi migrado para outro que eles escreveram em Erlang, para possibilitar alta concorrência junto com alta performance.
O problema a que nos referimos é o que, há 10 anos, chamamos de Problema C10K ou, literalmente, como suportar 10 mil conexões simultâneas ou mais? Assim como Rails, PHP, ASP, Perl tem o mesmo problema: cada conexão é um processo. Começa a ficar bastante difícil administrar uma quantidade tão massiva de processos abertos simultaneamente.
Alguns podem imaginar que o problema é a simples falta de multi-thread nativo (Ruby, Python, tem green-threads, ou lightweight threads). Portanto servidores Java (Tomcat, JBoss, Glassfish) ou .NET (Application Pool de IIS) seriam melhores. De fato, são melhores, mas não são “a” solução para o problema C10K.
O real problema é I/O bloqueante. Independente se é um processo ou um thread, se ele precisar gravar um arquivo, fazer uma query num banco de dados, esse processo precisa esperar o processamento de I/O terminar para prosseguir. Isso “pendura” esse processo ou thread, desde o cliente até a base de dados, por exemplo. A única diferença é que um servidor multi-thread vai aguentar mais tempo, mas o problema é o mesmo.
Portanto, uma solução é unir o modelo multi-processo/multi-thread com I/O assíncrono ou não-bloqueante, mais do que isso, mudar o paradigma procedural por um que é baseado em eventos. Existe um pattern para isso, chamado Reactor.
O mundo Java começou isso antes, com a implementação de NIO a partir do JDK 1.4.2. Sobre ela surgiram servidores de aplicação/frameworks como Grizzly, Apache Mina ou JBoss Netty. No mundo Python existe o framework Twisted e, felizmente para nós rubistas, temos o EventMachine.
Esse tipo de tecnologia, aliado a coisas como o driver assíncrono MySQLPlus para MySQL, permite uma quantidade absurda de conexões simultâneas não-bloqueantes por processo Ruby. Um processo de EventMachine poderia lidar com milhares ao mesmo tempo.
Daí vem o novo framework web Cramp, desenvolvido pelo Pratik Naik, do Rails Core Team, como um mini-framework de aplicações web assíncronas. Minha prova de conceito é justamente um chat – que demandaria muitas conexões simultâneas – e poderia facilmente ser transformado em livestream (que não deixa de ser um chat read-only, a grosso modo).
Eu implementei duas versões: a primeira é o jeito clássico onde o browser faz conexões Ajax em um intervalo regular. Aliado ao Cramp/EventMachine isso já ajuda porque ele suportaria várias conexões simultâneas.
Mas a segunda versão é mais interessante, porque usa uma tecnologia que está disponível no HTML 5, chamado WebSocket. Assim como o XmlHttpRequest, será um componente acessível por Javascript disponível em todo browser que implementar HTML 5. Já existe no Google Chrome 4.x, no Safari/WebKit e no Firefox 3.7.
Com o WebSocket é possível abrir uma conexão HTTP, mantê-la aberta, “escutar” seu stream por novas mensagens e reagir a elas via javascript e inclusive enviar novos dados (como uma nova mensagem), tudo na mesma conexão, sem precisar passar pelo peso da rotina de abrir conexão, enviar/receber dados, fechar conexão e repetir daqui a alguns segundos. Ou seja, a vantagem é diminuir a quantidade enorme de conexões batendo no servidor para uma ordem de grandeza menos.
Obviamente vai demorar um bom tempo para aparecer no IE, mas não temam, existe uma alternativa muito interessante chamada Web-Socket.js e que eu implemento no meu demo. Ela simula a mesma API do WebSocket padrão (e no Chrome ela nem se ativa), mas usa um pequeno Flash como ponte para criar a conexão HTTP permanente, com os mesmos eventos.
A imagem que você vê acima é justamente de Firefox 3.5, Safari 4 e Chrome 4 conversando simultaneamente com 3 conexões permanentes ao servidor baseado em Cramp. O WebSocket tem um handshake próprio para iniciar uma conversa, que precisa ser implementado do lado do servidor, depois disso é HTTP normal. No meu caso, eu trafego dados em formato JSON.
Como era uma prova de conceito, eu armazeno as mensagens num banco de dados MySQL, mas possivelmente eu teria implementado uma fila de mensagens usando um RabbitMQ ou coisa parecida. Outra coisa, obviamente ele não tem diversas funcionalidades básicas como controle de sessões e outras perfumarias.
Somando as tecnologias de WebSocket, servidor web baseado em Reactor, drivers de banco assíncronos e I/O assíncrono em geral, estamos caminhando para um paradigma levemente diferente do atual para escrever aplicações Web de alta demanda. Vale a pena entender essas tecnologias, especialmente para os casos que citei no começo: chat, livestream, apis e tudo mais que demanda alto nível de concorrência.
A prova de conceito do chat está disponível no Github e imagino que o README que deixei seja suficiente para vocês conseguirem duplicar o mesmo ambiente para rodar a aplicação. E, como sempre, pull requests são bem vindos :-)
blog comments powered by Disqus
Archives
- February 12(2)
- December 11(1)
- November 11(4)
- October 11(6)
- September 11(5)
- August 11(1)
- July 11(5)
- May 11(4)
- April 11(11)
- March 11(4)
- February 11(3)
- January 11(4)
- December 10(9)
- November 10(2)
- October 10(10)
- September 10(4)
- August 10(6)
- July 10(14)
- June 10(16)
- May 10(8)
- April 10(14)
- March 10(9)
- February 10(6)
- January 10(14)
- December 09(10)
- November 09(10)
- October 09(7)
- September 09(19)
- August 09(4)
- July 09(12)
- June 09(7)
- May 09(12)
- April 09(11)
- March 09(9)
- February 09(9)
- January 09(12)
- December 08(14)
- November 08(20)
- October 08(15)
- September 08(18)
- August 08(25)
- July 08(13)
- June 08(21)
- May 08(29)
- April 08(27)
- March 08(12)
- February 08(32)
- January 08(31)
- December 07(27)
- November 07(30)
- October 07(25)
- September 07(28)
- August 07(16)
- July 07(15)
- June 07(16)
- May 07(7)
- April 07(13)
- March 07(8)
- February 07(9)
- January 07(24)
- December 06(17)
- November 06(17)
- October 06(15)
- September 06(38)




