Eu estava quase perdendo as esperanças de conseguir utilizar NIO para um servidor. Sempre achei muito dificil programar utilizando um socket não-bloqueante, o parsing de qualquer protocolo um mais dificil é um inferno. Eu resolvi então experimentar escrever um servidor usando continuation-passing-style e o resultado foi supreendemente facil, intuitivo e escalavel.
Minha vontade de usar CPS veio das minhas brincadeiras com Erlang, processos são tão leves nessa linguagem que me leva a acreditar que threads em user-land com scheduling (pseudo)cooperativo são a melhor receita para escalabilidade e facilidade de programação. Quem lembra do windows 3.1, vai me afirmar que cooperative threading é uma enorme roubada e eu concordo, que para aplicações desktop, não funciona bem. Agora pensando em web-application, que basicamente se resume a fazer pequenas tarefas e esperar um tempão pelas operações de I/O completarem, cada thread decidir quando pausar não soa ruim.
Bom, vou descrever um pouco a solução que eu adotei. Utilizei o commons-javaflow para suportar continuations em Java. A biblioteca ainda é experimental, mas já estavel e usavel o suficiente para brincar. O ruim é que você precisa brincar com classloading ou pos-processamento dos arquivos .class. Fora isso uso o feijão com arroz de io multiplexado. Que é manter um Selector para verificar a disponibilidade de todos os sockets e I/O não bloqueante para transmissão de dados.
A grande sacada de usar i/o não bloqueante e continuations é até que simples, sempre que uma operação falhar por não ter dados disponiveis (leitura), ou falta de buffer (escrita), você registra o socket junto ao selector e da yield na continuation (suspende a execução dela). Depois disso, quando a operação ficar disponível, você simplesmente retoma a continuation. Eu implementei um servidor de echo, o código que implementa isso no servidor se resume a:
public void run() {
try {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.write("\n");
writer.flush();
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
O código parece o mesmo que se usarmos um socket normal e i/o bloqueante, de fato é, com a diferença que as classe de streams são customizadas. O código que se utiliza de continuations não é muito complicado, bem diferente de vários frameworks como mina ou seda, como mostro a seguir o método que faz a escrita usando NIO. O método current() retorna uma referencia a uma classe que controla a interface com o selector e o escalonador.
public void write(byte[] b, int off, int len) throws IOException {
ByteBuffer buff = ByteBuffer.wrap(b, off, len);
while (buff.remaining() > 0) {
channel.write(buff);
if (buff.remaining() == 0)
break;
current().register(channel, SelectionKey.OP_WRITE);
current().ioSuspend();
}
}
Eu ainda estou começando a bricandeira, ainda pretendo colocar um bom parser de HTTP para funcionar nesse esquema, algum driver opensource para banco de dados e, finalmente, file I/O assíncrono. A partir deste ponto já fica possivel realmente contribuir um sistema com essa tecnologia. Para quem ficou curioso, os links são: commons-javaflow e meu exemplo. Para quem gostou, pode ter certeza que vem muito mais por ai.
7 responses so far ↓
1 Rodrigo Urubatan // Oct 12, 2006 at 1:48 pm
ótima a solução, mas tu tem problemas :D
não era mais fácil usar o jetty em vez de implementar isto novamente?
2 kumpera // Oct 12, 2006 at 2:19 pm
Jetty não resolve meu problema. Beeem longe disso.
Jetty tem um treco que alega “suportar continuations”, mas na verdade é um mecanismo pelo qual você tem que implementar o suspend/restore manualmente, ou seja, tem uso super-super específico: comet.
Além disso, o jetty funciona usando threading e I/O bloqueante para todo o resto, o que não resolve meus problemas de escalabilidade. Para terminar, não adianta eu ter um servidor escalavel se eu vou continuar esbarrando no driver JDBC.
3 Rodrigo Urubatan // Oct 12, 2006 at 8:18 pm
isto é verdade :D
e parece que estou mal informado :D
acreditei na propaganda do jetty, e achava que ele estava usando NIO para tudo :(
4 Luca Bastos // Oct 17, 2006 at 10:54 am
Agora que o Gilad Bracha saiu da Sun, fui ler o antigo blog dele na Sun e o encontrei falando de continuantions. Lembrei logo de ti.
Não ainda não leu, dê uma lida em:
http://blogs.sun.com/gbracha/entry/will_continuations_continue
http://blogs.sun.com/gbracha/entry/continuations_continued
5 Willian Mitsuda // Oct 18, 2006 at 12:15 pm
Rodrigo, vc chegou a estudar a possibilidade de usar o Apache MINA?
Se entendi bem o seu problema, acho que ele já implementa toda a infra que vc precisa, só precisando implementar um codec p/ o seu protocolo.
http://directory.apache.org/subprojects/mina/index.html
6 kumpera // Oct 18, 2006 at 12:31 pm
Willian, eu já utilizei antes o Apache MINA. A arquitetura é boa e o software é de estavel. O grande problema é o inferno que é programar codecs e afins.
Com MINA você escreve basicamente 2 componentes, o (de)codicador de mensagens e o tratador de mensagens. O primeiro é responsavel por traduzir de/para o formato de rede (de stream para ServletRequest, por exemplo); o segundo lida apenas com objetos da aplicação e possui as regras de negocios, por assim dizer.
Implementar um codec é exponencialmente dificil em relação a complexidade do protocolo, e de quao eficiente você quer que ele seja. É basicamente implementar uma máquina de estados enorme. Já fiz para HTTP 1.1 parcialmente e conheci o inferno.
Depois vem o protocol handler, ele tem que se preocupar com coisas como flow control, para não mandar dados rápido demais e deixar um toneladas de buffers na mão do framework. Fora que você tem que programar utilizando um modelo orientado a eventos. Uma delicia. Willian, vou mostrar num artigo futuro então a diferença de dificuldade com um protocolo não trivial.
7 felipe // Nov 7, 2006 at 4:33 pm
Rodrigo,
gostaria de entender melhor o objetivo de usar i/o não bloqueante com continuations na leitura e escrita dos channels, já que o i/o não bloqueante já meio que funciona como se tivesse continuation (é lido o que está disponivel, mesmo que essa leitura precise continuar depois e o mesmo vale para escrita).
Se não existem dados para ler, qual o sentido de eu suspender o processamento?
Qual o ganho real dessa suspensao(usando continuations) se o i/o não é bloqueante? (ou seja, o thread continua executando outras coisas ao invés de ficar parado)
Leave a Comment